[Soft] adding new experimental shader interpreter based on new IR
Mirror Gitea refs to GitHub / mirror (push) Successful in 31s
Test / build_and_test (push) Successful in 11m51s
Build / build (push) Successful in 15m19s

This commit is contained in:
2026-08-08 00:42:13 +02:00
parent 937b84cbc3
commit f40e5b742d
20 changed files with 1635 additions and 38 deletions
+9 -1
View File
@@ -38,6 +38,7 @@ const SoftInstance = @import("SoftInstance.zig");
const SoftSampler = @import("SoftSampler.zig");
const SoftShaderModule = @import("SoftShaderModule.zig");
const SoftPipelineCache = @import("SoftPipelineCache.zig");
const InterpreterShader = @import("interpreter/Shader.zig");
const Self = @This();
pub const Interface = base.Pipeline;
@@ -51,6 +52,7 @@ const Shader = struct {
module: *SoftShaderModule,
runtimes: []Runtime,
entry: []const u8,
interpreter: ?InterpreterShader,
};
const Stages = enum {
@@ -185,6 +187,8 @@ pub fn destroy(interface: *Interface, allocator: std.mem.Allocator) void {
var it = self.stages.iterator();
while (it.next()) |entry| {
if (entry.value.interpreter) |*interpreter|
interpreter.deinit();
entry.value.module.unref(allocator);
for (entry.value.runtimes) |*runtime| {
runtime.rt.function_stack.clearAndFree(device_allocator); // Hacky to avoid leaks
@@ -255,11 +259,15 @@ fn createShader(
}
}
return .{
var shader: Shader = .{
.module = module,
.runtimes = runtimes,
.entry = runtimes_allocator.dupe(u8, entry) catch return VkError.OutOfDeviceMemory,
.interpreter = null,
};
if (comptime base.config.soft_ir_interpreter)
shader.interpreter = try InterpreterShader.compile(runtimes_allocator, module, stage, runtimes_count);
return shader;
}
fn initRuntime(allocator: std.mem.Allocator, module: *SoftShaderModule, stage: *const vk.PipelineShaderStageCreateInfo, image_api: spv.Runtime.ImageAPI) VkError!spv.Runtime {
@@ -7,6 +7,7 @@ const PipelineState = ExecutionDevice.PipelineState;
const SoftDevice = @import("../SoftDevice.zig");
const SoftPipeline = @import("../SoftPipeline.zig");
const ir_compute = @import("../interpreter/compute.zig");
const VkError = base.VkError;
const SpvRuntimeError = spv.Runtime.RuntimeError;
@@ -70,6 +71,10 @@ pub fn dispatchBase(self: *Self, base_group_x: u32, base_group_y: u32, base_grou
const pipeline = self.state.pipeline orelse return VkError.InvalidPipelineDrv;
const shader = pipeline.stages.getPtr(.compute) orelse return VkError.InvalidPipelineDrv;
if (comptime base.config.soft_ir_interpreter) {
if (shader.interpreter) |*interpreter_shader|
return ir_compute.dispatch(interpreter_shader, base_group_x, base_group_y, base_group_z, group_count_x, group_count_y, group_count_z);
}
const spv_module = &shader.module.module;
self.batch_size = if (spv_module.reflection_infos.has_atomics) 1 else shader.runtimes.len;
+23
View File
@@ -10,6 +10,7 @@ const SpvRuntimeError = spv.Runtime.RuntimeError;
const Renderer = @import("Renderer.zig");
const SoftPipeline = @import("../SoftPipeline.zig");
const blitter = @import("blitter.zig");
const ir_vertex = @import("../interpreter/vertex.zig");
const VkError = base.VkError;
const interface_blob_padding = @sizeOf(F32x4);
@@ -41,6 +42,28 @@ pub fn runWrapper(data: RunData) void {
inline fn run(data: RunData) !void {
const shader = data.pipeline.stages.getPtrAssertContains(.vertex);
if (comptime base.config.soft_ir_interpreter) {
// Interpolation decorations are not represented in the common IR yet,
// so fragment-linked graphics pipelines retain the SPIR-V path.
if (data.pipeline.stages.getPtr(.fragment) == null) {
if (shader.interpreter) |*interpreter_shader| {
return ir_vertex.run(
data.allocator,
data.pipeline,
interpreter_shader,
data.batch_id,
data.batch_size,
data.vertex_count,
data.first_vertex,
data.first_instance,
data.indices,
data.primitive_restart,
data.instance_index,
data.draw_call,
);
}
}
}
const runtime = &shader.runtimes[data.batch_id];
const mutex = &runtime.mutex;
const rt = &runtime.rt;
+498
View File
@@ -0,0 +1,498 @@
const std = @import("std");
const shader_ir = @import("shader_ir");
const bc = @import("bytecode.zig");
const ir = shader_ir.ir;
const ids = ir.id;
const inst_ir = ir.instruction;
const module_ir = ir.module;
pub const CompileError = error{
InvalidConstant,
InvalidControlFlow,
InvalidInterface,
InvalidOperation,
InvalidValue,
TooManyInstructions,
TooManyRegisters,
UnsupportedOperation,
UnsupportedType,
};
pub const InterfaceBinding = struct {
direction: module_ir.InterfaceDirection,
semantic: module_ir.InterfaceSemantic,
span: bc.Span,
};
pub const RegisterInit = struct {
register: bc.Register,
value: u32,
};
const Self = @This();
arena: std.heap.ArenaAllocator,
stage: module_ir.Stage,
entry_pc: u32,
register_count: usize,
scratch_count: usize,
code: []const bc.Instruction,
edges: []const bc.Edge,
copies: []const bc.Copy,
branches: []const bc.Branch,
initializers: []const RegisterInit,
interfaces: []const ?InterfaceBinding,
pub fn compile(backing_allocator: std.mem.Allocator, module: *const module_ir.Module) !Self {
try ir.validator.validate(module);
var result: Self = undefined;
result.arena = std.heap.ArenaAllocator.init(backing_allocator);
errdefer result.arena.deinit();
var lowerer = try Lowerer.init(result.arena.allocator(), module);
try lowerer.lower();
result.stage = module.stage;
result.entry_pc = lowerer.entry_pc;
result.register_count = lowerer.register_count;
result.scratch_count = lowerer.scratch_count;
result.code = lowerer.code.items;
result.edges = lowerer.edges.items;
result.copies = lowerer.copies.items;
result.branches = lowerer.branches.items;
result.initializers = lowerer.initializers.items;
result.interfaces = lowerer.interfaces;
return result;
}
pub fn deinit(self: *Self) void {
self.arena.deinit();
self.* = undefined;
}
pub fn interfaceBinding(self: *const Self, variable: ids.InterfaceVariableId) ?InterfaceBinding {
if (variable.index() >= self.interfaces.len)
return null;
return self.interfaces[variable.index()];
}
const Lowerer = struct {
allocator: std.mem.Allocator,
module: *const module_ir.Module,
function: *const module_ir.Function,
entry_block: ids.BlockId,
values: []?bc.Span,
interfaces: []?InterfaceBinding,
block_pcs: []?u32,
register_count: usize = 0,
scratch_count: usize = 0,
entry_pc: u32 = 0,
code: std.ArrayList(bc.Instruction) = .empty,
edges: std.ArrayList(bc.Edge) = .empty,
copies: std.ArrayList(bc.Copy) = .empty,
branches: std.ArrayList(bc.Branch) = .empty,
initializers: std.ArrayList(RegisterInit) = .empty,
fn init(allocator: std.mem.Allocator, module: *const module_ir.Module) !Lowerer {
const entry_id = module.entry_point orelse return CompileError.InvalidControlFlow;
const function = module.functions.get(entry_id) orelse return CompileError.InvalidControlFlow;
const entry_block = function.entry_block orelse return CompileError.InvalidControlFlow;
if (function.parameters.items.len != 0)
return CompileError.UnsupportedOperation;
const values = try allocator.alloc(?bc.Span, module.values.entries.items.len);
@memset(values, null);
const interfaces = try allocator.alloc(?InterfaceBinding, module.interface_variables.entries.items.len);
@memset(interfaces, null);
const block_pcs = try allocator.alloc(?u32, module.blocks.entries.items.len);
@memset(block_pcs, null);
return .{
.allocator = allocator,
.module = module,
.function = function,
.entry_block = entry_block,
.values = values,
.interfaces = interfaces,
.block_pcs = block_pcs,
};
}
fn lower(self: *Lowerer) !void {
for (self.module.values.entries.items, 0..) |entry, index| {
const value = entry orelse continue;
self.values[index] = try self.allocate(value.type);
}
for (self.module.interface_variables.entries.items, 0..) |entry, index| {
const variable = entry orelse continue;
self.interfaces[index] = .{
.direction = variable.direction,
.semantic = variable.semantic,
.span = try self.allocate(variable.type),
};
}
try self.initializeConstants();
for (self.function.blocks.items) |block_id| {
const block = self.module.blocks.get(block_id) orelse return CompileError.InvalidControlFlow;
self.block_pcs[block_id.index()] = try u32Index(self.code.items.len);
for (block.instructions.items) |instruction_id| {
const instruction = self.module.instructions.get(instruction_id) orelse return CompileError.InvalidOperation;
try self.lowerInstruction(instruction);
}
try self.lowerTerminator(block.terminator orelse return CompileError.InvalidControlFlow);
}
for (self.edges.items) |*edge| {
if (edge.target_block >= self.block_pcs.len)
return CompileError.InvalidControlFlow;
edge.target_pc = self.block_pcs[edge.target_block] orelse return CompileError.InvalidControlFlow;
}
self.entry_pc = self.block_pcs[self.entry_block.index()] orelse return CompileError.InvalidControlFlow;
}
fn allocate(self: *Lowerer, type_id: ids.TypeId) !bc.Span {
const ty = self.module.types.get(type_id) orelse return CompileError.UnsupportedType;
var kind: bc.ValueKind = undefined;
var components: u8 = 1;
switch (ty.*) {
.boolean => kind = .boolean,
.integer => |integer| {
if (integer.bits != 32)
return CompileError.UnsupportedType;
kind = if (integer.signedness == .signed) .signed_integer else .unsigned_integer;
},
.floating => |floating| {
if (floating.bits != 32)
return CompileError.UnsupportedType;
kind = .floating;
},
.vector => |vector| {
const element = self.module.types.get(vector.element_type) orelse return CompileError.UnsupportedType;
components = vector.length;
kind = switch (element.*) {
.boolean => .boolean,
.integer => |integer| blk: {
if (integer.bits != 32)
return CompileError.UnsupportedType;
break :blk if (integer.signedness == .signed) .signed_integer else .unsigned_integer;
},
.floating => |floating| if (floating.bits == 32) .floating else return CompileError.UnsupportedType,
else => return CompileError.UnsupportedType,
};
},
else => return CompileError.UnsupportedType,
}
const end = std.math.add(usize, self.register_count, components) catch return CompileError.TooManyRegisters;
if (end > @as(usize, std.math.maxInt(bc.Register)) + 1)
return CompileError.TooManyRegisters;
const allocated: bc.Span = .{ .base = @intCast(self.register_count), .components = components, .kind = kind };
self.register_count = end;
return allocated;
}
fn initializeConstants(self: *Lowerer) !void {
for (self.module.values.entries.items, self.values) |entry, destination| {
const value = entry orelse continue;
if (value.definition == .constant)
try self.initializeConstant(destination orelse return CompileError.InvalidValue, value.definition.constant);
}
}
fn initializeConstant(self: *Lowerer, destination: bc.Span, constant_id: ids.ConstantId) !void {
const constant = self.module.constants.get(constant_id) orelse return CompileError.InvalidConstant;
switch (constant.value) {
.boolean => |value| {
if (destination.components != 1 or destination.kind != .boolean)
return CompileError.InvalidConstant;
try self.initializers.append(self.allocator, .{ .register = destination.base, .value = @intFromBool(value) });
},
.integer_bits, .float_bits => |value| {
if (destination.components != 1)
return CompileError.InvalidConstant;
try self.initializers.append(self.allocator, .{ .register = destination.base, .value = @truncate(value) });
},
.null, .undef => for (0..destination.components) |component|
try self.initializers.append(self.allocator, .{ .register = try offset(destination.base, component), .value = 0 }),
.composite => |elements| {
if (elements.len != destination.components)
return CompileError.InvalidConstant;
for (elements, 0..) |element, component| {
try self.initializeConstant(.{
.base = try offset(destination.base, component),
.components = 1,
.kind = destination.kind,
}, element);
}
},
}
}
fn lowerInstruction(self: *Lowerer, instruction: *const inst_ir.Instruction) !void {
const result = if (instruction.result) |id| try self.span(id) else null;
switch (instruction.operation) {
.unary => |op| {
const dst = result orelse return CompileError.InvalidOperation;
const src = try self.span(op.operand);
if (!dst.sameShape(src))
return CompileError.InvalidOperation;
const opcode: bc.Opcode = switch (op.opcode) {
.negate => switch (dst.kind) {
.signed_integer => .negate_i32,
.floating => .negate_f32,
else => return CompileError.InvalidOperation,
},
.logical_not => if (dst.kind == .boolean) .logical_not else return CompileError.InvalidOperation,
.bitwise_not => switch (dst.kind) {
.signed_integer, .unsigned_integer => .bitwise_not,
else => return CompileError.InvalidOperation,
},
};
try self.emit(opcode, dst.components, dst.base, src.base, bc.invalid_register, bc.invalid_register, 0);
},
.binary => |op| {
const dst = result orelse return CompileError.InvalidOperation;
const lhs = try self.span(op.lhs);
const rhs = try self.span(op.rhs);
if (!dst.sameShape(lhs) or !dst.sameShape(rhs))
return CompileError.InvalidOperation;
try self.emit(try binaryOpcode(op.opcode, dst.kind), dst.components, dst.base, lhs.base, rhs.base, bc.invalid_register, 0);
},
.compare => |op| {
const dst = result orelse return CompileError.InvalidOperation;
const lhs = try self.span(op.lhs);
const rhs = try self.span(op.rhs);
if (dst.kind != .boolean or dst.components != 1 or lhs.components != 1 or !lhs.sameShape(rhs))
return CompileError.UnsupportedOperation;
try self.emit(try compareOpcode(op.opcode, lhs.kind), 1, dst.base, lhs.base, rhs.base, bc.invalid_register, 0);
},
.select => |op| {
const dst = result orelse return CompileError.InvalidOperation;
const condition = try self.span(op.condition);
const yes = try self.span(op.true_value);
const no = try self.span(op.false_value);
if (condition.kind != .boolean or condition.components != 1 or !dst.sameShape(yes) or !dst.sameShape(no))
return CompileError.InvalidOperation;
try self.emit(.select, dst.components, dst.base, condition.base, yes.base, no.base, 0);
},
.bitcast => |id| {
const dst = result orelse return CompileError.InvalidOperation;
const src = try self.span(id);
if (dst.components != src.components)
return CompileError.UnsupportedOperation;
try self.emitCopy(dst, src);
},
.composite_construct => |op| {
const dst = result orelse return CompileError.InvalidOperation;
if (dst.components != op.elements.len)
return CompileError.UnsupportedOperation;
for (op.elements, 0..) |id, component| {
const src = try self.span(id);
if (src.components != 1)
return CompileError.UnsupportedOperation;
try self.emit(.copy, 1, try offset(dst.base, component), src.base, bc.invalid_register, bc.invalid_register, 0);
}
},
.composite_extract => |op| {
const dst = result orelse return CompileError.InvalidOperation;
const src = try self.span(op.composite);
if (dst.components != 1 or op.indices.len != 1 or op.indices[0] >= src.components)
return CompileError.UnsupportedOperation;
try self.emit(.copy, 1, dst.base, try offset(src.base, op.indices[0]), bc.invalid_register, bc.invalid_register, 0);
},
.load_interface => |op| {
if (op.element_index != null)
return CompileError.UnsupportedOperation;
const dst = result orelse return CompileError.InvalidOperation;
const binding = self.interfaceFor(op.variable) orelse return CompileError.InvalidInterface;
if (binding.direction != .input or !dst.sameShape(binding.span))
return CompileError.InvalidInterface;
try self.emitCopy(dst, binding.span);
},
.store_interface => |op| {
if (op.element_index != null)
return CompileError.UnsupportedOperation;
const binding = self.interfaceFor(op.variable) orelse return CompileError.InvalidInterface;
const src = try self.span(op.value);
if (binding.direction != .output or !binding.span.sameShape(src))
return CompileError.InvalidInterface;
try self.emitCopy(binding.span, src);
},
.call => return CompileError.UnsupportedOperation,
}
}
fn lowerTerminator(self: *Lowerer, terminator: module_ir.Terminator) !void {
switch (terminator) {
.branch => |edge| try self.emit(.jump_edge, 1, bc.invalid_register, bc.invalid_register, bc.invalid_register, bc.invalid_register, try self.addEdge(edge)),
.conditional_branch => |branch| {
const condition = try self.span(branch.condition);
if (condition.kind != .boolean or condition.components != 1) return CompileError.InvalidControlFlow;
const index = try u32Index(self.branches.items.len);
try self.branches.append(self.allocator, .{
.true_edge = try self.addEdge(branch.true_edge),
.false_edge = try self.addEdge(branch.false_edge),
});
try self.emit(.branch, 1, condition.base, bc.invalid_register, bc.invalid_register, bc.invalid_register, index);
},
.return_void => try self.emit(.return_void, 1, bc.invalid_register, bc.invalid_register, bc.invalid_register, bc.invalid_register, 0),
.return_value => return CompileError.UnsupportedOperation,
.discard => try self.emit(.discard, 1, bc.invalid_register, bc.invalid_register, bc.invalid_register, bc.invalid_register, 0),
.@"unreachable" => try self.emit(.@"unreachable", 1, bc.invalid_register, bc.invalid_register, bc.invalid_register, bc.invalid_register, 0),
}
}
fn addEdge(self: *Lowerer, edge: module_ir.Edge) !u32 {
const target = self.module.blocks.get(edge.target) orelse return CompileError.InvalidControlFlow;
if (target.parameters.items.len != edge.arguments.len)
return CompileError.InvalidControlFlow;
const first = try u32Index(self.copies.items.len);
var scratch: usize = 0;
for (edge.arguments, target.parameters.items) |source_id, destination_id| {
const source = try self.span(source_id);
const destination = try self.span(destination_id);
if (!source.sameShape(destination)) return CompileError.InvalidControlFlow;
if (source.base == destination.base) continue;
try self.copies.append(self.allocator, .{
.destination = destination.base,
.source = source.base,
.components = source.components,
.scratch_base = @intCast(scratch),
});
scratch += source.components;
}
if (scratch > std.math.maxInt(bc.Register))
return CompileError.TooManyRegisters;
self.scratch_count = @max(self.scratch_count, scratch);
const copy_count = self.copies.items.len - first;
if (copy_count > std.math.maxInt(u16))
return CompileError.TooManyInstructions;
const index = try u32Index(self.edges.items.len);
try self.edges.append(self.allocator, .{
.target_block = @intFromEnum(edge.target),
.first_copy = first,
.copy_count = @intCast(copy_count),
});
return index;
}
fn emitCopy(self: *Lowerer, dst: bc.Span, src: bc.Span) !void {
if (dst.components != src.components)
return CompileError.InvalidOperation;
try self.emit(.copy, dst.components, dst.base, src.base, bc.invalid_register, bc.invalid_register, 0);
}
fn emit(self: *Lowerer, opcode: bc.Opcode, components: u16, a: bc.Register, b: bc.Register, c: bc.Register, d: bc.Register, immediate: u32) !void {
if (self.code.items.len >= std.math.maxInt(u32))
return CompileError.TooManyInstructions;
try self.code.append(self.allocator, .{ .opcode = opcode, .components = components, .a = a, .b = b, .c = c, .d = d, .immediate = immediate });
}
fn span(self: *const Lowerer, id: ids.ValueId) !bc.Span {
if (id.index() >= self.values.len)
return CompileError.InvalidValue;
return self.values[id.index()] orelse CompileError.InvalidValue;
}
fn interfaceFor(self: *const Lowerer, id: ids.InterfaceVariableId) ?InterfaceBinding {
if (id.index() >= self.interfaces.len)
return null;
return self.interfaces[id.index()];
}
};
fn binaryOpcode(op: inst_ir.BinaryOpcode, kind: bc.ValueKind) !bc.Opcode {
return switch (op) {
.integer_add => if (kind == .signed_integer or kind == .unsigned_integer) .integer_add else CompileError.InvalidOperation,
.integer_subtract => if (kind == .signed_integer or kind == .unsigned_integer) .integer_subtract else CompileError.InvalidOperation,
.integer_multiply => if (kind == .signed_integer or kind == .unsigned_integer) .integer_multiply else CompileError.InvalidOperation,
.unsigned_divide => if (kind == .unsigned_integer) .unsigned_divide else CompileError.InvalidOperation,
.signed_divide => if (kind == .signed_integer) .signed_divide else CompileError.InvalidOperation,
.unsigned_modulo => if (kind == .unsigned_integer) .unsigned_modulo else CompileError.InvalidOperation,
.signed_modulo => if (kind == .signed_integer) .signed_modulo else CompileError.InvalidOperation,
.float_add => if (kind == .floating) .float_add else CompileError.InvalidOperation,
.float_subtract => if (kind == .floating) .float_subtract else CompileError.InvalidOperation,
.float_multiply => if (kind == .floating) .float_multiply else CompileError.InvalidOperation,
.float_divide => if (kind == .floating) .float_divide else CompileError.InvalidOperation,
.float_modulo => if (kind == .floating) .float_modulo else CompileError.InvalidOperation,
.shift_left => if (kind == .signed_integer or kind == .unsigned_integer) .shift_left else CompileError.InvalidOperation,
.logical_shift_right => if (kind == .signed_integer or kind == .unsigned_integer) .logical_shift_right else CompileError.InvalidOperation,
.arithmetic_shift_right => if (kind == .signed_integer) .arithmetic_shift_right else CompileError.InvalidOperation,
.bitwise_and => if (kind == .signed_integer or kind == .unsigned_integer) .bitwise_and else CompileError.InvalidOperation,
.bitwise_or => if (kind == .signed_integer or kind == .unsigned_integer) .bitwise_or else CompileError.InvalidOperation,
.bitwise_xor => if (kind == .signed_integer or kind == .unsigned_integer) .bitwise_xor else CompileError.InvalidOperation,
.logical_and => if (kind == .boolean) .logical_and else CompileError.InvalidOperation,
.logical_or => if (kind == .boolean) .logical_or else CompileError.InvalidOperation,
};
}
fn compareOpcode(op: inst_ir.CompareOpcode, kind: bc.ValueKind) !bc.Opcode {
return switch (op) {
.equal => if (kind == .signed_integer or kind == .unsigned_integer or kind == .boolean) .compare_equal else CompileError.InvalidOperation,
.not_equal => if (kind == .signed_integer or kind == .unsigned_integer or kind == .boolean) .compare_not_equal else CompileError.InvalidOperation,
.unsigned_less => if (kind == .unsigned_integer) .compare_unsigned_less else CompileError.InvalidOperation,
.signed_less => if (kind == .signed_integer) .compare_signed_less else CompileError.InvalidOperation,
.ordered_float_equal => if (kind == .floating) .compare_ordered_float_equal else CompileError.InvalidOperation,
.unordered_float_equal => if (kind == .floating) .compare_unordered_float_equal else CompileError.InvalidOperation,
.ordered_float_not_equal => if (kind == .floating) .compare_ordered_float_not_equal else CompileError.InvalidOperation,
.unordered_float_not_equal => if (kind == .floating) .compare_unordered_float_not_equal else CompileError.InvalidOperation,
.ordered_float_less => if (kind == .floating) .compare_ordered_float_less else CompileError.InvalidOperation,
.unordered_float_less => if (kind == .floating) .compare_unordered_float_less else CompileError.InvalidOperation,
};
}
fn offset(base: bc.Register, component: usize) !bc.Register {
const value = @as(usize, base) + component;
if (value > std.math.maxInt(bc.Register))
return CompileError.TooManyRegisters;
return @intCast(value);
}
fn u32Index(value: usize) !u32 {
if (value > std.math.maxInt(u32))
return CompileError.TooManyInstructions;
return @intCast(value);
}
+307
View File
@@ -0,0 +1,307 @@
const std = @import("std");
const shader_ir = @import("shader_ir");
const bc = @import("bytecode.zig");
const Program = @import("Program.zig");
const ids = shader_ir.ir.id;
pub const RuntimeError = error{
DivisionByZero,
IntegerOverflow,
InvalidBytecode,
InvalidInterface,
ShiftOutOfRange,
StepLimitExceeded,
UnreachableExecuted,
WrongInterfaceDirection,
WrongInterfaceType,
};
pub const Outcome = enum {
returned,
discarded,
};
pub const RunOptions = struct {
max_steps: usize = 1_000_000,
};
const Self = @This();
allocator: std.mem.Allocator,
registers: []u32,
scratch: []u32,
pub fn init(allocator: std.mem.Allocator, program: *const Program) !Self {
const registers = try allocator.alloc(u32, program.register_count);
errdefer allocator.free(registers);
const scratch = try allocator.alloc(u32, program.scratch_count);
errdefer allocator.free(scratch);
@memset(registers, 0);
@memset(scratch, 0);
for (program.initializers) |initializer|
registers[initializer.register] = initializer.value;
return .{ .allocator = allocator, .registers = registers, .scratch = scratch };
}
pub fn deinit(self: *Self) void {
self.allocator.free(self.registers);
self.allocator.free(self.scratch);
self.* = undefined;
}
pub fn writeInput(self: *Self, program: *const Program, variable: ids.InterfaceVariableId, values: []const u32) RuntimeError!void {
const binding = program.interfaceBinding(variable) orelse return RuntimeError.InvalidInterface;
if (binding.direction != .input)
return RuntimeError.WrongInterfaceDirection;
if (binding.span.components != values.len)
return RuntimeError.WrongInterfaceType;
@memcpy(self.registers[binding.span.base..][0..values.len], values);
}
pub fn readOutput(self: *const Self, program: *const Program, variable: ids.InterfaceVariableId, values: []u32) RuntimeError!void {
const binding = program.interfaceBinding(variable) orelse return RuntimeError.InvalidInterface;
if (binding.direction != .output)
return RuntimeError.WrongInterfaceDirection;
if (binding.span.components != values.len)
return RuntimeError.WrongInterfaceType;
@memcpy(values, self.registers[binding.span.base..][0..values.len]);
}
pub fn run(self: *Self, program: *const Program, options: RunOptions) RuntimeError!Outcome {
var pc = program.entry_pc;
var steps: usize = 0;
while (true) {
if (steps >= options.max_steps)
return RuntimeError.StepLimitExceeded;
steps += 1;
if (pc >= program.code.len)
return RuntimeError.InvalidBytecode;
const instruction = program.code[pc];
pc += 1;
switch (instruction.opcode) {
.copy => self.copy(instruction),
.negate_i32 => self.unaryInt(instruction, .negate),
.negate_f32 => self.unaryFloat(instruction),
.logical_not => self.unaryInt(instruction, .logical_not),
.bitwise_not => self.unaryInt(instruction, .bitwise_not),
.integer_add => try self.binaryInt(instruction, .add),
.integer_subtract => try self.binaryInt(instruction, .subtract),
.integer_multiply => try self.binaryInt(instruction, .multiply),
.unsigned_divide => try self.binaryInt(instruction, .unsigned_divide),
.signed_divide => try self.binaryInt(instruction, .signed_divide),
.unsigned_modulo => try self.binaryInt(instruction, .unsigned_modulo),
.signed_modulo => try self.binaryInt(instruction, .signed_modulo),
.shift_left => try self.binaryInt(instruction, .shift_left),
.logical_shift_right => try self.binaryInt(instruction, .logical_shift_right),
.arithmetic_shift_right => try self.binaryInt(instruction, .arithmetic_shift_right),
.bitwise_and => try self.binaryInt(instruction, .bitwise_and),
.bitwise_or => try self.binaryInt(instruction, .bitwise_or),
.bitwise_xor => try self.binaryInt(instruction, .bitwise_xor),
.logical_and => try self.binaryInt(instruction, .logical_and),
.logical_or => try self.binaryInt(instruction, .logical_or),
.float_add => self.binaryFloat(instruction, .add),
.float_subtract => self.binaryFloat(instruction, .subtract),
.float_multiply => self.binaryFloat(instruction, .multiply),
.float_divide => self.binaryFloat(instruction, .divide),
.float_modulo => self.binaryFloat(instruction, .modulo),
.compare_equal => self.compareInt(instruction, .equal),
.compare_not_equal => self.compareInt(instruction, .not_equal),
.compare_unsigned_less => self.compareInt(instruction, .unsigned_less),
.compare_signed_less => self.compareInt(instruction, .signed_less),
.compare_ordered_float_equal => self.compareFloat(instruction, .ordered_equal),
.compare_unordered_float_equal => self.compareFloat(instruction, .unordered_equal),
.compare_ordered_float_not_equal => self.compareFloat(instruction, .ordered_not_equal),
.compare_unordered_float_not_equal => self.compareFloat(instruction, .unordered_not_equal),
.compare_ordered_float_less => self.compareFloat(instruction, .ordered_less),
.compare_unordered_float_less => self.compareFloat(instruction, .unordered_less),
.select => self.select(instruction),
.jump_edge => pc = try self.applyEdge(program, instruction.immediate),
.branch => {
if (instruction.immediate >= program.branches.len)
return RuntimeError.InvalidBytecode;
const branch = program.branches[instruction.immediate];
pc = try self.applyEdge(program, if (self.registers[instruction.a] != 0) branch.true_edge else branch.false_edge);
},
.return_void => return .returned,
.discard => return .discarded,
.@"unreachable" => return RuntimeError.UnreachableExecuted,
}
}
}
const UnaryInt = enum { negate, logical_not, bitwise_not };
const BinaryInt = enum {
add,
subtract,
multiply,
unsigned_divide,
signed_divide,
unsigned_modulo,
signed_modulo,
shift_left,
logical_shift_right,
arithmetic_shift_right,
bitwise_and,
bitwise_or,
bitwise_xor,
logical_and,
logical_or,
};
const BinaryFloat = enum { add, subtract, multiply, divide, modulo };
const CompareInt = enum { equal, not_equal, unsigned_less, signed_less };
const CompareFloat = enum { ordered_equal, unordered_equal, ordered_not_equal, unordered_not_equal, ordered_less, unordered_less };
fn copy(self: *Self, instruction: bc.Instruction) void {
for (0..instruction.components) |component|
self.registers[@as(usize, instruction.a) + component] = self.registers[@as(usize, instruction.b) + component];
}
fn unaryInt(self: *Self, instruction: bc.Instruction, comptime operation: UnaryInt) void {
for (0..instruction.components) |component| {
const value = self.registers[@as(usize, instruction.b) + component];
self.registers[@as(usize, instruction.a) + component] = switch (operation) {
.negate => 0 -% value,
.logical_not => @intFromBool(value == 0),
.bitwise_not => ~value,
};
}
}
fn unaryFloat(self: *Self, instruction: bc.Instruction) void {
for (0..instruction.components) |component| {
const value: f32 = @bitCast(self.registers[@as(usize, instruction.b) + component]);
self.registers[@as(usize, instruction.a) + component] = @bitCast(-value);
}
}
fn binaryInt(self: *Self, instruction: bc.Instruction, comptime operation: BinaryInt) RuntimeError!void {
for (0..instruction.components) |component| {
const lhs = self.registers[@as(usize, instruction.b) + component];
const rhs = self.registers[@as(usize, instruction.c) + component];
self.registers[@as(usize, instruction.a) + component] = switch (operation) {
.add => lhs +% rhs,
.subtract => lhs -% rhs,
.multiply => lhs *% rhs,
.unsigned_divide => if (rhs == 0) return RuntimeError.DivisionByZero else lhs / rhs,
.unsigned_modulo => if (rhs == 0) return RuntimeError.DivisionByZero else lhs % rhs,
.signed_divide => blk: {
const signed_lhs: i32 = @bitCast(lhs);
const signed_rhs: i32 = @bitCast(rhs);
if (signed_rhs == 0)
return RuntimeError.DivisionByZero;
if (signed_lhs == std.math.minInt(i32) and signed_rhs == -1)
return RuntimeError.IntegerOverflow;
break :blk @bitCast(@divTrunc(signed_lhs, signed_rhs));
},
.signed_modulo => blk: {
const signed_lhs: i32 = @bitCast(lhs);
const signed_rhs: i32 = @bitCast(rhs);
if (signed_rhs == 0)
return RuntimeError.DivisionByZero;
if (signed_lhs == std.math.minInt(i32) and signed_rhs == -1)
break :blk 0;
break :blk @bitCast(@mod(signed_lhs, signed_rhs));
},
.shift_left => if (rhs >= 32) return RuntimeError.ShiftOutOfRange else lhs << @intCast(rhs),
.logical_shift_right => if (rhs >= 32) return RuntimeError.ShiftOutOfRange else lhs >> @intCast(rhs),
.arithmetic_shift_right => if (rhs >= 32) return RuntimeError.ShiftOutOfRange else @bitCast(@as(i32, @bitCast(lhs)) >> @intCast(rhs)),
.bitwise_and => lhs & rhs,
.bitwise_or => lhs | rhs,
.bitwise_xor => lhs ^ rhs,
.logical_and => @intFromBool(lhs != 0 and rhs != 0),
.logical_or => @intFromBool(lhs != 0 or rhs != 0),
};
}
}
fn binaryFloat(self: *Self, instruction: bc.Instruction, comptime operation: BinaryFloat) void {
for (0..instruction.components) |component| {
const lhs: f32 = @bitCast(self.registers[@as(usize, instruction.b) + component]);
const rhs: f32 = @bitCast(self.registers[@as(usize, instruction.c) + component]);
const result = switch (operation) {
.add => lhs + rhs,
.subtract => lhs - rhs,
.multiply => lhs * rhs,
.divide => lhs / rhs,
.modulo => lhs - rhs * @floor(lhs / rhs),
};
self.registers[@as(usize, instruction.a) + component] = @bitCast(result);
}
}
fn compareInt(self: *Self, instruction: bc.Instruction, comptime operation: CompareInt) void {
const lhs = self.registers[instruction.b];
const rhs = self.registers[instruction.c];
self.registers[instruction.a] = @intFromBool(switch (operation) {
.equal => lhs == rhs,
.not_equal => lhs != rhs,
.unsigned_less => lhs < rhs,
.signed_less => @as(i32, @bitCast(lhs)) < @as(i32, @bitCast(rhs)),
});
}
fn compareFloat(self: *Self, instruction: bc.Instruction, comptime operation: CompareFloat) void {
const lhs: f32 = @bitCast(self.registers[instruction.b]);
const rhs: f32 = @bitCast(self.registers[instruction.c]);
const unordered = std.math.isNan(lhs) or std.math.isNan(rhs);
self.registers[instruction.a] = @intFromBool(switch (operation) {
.ordered_equal => !unordered and lhs == rhs,
.unordered_equal => unordered or lhs == rhs,
.ordered_not_equal => !unordered and lhs != rhs,
.unordered_not_equal => unordered or lhs != rhs,
.ordered_less => !unordered and lhs < rhs,
.unordered_less => unordered or lhs < rhs,
});
}
fn select(self: *Self, instruction: bc.Instruction) void {
const selected = if (self.registers[instruction.b] != 0) instruction.c else instruction.d;
for (0..instruction.components) |component|
self.registers[@as(usize, instruction.a) + component] = self.registers[@as(usize, selected) + component];
}
fn applyEdge(self: *Self, program: *const Program, edge_index: u32) RuntimeError!u32 {
if (edge_index >= program.edges.len)
return RuntimeError.InvalidBytecode;
const edge = program.edges[edge_index];
const end = @as(usize, edge.first_copy) + edge.copy_count;
if (end > program.copies.len)
return RuntimeError.InvalidBytecode;
const copies = program.copies[edge.first_copy..end];
for (copies) |item| {
for (0..item.components) |component| {
self.scratch[@as(usize, item.scratch_base) + component] = self.registers[@as(usize, item.source) + component];
}
}
for (copies) |item| {
for (0..item.components) |component| {
self.registers[@as(usize, item.destination) + component] = self.scratch[@as(usize, item.scratch_base) + component];
}
}
if (edge.target_pc >= program.code.len)
return RuntimeError.InvalidBytecode;
return edge.target_pc;
}
+162
View File
@@ -0,0 +1,162 @@
const std = @import("std");
const vk = @import("vulkan");
const base = @import("base");
const shader_ir = @import("shader_ir");
const Program = @import("Program.zig");
const Runtime = @import("Runtime.zig");
const SoftShaderModule = @import("../SoftShaderModule.zig");
const VkError = base.VkError;
const ir = shader_ir.ir;
pub const RuntimeSlot = struct {
mutex: std.Io.Mutex = .init,
runtime: Runtime,
};
const Self = @This();
program: Program,
runtimes: []RuntimeSlot,
workgroup_size: ?[3]u32,
/// Compiles a stage when the current interpreter can execute its complete
/// interface. `null` deliberately selects the existing SPIR-V runtime.
pub fn compile(
allocator: std.mem.Allocator,
module: *SoftShaderModule,
stage: *const vk.PipelineShaderStageCreateInfo,
runtime_count: usize,
) VkError!?Self {
const expected_stage = commonStage(stage.stage) orelse return null;
if (expected_stage == .fragment)
return null;
const specializations = try specializationValues(allocator, stage.p_specialization_info);
defer if (specializations.len != 0) allocator.free(specializations);
var module_ir = module.interface.instantiateIr(allocator, .{
.entry_point = std.mem.span(stage.p_name),
.stage = expected_stage,
.specializations = specializations,
}) catch |err| {
if (err == error.OutOfMemory)
return VkError.OutOfDeviceMemory;
std.log.scoped(.SoftIrInterpreter).debug("IR translation fallback: {s}", .{@errorName(err)});
return null;
};
defer module_ir.deinit();
var program = Program.compile(allocator, &module_ir) catch |err| {
if (err == error.OutOfMemory)
return VkError.OutOfDeviceMemory;
std.log.scoped(.SoftIrInterpreter).debug("bytecode lowering fallback: {s}", .{@errorName(err)});
return null;
};
errdefer program.deinit();
if (!hasCompatibleInterface(&program, expected_stage) or
(expected_stage == .compute and module_ir.execution_modes.workgroup_size == null))
{
std.log.scoped(.SoftIrInterpreter).debug("stage interface or execution modes require the SPIR-V runtime", .{});
program.deinit();
return null;
}
const runtimes = allocator.alloc(RuntimeSlot, runtime_count) catch return VkError.OutOfDeviceMemory;
var initialized: usize = 0;
errdefer {
for (runtimes[0..initialized]) |*slot|
slot.runtime.deinit();
allocator.free(runtimes);
}
for (runtimes) |*slot| {
slot.* = .{ .runtime = Runtime.init(allocator, &program) catch return VkError.OutOfDeviceMemory };
initialized += 1;
}
std.log.scoped(.SoftIrInterpreter).debug("compiled {s} stage to {d} bytecode instructions", .{
@tagName(expected_stage),
program.code.len,
});
return .{
.program = program,
.runtimes = runtimes,
.workgroup_size = module_ir.execution_modes.workgroup_size,
};
}
pub fn deinit(self: *Self) void {
for (self.runtimes) |*slot|
slot.runtime.deinit();
self.program.deinit();
self.* = undefined;
}
fn hasCompatibleInterface(program: *const Program, stage: ir.module.Stage) bool {
var has_position = false;
for (program.interfaces) |optional_binding| {
const binding = optional_binding orelse continue;
switch (binding.semantic) {
.location => |location| {
if (stage == .compute or location.index != 0 or
@as(u16, location.component) + binding.span.components > 4)
return false;
},
.builtin => |builtin| switch (stage) {
.vertex => switch (builtin) {
.vertex_index, .instance_index => if (binding.direction != .input) return false,
.position => {
if (binding.direction != .output or binding.span.kind != .floating or binding.span.components != 4)
return false;
has_position = true;
},
else => return false,
},
.compute => if (builtin != .global_invocation_id or binding.direction != .input or binding.span.components != 3)
return false,
.fragment => return false,
},
}
}
return stage != .vertex or has_position;
}
fn specializationValues(allocator: std.mem.Allocator, info: ?*const vk.SpecializationInfo) VkError![]shader_ir.spirv.translator.SpecializationValue {
const specialization = info orelse return &.{};
if (specialization.map_entry_count == 0)
return &.{};
const entries = specialization.p_map_entries orelse return VkError.ValidationFailed;
const data: []const u8 = if (specialization.data_size == 0)
&.{}
else
@as([*]const u8, @ptrCast(@alignCast(specialization.p_data)))[0..specialization.data_size];
const values = allocator.alloc(shader_ir.spirv.translator.SpecializationValue, specialization.map_entry_count) catch
return VkError.OutOfDeviceMemory;
errdefer allocator.free(values);
for (entries[0..specialization.map_entry_count], values) |entry, *value| {
const offset: usize = entry.offset;
const end = std.math.add(usize, offset, entry.size) catch return VkError.ValidationFailed;
if (end > data.len)
return VkError.ValidationFailed;
value.* = .{ .constant_id = entry.constant_id, .data = data[offset..end] };
}
return values;
}
fn commonStage(stage: vk.ShaderStageFlags) ?ir.module.Stage {
const bits: u32 = @bitCast(stage);
const vertex_bits: u32 = @bitCast(vk.ShaderStageFlags{ .vertex_bit = true });
const fragment_bits: u32 = @bitCast(vk.ShaderStageFlags{ .fragment_bit = true });
const compute_bits: u32 = @bitCast(vk.ShaderStageFlags{ .compute_bit = true });
return if (bits == vertex_bits)
.vertex
else if (bits == fragment_bits)
.fragment
else if (bits == compute_bits)
.compute
else
null;
}
+99
View File
@@ -0,0 +1,99 @@
const std = @import("std");
pub const Register = u16;
pub const invalid_register = std.math.maxInt(Register);
pub const ValueKind = enum(u8) {
boolean,
signed_integer,
unsigned_integer,
floating,
};
pub const Span = struct {
base: Register,
components: u8,
kind: ValueKind,
pub fn sameShape(a: Span, b: Span) bool {
return a.components == b.components and a.kind == b.kind;
}
};
/// Native-endian internal bytecode. It is not a serialized or stable ABI.
pub const Instruction = extern struct {
opcode: Opcode,
components: u16 = 1,
a: Register = invalid_register,
b: Register = invalid_register,
c: Register = invalid_register,
d: Register = invalid_register,
immediate: u32 = 0,
};
comptime {
std.debug.assert(@sizeOf(Instruction) == 16);
}
pub const Opcode = enum(u16) {
copy,
negate_i32,
negate_f32,
logical_not,
bitwise_not,
integer_add,
integer_subtract,
integer_multiply,
unsigned_divide,
signed_divide,
unsigned_modulo,
signed_modulo,
float_add,
float_subtract,
float_multiply,
float_divide,
float_modulo,
shift_left,
logical_shift_right,
arithmetic_shift_right,
bitwise_and,
bitwise_or,
bitwise_xor,
logical_and,
logical_or,
compare_equal,
compare_not_equal,
compare_unsigned_less,
compare_signed_less,
compare_ordered_float_equal,
compare_unordered_float_equal,
compare_ordered_float_not_equal,
compare_unordered_float_not_equal,
compare_ordered_float_less,
compare_unordered_float_less,
select,
jump_edge,
branch,
return_void,
discard,
@"unreachable",
};
pub const Copy = struct {
destination: Register,
source: Register,
components: u8,
scratch_base: Register,
};
pub const Edge = struct {
target_block: u32,
target_pc: u32 = 0,
first_copy: u32,
copy_count: u16,
};
pub const Branch = struct {
true_edge: u32,
false_edge: u32,
};
+48
View File
@@ -0,0 +1,48 @@
const std = @import("std");
const base = @import("base");
const shader_ir = @import("shader_ir");
const Shader = @import("Shader.zig");
const VkError = base.VkError;
const ir = shader_ir.ir;
pub fn dispatch(shader: *Shader, base_group_x: u32, base_group_y: u32, base_group_z: u32, group_count_x: u32, group_count_y: u32, group_count_z: u32) VkError!void {
const local_size = shader.workgroup_size orelse return VkError.ValidationFailed;
const local_xy = std.math.mul(usize, local_size[0], local_size[1]) catch return VkError.ValidationFailed;
const local_count = std.math.mul(usize, local_xy, local_size[2]) catch return VkError.ValidationFailed;
if (shader.runtimes.len == 0)
return VkError.InvalidPipelineDrv;
const global_id = findGlobalInvocationId(&shader.program);
var runtime = &shader.runtimes[0].runtime;
for (0..group_count_z) |group_z| {
for (0..group_count_y) |group_y| {
for (0..group_count_x) |group_x| {
for (0..local_count) |local_index| {
if (global_id) |variable| {
const local_z = local_index / local_xy;
const local_remainder = local_index - local_z * local_xy;
const local_y = local_remainder / local_size[0];
const local_x = local_remainder - local_y * local_size[0];
runtime.writeInput(&shader.program, variable, &.{
(base_group_x + @as(u32, @intCast(group_x))) * local_size[0] + @as(u32, @intCast(local_x)),
(base_group_y + @as(u32, @intCast(group_y))) * local_size[1] + @as(u32, @intCast(local_y)),
(base_group_z + @as(u32, @intCast(group_z))) * local_size[2] + @as(u32, @intCast(local_z)),
}) catch return VkError.Unknown;
}
_ = runtime.run(&shader.program, .{}) catch return VkError.Unknown;
}
}
}
}
}
fn findGlobalInvocationId(program: *const @import("Program.zig")) ?ir.id.InterfaceVariableId {
for (program.interfaces, 0..) |optional_binding, index| {
const binding = optional_binding orelse continue;
if (binding.direction == .input and binding.semantic == .builtin and binding.semantic.builtin == .global_invocation_id)
return ir.id.InterfaceVariableId.fromIndex(index);
}
return null;
}
+16
View File
@@ -0,0 +1,16 @@
//! Software bytecode interpreter for the backend-agnostic shader IR.
//!
//! This first slice supports allocation-free scalar execution of 32-bit scalar
//! and vector arithmetic, interface I/O, control flow, and block parameters.
pub const bytecode = @import("bytecode.zig");
pub const Program = @import("Program.zig");
pub const Runtime = @import("Runtime.zig");
pub const Outcome = Runtime.Outcome;
pub const RunOptions = Runtime.RunOptions;
pub const RuntimeError = Runtime.RuntimeError;
comptime {
_ = @import("test/test.zig");
}
@@ -0,0 +1,56 @@
const std = @import("std");
const shader_ir = @import("shader_ir");
const Program = @import("../Program.zig");
const Runtime = @import("../Runtime.zig");
const ir = shader_ir.ir;
fn f32Bits(value: f32) u32 {
return @bitCast(value);
}
fn bitsF32(value: u32) f32 {
return @bitCast(value);
}
test "[interpreter] vector floating-point arithmetic" {
var module = try ir.parser.parseString(std.testing.allocator,
\\ shader vertex @main
\\ {
\\ @lhs: vec4[f32] = input[location(0), component(0), index(0)]
\\ @rhs: vec4[f32] = input[location(1), component(0), index(0)]
\\ @output: vec4[f32] = output[location(0), component(0), index(0)]
\\
\\ fn @main() -> void
\\ {
\\ .entry():
\\ %lhs_value: vec4[f32] = load_interface @lhs
\\ %rhs_value: vec4[f32] = load_interface @rhs
\\ %product: vec4[f32] = float_multiply %lhs_value, %rhs_value
\\ store_interface @output, %product
\\ return
\\ }
\\ }
);
defer module.deinit();
const lhs = ir.id.InterfaceVariableId.fromIndex(0);
const rhs = ir.id.InterfaceVariableId.fromIndex(1);
const output = ir.id.InterfaceVariableId.fromIndex(2);
var program = try Program.compile(std.testing.allocator, &module);
defer program.deinit();
var runtime = try Runtime.init(std.testing.allocator, &program);
defer runtime.deinit();
try runtime.writeInput(&program, lhs, &.{ f32Bits(2), f32Bits(-3), f32Bits(0.5), f32Bits(8) });
try runtime.writeInput(&program, rhs, &.{ f32Bits(4), f32Bits(2), f32Bits(6), f32Bits(0.25) });
try std.testing.expectEqual(Runtime.Outcome.returned, try runtime.run(&program, .{}));
var result: [4]u32 = undefined;
try runtime.readOutput(&program, output, &result);
const expected = [_]f32{ 8, -6, 3, 2 };
for (result, expected) |actual, wanted|
try std.testing.expectEqual(wanted, bitsF32(actual));
}
@@ -0,0 +1,65 @@
const std = @import("std");
const shader_ir = @import("shader_ir");
const Program = @import("../Program.zig");
const Runtime = @import("../Runtime.zig");
const ir = shader_ir.ir;
fn i32Bits(value: i32) u32 {
return @bitCast(value);
}
test "[interpreter] branches with block parameters" {
var module = try ir.parser.parseString(std.testing.allocator,
\\ shader vertex @main
\\ {
\\ @input: i32 = input[location(0), component(0), index(0)]
\\ @output: i32 = output[location(0), component(0), index(0)]
\\
\\ %one: constant i32 = 1
\\ %ten: constant i32 = 10
\\
\\ fn @main() -> void
\\ {
\\ .entry():
\\ %value: i32 = load_interface @input
\\ %condition: bool = cmp_signed_less %value, %ten
\\ conditional_branch %condition, .less(), .greater_equal()
\\
\\ .less():
\\ %incremented: i32 = integer_add %value, %one
\\ branch .merge(%incremented)
\\
\\ .greater_equal():
\\ %decremented: i32 = integer_subtract %value, %one
\\ branch .merge(%decremented)
\\
\\ .merge(%merged: i32):
\\ store_interface @output, %merged
\\ return
\\ }
\\ }
);
const input = ir.id.InterfaceVariableId.fromIndex(0);
const output = ir.id.InterfaceVariableId.fromIndex(1);
var program = try Program.compile(std.testing.allocator, &module);
defer program.deinit();
module.deinit();
var runtime = try Runtime.init(std.testing.allocator, &program);
defer runtime.deinit();
try runtime.writeInput(&program, input, &.{i32Bits(5)});
try std.testing.expectEqual(Runtime.Outcome.returned, try runtime.run(&program, .{}));
var result: [1]u32 = undefined;
try runtime.readOutput(&program, output, &result);
try std.testing.expectEqual(@as(i32, 6), @as(i32, @bitCast(result[0])));
try runtime.writeInput(&program, input, &.{i32Bits(20)});
try std.testing.expectEqual(Runtime.Outcome.returned, try runtime.run(&program, .{}));
try runtime.readOutput(&program, output, &result);
try std.testing.expectEqual(@as(i32, 19), @as(i32, @bitCast(result[0])));
}
+58
View File
@@ -0,0 +1,58 @@
const std = @import("std");
const shader_ir = @import("shader_ir");
const Program = @import("../Program.zig");
const Runtime = @import("../Runtime.zig");
const ir = shader_ir.ir;
test "[interpreter] loop back edges copy block arguments in parallel" {
var module = try ir.parser.parseString(std.testing.allocator,
\\ shader compute @main
\\ {
\\ @output_a: u32 = output[location(0), component(0), index(0)]
\\ @output_b: u32 = output[location(1), component(0), index(0)]
\\
\\ %zero: constant u32 = 0
\\ %one: constant u32 = 1
\\ %two: constant u32 = 2
\\ %three: constant u32 = 3
\\
\\ fn @main() -> void
\\ {
\\ .entry():
\\ branch .loop(%one, %two, %zero)
\\
\\ .loop(%a: u32, %b: u32, %index: u32):
\\ %condition: bool = cmp_unsigned_less %index, %three
\\ conditional_branch %condition, .body(), .exit(%a, %b)
\\
\\ .body():
\\ %next_index: u32 = integer_add %index, %one
\\ branch .loop(%b, %a, %next_index)
\\
\\ .exit(%final_a: u32, %final_b: u32):
\\ store_interface @output_a, %final_a
\\ store_interface @output_b, %final_b
\\ return
\\ }
\\ }
);
defer module.deinit();
const output_a = ir.id.InterfaceVariableId.fromIndex(0);
const output_b = ir.id.InterfaceVariableId.fromIndex(1);
var program = try Program.compile(std.testing.allocator, &module);
defer program.deinit();
var runtime = try Runtime.init(std.testing.allocator, &program);
defer runtime.deinit();
try std.testing.expectEqual(Runtime.Outcome.returned, try runtime.run(&program, .{}));
var a: [1]u32 = undefined;
var b: [1]u32 = undefined;
try runtime.readOutput(&program, output_a, &a);
try runtime.readOutput(&program, output_b, &b);
try std.testing.expectEqual(@as(u32, 2), a[0]);
try std.testing.expectEqual(@as(u32, 1), b[0]);
}
@@ -0,0 +1,51 @@
const std = @import("std");
const shader_ir = @import("shader_ir");
const Program = @import("../Program.zig");
const Runtime = @import("../Runtime.zig");
const ir = shader_ir.ir;
test "[interpreter] fragment discard is an execution outcome" {
var module = try ir.parser.parseString(std.testing.allocator,
\\ shader fragment @main
\\ {
\\ fn @main() -> void
\\ {
\\ .entry():
\\ discard
\\ }
\\ }
);
defer module.deinit();
var program = try Program.compile(std.testing.allocator, &module);
defer program.deinit();
var runtime = try Runtime.init(std.testing.allocator, &program);
defer runtime.deinit();
try std.testing.expectEqual(Runtime.Outcome.discarded, try runtime.run(&program, .{}));
}
test "[interpreter] execution budget stops an infinite loop" {
var module = try ir.parser.parseString(std.testing.allocator,
\\ shader compute @main
\\ {
\\ fn @main() -> void
\\ {
\\ .entry():
\\ branch .loop()
\\ .loop():
\\ branch .loop()
\\ }
\\ }
);
defer module.deinit();
var program = try Program.compile(std.testing.allocator, &module);
defer program.deinit();
var runtime = try Runtime.init(std.testing.allocator, &program);
defer runtime.deinit();
try std.testing.expectError(Runtime.RuntimeError.StepLimitExceeded, runtime.run(&program, .{ .max_steps = 8 }));
}
+14
View File
@@ -0,0 +1,14 @@
const std = @import("std");
const bytecode = @import("../bytecode.zig");
test "[interpreter] bytecode instruction size" {
try std.testing.expectEqual(@as(usize, 16), @sizeOf(bytecode.Instruction));
}
comptime {
_ = @import("arithmetic.zig");
_ = @import("branching.zig");
_ = @import("loops.zig");
_ = @import("termination.zig");
}
+173
View File
@@ -0,0 +1,173 @@
const std = @import("std");
const vk = @import("vulkan");
const base = @import("base");
const shader_ir = @import("shader_ir");
const bc = @import("bytecode.zig");
const Shader = @import("Shader.zig");
const SoftPipeline = @import("../SoftPipeline.zig");
const Renderer = @import("../device/Renderer.zig");
const blitter = @import("../device/blitter.zig");
const VkError = base.VkError;
const ir = shader_ir.ir;
const interface_blob_padding = @sizeOf(base.zm.F32x4);
pub fn run(
allocator: std.mem.Allocator,
pipeline: *SoftPipeline,
shader: *Shader,
batch_id: usize,
batch_size: usize,
vertex_count: usize,
first_vertex: usize,
first_instance: usize,
indices: ?[]const u32,
primitive_restart: ?[]const bool,
instance_index: usize,
draw_call: *Renderer.DrawCall,
) VkError!void {
const slot = &shader.runtimes[batch_id];
const io = draw_call.renderer.device.interface.io();
slot.mutex.lock(io) catch return VkError.DeviceLost;
defer slot.mutex.unlock(io);
var invocation_index = batch_id;
while (invocation_index < vertex_count) : (invocation_index += batch_size) {
const output = &draw_call.vertices[(instance_index * vertex_count) + invocation_index];
if (primitive_restart) |restart| {
if (restart[invocation_index]) {
output.primitive_restart = true;
continue;
}
}
const vertex_index: u32 = if (indices) |draw_indices| draw_indices[invocation_index] else @intCast(first_vertex + invocation_index);
try populateInputs(
&slot.runtime,
&shader.program,
pipeline,
draw_call,
vertex_index,
@intCast(first_instance + instance_index),
);
const outcome = slot.runtime.run(&shader.program, .{}) catch return VkError.Unknown;
if (outcome == .discarded)
continue;
try collectOutputs(allocator, &slot.runtime, &shader.program, output);
}
}
fn populateInputs(
runtime: anytype,
program: *const @import("Program.zig"),
pipeline: *SoftPipeline,
draw_call: *Renderer.DrawCall,
vertex_index: u32,
instance_index: u32,
) VkError!void {
for (program.interfaces, 0..) |optional_binding, index| {
const binding = optional_binding orelse continue;
if (binding.direction != .input)
continue;
const variable = ir.id.InterfaceVariableId.fromIndex(index);
var values: [4]u32 = @splat(0);
switch (binding.semantic) {
.builtin => |builtin| values[0] = switch (builtin) {
.vertex_index => vertex_index,
.instance_index => instance_index,
else => return VkError.InvalidPipelineDrv,
},
.location => |location| {
const attribute = findAttribute(
pipeline.interface.mode.graphics.input_assembly.attribute_description orelse &.{},
location.location,
) orelse {
runtime.writeInput(program, variable, values[0..binding.span.components]) catch return VkError.Unknown;
continue;
};
const binding_description = findBinding(
pipeline.interface.mode.graphics.input_assembly.binding_description orelse return VkError.ValidationFailed,
attribute.binding,
) orelse return VkError.ValidationFailed;
const vertex_buffer = draw_call.renderer.state.data.graphics.vertex_buffers[attribute.binding];
const buffer = vertex_buffer.buffer;
const memory = buffer.interface.memory orelse return VkError.InvalidDeviceMemoryDrv;
const input_index = switch (binding_description.input_rate) {
.vertex => @as(usize, vertex_index),
.instance => @as(usize, instance_index),
else => return VkError.ValidationFailed,
};
const offset = buffer.interface.offset + vertex_buffer.offset + binding_description.stride * input_index + attribute.offset;
const input_size = base.format.texelSize(attribute.format);
var robust_bytes: [64]u8 = @splat(0);
if (input_size > robust_bytes.len)
return VkError.Unknown;
if (offset < memory.size) {
const available = @min(input_size, @as(usize, @intCast(memory.size - offset)));
const mapped = memory.map(offset, available) catch &.{};
@memcpy(robust_bytes[0..mapped.len], mapped);
}
values = if (base.format.isUnnormalizedInteger(attribute.format))
blitter.readInt4(robust_bytes[0..input_size], attribute.format)
else
@bitCast(blitter.readFloat4(robust_bytes[0..input_size], attribute.format));
const first_component: usize = location.component;
const end_component = first_component + binding.span.components;
runtime.writeInput(program, variable, values[first_component..end_component]) catch return VkError.Unknown;
continue;
},
}
runtime.writeInput(program, variable, values[0..binding.span.components]) catch return VkError.Unknown;
}
}
fn collectOutputs(allocator: std.mem.Allocator, runtime: anytype, program: *const @import("Program.zig"), output: *Renderer.Vertex) VkError!void {
for (program.interfaces, 0..) |optional_binding, index| {
const binding = optional_binding orelse continue;
if (binding.direction != .output)
continue;
const variable = ir.id.InterfaceVariableId.fromIndex(index);
var values: [4]u32 = @splat(0);
runtime.readOutput(program, variable, values[0..binding.span.components]) catch return VkError.Unknown;
switch (binding.semantic) {
.builtin => |builtin| switch (builtin) {
.position => @memcpy(std.mem.asBytes(&output.position), std.mem.asBytes(&values)),
else => return VkError.InvalidPipelineDrv,
},
.location => |location| {
if (location.location >= output.outputs.len or location.component >= output.outputs[0].len)
return VkError.ValidationFailed;
const size = @as(usize, binding.span.components) * @sizeOf(u32);
const blob = allocator.alloc(u8, size + interface_blob_padding) catch return VkError.OutOfDeviceMemory;
@memset(blob, 0);
@memcpy(blob[0..size], std.mem.asBytes(&values)[0..size]);
output.outputs[location.location][location.component] = .{
.interpolation_type = switch (binding.span.kind) {
.signed_integer, .unsigned_integer, .boolean => .flat,
.floating => .smooth,
},
.centroid = false,
.blob = blob,
.size = size,
};
},
}
}
}
fn findAttribute(attributes: []const vk.VertexInputAttributeDescription, location: u32) ?vk.VertexInputAttributeDescription {
for (attributes) |attribute|
if (attribute.location == location) return attribute;
return null;
}
fn findBinding(bindings: []const vk.VertexInputBindingDescription, binding: u32) ?vk.VertexInputBindingDescription {
for (bindings) |description|
if (description.binding == binding) return description;
return null;
}
comptime {
_ = bc;
}
+2
View File
@@ -6,6 +6,7 @@ pub const c = @import("soft_c");
pub const config = base.config;
pub const Device = @import("device/Device.zig");
pub const interpreter = @import("interpreter/root.zig");
pub const SoftInstance = @import("SoftInstance.zig");
pub const SoftDevice = @import("SoftDevice.zig");
@@ -84,6 +85,7 @@ comptime {
test {
std.testing.refAllDecls(Device);
std.testing.refAllDecls(interpreter);
std.testing.refAllDecls(SoftBinarySemaphore);
std.testing.refAllDecls(SoftBuffer);
std.testing.refAllDecls(SoftBufferView);