Merge pull request #15068 from Snektron/spirv-test-runner-support

spirv: test runner support
This commit is contained in:
Andrew Kelley 2023-04-10 10:44:35 -04:00 committed by GitHub
commit aeae71f462
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 2662 additions and 634 deletions

View File

@ -160,8 +160,7 @@ pub const CallingConvention = enum {
AAPCSVFP,
SysV,
Win64,
PtxKernel,
AmdgpuKernel,
Kernel,
};
/// This data structure is used by the Zig language code generation and

View File

@ -24,7 +24,9 @@ pub const simplified_logic =
builtin.zig_backend == .stage2_aarch64 or
builtin.zig_backend == .stage2_arm or
builtin.zig_backend == .stage2_riscv64 or
builtin.zig_backend == .stage2_sparc64;
builtin.zig_backend == .stage2_sparc64 or
builtin.cpu.arch == .spirv32 or
builtin.cpu.arch == .spirv64;
comptime {
// No matter what, we import the root file, so that any export, test, comptime
@ -43,6 +45,9 @@ comptime {
}
} else if (builtin.os.tag == .wasi and @hasDecl(root, "main")) {
@export(wasiMain2, .{ .name = "_start" });
} else if (builtin.os.tag == .opencl) {
if (@hasDecl(root, "main"))
@export(spirvMain2, .{ .name = "main" });
} else {
if (!@hasDecl(root, "_start")) {
@export(_start2, .{ .name = "_start" });
@ -127,6 +132,10 @@ fn wasiMain2() callconv(.C) noreturn {
}
}
fn spirvMain2() callconv(.Kernel) void {
root.main();
}
fn wWinMainCRTStartup2() callconv(.C) noreturn {
root.main();
exit2(0);

View File

@ -944,7 +944,7 @@ pub const Target = struct {
};
}
pub fn isSPIRV(arch: Arch) bool {
pub fn isSpirV(arch: Arch) bool {
return switch (arch) {
.spirv32, .spirv64 => true,
else => false,
@ -1276,7 +1276,7 @@ pub const Target = struct {
.x86, .x86_64 => "x86",
.nvptx, .nvptx64 => "nvptx",
.wasm32, .wasm64 => "wasm",
.spirv32, .spirv64 => "spir-v",
.spirv32, .spirv64 => "spirv",
else => @tagName(arch),
};
}
@ -1329,6 +1329,7 @@ pub const Target = struct {
.amdgcn => comptime allCpusFromDecls(amdgpu.cpu),
.riscv32, .riscv64 => comptime allCpusFromDecls(riscv.cpu),
.sparc, .sparc64, .sparcel => comptime allCpusFromDecls(sparc.cpu),
.spirv32, .spirv64 => comptime allCpusFromDecls(spirv.cpu),
.s390x => comptime allCpusFromDecls(s390x.cpu),
.x86, .x86_64 => comptime allCpusFromDecls(x86.cpu),
.xtensa => comptime allCpusFromDecls(xtensa.cpu),
@ -1392,6 +1393,7 @@ pub const Target = struct {
.amdgcn => &amdgpu.cpu.generic,
.riscv32 => &riscv.cpu.generic_rv32,
.riscv64 => &riscv.cpu.generic_rv64,
.spirv32, .spirv64 => &spirv.cpu.generic,
.sparc, .sparcel => &sparc.cpu.generic,
.sparc64 => &sparc.cpu.v9, // 64-bit SPARC needs v9 as the baseline
.s390x => &s390x.cpu.generic,
@ -1532,6 +1534,10 @@ pub const Target = struct {
return !self.cpu.arch.isWasm();
}
pub fn isSpirV(self: Target) bool {
return self.cpu.arch.isSpirV();
}
pub const FloatAbi = enum {
hard,
soft,

View File

@ -2081,3 +2081,11 @@ pub const all_features = blk: {
}
break :blk result;
};
pub const cpu = struct {
pub const generic = CpuModel{
.name = "generic",
.llvm_name = "generic",
.features = featureSet(&[_]Feature{}),
};
};

View File

@ -722,10 +722,11 @@ pub fn create(gpa: Allocator, options: InitOptions) !*Compilation {
// Once they are capable this condition could be removed. When removing this condition,
// also test the use case of `build-obj -fcompiler-rt` with the native backends
// and make sure the compiler-rt symbols are emitted.
const capable_of_building_compiler_rt = build_options.have_llvm and options.target.os.tag != .plan9;
const capable_of_building_zig_libc = build_options.have_llvm and options.target.os.tag != .plan9;
const capable_of_building_ssp = build_options.have_llvm and options.target.os.tag != .plan9;
const is_p9 = options.target.os.tag == .plan9;
const is_spv = options.target.cpu.arch.isSpirV();
const capable_of_building_compiler_rt = build_options.have_llvm and !is_p9 and !is_spv;
const capable_of_building_zig_libc = build_options.have_llvm and !is_p9 and !is_spv;
const capable_of_building_ssp = build_options.have_llvm and !is_p9 and !is_spv;
const comp: *Compilation = comp: {
// For allocations that have the same lifetime as Compilation. This arena is used only during this
@ -1948,8 +1949,9 @@ pub fn update(comp: *Compilation, main_progress_node: *std.Progress.Node) !void
.sub_path = std.fs.path.basename(sub_path),
};
}
comp.bin_file.destroy();
var old_bin_file = comp.bin_file;
comp.bin_file = try link.File.openPath(comp.gpa, options);
old_bin_file.destroy();
}
// For compiling C objects, we rely on the cache hash system to avoid duplicating work.

View File

@ -8883,7 +8883,7 @@ fn funcCommon(
};
return sema.failWithOwnedErrorMsg(msg);
}
if (!ret_poison and !Type.fnCallingConventionAllowsZigTypes(cc_resolved) and !try sema.validateExternType(return_type, .ret_ty)) {
if (!ret_poison and !Type.fnCallingConventionAllowsZigTypes(target, cc_resolved) and !try sema.validateExternType(return_type, .ret_ty)) {
const msg = msg: {
const msg = try sema.errMsg(block, ret_ty_src, "return type '{}' not allowed in function with calling convention '{s}'", .{
return_type.fmt(sema.mod), @tagName(cc_resolved),
@ -8961,13 +8961,9 @@ fn funcCommon(
.x86_64 => null,
else => @as([]const u8, "x86_64"),
},
.PtxKernel => switch (arch) {
.nvptx, .nvptx64 => null,
else => @as([]const u8, "nvptx and nvptx64"),
},
.AmdgpuKernel => switch (arch) {
.amdgcn => null,
else => @as([]const u8, "amdgcn"),
.Kernel => switch (arch) {
.nvptx, .nvptx64, .amdgcn, .spirv32, .spirv64 => null,
else => @as([]const u8, "nvptx, amdgcn and SPIR-V"),
},
}) |allowed_platform| {
return sema.fail(block, cc_src, "callconv '{s}' is only available on {s}, not {s}", .{
@ -9093,10 +9089,11 @@ fn analyzeParameter(
comptime_params[i] = param.is_comptime or requires_comptime;
const this_generic = param.ty.tag() == .generic_poison;
is_generic.* = is_generic.* or this_generic;
if (param.is_comptime and !Type.fnCallingConventionAllowsZigTypes(cc)) {
const target = sema.mod.getTarget();
if (param.is_comptime and !Type.fnCallingConventionAllowsZigTypes(target, cc)) {
return sema.fail(block, param_src, "comptime parameters not allowed in function with calling convention '{s}'", .{@tagName(cc)});
}
if (this_generic and !sema.no_partial_func_ty and !Type.fnCallingConventionAllowsZigTypes(cc)) {
if (this_generic and !sema.no_partial_func_ty and !Type.fnCallingConventionAllowsZigTypes(target, cc)) {
return sema.fail(block, param_src, "generic parameters not allowed in function with calling convention '{s}'", .{@tagName(cc)});
}
if (!param.ty.isValidParamType()) {
@ -9112,7 +9109,7 @@ fn analyzeParameter(
};
return sema.failWithOwnedErrorMsg(msg);
}
if (!this_generic and !Type.fnCallingConventionAllowsZigTypes(cc) and !try sema.validateExternType(param.ty, .param_ty)) {
if (!this_generic and !Type.fnCallingConventionAllowsZigTypes(target, cc) and !try sema.validateExternType(param.ty, .param_ty)) {
const msg = msg: {
const msg = try sema.errMsg(block, param_src, "parameter of type '{}' not allowed in function with calling convention '{s}'", .{
param.ty.fmt(sema.mod), @tagName(cc),
@ -22786,12 +22783,13 @@ fn validateExternType(
},
.Fn => {
if (position != .other) return false;
return switch (ty.fnCallingConvention()) {
// For now we want to authorize PTX kernel to use zig objects, even if we end up exposing the ABI.
// The goal is to experiment with more integrated CPU/GPU code.
.PtxKernel => true,
else => !Type.fnCallingConventionAllowsZigTypes(ty.fnCallingConvention()),
};
const target = sema.mod.getTarget();
// For now we want to authorize PTX kernel to use zig objects, even if we end up exposing the ABI.
// The goal is to experiment with more integrated CPU/GPU code.
if (ty.fnCallingConvention() == .Kernel and (target.cpu.arch == .nvptx or target.cpu.arch == .nvptx64)) {
return true;
}
return !Type.fnCallingConventionAllowsZigTypes(target, ty.fnCallingConvention());
},
.Enum => {
var buf: Type.Payload.Bits = undefined;

View File

@ -10350,11 +10350,8 @@ fn toLlvmCallConv(cc: std.builtin.CallingConvention, target: std.Target) llvm.Ca
.Signal => .AVR_SIGNAL,
.SysV => .X86_64_SysV,
.Win64 => .Win64,
.PtxKernel => return switch (target.cpu.arch) {
.Kernel => return switch (target.cpu.arch) {
.nvptx, .nvptx64 => .PTX_Kernel,
else => unreachable,
},
.AmdgpuKernel => return switch (target.cpu.arch) {
.amdgcn => .AMDGPU_KERNEL,
else => unreachable,
},

File diff suppressed because it is too large Load Diff

View File

@ -135,7 +135,7 @@ const AsmValue = union(enum) {
return switch (self) {
.just_declared, .unresolved_forward_reference => unreachable,
.value => |result| result,
.ty => |ref| spv.typeResultId(ref).toRef(),
.ty => |ref| spv.typeId(ref),
};
}
};
@ -239,12 +239,17 @@ fn todo(self: *Assembler, comptime fmt: []const u8, args: anytype) Error {
/// If this function returns `error.AssembleFail`, an explanatory
/// error message has already been emitted into `self.errors`.
fn processInstruction(self: *Assembler) !void {
const result = switch (self.inst.opcode.class()) {
.TypeDeclaration => try self.processTypeInstruction(),
else => if (try self.processGenericInstruction()) |result|
result
else
return,
const result = switch (self.inst.opcode) {
.OpEntryPoint => {
return self.fail(0, "cannot export entry points via OpEntryPoint, export the kernel using callconv(.Kernel)", .{});
},
else => switch (self.inst.opcode.class()) {
.TypeDeclaration => try self.processTypeInstruction(),
else => if (try self.processGenericInstruction()) |result|
result
else
return,
},
};
const result_ref = self.inst.result().?;
@ -266,27 +271,28 @@ fn processTypeInstruction(self: *Assembler) !AsmValue {
.OpTypeVoid => SpvType.initTag(.void),
.OpTypeBool => SpvType.initTag(.bool),
.OpTypeInt => blk: {
const payload = try self.spv.arena.create(SpvType.Payload.Int);
const signedness: std.builtin.Signedness = switch (operands[2].literal32) {
0 => .unsigned,
1 => .signed,
else => {
// TODO: Improve source location.
return self.fail(0, "'{}' is not a valid signedness (expected 0 or 1)", .{operands[2].literal32});
return self.fail(0, "{} is not a valid signedness (expected 0 or 1)", .{operands[2].literal32});
},
};
payload.* = .{
.width = operands[1].literal32,
.signedness = signedness,
const width = std.math.cast(u16, operands[1].literal32) orelse {
return self.fail(0, "int type of {} bits is too large", .{operands[1].literal32});
};
break :blk SpvType.initPayload(&payload.base);
break :blk try SpvType.int(self.spv.arena, signedness, width);
},
.OpTypeFloat => blk: {
const payload = try self.spv.arena.create(SpvType.Payload.Float);
payload.* = .{
.width = operands[1].literal32,
};
break :blk SpvType.initPayload(&payload.base);
const bits = operands[1].literal32;
switch (bits) {
16, 32, 64 => {},
else => {
return self.fail(0, "{} is not a valid bit count for floats (expected 16, 32 or 64)", .{bits});
},
}
break :blk SpvType.float(@intCast(u16, bits));
},
.OpTypeVector => blk: {
const payload = try self.spv.arena.create(SpvType.Payload.Vector);
@ -382,10 +388,7 @@ fn processTypeInstruction(self: *Assembler) !AsmValue {
payload.* = .{
.storage_class = @intToEnum(spec.StorageClass, operands[1].value),
.child_type = try self.resolveTypeRef(operands[2].ref_id),
// TODO: Fetch these values from decorations.
.array_stride = 0,
.alignment = null,
.max_byte_offset = null,
// TODO: Fetch decorations
};
break :blk SpvType.initPayload(&payload.base);
},
@ -434,11 +437,16 @@ fn processGenericInstruction(self: *Assembler) !?AsmValue {
.Annotation => &self.spv.sections.annotations,
.TypeDeclaration => unreachable, // Handled elsewhere.
else => switch (self.inst.opcode) {
.OpEntryPoint => &self.spv.sections.entry_points,
.OpEntryPoint => unreachable,
.OpExecutionMode, .OpExecutionModeId => &self.spv.sections.execution_modes,
.OpVariable => switch (@intToEnum(spec.StorageClass, operands[2].value)) {
.Function => &self.func.prologue,
else => &self.spv.sections.types_globals_constants,
else => {
// This is currently disabled because global variables are required to be
// emitted in the proper order, and this should be honored in inline assembly
// as well.
return self.todo("global variables", .{});
},
},
// Default case - to be worked out further.
else => &self.func.body,
@ -485,7 +493,7 @@ fn processGenericInstruction(self: *Assembler) !?AsmValue {
section.instructions.items[first_word] |= @as(u32, @intCast(u16, actual_word_count)) << 16 | @enumToInt(self.inst.opcode);
if (maybe_result_id) |result| {
return AsmValue{ .value = result.toRef() };
return AsmValue{ .value = result };
}
return null;
}
@ -753,22 +761,19 @@ fn parseContextDependentNumber(self: *Assembler) !void {
const tok = self.currentToken();
const result_type_ref = try self.resolveTypeRef(self.inst.operands.items[0].ref_id);
const result_type = self.spv.type_cache.keys()[result_type_ref];
switch (result_type.tag()) {
.int => {
const int = result_type.castTag(.int).?;
try self.parseContextDependentInt(int.signedness, int.width);
},
.float => {
const width = result_type.castTag(.float).?.width;
switch (width) {
16 => try self.parseContextDependentFloat(16),
32 => try self.parseContextDependentFloat(32),
64 => try self.parseContextDependentFloat(64),
else => return self.fail(tok.start, "cannot parse {}-bit float literal", .{width}),
}
},
else => return self.fail(tok.start, "cannot parse literal constant {s}", .{@tagName(result_type.tag())}),
const result_type = self.spv.type_cache.keys()[@enumToInt(result_type_ref)];
if (result_type.isInt()) {
try self.parseContextDependentInt(result_type.intSignedness(), result_type.intFloatBits());
} else if (result_type.isFloat()) {
const width = result_type.intFloatBits();
switch (width) {
16 => try self.parseContextDependentFloat(16),
32 => try self.parseContextDependentFloat(32),
64 => try self.parseContextDependentFloat(64),
else => return self.fail(tok.start, "cannot parse {}-bit float literal", .{width}),
}
} else {
return self.fail(tok.start, "cannot parse literal constant {s}", .{@tagName(result_type.tag())});
}
}

View File

@ -39,22 +39,68 @@ pub const Fn = struct {
/// This section should also contain the OpFunctionEnd instruction marking
/// the end of this function definition.
body: Section = .{},
/// The decl dependencies that this function depends on.
decl_deps: std.ArrayListUnmanaged(Decl.Index) = .{},
/// Reset this function without deallocating resources, so that
/// it may be used to emit code for another function.
pub fn reset(self: *Fn) void {
self.prologue.reset();
self.body.reset();
self.decl_deps.items.len = 0;
}
/// Free the resources owned by this function.
pub fn deinit(self: *Fn, a: Allocator) void {
self.prologue.deinit(a);
self.body.deinit(a);
self.decl_deps.deinit(a);
self.* = undefined;
}
};
/// Declarations, both functions and globals, can have dependencies. These are used for 2 things:
/// - Globals must be declared before they are used, also between globals. The compiler processes
/// globals unordered, so we must use the dependencies here to figure out how to order the globals
/// in the final module. The Globals structure is also used for that.
/// - Entry points must declare the complete list of OpVariable instructions that they access.
/// For these we use the same dependency structure.
/// In this mechanism, globals will only depend on other globals, while functions may depend on
/// globals or other functions.
pub const Decl = struct {
/// Index to refer to a Decl by.
pub const Index = enum(u32) { _ };
/// The result-id to be used for this declaration. This is the final result-id
/// of the decl, which may be an OpFunction, OpVariable, or the result of a sequence
/// of OpSpecConstantOp operations.
result_id: IdRef,
/// The offset of the first dependency of this decl in the `decl_deps` array.
begin_dep: u32,
/// The past-end offset of the dependencies of this decl in the `decl_deps` array.
end_dep: u32,
};
/// Globals must be kept in order: operations involving globals must be ordered
/// so that the global declaration precedes any usage.
pub const Global = struct {
/// This is the result-id of the OpVariable instruction that declares the global.
result_id: IdRef,
/// The offset into `self.globals.section` of the first instruction of this global
/// declaration.
begin_inst: u32,
/// The past-end offset into `self.flobals.section`.
end_inst: u32,
};
/// This models a kernel entry point.
pub const EntryPoint = struct {
/// The declaration that should be exported.
decl_index: Decl.Index,
/// The name of the kernel to be exported.
name: []const u8,
};
/// A general-purpose allocator which may be used to allocate resources for this module
gpa: Allocator,
@ -69,13 +115,13 @@ sections: struct {
extensions: Section = .{},
// OpExtInstImport instructions - skip for now.
// memory model defined by target, not required here.
/// OpEntryPoint instructions.
entry_points: Section = .{},
/// OpEntryPoint instructions - Handled by `self.entry_points`.
/// OpExecutionMode and OpExecutionModeId instructions.
execution_modes: Section = .{},
/// OpString, OpSourcExtension, OpSource, OpSourceContinued.
debug_strings: Section = .{},
// OpName, OpMemberName - skip for now.
// OpName, OpMemberName.
debug_names: Section = .{},
// OpModuleProcessed - skip for now.
/// Annotation instructions (OpDecorate etc).
annotations: Section = .{},
@ -101,6 +147,26 @@ source_file_names: std.StringHashMapUnmanaged(IdRef) = .{},
/// Note: Uses ArrayHashMap which is insertion ordered, so that we may refer to other types by index (Type.Ref).
type_cache: TypeCache = .{},
/// Set of Decls, referred to by Decl.Index.
decls: std.ArrayListUnmanaged(Decl) = .{},
/// List of dependencies, per decl. This list holds all the dependencies, sliced by the
/// begin_dep and end_dep in `self.decls`.
decl_deps: std.ArrayListUnmanaged(Decl.Index) = .{},
/// The list of entry points that should be exported from this module.
entry_points: std.ArrayListUnmanaged(EntryPoint) = .{},
/// The fields in this structure help to maintain the required order for global variables.
globals: struct {
/// Set of globals, referred to by Decl.Index.
globals: std.AutoArrayHashMapUnmanaged(Decl.Index, Global) = .{},
/// This pseudo-section contains the initialization code for all the globals. Instructions from
/// here are reordered when flushing the module. Its contents should be part of the
/// `types_globals_constants` SPIR-V section.
section: Section = .{},
} = .{},
pub fn init(gpa: Allocator, arena: Allocator) Module {
return .{
.gpa = gpa,
@ -112,9 +178,9 @@ pub fn init(gpa: Allocator, arena: Allocator) Module {
pub fn deinit(self: *Module) void {
self.sections.capabilities.deinit(self.gpa);
self.sections.extensions.deinit(self.gpa);
self.sections.entry_points.deinit(self.gpa);
self.sections.execution_modes.deinit(self.gpa);
self.sections.debug_strings.deinit(self.gpa);
self.sections.debug_names.deinit(self.gpa);
self.sections.annotations.deinit(self.gpa);
self.sections.types_globals_constants.deinit(self.gpa);
self.sections.functions.deinit(self.gpa);
@ -122,6 +188,14 @@ pub fn deinit(self: *Module) void {
self.source_file_names.deinit(self.gpa);
self.type_cache.deinit(self.gpa);
self.decls.deinit(self.gpa);
self.decl_deps.deinit(self.gpa);
self.entry_points.deinit(self.gpa);
self.globals.globals.deinit(self.gpa);
self.globals.section.deinit(self.gpa);
self.* = undefined;
}
@ -130,32 +204,138 @@ pub fn allocId(self: *Module) spec.IdResult {
return .{ .id = self.next_result_id };
}
pub fn allocIds(self: *Module, n: u32) spec.IdResult {
defer self.next_result_id += n;
return .{ .id = self.next_result_id };
}
pub fn idBound(self: Module) Word {
return self.next_result_id;
}
fn orderGlobalsInto(
self: *Module,
decl_index: Decl.Index,
section: *Section,
seen: *std.DynamicBitSetUnmanaged,
) !void {
const decl = self.declPtr(decl_index);
const deps = self.decl_deps.items[decl.begin_dep..decl.end_dep];
const global = self.globalPtr(decl_index).?;
const insts = self.globals.section.instructions.items[global.begin_inst..global.end_inst];
seen.set(@enumToInt(decl_index));
for (deps) |dep| {
if (!seen.isSet(@enumToInt(dep))) {
try self.orderGlobalsInto(dep, section, seen);
}
}
try section.instructions.appendSlice(self.gpa, insts);
}
fn orderGlobals(self: *Module) !Section {
const globals = self.globals.globals.keys();
var seen = try std.DynamicBitSetUnmanaged.initEmpty(self.gpa, self.decls.items.len);
defer seen.deinit(self.gpa);
var ordered_globals = Section{};
errdefer ordered_globals.deinit(self.gpa);
for (globals) |decl_index| {
if (!seen.isSet(@enumToInt(decl_index))) {
try self.orderGlobalsInto(decl_index, &ordered_globals, &seen);
}
}
return ordered_globals;
}
fn addEntryPointDeps(
self: *Module,
decl_index: Decl.Index,
seen: *std.DynamicBitSetUnmanaged,
interface: *std.ArrayList(IdRef),
) !void {
const decl = self.declPtr(decl_index);
const deps = self.decl_deps.items[decl.begin_dep..decl.end_dep];
seen.set(@enumToInt(decl_index));
if (self.globalPtr(decl_index)) |global| {
try interface.append(global.result_id);
}
for (deps) |dep| {
if (!seen.isSet(@enumToInt(dep))) {
try self.addEntryPointDeps(dep, seen, interface);
}
}
}
fn entryPoints(self: *Module) !Section {
var entry_points = Section{};
errdefer entry_points.deinit(self.gpa);
var interface = std.ArrayList(IdRef).init(self.gpa);
defer interface.deinit();
var seen = try std.DynamicBitSetUnmanaged.initEmpty(self.gpa, self.decls.items.len);
defer seen.deinit(self.gpa);
for (self.entry_points.items) |entry_point| {
interface.items.len = 0;
seen.setRangeValue(.{ .start = 0, .end = self.decls.items.len }, false);
try self.addEntryPointDeps(entry_point.decl_index, &seen, &interface);
const entry_point_id = self.declPtr(entry_point.decl_index).result_id;
try entry_points.emit(self.gpa, .OpEntryPoint, .{
.execution_model = .Kernel,
.entry_point = entry_point_id,
.name = entry_point.name,
.interface = interface.items,
});
}
return entry_points;
}
/// Emit this module as a spir-v binary.
pub fn flush(self: Module, file: std.fs.File) !void {
pub fn flush(self: *Module, file: std.fs.File) !void {
// See SPIR-V Spec section 2.3, "Physical Layout of a SPIR-V Module and Instruction"
const header = [_]Word{
spec.magic_number,
(1 << 16) | (5 << 8),
// TODO: From cpu features
// Emit SPIR-V 1.4 for now. This is the highest version that Intel's CPU OpenCL supports.
(1 << 16) | (4 << 8),
0, // TODO: Register Zig compiler magic number.
self.idBound(),
0, // Schema (currently reserved for future use)
};
// TODO: Perform topological sort on the globals.
var globals = try self.orderGlobals();
defer globals.deinit(self.gpa);
var entry_points = try self.entryPoints();
defer entry_points.deinit(self.gpa);
// Note: needs to be kept in order according to section 2.3!
const buffers = &[_][]const Word{
&header,
self.sections.capabilities.toWords(),
self.sections.extensions.toWords(),
self.sections.entry_points.toWords(),
entry_points.toWords(),
self.sections.execution_modes.toWords(),
self.sections.debug_strings.toWords(),
self.sections.debug_names.toWords(),
self.sections.annotations.toWords(),
self.sections.types_globals_constants.toWords(),
globals.toWords(),
self.sections.functions.toWords(),
};
@ -175,9 +355,10 @@ pub fn flush(self: Module, file: std.fs.File) !void {
}
/// Merge the sections making up a function declaration into this module.
pub fn addFunction(self: *Module, func: Fn) !void {
pub fn addFunction(self: *Module, decl_index: Decl.Index, func: Fn) !void {
try self.sections.functions.append(self.gpa, func.prologue);
try self.sections.functions.append(self.gpa, func.body);
try self.declareDeclDeps(decl_index, func.decl_deps.items);
}
/// Fetch the result-id of an OpString instruction that encodes the path of the source
@ -188,7 +369,7 @@ pub fn resolveSourceFileName(self: *Module, decl: *ZigDecl) !IdRef {
const result = try self.source_file_names.getOrPut(self.gpa, path);
if (!result.found_existing) {
const file_result_id = self.allocId();
result.value_ptr.* = file_result_id.toRef();
result.value_ptr.* = file_result_id;
try self.sections.debug_strings.emit(self.gpa, .OpString, .{
.id_result = file_result_id,
.string = path,
@ -197,7 +378,7 @@ pub fn resolveSourceFileName(self: *Module, decl: *ZigDecl) !IdRef {
try self.sections.debug_strings.emit(self.gpa, .OpSource, .{
.source_language = .Unknown, // TODO: Register Zig source language.
.version = 0, // TODO: Zig version as u32?
.file = file_result_id.toRef(),
.file = file_result_id,
.source = null, // TODO: Store actual source also?
});
}
@ -216,22 +397,21 @@ pub fn resolveType(self: *Module, ty: Type) !Type.Ref {
result.value_ptr.* = try self.emitType(ty);
}
return result.index;
return @intToEnum(Type.Ref, result.index);
}
pub fn resolveTypeId(self: *Module, ty: Type) !IdRef {
const type_ref = try self.resolveType(ty);
return self.typeResultId(type_ref);
pub fn resolveTypeId(self: *Module, ty: Type) !IdResultType {
const ty_ref = try self.resolveType(ty);
return self.typeId(ty_ref);
}
pub fn typeRefType(self: Module, ty_ref: Type.Ref) Type {
return self.type_cache.keys()[@enumToInt(ty_ref)];
}
/// Get the result-id of a particular type, by reference. Asserts type_ref is valid.
pub fn typeResultId(self: Module, type_ref: Type.Ref) IdResultType {
return self.type_cache.values()[type_ref];
}
/// Get the result-id of a particular type as IdRef, by Type.Ref. Asserts type_ref is valid.
pub fn typeRefId(self: Module, type_ref: Type.Ref) IdRef {
return self.type_cache.values()[type_ref].toRef();
pub fn typeId(self: Module, ty_ref: Type.Ref) IdResultType {
return self.type_cache.values()[@enumToInt(ty_ref)];
}
/// Unconditionally emit a spir-v type into the appropriate section.
@ -240,47 +420,94 @@ pub fn typeRefId(self: Module, type_ref: Type.Ref) IdRef {
/// Note: This function does not attempt to perform any validation on the type.
/// The type is emitted in a shallow fashion; any child types should already
/// be emitted at this point.
pub fn emitType(self: *Module, ty: Type) !IdResultType {
pub fn emitType(self: *Module, ty: Type) error{OutOfMemory}!IdResultType {
const result_id = self.allocId();
const ref_id = result_id.toRef();
const ref_id = result_id;
const types = &self.sections.types_globals_constants;
const annotations = &self.sections.annotations;
const debug_names = &self.sections.debug_names;
const result_id_operand = .{ .id_result = result_id };
switch (ty.tag()) {
.void => try types.emit(self.gpa, .OpTypeVoid, result_id_operand),
.bool => try types.emit(self.gpa, .OpTypeBool, result_id_operand),
.int => {
const signedness: spec.LiteralInteger = switch (ty.payload(.int).signedness) {
.void => {
try types.emit(self.gpa, .OpTypeVoid, result_id_operand);
try debug_names.emit(self.gpa, .OpName, .{
.target = result_id,
.name = "void",
});
},
.bool => {
try types.emit(self.gpa, .OpTypeBool, result_id_operand);
try debug_names.emit(self.gpa, .OpName, .{
.target = result_id,
.name = "bool",
});
},
.u8,
.u16,
.u32,
.u64,
.i8,
.i16,
.i32,
.i64,
.int,
=> {
// TODO: Kernels do not support OpTypeInt that is signed. We can probably
// can get rid of the signedness all together, in Shaders also.
const bits = ty.intFloatBits();
const signedness: spec.LiteralInteger = switch (ty.intSignedness()) {
.unsigned => 0,
.signed => 1,
};
try types.emit(self.gpa, .OpTypeInt, .{
.id_result = result_id,
.width = ty.payload(.int).width,
.width = bits,
.signedness = signedness,
});
const ui: []const u8 = switch (signedness) {
0 => "u",
1 => "i",
else => unreachable,
};
const name = try std.fmt.allocPrint(self.gpa, "{s}{}", .{ ui, bits });
defer self.gpa.free(name);
try debug_names.emit(self.gpa, .OpName, .{
.target = result_id,
.name = name,
});
},
.f16, .f32, .f64 => {
const bits = ty.intFloatBits();
try types.emit(self.gpa, .OpTypeFloat, .{
.id_result = result_id,
.width = bits,
});
const name = try std.fmt.allocPrint(self.gpa, "f{}", .{bits});
defer self.gpa.free(name);
try debug_names.emit(self.gpa, .OpName, .{
.target = result_id,
.name = name,
});
},
.float => try types.emit(self.gpa, .OpTypeFloat, .{
.id_result = result_id,
.width = ty.payload(.float).width,
}),
.vector => try types.emit(self.gpa, .OpTypeVector, .{
.id_result = result_id,
.component_type = self.typeResultId(ty.childType()).toRef(),
.component_type = self.typeId(ty.childType()),
.component_count = ty.payload(.vector).component_count,
}),
.matrix => try types.emit(self.gpa, .OpTypeMatrix, .{
.id_result = result_id,
.column_type = self.typeResultId(ty.childType()).toRef(),
.column_type = self.typeId(ty.childType()),
.column_count = ty.payload(.matrix).column_count,
}),
.image => {
const info = ty.payload(.image);
try types.emit(self.gpa, .OpTypeImage, .{
.id_result = result_id,
.sampled_type = self.typeResultId(ty.childType()).toRef(),
.sampled_type = self.typeId(ty.childType()),
.dim = info.dim,
.depth = @enumToInt(info.depth),
.arrayed = @boolToInt(info.arrayed),
@ -293,28 +520,34 @@ pub fn emitType(self: *Module, ty: Type) !IdResultType {
.sampler => try types.emit(self.gpa, .OpTypeSampler, result_id_operand),
.sampled_image => try types.emit(self.gpa, .OpTypeSampledImage, .{
.id_result = result_id,
.image_type = self.typeResultId(ty.childType()).toRef(),
.image_type = self.typeId(ty.childType()),
}),
.array => {
const info = ty.payload(.array);
assert(info.length != 0);
const size_type = Type.initTag(.u32);
const size_type_id = try self.resolveTypeId(size_type);
const length_id = self.allocId();
try self.emitConstant(size_type_id, length_id, .{ .uint32 = info.length });
try types.emit(self.gpa, .OpTypeArray, .{
.id_result = result_id,
.element_type = self.typeResultId(ty.childType()).toRef(),
.length = .{ .id = 0 }, // TODO: info.length must be emitted as constant!
.element_type = self.typeId(ty.childType()),
.length = length_id,
});
if (info.array_stride != 0) {
try annotations.decorate(self.gpa, ref_id, .{ .ArrayStride = .{ .array_stride = info.array_stride } });
try self.decorate(ref_id, .{ .ArrayStride = .{ .array_stride = info.array_stride } });
}
},
.runtime_array => {
const info = ty.payload(.runtime_array);
try types.emit(self.gpa, .OpTypeRuntimeArray, .{
.id_result = result_id,
.element_type = self.typeResultId(ty.childType()).toRef(),
.element_type = self.typeId(ty.childType()),
});
if (info.array_stride != 0) {
try annotations.decorate(self.gpa, ref_id, .{ .ArrayStride = .{ .array_stride = info.array_stride } });
try self.decorate(ref_id, .{ .ArrayStride = .{ .array_stride = info.array_stride } });
}
},
.@"struct" => {
@ -322,7 +555,7 @@ pub fn emitType(self: *Module, ty: Type) !IdResultType {
try types.emitRaw(self.gpa, .OpTypeStruct, 1 + info.members.len);
types.writeOperand(IdResult, result_id);
for (info.members) |member| {
types.writeOperand(IdRef, self.typeResultId(member.ty).toRef());
types.writeOperand(IdRef, self.typeId(member.ty));
}
try self.decorateStruct(ref_id, info);
},
@ -335,25 +568,25 @@ pub fn emitType(self: *Module, ty: Type) !IdResultType {
try types.emit(self.gpa, .OpTypePointer, .{
.id_result = result_id,
.storage_class = info.storage_class,
.type = self.typeResultId(ty.childType()).toRef(),
.type = self.typeId(ty.childType()),
});
if (info.array_stride != 0) {
try annotations.decorate(self.gpa, ref_id, .{ .ArrayStride = .{ .array_stride = info.array_stride } });
try self.decorate(ref_id, .{ .ArrayStride = .{ .array_stride = info.array_stride } });
}
if (info.alignment) |alignment| {
try annotations.decorate(self.gpa, ref_id, .{ .Alignment = .{ .alignment = alignment } });
if (info.alignment != 0) {
try self.decorate(ref_id, .{ .Alignment = .{ .alignment = info.alignment } });
}
if (info.max_byte_offset) |max_byte_offset| {
try annotations.decorate(self.gpa, ref_id, .{ .MaxByteOffset = .{ .max_byte_offset = max_byte_offset } });
try self.decorate(ref_id, .{ .MaxByteOffset = .{ .max_byte_offset = max_byte_offset } });
}
},
.function => {
const info = ty.payload(.function);
try types.emitRaw(self.gpa, .OpTypeFunction, 2 + info.parameters.len);
types.writeOperand(IdResult, result_id);
types.writeOperand(IdRef, self.typeResultId(info.return_type).toRef());
types.writeOperand(IdRef, self.typeId(info.return_type));
for (info.parameters) |parameter_type| {
types.writeOperand(IdRef, self.typeResultId(parameter_type).toRef());
types.writeOperand(IdRef, self.typeId(parameter_type));
}
},
.event => try types.emit(self.gpa, .OpTypeEvent, result_id_operand),
@ -368,23 +601,30 @@ pub fn emitType(self: *Module, ty: Type) !IdResultType {
.named_barrier => try types.emit(self.gpa, .OpTypeNamedBarrier, result_id_operand),
}
return result_id.toResultType();
return result_id;
}
fn decorateStruct(self: *Module, target: IdRef, info: *const Type.Payload.Struct) !void {
const annotations = &self.sections.annotations;
const debug_names = &self.sections.debug_names;
if (info.name.len != 0) {
try debug_names.emit(self.gpa, .OpName, .{
.target = target,
.name = info.name,
});
}
// Decorations for the struct type itself.
if (info.decorations.block)
try annotations.decorate(self.gpa, target, .Block);
try self.decorate(target, .Block);
if (info.decorations.buffer_block)
try annotations.decorate(self.gpa, target, .BufferBlock);
try self.decorate(target, .BufferBlock);
if (info.decorations.glsl_shared)
try annotations.decorate(self.gpa, target, .GLSLShared);
try self.decorate(target, .GLSLShared);
if (info.decorations.glsl_packed)
try annotations.decorate(self.gpa, target, .GLSLPacked);
try self.decorate(target, .GLSLPacked);
if (info.decorations.c_packed)
try annotations.decorate(self.gpa, target, .CPacked);
try self.decorate(target, .CPacked);
// Decorations for the struct members.
const extra = info.member_decoration_extra;
@ -392,71 +632,89 @@ fn decorateStruct(self: *Module, target: IdRef, info: *const Type.Payload.Struct
for (info.members, 0..) |member, i| {
const d = member.decorations;
const index = @intCast(Word, i);
if (member.name.len != 0) {
try debug_names.emit(self.gpa, .OpMemberName, .{
.type = target,
.member = index,
.name = member.name,
});
}
switch (member.offset) {
.none => {},
else => try self.decorateMember(
target,
index,
.{ .Offset = .{ .byte_offset = @enumToInt(member.offset) } },
),
}
switch (d.matrix_layout) {
.row_major => try annotations.decorateMember(self.gpa, target, index, .RowMajor),
.col_major => try annotations.decorateMember(self.gpa, target, index, .ColMajor),
.row_major => try self.decorateMember(target, index, .RowMajor),
.col_major => try self.decorateMember(target, index, .ColMajor),
.none => {},
}
if (d.matrix_layout != .none) {
try annotations.decorateMember(self.gpa, target, index, .{
try self.decorateMember(target, index, .{
.MatrixStride = .{ .matrix_stride = extra[extra_i] },
});
extra_i += 1;
}
if (d.no_perspective)
try annotations.decorateMember(self.gpa, target, index, .NoPerspective);
try self.decorateMember(target, index, .NoPerspective);
if (d.flat)
try annotations.decorateMember(self.gpa, target, index, .Flat);
try self.decorateMember(target, index, .Flat);
if (d.patch)
try annotations.decorateMember(self.gpa, target, index, .Patch);
try self.decorateMember(target, index, .Patch);
if (d.centroid)
try annotations.decorateMember(self.gpa, target, index, .Centroid);
try self.decorateMember(target, index, .Centroid);
if (d.sample)
try annotations.decorateMember(self.gpa, target, index, .Sample);
try self.decorateMember(target, index, .Sample);
if (d.invariant)
try annotations.decorateMember(self.gpa, target, index, .Invariant);
try self.decorateMember(target, index, .Invariant);
if (d.@"volatile")
try annotations.decorateMember(self.gpa, target, index, .Volatile);
try self.decorateMember(target, index, .Volatile);
if (d.coherent)
try annotations.decorateMember(self.gpa, target, index, .Coherent);
try self.decorateMember(target, index, .Coherent);
if (d.non_writable)
try annotations.decorateMember(self.gpa, target, index, .NonWritable);
try self.decorateMember(target, index, .NonWritable);
if (d.non_readable)
try annotations.decorateMember(self.gpa, target, index, .NonReadable);
try self.decorateMember(target, index, .NonReadable);
if (d.builtin) {
try annotations.decorateMember(self.gpa, target, index, .{
try self.decorateMember(target, index, .{
.BuiltIn = .{ .built_in = @intToEnum(spec.BuiltIn, extra[extra_i]) },
});
extra_i += 1;
}
if (d.stream) {
try annotations.decorateMember(self.gpa, target, index, .{
try self.decorateMember(target, index, .{
.Stream = .{ .stream_number = extra[extra_i] },
});
extra_i += 1;
}
if (d.location) {
try annotations.decorateMember(self.gpa, target, index, .{
try self.decorateMember(target, index, .{
.Location = .{ .location = extra[extra_i] },
});
extra_i += 1;
}
if (d.component) {
try annotations.decorateMember(self.gpa, target, index, .{
try self.decorateMember(target, index, .{
.Component = .{ .component = extra[extra_i] },
});
extra_i += 1;
}
if (d.xfb_buffer) {
try annotations.decorateMember(self.gpa, target, index, .{
try self.decorateMember(target, index, .{
.XfbBuffer = .{ .xfb_buffer_number = extra[extra_i] },
});
extra_i += 1;
}
if (d.xfb_stride) {
try annotations.decorateMember(self.gpa, target, index, .{
try self.decorateMember(target, index, .{
.XfbStride = .{ .xfb_stride = extra[extra_i] },
});
extra_i += 1;
@ -465,10 +723,150 @@ fn decorateStruct(self: *Module, target: IdRef, info: *const Type.Payload.Struct
const len = extra[extra_i];
extra_i += 1;
const semantic = @ptrCast([*]const u8, &extra[extra_i])[0..len];
try annotations.decorateMember(self.gpa, target, index, .{
try self.decorateMember(target, index, .{
.UserSemantic = .{ .semantic = semantic },
});
extra_i += std.math.divCeil(u32, extra_i, @sizeOf(u32)) catch unreachable;
}
}
}
pub fn simpleStructType(self: *Module, members: []const Type.Payload.Struct.Member) !Type.Ref {
const payload = try self.arena.create(Type.Payload.Struct);
payload.* = .{
.members = try self.arena.dupe(Type.Payload.Struct.Member, members),
.decorations = .{},
};
return try self.resolveType(Type.initPayload(&payload.base));
}
pub fn arrayType(self: *Module, len: u32, ty: Type.Ref) !Type.Ref {
const payload = try self.arena.create(Type.Payload.Array);
payload.* = .{
.element_type = ty,
.length = len,
};
return try self.resolveType(Type.initPayload(&payload.base));
}
pub fn ptrType(
self: *Module,
child: Type.Ref,
storage_class: spec.StorageClass,
alignment: u32,
) !Type.Ref {
const ptr_payload = try self.arena.create(Type.Payload.Pointer);
ptr_payload.* = .{
.storage_class = storage_class,
.child_type = child,
.alignment = alignment,
};
return try self.resolveType(Type.initPayload(&ptr_payload.base));
}
pub fn changePtrStorageClass(self: *Module, ptr_ty_ref: Type.Ref, new_storage_class: spec.StorageClass) !Type.Ref {
const payload = try self.arena.create(Type.Payload.Pointer);
payload.* = self.typeRefType(ptr_ty_ref).payload(.pointer).*;
payload.storage_class = new_storage_class;
return try self.resolveType(Type.initPayload(&payload.base));
}
pub fn emitConstant(
self: *Module,
ty_id: IdRef,
result_id: IdRef,
value: spec.LiteralContextDependentNumber,
) !void {
try self.sections.types_globals_constants.emit(self.gpa, .OpConstant, .{
.id_result_type = ty_id,
.id_result = result_id,
.value = value,
});
}
/// Decorate a result-id.
pub fn decorate(
self: *Module,
target: IdRef,
decoration: spec.Decoration.Extended,
) !void {
try self.sections.annotations.emit(self.gpa, .OpDecorate, .{
.target = target,
.decoration = decoration,
});
}
/// Decorate a result-id which is a member of some struct.
pub fn decorateMember(
self: *Module,
structure_type: IdRef,
member: u32,
decoration: spec.Decoration.Extended,
) !void {
try self.sections.annotations.emit(self.gpa, .OpMemberDecorate, .{
.structure_type = structure_type,
.member = member,
.decoration = decoration,
});
}
pub const DeclKind = enum {
func,
global,
};
pub fn allocDecl(self: *Module, kind: DeclKind) !Decl.Index {
try self.decls.append(self.gpa, .{
.result_id = self.allocId(),
.begin_dep = undefined,
.end_dep = undefined,
});
const index = @intToEnum(Decl.Index, @intCast(u32, self.decls.items.len - 1));
switch (kind) {
.func => {},
// If the decl represents a global, also allocate a global node.
.global => try self.globals.globals.putNoClobber(self.gpa, index, .{
.result_id = undefined,
.begin_inst = undefined,
.end_inst = undefined,
}),
}
return index;
}
pub fn declPtr(self: *Module, index: Decl.Index) *Decl {
return &self.decls.items[@enumToInt(index)];
}
pub fn globalPtr(self: *Module, index: Decl.Index) ?*Global {
return self.globals.globals.getPtr(index);
}
/// Declare ALL dependencies for a decl.
pub fn declareDeclDeps(self: *Module, decl_index: Decl.Index, deps: []const Decl.Index) !void {
const begin_dep = @intCast(u32, self.decl_deps.items.len);
try self.decl_deps.appendSlice(self.gpa, deps);
const end_dep = @intCast(u32, self.decl_deps.items.len);
const decl = self.declPtr(decl_index);
decl.begin_dep = begin_dep;
decl.end_dep = end_dep;
}
pub fn beginGlobal(self: *Module) u32 {
return @intCast(u32, self.globals.section.instructions.items.len);
}
pub fn endGlobal(self: *Module, global_index: Decl.Index, begin_inst: u32) void {
const global = self.globalPtr(global_index).?;
global.begin_inst = begin_inst;
global.end_inst = @intCast(u32, self.globals.section.instructions.items.len);
}
pub fn declareEntryPoint(self: *Module, decl_index: Decl.Index, name: []const u8) !void {
try self.entry_points.append(self.gpa, .{
.decl_index = decl_index,
.name = try self.arena.dupe(u8, name),
});
}

View File

@ -65,32 +65,23 @@ pub fn emit(
section.writeOperands(opcode.Operands(), operands);
}
/// Decorate a result-id.
pub fn decorate(
pub fn emitSpecConstantOp(
section: *Section,
allocator: Allocator,
target: spec.IdRef,
decoration: spec.Decoration.Extended,
comptime opcode: spec.Opcode,
operands: opcode.Operands(),
) !void {
try section.emit(allocator, .OpDecorate, .{
.target = target,
.decoration = decoration,
});
}
const word_count = operandsSize(opcode.Operands(), operands);
try section.emitRaw(allocator, .OpSpecConstantOp, 1 + word_count);
section.writeOperand(spec.IdRef, operands.id_result_type);
section.writeOperand(spec.IdRef, operands.id_result);
section.writeOperand(Opcode, opcode);
/// Decorate a result-id which is a member of some struct.
pub fn decorateMember(
section: *Section,
allocator: Allocator,
structure_type: spec.IdRef,
member: u32,
decoration: spec.Decoration.Extended,
) !void {
try section.emit(allocator, .OpMemberDecorate, .{
.structure_type = structure_type,
.member = member,
.decoration = decoration,
});
const fields = @typeInfo(opcode.Operands()).Struct.fields;
// First 2 fields are always id_result_type and id_result.
inline for (fields[2..]) |field| {
section.writeOperand(field.type, @field(operands, field.name));
}
}
pub fn writeWord(section: *Section, word: Word) void {
@ -122,7 +113,7 @@ fn writeOperands(section: *Section, comptime Operands: type, operands: Operands)
pub fn writeOperand(section: *Section, comptime Operand: type, operand: Operand) void {
switch (Operand) {
spec.IdResultType, spec.IdResult, spec.IdRef => section.writeWord(operand.id),
spec.IdResult => section.writeWord(operand.id),
spec.LiteralInteger => section.writeWord(operand),
@ -258,9 +249,7 @@ fn operandsSize(comptime Operands: type, operands: Operands) usize {
fn operandSize(comptime Operand: type, operand: Operand) usize {
return switch (Operand) {
spec.IdResultType,
spec.IdResult,
spec.IdRef,
spec.LiteralInteger,
spec.LiteralExtInstInteger,
=> 1,
@ -382,7 +371,9 @@ test "SPIR-V Section emit() - string" {
}, section.instructions.items);
}
test "SPIR-V Section emit()- extended mask" {
test "SPIR-V Section emit() - extended mask" {
if (@import("builtin").zig_backend == .stage1) return error.SkipZigTest;
var section = Section{};
defer section.deinit(std.testing.allocator);

View File

@ -3,22 +3,11 @@
const Version = @import("std").builtin.Version;
pub const Word = u32;
pub const IdResultType = struct {
id: Word,
pub fn toRef(self: IdResultType) IdRef {
return .{ .id = self.id };
}
};
pub const IdResult = struct {
id: Word,
pub fn toRef(self: IdResult) IdRef {
return .{ .id = self.id };
}
pub fn toResultType(self: IdResult) IdResultType {
return .{ .id = self.id };
}
};
pub const IdRef = struct { id: Word };
pub const IdResultType = IdResult;
pub const IdRef = IdResult;
pub const IdMemorySemantics = IdRef;
pub const IdScope = IdRef;

View File

@ -3,6 +3,8 @@
const std = @import("std");
const assert = std.debug.assert;
const Signedness = std.builtin.Signedness;
const Allocator = std.mem.Allocator;
const spec = @import("spec.zig");
@ -11,7 +13,7 @@ pub const Type = extern union {
ptr_otherwise: *Payload,
/// A reference to another SPIR-V type.
pub const Ref = usize;
pub const Ref = enum(u32) { _ };
pub fn initTag(comptime small_tag: Tag) Type {
comptime assert(@enumToInt(small_tag) < Tag.no_payload_count);
@ -23,6 +25,41 @@ pub const Type = extern union {
return .{ .ptr_otherwise = pl };
}
pub fn int(arena: Allocator, signedness: Signedness, bits: u16) !Type {
const bits_and_signedness = switch (signedness) {
.signed => -@as(i32, bits),
.unsigned => @as(i32, bits),
};
return switch (bits_and_signedness) {
8 => initTag(.u8),
16 => initTag(.u16),
32 => initTag(.u32),
64 => initTag(.u64),
-8 => initTag(.i8),
-16 => initTag(.i16),
-32 => initTag(.i32),
-64 => initTag(.i64),
else => {
const int_payload = try arena.create(Payload.Int);
int_payload.* = .{
.width = bits,
.signedness = signedness,
};
return initPayload(&int_payload.base);
},
};
}
pub fn float(bits: u16) Type {
return switch (bits) {
16 => initTag(.f16),
32 => initTag(.f32),
64 => initTag(.f64),
else => unreachable, // Enable more types if required.
};
}
pub fn tag(self: Type) Tag {
if (@enumToInt(self.tag_if_small_enough) < Tag.no_payload_count) {
return self.tag_if_small_enough;
@ -80,9 +117,19 @@ pub const Type = extern union {
.queue,
.pipe_storage,
.named_barrier,
.u8,
.u16,
.u32,
.u64,
.i8,
.i16,
.i32,
.i64,
.f16,
.f32,
.f64,
=> return true,
.int,
.float,
.vector,
.matrix,
.sampled_image,
@ -132,6 +179,17 @@ pub const Type = extern union {
.queue,
.pipe_storage,
.named_barrier,
.u8,
.u16,
.u32,
.u64,
.i8,
.i16,
.i32,
.i64,
.f16,
.f32,
.f64,
=> {},
else => self.hashPayload(@field(Tag, field.name), &hasher),
}
@ -185,6 +243,53 @@ pub const Type = extern union {
};
}
pub fn isInt(self: Type) bool {
return switch (self.tag()) {
.u8,
.u16,
.u32,
.u64,
.i8,
.i16,
.i32,
.i64,
.int,
=> true,
else => false,
};
}
pub fn isFloat(self: Type) bool {
return switch (self.tag()) {
.f16, .f32, .f64 => true,
else => false,
};
}
/// Returns the number of bits that make up an int or float type.
/// Asserts type is either int or float.
pub fn intFloatBits(self: Type) u16 {
return switch (self.tag()) {
.u8, .i8 => 8,
.u16, .i16, .f16 => 16,
.u32, .i32, .f32 => 32,
.u64, .i64, .f64 => 64,
.int => self.payload(.int).width,
else => unreachable,
};
}
/// Returns the signedness of an integer type.
/// Asserts that the type is an int.
pub fn intSignedness(self: Type) Signedness {
return switch (self.tag()) {
.u8, .u16, .u32, .u64 => .unsigned,
.i8, .i16, .i32, .i64 => .signed,
.int => self.payload(.int).signedness,
else => unreachable,
};
}
pub const Tag = enum(usize) {
void,
bool,
@ -195,10 +300,20 @@ pub const Type = extern union {
queue,
pipe_storage,
named_barrier,
u8,
u16,
u32,
u64,
i8,
i16,
i32,
i64,
f16,
f32,
f64,
// After this, the tag requires a payload.
int,
float,
vector,
matrix,
image,
@ -211,14 +326,33 @@ pub const Type = extern union {
function,
pipe,
pub const last_no_payload_tag = Tag.named_barrier;
pub const last_no_payload_tag = Tag.f64;
pub const no_payload_count = @enumToInt(last_no_payload_tag) + 1;
pub fn Type(comptime t: Tag) type {
return switch (t) {
.void, .bool, .sampler, .event, .device_event, .reserve_id, .queue, .pipe_storage, .named_barrier => @compileError("Type Tag " ++ @tagName(t) ++ " has no payload"),
.void,
.bool,
.sampler,
.event,
.device_event,
.reserve_id,
.queue,
.pipe_storage,
.named_barrier,
.u8,
.u16,
.u32,
.u64,
.i8,
.i16,
.i32,
.i64,
.f16,
.f32,
.f64,
=> @compileError("Type Tag " ++ @tagName(t) ++ " has no payload"),
.int => Payload.Int,
.float => Payload.Float,
.vector => Payload.Vector,
.matrix => Payload.Matrix,
.image => Payload.Image,
@ -239,13 +373,8 @@ pub const Type = extern union {
pub const Int = struct {
base: Payload = .{ .tag = .int },
width: u32,
signedness: std.builtin.Signedness,
};
pub const Float = struct {
base: Payload = .{ .tag = .float },
width: u32,
width: u16,
signedness: Signedness,
};
pub const Vector = struct {
@ -292,7 +421,7 @@ pub const Type = extern union {
length: u32,
/// Type has the 'ArrayStride' decoration.
/// If zero, no stride is present.
array_stride: u32,
array_stride: u32 = 0,
};
pub const RuntimeArray = struct {
@ -300,35 +429,39 @@ pub const Type = extern union {
element_type: Ref,
/// Type has the 'ArrayStride' decoration.
/// If zero, no stride is present.
array_stride: u32,
array_stride: u32 = 0,
};
pub const Struct = struct {
base: Payload = .{ .tag = .@"struct" },
members: []Member,
decorations: StructDecorations,
name: []const u8 = "",
decorations: StructDecorations = .{},
/// Extra information for decorations, packed for efficiency. Fields are stored sequentially by
/// order of the `members` slice and `MemberDecorations` struct.
member_decoration_extra: []u32,
member_decoration_extra: []u32 = &.{},
pub const Member = struct {
ty: Ref,
offset: u32,
decorations: MemberDecorations,
name: []const u8 = "",
offset: MemberOffset = .none,
decorations: MemberDecorations = .{},
};
pub const MemberOffset = enum(u32) { none = 0xFFFF_FFFF, _ };
pub const StructDecorations = packed struct {
/// Type has the 'Block' decoration.
block: bool,
block: bool = false,
/// Type has the 'BufferBlock' decoration.
buffer_block: bool,
buffer_block: bool = false,
/// Type has the 'GLSLShared' decoration.
glsl_shared: bool,
glsl_shared: bool = false,
/// Type has the 'GLSLPacked' decoration.
glsl_packed: bool,
glsl_packed: bool = false,
/// Type has the 'CPacked' decoration.
c_packed: bool,
c_packed: bool = false,
};
pub const MemberDecorations = packed struct {
@ -344,31 +477,31 @@ pub const Type = extern union {
col_major,
/// Member is not a matrix or array of matrices.
none,
},
} = .none,
// Regular decorations, these do not imply extra fields.
/// Member has the 'NoPerspective' decoration.
no_perspective: bool,
no_perspective: bool = false,
/// Member has the 'Flat' decoration.
flat: bool,
flat: bool = false,
/// Member has the 'Patch' decoration.
patch: bool,
patch: bool = false,
/// Member has the 'Centroid' decoration.
centroid: bool,
centroid: bool = false,
/// Member has the 'Sample' decoration.
sample: bool,
sample: bool = false,
/// Member has the 'Invariant' decoration.
/// Note: requires parent struct to have 'Block'.
invariant: bool,
invariant: bool = false,
/// Member has the 'Volatile' decoration.
@"volatile": bool,
@"volatile": bool = false,
/// Member has the 'Coherent' decoration.
coherent: bool,
coherent: bool = false,
/// Member has the 'NonWritable' decoration.
non_writable: bool,
non_writable: bool = false,
/// Member has the 'NonReadable' decoration.
non_readable: bool,
non_readable: bool = false,
// The following decorations all imply extra field(s).
@ -377,27 +510,27 @@ pub const Type = extern union {
/// Note: If any member of a struct has the BuiltIn decoration, all members must have one.
/// Note: Each builtin may only be reachable once for a particular entry point.
/// Note: The member type may be constrained by a particular built-in, defined in the client API specification.
builtin: bool,
builtin: bool = false,
/// Member has the 'Stream' decoration.
/// This member has an extra field of type `u32`.
stream: bool,
stream: bool = false,
/// Member has the 'Location' decoration.
/// This member has an extra field of type `u32`.
location: bool,
location: bool = false,
/// Member has the 'Component' decoration.
/// This member has an extra field of type `u32`.
component: bool,
component: bool = false,
/// Member has the 'XfbBuffer' decoration.
/// This member has an extra field of type `u32`.
xfb_buffer: bool,
xfb_buffer: bool = false,
/// Member has the 'XfbStride' decoration.
/// This member has an extra field of type `u32`.
xfb_stride: bool,
xfb_stride: bool = false,
/// Member has the 'UserSemantic' decoration.
/// This member has an extra field of type `[]u8`, which is encoded
/// by an `u32` containing the number of chars exactly, and then the string padded to
/// a multiple of 4 bytes with zeroes.
user_semantic: bool,
user_semantic: bool = false,
};
};
@ -413,11 +546,11 @@ pub const Type = extern union {
/// Type has the 'ArrayStride' decoration.
/// This is valid for pointers to elements of an array.
/// If zero, no stride is present.
array_stride: u32,
/// Type has the 'Alignment' decoration.
alignment: ?u32,
array_stride: u32 = 0,
/// If nonzero, type has the 'Alignment' decoration.
alignment: u32 = 0,
/// Type has the 'MaxByteOffset' decoration.
max_byte_offset: ?u32,
max_byte_offset: ?u32 = null,
};
pub const Function = struct {

View File

@ -1,7 +1,7 @@
//! NVidia PTX (Paralle Thread Execution)
//! https://docs.nvidia.com/cuda/parallel-thread-execution/index.html
//! For this we rely on the nvptx backend of LLVM
//! Kernel functions need to be marked both as "export" and "callconv(.PtxKernel)"
//! Kernel functions need to be marked both as "export" and "callconv(.Kernel)"
const NvPtx = @This();

View File

@ -44,34 +44,25 @@ const IdResult = spec.IdResult;
base: link.File,
/// This linker backend does not try to incrementally link output SPIR-V code.
/// Instead, it tracks all declarations in this table, and iterates over it
/// in the flush function.
decl_table: std.AutoArrayHashMapUnmanaged(Module.Decl.Index, DeclGenContext) = .{},
const DeclGenContext = struct {
air: Air,
air_arena: ArenaAllocator.State,
liveness: Liveness,
fn deinit(self: *DeclGenContext, gpa: Allocator) void {
self.air.deinit(gpa);
self.liveness.deinit(gpa);
self.air_arena.promote(gpa).deinit();
self.* = undefined;
}
};
spv: SpvModule,
spv_arena: ArenaAllocator,
decl_link: codegen.DeclLinkMap,
pub fn createEmpty(gpa: Allocator, options: link.Options) !*SpirV {
const spirv = try gpa.create(SpirV);
spirv.* = .{
const self = try gpa.create(SpirV);
self.* = .{
.base = .{
.tag = .spirv,
.options = options,
.file = null,
.allocator = gpa,
},
.spv = undefined,
.spv_arena = ArenaAllocator.init(gpa),
.decl_link = codegen.DeclLinkMap.init(self.base.allocator),
};
self.spv = SpvModule.init(gpa, self.spv_arena.allocator());
errdefer self.deinit();
// TODO: Figure out where to put all of these
switch (options.target.cpu.arch) {
@ -88,7 +79,7 @@ pub fn createEmpty(gpa: Allocator, options: link.Options) !*SpirV {
return error.TODOAbiNotSupported;
}
return spirv;
return self;
}
pub fn openPath(allocator: Allocator, sub_path: []const u8, options: link.Options) !*SpirV {
@ -107,44 +98,35 @@ pub fn openPath(allocator: Allocator, sub_path: []const u8, options: link.Option
}
pub fn deinit(self: *SpirV) void {
self.decl_table.deinit(self.base.allocator);
self.spv.deinit();
self.spv_arena.deinit();
self.decl_link.deinit();
}
pub fn updateFunc(self: *SpirV, module: *Module, func: *Module.Fn, air: Air, liveness: Liveness) !void {
if (build_options.skip_non_native) {
@panic("Attempted to compile for architecture that was disabled by build configuration");
}
_ = module;
// Keep track of all decls so we can iterate over them on flush().
const result = try self.decl_table.getOrPut(self.base.allocator, func.owner_decl);
if (result.found_existing) {
result.value_ptr.deinit(self.base.allocator);
var decl_gen = codegen.DeclGen.init(self.base.allocator, module, &self.spv, &self.decl_link);
defer decl_gen.deinit();
if (try decl_gen.gen(func.owner_decl, air, liveness)) |msg| {
try module.failed_decls.put(module.gpa, func.owner_decl, msg);
}
var arena = ArenaAllocator.init(self.base.allocator);
errdefer arena.deinit();
var new_air = try cloneAir(air, self.base.allocator, arena.allocator());
errdefer new_air.deinit(self.base.allocator);
var new_liveness = try cloneLiveness(liveness, self.base.allocator);
errdefer new_liveness.deinit(self.base.allocator);
result.value_ptr.* = .{
.air = new_air,
.air_arena = arena.state,
.liveness = new_liveness,
};
}
pub fn updateDecl(self: *SpirV, module: *Module, decl_index: Module.Decl.Index) !void {
if (build_options.skip_non_native) {
@panic("Attempted to compile for architecture that was disabled by build configuration");
}
_ = module;
// Keep track of all decls so we can iterate over them on flush().
_ = try self.decl_table.getOrPut(self.base.allocator, decl_index);
var decl_gen = codegen.DeclGen.init(self.base.allocator, module, &self.spv, &self.decl_link);
defer decl_gen.deinit();
if (try decl_gen.gen(decl_index, undefined, undefined)) |msg| {
try module.failed_decls.put(module.gpa, decl_index, msg);
}
}
pub fn updateDeclExports(
@ -153,20 +135,26 @@ pub fn updateDeclExports(
decl_index: Module.Decl.Index,
exports: []const *Module.Export,
) !void {
_ = self;
_ = module;
_ = decl_index;
_ = exports;
const decl = module.declPtr(decl_index);
if (decl.val.tag() == .function and decl.ty.fnCallingConvention() == .Kernel) {
// TODO: Unify with resolveDecl in spirv.zig.
const entry = try self.decl_link.getOrPut(decl_index);
if (!entry.found_existing) {
entry.value_ptr.* = try self.spv.allocDecl(.func);
}
const spv_decl_index = entry.value_ptr.*;
for (exports) |exp| {
try self.spv.declareEntryPoint(spv_decl_index, exp.options.name);
}
}
// TODO: Export regular functions, variables, etc using Linkage attributes.
}
pub fn freeDecl(self: *SpirV, decl_index: Module.Decl.Index) void {
if (self.decl_table.getIndex(decl_index)) |index| {
const module = self.base.options.module.?;
const decl = module.declPtr(decl_index);
if (decl.val.tag() == .function) {
self.decl_table.values()[index].deinit(self.base.allocator);
}
}
_ = self;
_ = decl_index;
}
pub fn flush(self: *SpirV, comp: *Compilation, prog_node: *std.Progress.Node) link.File.FlushError!void {
@ -189,60 +177,38 @@ pub fn flushModule(self: *SpirV, comp: *Compilation, prog_node: *std.Progress.No
sub_prog_node.activate();
defer sub_prog_node.end();
const module = self.base.options.module.?;
const target = comp.getTarget();
try writeCapabilities(&self.spv, target);
try writeMemoryModel(&self.spv, target);
var arena = std.heap.ArenaAllocator.init(self.base.allocator);
defer arena.deinit();
// We need to export the list of error names somewhere so that we can pretty-print them in the
// executor. This is not really an important thing though, so we can just dump it in any old
// nonsemantic instruction. For now, just put it in OpSourceExtension with a special name.
var spv = SpvModule.init(self.base.allocator, arena.allocator());
defer spv.deinit();
var error_info = std.ArrayList(u8).init(self.spv.arena);
try error_info.appendSlice("zig_errors");
const module = self.base.options.module.?;
for (module.error_name_list.items) |name| {
// Errors can contain pretty much any character - to encode them in a string we must escape
// them somehow. Easiest here is to use some established scheme, one which also preseves the
// name if it contains no strange characters is nice for debugging. URI encoding fits the bill.
// We're using : as separator, which is a reserved character.
// Allocate an ID for every declaration before generating code,
// so that we can access them before processing them.
// TODO: We're allocating an ID unconditionally now, are there
// declarations which don't generate a result?
var ids = std.AutoHashMap(Module.Decl.Index, IdResult).init(self.base.allocator);
defer ids.deinit();
try ids.ensureTotalCapacity(@intCast(u32, self.decl_table.count()));
for (self.decl_table.keys()) |decl_index| {
const decl = module.declPtr(decl_index);
if (decl.has_tv) {
ids.putAssumeCapacityNoClobber(decl_index, spv.allocId());
}
const escaped_name = try std.Uri.escapeString(self.base.allocator, name);
defer self.base.allocator.free(escaped_name);
try error_info.writer().print(":{s}", .{escaped_name});
}
try self.spv.sections.debug_strings.emit(self.spv.gpa, .OpSourceExtension, .{
.extension = error_info.items,
});
// Now, actually generate the code for all declarations.
var decl_gen = codegen.DeclGen.init(self.base.allocator, module, &spv, &ids);
defer decl_gen.deinit();
var it = self.decl_table.iterator();
while (it.next()) |entry| {
const decl_index = entry.key_ptr.*;
const decl = module.declPtr(decl_index);
if (!decl.has_tv) continue;
const air = entry.value_ptr.air;
const liveness = entry.value_ptr.liveness;
// Note, if `decl` is not a function, air/liveness may be undefined.
if (try decl_gen.gen(decl_index, air, liveness)) |msg| {
try module.failed_decls.put(module.gpa, decl_index, msg);
return; // TODO: Attempt to generate more decls?
}
}
try writeCapabilities(&spv, target);
try writeMemoryModel(&spv, target);
try spv.flush(self.base.file.?);
try self.spv.flush(self.base.file.?);
}
fn writeCapabilities(spv: *SpvModule, target: std.Target) !void {
// TODO: Integrate with a hypothetical feature system
const caps: []const spec.Capability = switch (target.os.tag) {
.opencl => &.{.Kernel},
.opencl => &.{ .Kernel, .Addresses, .Int8, .Int16, .Int64, .GenericPointer },
.glsl450 => &.{.Shader},
.vulkan => &.{.Shader},
else => unreachable, // TODO
@ -279,45 +245,3 @@ fn writeMemoryModel(spv: *SpvModule, target: std.Target) !void {
.memory_model = memory_model,
});
}
fn cloneLiveness(l: Liveness, gpa: Allocator) !Liveness {
const tomb_bits = try gpa.dupe(usize, l.tomb_bits);
errdefer gpa.free(tomb_bits);
const extra = try gpa.dupe(u32, l.extra);
errdefer gpa.free(extra);
return Liveness{
.tomb_bits = tomb_bits,
.extra = extra,
.special = try l.special.clone(gpa),
};
}
fn cloneAir(air: Air, gpa: Allocator, air_arena: Allocator) !Air {
const values = try gpa.alloc(Value, air.values.len);
errdefer gpa.free(values);
for (values, 0..) |*value, i| {
value.* = try air.values[i].copy(air_arena);
}
var instructions = try air.instructions.toMultiArrayList().clone(gpa);
errdefer instructions.deinit(gpa);
const air_tags = instructions.items(.tag);
const air_datas = instructions.items(.data);
for (air_tags, 0..) |tag, i| {
switch (tag) {
.alloc, .ret_ptr, .const_ty => air_datas[i].ty = try air_datas[i].ty.copy(air_arena),
else => {},
}
}
return Air{
.instructions = instructions.slice(),
.extra = try gpa.dupe(u32, air.extra),
.values = values,
};
}

View File

@ -163,7 +163,7 @@ pub fn canBuildLibC(target: std.Target) bool {
pub fn cannotDynamicLink(target: std.Target) bool {
return switch (target.os.tag) {
.freestanding, .other => true,
else => false,
else => target.isSpirV(),
};
}
@ -331,18 +331,18 @@ pub fn supportsStackProbing(target: std.Target) bool {
}
pub fn supportsStackProtector(target: std.Target) bool {
_ = target;
return true;
return !target.isSpirV();
}
pub fn libcProvidesStackProtector(target: std.Target) bool {
return !target.isMinGW() and target.os.tag != .wasi;
return !target.isMinGW() and target.os.tag != .wasi and !target.isSpirV();
}
pub fn supportsReturnAddress(target: std.Target) bool {
return switch (target.cpu.arch) {
.wasm32, .wasm64 => target.os.tag == .emscripten,
.bpfel, .bpfeb => false,
.spirv32, .spirv64 => false,
else => true,
};
}

View File

@ -4796,9 +4796,12 @@ pub const Type = extern union {
}
/// Asserts the type is a function.
pub fn fnCallingConventionAllowsZigTypes(cc: std.builtin.CallingConvention) bool {
pub fn fnCallingConventionAllowsZigTypes(target: Target, cc: std.builtin.CallingConvention) bool {
return switch (cc) {
.Unspecified, .Async, .Inline, .PtxKernel => true,
.Unspecified, .Async, .Inline => true,
// For now we want to authorize PTX kernel to use zig objects, even if we end up exposing the ABI.
// The goal is to experiment with more integrated CPU/GPU code.
.Kernel => target.cpu.arch == .nvptx or target.cpu.arch == .nvptx64,
else => false,
};
}

View File

@ -10,7 +10,7 @@ pub fn addCases(ctx: *Cases) !void {
\\ return a + b;
\\}
\\
\\pub export fn add_and_substract(a: i32, out: *i32) callconv(.PtxKernel) void {
\\pub export fn add_and_substract(a: i32, out: *i32) callconv(.Kernel) void {
\\ const x = add(a, 7);
\\ var y = add(2, 0);
\\ y -= x;
@ -29,7 +29,7 @@ pub fn addCases(ctx: *Cases) !void {
\\ );
\\}
\\
\\pub export fn special_reg(a: []const i32, out: []i32) callconv(.PtxKernel) void {
\\pub export fn special_reg(a: []const i32, out: []i32) callconv(.Kernel) void {
\\ const i = threadIdX();
\\ out[i] = a[i] + 7;
\\}
@ -42,7 +42,7 @@ pub fn addCases(ctx: *Cases) !void {
case.addCompile(
\\var x: i32 addrspace(.global) = 0;
\\
\\pub export fn increment(out: *i32) callconv(.PtxKernel) void {
\\pub export fn increment(out: *i32) callconv(.Kernel) void {
\\ x += 1;
\\ out.* = x;
\\}
@ -59,7 +59,7 @@ pub fn addCases(ctx: *Cases) !void {
\\}
\\
\\ var _sdata: [1024]f32 addrspace(.shared) = undefined;
\\ pub export fn reduceSum(d_x: []const f32, out: *f32) callconv(.PtxKernel) void {
\\ pub export fn reduceSum(d_x: []const f32, out: *f32) callconv(.Kernel) void {
\\ var sdata = @addrSpaceCast(.generic, &_sdata);
\\ const tid: u32 = threadIdX();
\\ var sum = d_x[tid];

View File

@ -80,22 +80,11 @@ fn render(writer: anytype, allocator: Allocator, registry: g.CoreRegistry) !void
\\const Version = @import("std").builtin.Version;
\\
\\pub const Word = u32;
\\pub const IdResultType = struct{
\\ id: Word,
\\ pub fn toRef(self: IdResultType) IdRef {
\\ return .{.id = self.id};
\\ }
\\};
\\pub const IdResult = struct{
\\ id: Word,
\\ pub fn toRef(self: IdResult) IdRef {
\\ return .{.id = self.id};
\\ }
\\ pub fn toResultType(self: IdResult) IdResultType {
\\ return .{.id = self.id};
\\ }
\\};
\\pub const IdRef = struct{ id: Word };
\\pub const IdResultType = IdResult;
\\pub const IdRef = IdResult;
\\
\\pub const IdMemorySemantics = IdRef;
\\pub const IdScope = IdRef;