//! A runtime meant for actual shader invocations. const std = @import("std"); const spv = @import("spv.zig"); const op = @import("opcodes.zig"); const lib = @import("lib.zig"); const pretty = @import("pretty"); const SpvVoid = spv.SpvVoid; const SpvByte = spv.SpvByte; const SpvWord = spv.SpvWord; const SpvBool = spv.SpvBool; const Module = @import("Module.zig"); const PrimitiveType = @import("Value.zig").PrimitiveType; const Result = @import("Result.zig"); const Value = @import("Value.zig").Value; const WordIterator = @import("WordIterator.zig"); const Self = @This(); pub const RuntimeError = error{ Barrier, InvalidEntryPoint, InvalidSpirV, InvalidValueType, Killed, NotFound, OutOfMemory, OutOfBounds, ToDo, Unreachable, UnsupportedSpirV, UnsupportedExtension, Unknown, }; pub const EntryPointStatus = enum { completed, barrier, }; pub const SpecializationEntry = struct { id: SpvWord, offset: usize, size: usize, }; pub const WorkgroupMemory = struct { result: SpvWord, bytes: []u8, }; pub const Derivative = struct { dx: Value, dy: Value, pub fn dupe(self: *const @This(), allocator: std.mem.Allocator) RuntimeError!@This() { return .{ .dx = try self.dx.dupe(allocator), .dy = try self.dy.dupe(allocator), }; } pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void { self.dx.deinit(allocator); self.dy.deinit(allocator); } }; pub const Function = struct { source_location: usize, result: *Result, ret: *Result, current_label: ?SpvWord, previous_label: ?SpvWord, }; pub fn Vec4(comptime T: type) type { return struct { x: T, y: T, z: T, w: T, }; } pub const ImageOffset = struct { x: i32 = 0, y: i32 = 0, z: i32 = 0, }; pub const ImageDerivatives = struct { dx: Vec4(f32), dy: Vec4(f32), }; pub const ImageAPI = struct { readImageFloat4: *const fn (driver_image: *anyopaque, dim: spv.SpvDim, x: i32, y: i32, z: i32, lod: ?i32) RuntimeError!Vec4(f32), readImageInt4: *const fn (driver_image: *anyopaque, dim: spv.SpvDim, x: i32, y: i32, z: i32, lod: ?i32) RuntimeError!Vec4(u32), writeImageFloat4: *const fn (driver_image: *anyopaque, dim: spv.SpvDim, x: i32, y: i32, z: i32, pixel: Vec4(f32)) RuntimeError!void, writeImageInt4: *const fn (driver_image: *anyopaque, dim: spv.SpvDim, x: i32, y: i32, z: i32, pixel: Vec4(u32)) RuntimeError!void, sampleImageFloat4: *const fn (driver_image: *anyopaque, driver_sampler: *anyopaque, dim: spv.SpvDim, x: f32, y: f32, z: f32, lod: ?f32, offset: ImageOffset) RuntimeError!Vec4(f32), sampleImageInt4: *const fn (driver_image: *anyopaque, driver_sampler: *anyopaque, dim: spv.SpvDim, x: f32, y: f32, z: f32, lod: ?f32, offset: ImageOffset) RuntimeError!Vec4(u32), sampleImageDref: *const fn (driver_image: *anyopaque, driver_sampler: *anyopaque, dim: spv.SpvDim, x: f32, y: f32, z: f32, w: f32, dref: f32, lod: ?f32, offset: ImageOffset) RuntimeError!f32, queryImageSize: *const fn (driver_image: *anyopaque, dim: spv.SpvDim, arrayed: bool, lod: ?i32) RuntimeError!Vec4(u32), queryImageLevels: *const fn (driver_image: *anyopaque) RuntimeError!u32, queryImageSamples: *const fn (driver_image: *anyopaque) RuntimeError!u32, queryImageLod: *const fn (driver_image: *anyopaque, driver_sampler: *anyopaque, dim: spv.SpvDim, derivatives: ImageDerivatives) RuntimeError!Vec4(f32), }; mod: *Module, it: WordIterator, /// Local deep copy of module's results to be able to run multiple runtimes concurrently results: []Result, current_parameter_index: SpvWord, current_function: ?*Result, 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), helper_invocation: bool, image_api: ImageAPI, pub fn init(allocator: std.mem.Allocator, module: *Module, image_api: ImageAPI) RuntimeError!Self { var self: Self = .{ .mod = module, .it = module.it, .results = blk: { const results = allocator.dupe(Result, module.results) catch return RuntimeError.OutOfMemory; for (results, module.results) |*new_result, result| { new_result.* = result.dupe(allocator) catch return RuntimeError.OutOfMemory; if (new_result.variant) |*variant| { switch (variant.*) { .AccessChain => |*access_chain| { allocator.free(access_chain.indexes); access_chain.value.deinit(allocator); new_result.variant = null; }, else => {}, } } } break :blk results; }, .current_parameter_index = 0, .current_function = null, .function_stack = .empty, .current_label = null, .previous_label = null, .active_entry_point = null, .specialization_constants = .empty, .derivatives = .empty, .phi_values = .empty, .helper_invocation = false, .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 { const results = allocator.alloc(Result, other.results.len) catch return RuntimeError.OutOfMemory; var initialized: usize = 0; errdefer { for (results[0..initialized]) |*result| { result.deinit(allocator); } allocator.free(results); } for (results, other.results) |*new_result, result| { new_result.* = result.dupe(allocator) catch return RuntimeError.OutOfMemory; initialized += 1; } var self: Self = .{ .mod = other.mod, .it = other.mod.it, .results = results, .current_parameter_index = 0, .current_function = null, .function_stack = .empty, .current_label = null, .previous_label = null, .active_entry_point = other.active_entry_point, .specialization_constants = .empty, .derivatives = .empty, .phi_values = .empty, .helper_invocation = other.helper_invocation, .image_api = image_api, }; errdefer self.deinit(allocator); try self.copySpecializationConstantsFrom(allocator, other); try self.refreshValueLayouts(); return self; } pub fn deinit(self: *Self, allocator: std.mem.Allocator) void { for (self.results) |*result| { result.deinit(allocator); } allocator.free(self.results); self.function_stack.deinit(allocator); var it = self.specialization_constants.iterator(); while (it.next()) |entry| { allocator.free(entry.value_ptr.*); } self.specialization_constants.deinit(allocator); var derivatives = self.derivatives.iterator(); while (derivatives.next()) |entry| { 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 { const slice = allocator.dupe(u8, data[entry.offset .. entry.offset + entry.size]) catch return RuntimeError.OutOfMemory; self.specialization_constants.put(allocator, entry.id, slice) catch return RuntimeError.OutOfMemory; } pub fn copySpecializationConstantsFrom(self: *Self, allocator: std.mem.Allocator, other: *const Self) RuntimeError!void { var it = other.specialization_constants.iterator(); while (it.next()) |entry| { const slice = allocator.dupe(u8, entry.value_ptr.*) catch return RuntimeError.OutOfMemory; self.specialization_constants.put(allocator, entry.key_ptr.*, slice) catch { allocator.free(slice); return RuntimeError.OutOfMemory; }; } } 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 => {}, }; } } pub fn refreshResultValueLayout(self: *Self, result: SpvWord) RuntimeError!void { const type_word = try self.results[result].getValueTypeWord(); try applyValueLayout(self.results, try self.results[result].getValue(), type_word); } 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), .dy = try dy.dupe(allocator), }; errdefer { var tmp = derivative; tmp.deinit(allocator); } const gop = self.derivatives.getOrPut(allocator, result) catch return RuntimeError.OutOfMemory; if (gop.found_existing) { gop.value_ptr.deinit(allocator); } gop.value_ptr.* = derivative; } pub fn setDerivativeFromMemory(self: *Self, allocator: std.mem.Allocator, result: SpvWord, dx: []const u8, dy: []const u8) RuntimeError!void { const target_type = try self.getResultTargetTypeWord(result); var dx_value = try Value.init(allocator, self.results, target_type, false); defer dx_value.deinit(allocator); const memory_size = try dx_value.getPlainMemorySize(); if (dx.len < memory_size or dy.len < memory_size) return RuntimeError.OutOfBounds; _ = try dx_value.write(dx); var dy_value = try Value.init(allocator, self.results, target_type, false); defer dy_value.deinit(allocator); _ = try dy_value.write(dy); try self.setDerivative(allocator, result, &dx_value, &dy_value); } fn getResultTargetTypeWord(self: *const Self, result: SpvWord) RuntimeError!SpvWord { return switch ((try self.results[result].getConstVariant()).*) { .Variable => |v| v.type_word, .Constant => |c| c.type_word, .FunctionParameter => |p| p.type_word, .AccessChain => |a| a.target, else => return RuntimeError.InvalidSpirV, }; } pub fn clearDerivative(self: *Self, allocator: std.mem.Allocator, result: SpvWord) void { if (self.derivatives.fetchRemove(result)) |kv| { var derivative = kv.value; derivative.deinit(allocator); } } pub fn copyDerivative(self: *Self, allocator: std.mem.Allocator, dst: SpvWord, src: SpvWord) RuntimeError!void { if (self.derivatives.get(src)) |derivative| { try self.setDerivative(allocator, dst, &derivative.dx, &derivative.dy); } else { self.clearDerivative(allocator, dst); } } 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 (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| { if (blk: { // Same as entry points for (0..@min(name.len, result_name.len)) |j| { if (name[j] != result_name[j]) break :blk false; } break :blk true; }) return @intCast(i); } } return RuntimeError.NotFound; } pub const LocationKind = enum { input, output }; pub inline fn getResultByLocation(self: *const Self, location: SpvWord, kind: LocationKind) RuntimeError!SpvWord { return self.getResultByLocationComponent(location, 0, kind); } pub fn getResultByLocationComponent(self: *const Self, location: SpvWord, component: SpvWord, kind: LocationKind) RuntimeError!SpvWord { 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; return (try self.results[result].getConstValue()).resolvePrimitiveType(); } pub fn getWorkgroupSize(self: *Self, allocator: std.mem.Allocator) RuntimeError!?@Vector(3, u32) { try self.pass(allocator, .initMany(&.{ .SpecConstantTrue, .SpecConstantFalse, .SpecConstantComposite, .ConstantNull, .SpecConstant, .SpecConstantOp, })); for (self.results) |*result| { for (result.decorations.items) |decoration| { if (decoration.rtype != .BuiltIn or decoration.literal_1 != @intFromEnum(spv.SpvBuiltIn.WorkgroupSize)) continue; const value = try result.getValue(); return switch (value.*) { .Vector3u32 => |v| v, .Vector => |values| blk: { if (values.len != 3) return RuntimeError.InvalidValueType; var result_value = @Vector(3, u32){ 0, 0, 0 }; inline for (0..3) |i| { result_value[i] = switch (values[i]) { .Int => |int| int.value.uint32, else => return RuntimeError.InvalidValueType, }; } break :blk result_value; }, else => return RuntimeError.InvalidValueType, }; } } return null; } pub fn dumpResultsTable(self: *Self, allocator: std.mem.Allocator, writer: *std.Io.Writer) RuntimeError!void { const dump = pretty.dump(allocator, self.results, .{ .tab_size = 4, .max_depth = 0, .struct_max_len = 0, .array_max_len = 0, }) catch return RuntimeError.OutOfMemory; defer allocator.free(dump); writer.print("{s}", .{dump}) catch return RuntimeError.Unknown; writer.flush() catch return RuntimeError.Unknown; } /// Calls an entry point, `entry_point_index` being the index of the entry point ordered by declaration in the bytecode pub inline fn callEntryPoint(self: *Self, allocator: std.mem.Allocator, entry_point_index: SpvWord) RuntimeError!void { _ = try self.beginEntryPoint(allocator, entry_point_index); } pub fn beginEntryPoint(self: *Self, allocator: std.mem.Allocator, entry_point_index: SpvWord) RuntimeError!EntryPointStatus { self.reset(); if (entry_point_index >= self.mod.entry_points.items.len) return RuntimeError.InvalidEntryPoint; // Spec constants pass if (self.specialization_constants.count() != 0) try self.applySpecializationConstants(allocator); { const entry_point_desc = &self.mod.entry_points.items[entry_point_index]; const entry_point_result = &self.mod.results[entry_point_desc.id]; if (entry_point_result.variant) |variant| { switch (variant) { .Function => |f| { if (!self.it.jumpToSourceLocation(f.source_location)) return RuntimeError.InvalidEntryPoint; self.function_stack.append(allocator, .{ .source_location = f.source_location, .result = entry_point_result, .ret = &self.results[f.return_type], .current_label = null, .previous_label = null, }) catch return RuntimeError.OutOfMemory; }, else => return RuntimeError.InvalidEntryPoint, } } else { return RuntimeError.InvalidEntryPoint; } } // Execution pass return self.continueEntryPoint(allocator); } pub fn continueEntryPoint(self: *Self, allocator: std.mem.Allocator) RuntimeError!EntryPointStatus { self.pass(allocator, null) catch |err| switch (err) { RuntimeError.Barrier => return .barrier, else => return err, }; return .completed; } fn pass(self: *Self, allocator: std.mem.Allocator, op_set: ?std.EnumSet(spv.SpvOp)) RuntimeError!void { self.it.did_jump = false; // To reset function jump while (self.it.nextOrNull()) |opcode_data| { const word_count_with_header = (opcode_data & (~spv.SpvOpCodeMask)) >> spv.SpvWordCountShift; if (word_count_with_header == 0) return RuntimeError.InvalidSpirV; const word_count = word_count_with_header - 1; const opcode = (opcode_data & spv.SpvOpCodeMask); if (op_set) |set| { @branchHint(.unlikely); if (!set.contains(@enumFromInt(opcode))) { _ = self.it.skipN(word_count); continue; } } var it_tmp = self.it; // Save because operations may iter on this iterator if (op.runtime_dispatcher[opcode]) |pfn| { try pfn(allocator, word_count, self); } if (!self.it.did_jump) { _ = it_tmp.skipN(word_count); self.it = it_tmp; } else { self.it.did_jump = false; } } } pub fn populatePushConstants(self: *Self, blob: []const u8) RuntimeError!void { for (self.results) |*result| { if (result.variant == null or std.meta.activeTag(result.variant.?) != .Variable) continue; const variable = &result.variant.?.Variable; if (variable.storage_class != .PushConstant) continue; _ = try variable.value.write(blob); } } pub fn writeDescriptorSet(self: *const Self, input: []const u8, set: SpvWord, binding: SpvWord, descriptor_index: SpvWord) RuntimeError!void { const result = self.mod.getBindingResult(set, binding) orelse return RuntimeError.NotFound; const value = if (self.results[result].variant) |*variant| switch (variant.*) { .Variable => |*variable| &variable.value, else => return RuntimeError.InvalidSpirV, } else return; switch (value.*) { .Array => |arr| { if (descriptor_index >= arr.values.len) return RuntimeError.NotFound; _ = try arr.values[descriptor_index].write(input); }, else => { if (descriptor_index != 0) return RuntimeError.NotFound; _ = try value.write(input); }, } } fn readResultValue(self: *const Self, output: []u8, result: SpvWord) RuntimeError!void { const variant = self.results[result].variant orelse return RuntimeError.InvalidSpirV; switch (variant) { .Variable => |v| _ = try v.value.read(output), .AccessChain => |a| switch (a.value) { .Pointer => |ptr| switch (ptr.ptr) { .common => |value_ptr| _ = try value_ptr.read(output), .f32_ptr => |value_ptr| { if (output.len < @sizeOf(f32)) return RuntimeError.OutOfBounds; std.mem.copyForwards(u8, output[0..@sizeOf(f32)], std.mem.asBytes(value_ptr)); }, .i32_ptr => |value_ptr| { if (output.len < @sizeOf(i32)) return RuntimeError.OutOfBounds; std.mem.copyForwards(u8, output[0..@sizeOf(i32)], std.mem.asBytes(value_ptr)); }, .u32_ptr => |value_ptr| { if (output.len < @sizeOf(u32)) return RuntimeError.OutOfBounds; std.mem.copyForwards(u8, output[0..@sizeOf(u32)], std.mem.asBytes(value_ptr)); }, }, else => _ = try a.value.read(output), }, else => return RuntimeError.InvalidSpirV, } } fn readConstantIndex(self: *const Self, result: SpvWord) RuntimeError!usize { const value = try self.results[result].getConstValue(); return switch (value.*) { .Int => |i| 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, }, else => RuntimeError.InvalidSpirV, }; } fn accessChainPrefixValue(self: *const Self, result: SpvWord, prefix_len: usize) RuntimeError!*Value { const access_chain = switch ((self.results[result].variant orelse return RuntimeError.InvalidSpirV)) { .AccessChain => |*a| a, else => return RuntimeError.InvalidSpirV, }; if (prefix_len > access_chain.indexes.len) return RuntimeError.OutOfBounds; var value = switch ((self.results[access_chain.base].variant orelse return RuntimeError.InvalidSpirV)) { .Variable => |*v| &v.value, .AccessChain => |*a| switch (a.value) { .Pointer => |ptr| switch (ptr.ptr) { .common => |value_ptr| value_ptr, else => return RuntimeError.InvalidSpirV, }, else => &a.value, }, else => return RuntimeError.InvalidSpirV, }; for (access_chain.indexes[0..prefix_len]) |index_id| { const index = try self.readConstantIndex(index_id); value = switch (value.*) { .Vector, .Matrix => |values| blk: { if (index >= values.len) return RuntimeError.OutOfBounds; break :blk &values[index]; }, .Array => |arr| blk: { if (index >= arr.values.len) return RuntimeError.OutOfBounds; break :blk &arr.values[index]; }, .Structure => |structure| blk: { if (index >= structure.values.len) return RuntimeError.OutOfBounds; break :blk &structure.values[index]; }, else => return RuntimeError.InvalidValueType, }; } return value; } fn writeResultValue(self: *const Self, allocator: std.mem.Allocator, input: []const u8, result: SpvWord) RuntimeError!void { if (self.results[result].variant) |*variant| { switch (variant.*) { .Variable => |*v| switch (v.value) { .Pointer => |*ptr| { if (ptr.owns_uniform_backing_value) { if (ptr.uniform_backing_value) |value_ptr| { _ = try value_ptr.write(input); return; } } const target_type = switch ((try self.results[v.type_word].getConstVariant()).*) { .Type => |t| switch (t) { .Pointer => |p| p.target, else => return RuntimeError.InvalidSpirV, }, else => return RuntimeError.InvalidSpirV, }; const value_ptr = allocator.create(Value) catch return RuntimeError.OutOfMemory; errdefer allocator.destroy(value_ptr); value_ptr.* = try Value.init(allocator, self.results, target_type, false); errdefer value_ptr.deinit(allocator); _ = try value_ptr.write(input); ptr.* = .{ .ptr = .{ .common = value_ptr }, .uniform_backing_value = value_ptr, .owns_uniform_backing_value = true, }; }, else => _ = try v.value.write(input), }, .AccessChain => |*a| switch (a.value) { .Pointer => |ptr| switch (ptr.ptr) { .common => |value_ptr| { const value_size = try value_ptr.getPlainMemorySize(); if (a.indexes.len > 1 and input.len > value_size) { const parent = try self.accessChainPrefixValue(result, a.indexes.len - 1); _ = try parent.write(input); } else { _ = try value_ptr.write(input); } }, .f32_ptr => |value_ptr| { if (input.len < @sizeOf(f32)) return RuntimeError.OutOfBounds; if (a.indexes.len > 1 and input.len > @sizeOf(f32)) { const parent = try self.accessChainPrefixValue(result, a.indexes.len - 1); _ = try parent.write(input); } else { std.mem.copyForwards(u8, std.mem.asBytes(value_ptr), input[0..@sizeOf(f32)]); } }, .i32_ptr => |value_ptr| { if (input.len < @sizeOf(i32)) return RuntimeError.OutOfBounds; if (a.indexes.len > 1 and input.len > @sizeOf(i32)) { const parent = try self.accessChainPrefixValue(result, a.indexes.len - 1); _ = try parent.write(input); } else { std.mem.copyForwards(u8, std.mem.asBytes(value_ptr), input[0..@sizeOf(i32)]); } }, .u32_ptr => |value_ptr| { if (input.len < @sizeOf(u32)) return RuntimeError.OutOfBounds; if (a.indexes.len > 1 and input.len > @sizeOf(u32)) { const parent = try self.accessChainPrefixValue(result, a.indexes.len - 1); _ = try parent.write(input); } else { std.mem.copyForwards(u8, std.mem.asBytes(value_ptr), input[0..@sizeOf(u32)]); } }, }, else => _ = try a.value.write(input), }, else => return RuntimeError.InvalidSpirV, } } else { return RuntimeError.InvalidSpirV; } } const InputLocationTarget = struct { result: SpvWord, struct_member: ?usize = null, array_element: ?usize = null, matrix_column: ?usize = null, }; fn resolveInputLocationTarget(self: *const Self, location: SpvWord) RuntimeError!InputLocationTarget { 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 }; }, .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| { const variant = result.variant orelse continue; const variable = switch (variant) { .Variable => |v| v, else => continue, }; if (variable.storage_class != .Input) continue; const type_word = switch ((self.results[variable.type_word].variant orelse continue)) { .Type => |t| switch (t) { .Pointer => |ptr| ptr.target, else => variable.type_word, }, else => continue, }; const type_result = &self.results[type_word]; const type_variant = type_result.variant orelse continue; switch (type_variant) { .Type => |t| switch (t) { .Structure => { for (type_result.decorations.items) |decoration| { if (decoration.rtype == .Location and decoration.literal_1 == location) { return .{ .result = @intCast(id), .struct_member = @intCast(decoration.index), }; } } }, else => {}, }, else => {}, } } var base_location = location; while (base_location > 0) { base_location -= 1; 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(); switch (value.*) { .Array => |arr| { if (arr.values.len == 0) continue; const element_locations = switch (arr.values[0]) { .Matrix => |columns| columns.len, else => 1, }; if (element_locations == 0) continue; const element_index = location_offset / element_locations; if (element_index >= arr.values.len) continue; const element_location = location_offset % element_locations; return .{ .result = result, .array_element = element_index, .matrix_column = if (std.meta.activeTag(arr.values[element_index]) == .Matrix) element_location else null, }; }, .Matrix => |columns| { if (location_offset < columns.len) { return .{ .result = result, .matrix_column = location_offset, }; } }, else => {}, } } return RuntimeError.NotFound; } fn getInputLocationTargetValue(self: *const Self, target: InputLocationTarget) RuntimeError!*Value { const value = switch ((try self.results[target.result].getVariant()).*) { .Variable => |*v| &v.value, .AccessChain => |*a| &a.value, else => return RuntimeError.InvalidSpirV, }; const element_value = if (target.array_element) |element| blk: { switch (value.*) { .Array => |arr| { if (element >= arr.values.len) return RuntimeError.OutOfBounds; break :blk &arr.values[element]; }, else => return RuntimeError.InvalidValueType, } } else value; const member_value = if (target.struct_member) |member| blk: { switch (element_value.*) { .Structure => |structure| { if (member >= structure.values.len) return RuntimeError.OutOfBounds; break :blk &structure.values[member]; }, else => return RuntimeError.InvalidValueType, } } else element_value; if (target.matrix_column) |column| { switch (member_value.*) { .Matrix => |columns| { if (column >= columns.len) return RuntimeError.OutOfBounds; return &columns[column]; }, else => return RuntimeError.InvalidValueType, } } return member_value; } pub fn readOutput(self: *const Self, output: []u8, result: SpvWord) RuntimeError!void { for (&self.mod.output_locations) |*location| { if (std.mem.indexOfScalar(SpvWord, location, result)) |_| { try self.readResultValue(output, result); return; } } return RuntimeError.NotFound; } pub fn readBuiltIn(self: *const Self, output: []u8, builtin: spv.SpvBuiltIn) RuntimeError!void { if (self.getBuiltinResult(builtin)) |result| { try self.readResultValue(output, result); } else { return RuntimeError.NotFound; } } pub fn writeInput(self: *const Self, allocator: std.mem.Allocator, input: []const u8, result: SpvWord) RuntimeError!void { for (&self.mod.input_locations) |*location| { if (std.mem.indexOfScalar(SpvWord, location, result)) |_| { try self.writeResultValue(allocator, input, result); if (self.results[result].variant) |*variant| switch (variant.*) { .Variable => |*v| v.value.clearExternalData(), .AccessChain => |*a| a.value.clearExternalData(), else => {}, }; return; } } return RuntimeError.NotFound; } pub fn getInputLocationMemorySize(self: *const Self, location: SpvWord) RuntimeError!usize { const target = try self.resolveInputLocationTarget(location); return (try self.getInputLocationTargetValue(target)).getPlainMemorySize(); } pub fn writeInputLocation(self: *const Self, input: []const u8, location: SpvWord) RuntimeError!void { const target = try self.resolveInputLocationTarget(location); const value = try self.getInputLocationTargetValue(target); _ = try value.write(input); value.clearExternalData(); } pub fn writeBuiltIn(self: *const Self, allocator: std.mem.Allocator, input: []const u8, builtin: spv.SpvBuiltIn) RuntimeError!void { 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); } } pub fn getResultMemorySize(self: *const Self, result: SpvWord) RuntimeError!usize { const variant = self.results[result].variant orelse return RuntimeError.InvalidSpirV; return switch (variant) { .AccessChain => |a| switch (a.value) { .Pointer => |ptr| switch (ptr.ptr) { .common => |value_ptr| value_ptr.getPlainMemorySize(), .f32_ptr => @sizeOf(f32), .i32_ptr => @sizeOf(i32), .u32_ptr => @sizeOf(u32), }, else => a.value.getPlainMemorySize(), }, else => (try self.results[result].getConstValue()).getPlainMemorySize(), }; } pub fn hasResultDecoration(self: *const Self, result: SpvWord, decoration: spv.SpvDecoration) bool { for (self.results[result].decorations.items) |result_decoration| { if (result_decoration.rtype == decoration) return true; } 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| { switch (variant.*) { .AccessChain => |*access_chain| { if (std.mem.allEqual(u8, std.mem.asBytes(&access_chain.value), 0xaa)) { result.variant = null; continue; } access_chain.value.deinit(allocator); allocator.free(access_chain.indexes); result.variant = null; }, .FunctionParameter => |*parameter| { parameter.value_ptr = null; }, else => {}, } } } self.reset(); } fn reset(self: *Self) void { self.it = self.mod.it; self.function_stack.clearRetainingCapacity(); self.current_parameter_index = 0; self.current_function = null; self.current_label = null; self.previous_label = null; self.helper_invocation = false; }