Merge pull request #7827 from Snektron/spirv-setup

Stage 2: SPIR-V setup
This commit is contained in:
Andrew Kelley 2021-02-01 12:49:51 -08:00 committed by GitHub
commit 102d954220
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 2258 additions and 8 deletions

View File

@ -57,6 +57,9 @@ pub const Target = struct {
wasi,
emscripten,
uefi,
opencl,
glsl450,
vulkan,
other,
pub fn isDarwin(tag: Tag) bool {
@ -248,6 +251,9 @@ pub const Target = struct {
.wasi,
.emscripten,
.uefi,
.opencl, // TODO: OpenCL versions
.glsl450, // TODO: GLSL versions
.vulkan,
.other,
=> return .{ .none = {} },
@ -403,6 +409,9 @@ pub const Target = struct {
.wasi,
.emscripten,
.uefi,
.opencl,
.glsl450,
.vulkan,
.other,
=> false,
};
@ -421,6 +430,7 @@ pub const Target = struct {
pub const powerpc = @import("target/powerpc.zig");
pub const riscv = @import("target/riscv.zig");
pub const sparc = @import("target/sparc.zig");
pub const spirv = @import("target/spirv.zig");
pub const systemz = @import("target/systemz.zig");
pub const wasm = @import("target/wasm.zig");
pub const x86 = @import("target/x86.zig");
@ -493,6 +503,10 @@ pub const Target = struct {
.wasi,
.emscripten,
=> return .musl,
.opencl, // TODO: SPIR-V ABIs with Linkage capability
.glsl450,
.vulkan,
=> return .none,
}
}
@ -528,6 +542,7 @@ pub const Target = struct {
macho,
wasm,
c,
spirv,
hex,
raw,
};
@ -744,6 +759,8 @@ pub const Target = struct {
// Stage1 currently assumes that architectures above this comment
// map one-to-one with the ZigLLVM_ArchType enum.
spu_2,
spirv32,
spirv64,
pub fn isARM(arch: Arch) bool {
return switch (arch) {
@ -857,6 +874,8 @@ pub const Target = struct {
.s390x => ._S390,
.ve => ._NONE,
.spu_2 => ._SPU_2,
.spirv32 => ._NONE,
.spirv64 => ._NONE,
};
}
@ -914,6 +933,8 @@ pub const Target = struct {
.s390x => .Unknown,
.ve => .Unknown,
.spu_2 => .Unknown,
.spirv32 => .Unknown,
.spirv64 => .Unknown,
};
}
@ -957,6 +978,9 @@ pub const Target = struct {
.shave,
.ve,
.spu_2,
// GPU bitness is opaque. For now, assume little endian.
.spirv32,
.spirv64,
=> .Little,
.arc,
@ -1012,6 +1036,7 @@ pub const Target = struct {
.wasm32,
.renderscript32,
.aarch64_32,
.spirv32,
=> return 32,
.aarch64,
@ -1035,6 +1060,7 @@ pub const Target = struct {
.sparcv9,
.s390x,
.ve,
.spirv64,
=> return 64,
}
}
@ -1057,6 +1083,7 @@ pub const Target = struct {
.i386, .x86_64 => "x86",
.nvptx, .nvptx64 => "nvptx",
.wasm32, .wasm64 => "wasm",
.spirv32, .spirv64 => "spir-v",
else => @tagName(arch),
};
}
@ -1347,6 +1374,9 @@ pub const Target = struct {
.uefi,
.windows,
.emscripten,
.opencl,
.glsl450,
.vulkan,
.other,
=> return false,
else => return true,
@ -1482,6 +1512,8 @@ pub const Target = struct {
.nvptx64,
.spu_2,
.avr,
.spirv32,
.spirv64,
=> return result,
// TODO go over each item in this list and either move it to the above list, or
@ -1524,6 +1556,9 @@ pub const Target = struct {
.windows,
.emscripten,
.wasi,
.opencl,
.glsl450,
.vulkan,
.other,
=> return result,

View File

@ -141,6 +141,7 @@ pub fn binNameAlloc(allocator: *std.mem.Allocator, options: BinNameOptions) erro
.Lib => return std.fmt.allocPrint(allocator, "{s}.wasm", .{root_name}),
},
.c => return std.fmt.allocPrint(allocator, "{s}.c", .{root_name}),
.spirv => return std.fmt.allocPrint(allocator, "{s}.spv", .{root_name}),
.hex => return std.fmt.allocPrint(allocator, "{s}.ihex", .{root_name}),
.raw => return std.fmt.allocPrint(allocator, "{s}.bin", .{root_name}),
}

View File

@ -130,6 +130,9 @@ pub const CrossTarget = struct {
.wasi,
.emscripten,
.uefi,
.opencl,
.glsl450,
.vulkan,
.other,
=> {
self.os_version_min = .{ .none = {} };
@ -730,6 +733,9 @@ pub const CrossTarget = struct {
.wasi,
.emscripten,
.uefi,
.opencl,
.glsl450,
.vulkan,
.other,
=> return error.InvalidOperatingSystemVersion,

View File

@ -1673,7 +1673,7 @@ pub fn analyzeContainer(self: *Module, container_scope: *Scope.Container) !void
// in `Decl` to notice that the line number did not change.
self.comp.work_queue.writeItemAssumeCapacity(.{ .update_line_number = decl });
},
.c, .wasm => {},
.c, .wasm, .spirv => {},
}
}
} else {
@ -1906,6 +1906,7 @@ fn allocateNewDecl(
.macho => .{ .macho = link.File.MachO.TextBlock.empty },
.c => .{ .c = link.File.C.DeclBlock.empty },
.wasm => .{ .wasm = {} },
.spirv => .{ .spirv = {} },
},
.fn_link = switch (mod.comp.bin_file.tag) {
.coff => .{ .coff = {} },
@ -1913,6 +1914,7 @@ fn allocateNewDecl(
.macho => .{ .macho = link.File.MachO.SrcFn.empty },
.c => .{ .c = link.File.C.FnBlock.empty },
.wasm => .{ .wasm = null },
.spirv => .{ .spirv = .{} },
},
.generation = 0,
.is_pub = false,
@ -2010,6 +2012,7 @@ pub fn analyzeExport(
.macho => .{ .macho = link.File.MachO.Export{} },
.c => .{ .c = {} },
.wasm => .{ .wasm = {} },
.spirv => .{ .spirv = {} },
},
.owner_decl = owner_decl,
.exported_decl = exported_decl,

View File

@ -69,6 +69,8 @@ pub fn targetTriple(allocator: *Allocator, target: std.Target) ![:0]u8 {
.renderscript64 => "renderscript64",
.ve => "ve",
.spu_2 => return error.LLVMBackendDoesNotSupportSPUMarkII,
.spirv32 => return error.LLVMBackendDoesNotSupportSPIRV,
.spirv64 => return error.LLVMBackendDoesNotSupportSPIRV,
};
// TODO Add a sub-arch for some architectures depending on CPU features.
@ -109,6 +111,9 @@ pub fn targetTriple(allocator: *Allocator, target: std.Target) ![:0]u8 {
.wasi => "wasi",
.emscripten => "emscripten",
.uefi => "windows",
.opencl => return error.LLVMBackendDoesNotSupportOpenCL,
.glsl450 => return error.LLVMBackendDoesNotSupportGLSL450,
.vulkan => return error.LLVMBackendDoesNotSupportVulkan,
.other => "unknown",
};

51
src/codegen/spirv.zig Normal file
View File

@ -0,0 +1,51 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const spec = @import("spirv/spec.zig");
const Module = @import("../Module.zig");
const Decl = Module.Decl;
pub fn writeInstruction(code: *std.ArrayList(u32), instr: spec.Opcode, args: []const u32) !void {
const word_count = @intCast(u32, args.len + 1);
try code.append((word_count << 16) | @enumToInt(instr));
try code.appendSlice(args);
}
pub const SPIRVModule = struct {
next_id: u32 = 0,
free_id_list: std.ArrayList(u32),
pub fn init(allocator: *Allocator) SPIRVModule {
return .{
.free_id_list = std.ArrayList(u32).init(allocator),
};
}
pub fn deinit(self: *SPIRVModule) void {
self.free_id_list.deinit();
}
pub fn allocId(self: *SPIRVModule) u32 {
if (self.free_id_list.popOrNull()) |id| return id;
defer self.next_id += 1;
return self.next_id;
}
pub fn freeId(self: *SPIRVModule, id: u32) void {
if (id + 1 == self.next_id) {
self.next_id -= 1;
} else {
// If no more memory to append the id to the free list, just ignore it.
self.free_id_list.append(id) catch {};
}
}
pub fn idBound(self: *SPIRVModule) u32 {
return self.next_id;
}
pub fn genDecl(self: SPIRVModule, id: u32, code: *std.ArrayList(u32), decl: *Decl) !void {
}
};

1645
src/codegen/spirv/spec.zig Normal file

File diff suppressed because it is too large Load Diff

View File

@ -139,6 +139,7 @@ pub const File = struct {
macho: MachO.TextBlock,
c: C.DeclBlock,
wasm: void,
spirv: void,
};
pub const LinkFn = union {
@ -147,6 +148,7 @@ pub const File = struct {
macho: MachO.SrcFn,
c: C.FnBlock,
wasm: ?Wasm.FnData,
spirv: SpirV.FnData,
};
pub const Export = union {
@ -155,6 +157,7 @@ pub const File = struct {
macho: MachO.Export,
c: void,
wasm: void,
spirv: void,
};
/// For DWARF .debug_info.
@ -183,6 +186,7 @@ pub const File = struct {
.macho => &(try MachO.createEmpty(allocator, options)).base,
.wasm => &(try Wasm.createEmpty(allocator, options)).base,
.c => unreachable, // Reported error earlier.
.spirv => &(try SpirV.createEmpty(allocator, options)).base,
.hex => return error.HexObjectFormatUnimplemented,
.raw => return error.RawObjectFormatUnimplemented,
};
@ -198,6 +202,7 @@ pub const File = struct {
.macho => &(try MachO.createEmpty(allocator, options)).base,
.wasm => &(try Wasm.createEmpty(allocator, options)).base,
.c => unreachable, // Reported error earlier.
.spirv => &(try SpirV.createEmpty(allocator, options)).base,
.hex => return error.HexObjectFormatUnimplemented,
.raw => return error.RawObjectFormatUnimplemented,
};
@ -213,6 +218,7 @@ pub const File = struct {
.macho => &(try MachO.openPath(allocator, sub_path, options)).base,
.wasm => &(try Wasm.openPath(allocator, sub_path, options)).base,
.c => &(try C.openPath(allocator, sub_path, options)).base,
.spirv => &(try SpirV.openPath(allocator, sub_path, options)).base,
.hex => return error.HexObjectFormatUnimplemented,
.raw => return error.RawObjectFormatUnimplemented,
};
@ -242,7 +248,7 @@ pub const File = struct {
.mode = determineMode(base.options),
});
},
.c, .wasm => {},
.c, .wasm, .spirv => {},
}
}
@ -287,7 +293,7 @@ pub const File = struct {
f.close();
base.file = null;
},
.c, .wasm => {},
.c, .wasm, .spirv => {},
}
}
@ -300,6 +306,7 @@ pub const File = struct {
.macho => return @fieldParentPtr(MachO, "base", base).updateDecl(module, decl),
.c => return @fieldParentPtr(C, "base", base).updateDecl(module, decl),
.wasm => return @fieldParentPtr(Wasm, "base", base).updateDecl(module, decl),
.spirv => return @fieldParentPtr(SpirV, "base", base).updateDecl(module, decl),
}
}
@ -309,7 +316,7 @@ pub const File = struct {
.elf => return @fieldParentPtr(Elf, "base", base).updateDeclLineNumber(module, decl),
.macho => return @fieldParentPtr(MachO, "base", base).updateDeclLineNumber(module, decl),
.c => return @fieldParentPtr(C, "base", base).updateDeclLineNumber(module, decl),
.wasm => {},
.wasm, .spirv => {},
}
}
@ -321,7 +328,7 @@ pub const File = struct {
.elf => return @fieldParentPtr(Elf, "base", base).allocateDeclIndexes(decl),
.macho => return @fieldParentPtr(MachO, "base", base).allocateDeclIndexes(decl),
.c => return @fieldParentPtr(C, "base", base).allocateDeclIndexes(decl),
.wasm => {},
.wasm, .spirv => {},
}
}
@ -368,6 +375,11 @@ pub const File = struct {
parent.deinit();
base.allocator.destroy(parent);
},
.spirv => {
const parent = @fieldParentPtr(SpirV, "base", base);
parent.deinit();
base.allocator.destroy(parent);
},
}
}
@ -401,6 +413,7 @@ pub const File = struct {
.macho => return @fieldParentPtr(MachO, "base", base).flush(comp),
.c => return @fieldParentPtr(C, "base", base).flush(comp),
.wasm => return @fieldParentPtr(Wasm, "base", base).flush(comp),
.spirv => return @fieldParentPtr(SpirV, "base", base).flush(comp),
}
}
@ -413,6 +426,7 @@ pub const File = struct {
.macho => return @fieldParentPtr(MachO, "base", base).flushModule(comp),
.c => return @fieldParentPtr(C, "base", base).flushModule(comp),
.wasm => return @fieldParentPtr(Wasm, "base", base).flushModule(comp),
.spirv => return @fieldParentPtr(SpirV, "base", base).flushModule(comp),
}
}
@ -424,6 +438,7 @@ pub const File = struct {
.macho => @fieldParentPtr(MachO, "base", base).freeDecl(decl),
.c => @fieldParentPtr(C, "base", base).freeDecl(decl),
.wasm => @fieldParentPtr(Wasm, "base", base).freeDecl(decl),
.spirv => @fieldParentPtr(SpirV, "base", base).freeDecl(decl),
}
}
@ -433,7 +448,7 @@ pub const File = struct {
.elf => return @fieldParentPtr(Elf, "base", base).error_flags,
.macho => return @fieldParentPtr(MachO, "base", base).error_flags,
.c => return .{ .no_entry_point_found = false },
.wasm => return ErrorFlags{},
.wasm, .spirv => return ErrorFlags{},
}
}
@ -451,6 +466,7 @@ pub const File = struct {
.macho => return @fieldParentPtr(MachO, "base", base).updateDeclExports(module, decl, exports),
.c => return @fieldParentPtr(C, "base", base).updateDeclExports(module, decl, exports),
.wasm => return @fieldParentPtr(Wasm, "base", base).updateDeclExports(module, decl, exports),
.spirv => return @fieldParentPtr(SpirV, "base", base).updateDeclExports(module, decl, exports),
}
}
@ -461,6 +477,7 @@ pub const File = struct {
.macho => return @fieldParentPtr(MachO, "base", base).getDeclVAddr(decl),
.c => unreachable,
.wasm => unreachable,
.spirv => unreachable,
}
}
@ -601,6 +618,7 @@ pub const File = struct {
macho,
c,
wasm,
spirv,
};
pub const ErrorFlags = struct {
@ -611,6 +629,7 @@ pub const File = struct {
pub const Coff = @import("link/Coff.zig");
pub const Elf = @import("link/Elf.zig");
pub const MachO = @import("link/MachO.zig");
pub const SpirV = @import("link/SpirV.zig");
pub const Wasm = @import("link/Wasm.zig");
};

234
src/link/SpirV.zig Normal file
View File

@ -0,0 +1,234 @@
const SpirV = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const Module = @import("../Module.zig");
const Compilation = @import("../Compilation.zig");
const link = @import("../link.zig");
const codegen = @import("../codegen/spirv.zig");
const trace = @import("../tracy.zig").trace;
const build_options = @import("build_options");
const spec = @import("../codegen/spirv/spec.zig");
//! SPIR-V Spec documentation: https://www.khronos.org/registry/spir-v/specs/unified1/SPIRV.html
//! According to above documentation, a SPIR-V module has the following logical layout:
//! Header.
//! OpCapability instructions.
//! OpExtension instructions.
//! OpExtInstImport instructions.
//! A single OpMemoryModel instruction.
//! All entry points, declared with OpEntryPoint instructions.
//! All execution-mode declarators; OpExecutionMode and OpExecutionModeId instructions.
//! Debug instructions:
//! - First, OpString, OpSourceExtension, OpSource, OpSourceContinued (no forward references).
//! - OpName and OpMemberName instructions.
//! - OpModuleProcessed instructions.
//! All annotation (decoration) instructions.
//! All type declaration instructions, constant instructions, global variable declarations, (preferrably) OpUndef instructions.
//! All function declarations without a body (extern functions presumably).
//! All regular functions.
pub const FnData = struct {
id: ?u32 = null,
code: std.ArrayListUnmanaged(u32) = .{},
};
base: link.File,
// TODO: Does this file need to support multiple independent modules?
spirv_module: codegen.SPIRVModule,
pub fn createEmpty(gpa: *Allocator, options: link.Options) !*SpirV {
const spirv = try gpa.create(SpirV);
spirv.* = .{
.base = .{
.tag = .spirv,
.options = options,
.file = null,
.allocator = gpa,
},
.spirv_module = codegen.SPIRVModule.init(gpa),
};
// TODO: Figure out where to put all of these
switch (options.target.cpu.arch) {
.spirv32, .spirv64 => {},
else => return error.TODOArchNotSupported,
}
switch (options.target.os.tag) {
.opencl, .glsl450, .vulkan => {},
else => return error.TODOOsNotSupported,
}
if (options.target.abi != .none) {
return error.TODOAbiNotSupported;
}
return spirv;
}
pub fn openPath(allocator: *Allocator, sub_path: []const u8, options: link.Options) !*SpirV {
assert(options.object_format == .spirv);
if (options.use_llvm) return error.LLVM_BackendIsTODO_ForSpirV; // TODO: LLVM Doesn't support SpirV at all.
if (options.use_lld) return error.LLD_LinkingIsTODO_ForSpirV; // TODO: LLD Doesn't support SpirV at all.
// TODO: read the file and keep vaild parts instead of truncating
const file = try options.emit.?.directory.handle.createFile(sub_path, .{ .truncate = true, .read = true });
errdefer file.close();
const spirv = try createEmpty(allocator, options);
errdefer spirv.base.destroy();
spirv.base.file = file;
return spirv;
}
pub fn deinit(self: *SpirV) void {
self.spirv_module.deinit();
}
pub fn updateDecl(self: *SpirV, module: *Module, decl: *Module.Decl) !void {
const tracy = trace(@src());
defer tracy.end();
const fn_data = &decl.fn_link.spirv;
if (fn_data.id == null) {
fn_data.id = self.spirv_module.allocId();
}
var managed_code = fn_data.code.toManaged(self.base.allocator);
managed_code.items.len = 0;
try self.spirv_module.genDecl(fn_data.id.?, &managed_code, decl);
fn_data.code = managed_code.toUnmanaged();
// Free excess allocated memory for this Decl.
fn_data.code.shrinkAndFree(self.base.allocator, fn_data.code.items.len);
}
pub fn updateDeclExports(
self: *SpirV,
module: *Module,
decl: *const Module.Decl,
exports: []const *Module.Export,
) !void {}
pub fn freeDecl(self: *SpirV, decl: *Module.Decl) void {
var fn_data = decl.fn_link.spirv;
fn_data.code.deinit(self.base.allocator);
if (fn_data.id) |id| self.spirv_module.freeId(id);
decl.fn_link.spirv = undefined;
}
pub fn flush(self: *SpirV, comp: *Compilation) !void {
if (build_options.have_llvm and self.base.options.use_lld) {
return error.LLD_LinkingIsTODO_ForSpirV; // TODO: LLD Doesn't support SpirV at all.
} else {
return self.flushModule(comp);
}
}
pub fn flushModule(self: *SpirV, comp: *Compilation) !void {
const tracy = trace(@src());
defer tracy.end();
const module = self.base.options.module.?;
const target = comp.getTarget();
var binary = std.ArrayList(u32).init(self.base.allocator);
defer binary.deinit();
// Note: The order of adding sections to the final binary
// follows the SPIR-V logical module format!
try binary.appendSlice(&[_]u32{
spec.magic_number,
(spec.version.major << 16) | (spec.version.minor << 8),
0, // TODO: Register Zig compiler magic number.
self.spirv_module.idBound(),
0, // Schema (currently reserved for future use in the SPIR-V spec).
});
try writeCapabilities(&binary, target);
try writeMemoryModel(&binary, target);
// Collect list of buffers to write.
// SPIR-V files support both little and big endian words. The actual format is
// disambiguated by the magic number, and so theoretically we don't need to worry
// about endian-ness when writing the final binary.
var all_buffers = std.ArrayList(std.os.iovec_const).init(self.base.allocator);
defer all_buffers.deinit();
// Pre-allocate enough for the binary info + all functions
try all_buffers.ensureCapacity(module.decl_table.count() + 1);
all_buffers.appendAssumeCapacity(wordsToIovConst(binary.items));
for (module.decl_table.items()) |entry| {
const decl = entry.value;
switch (decl.typed_value) {
.most_recent => |tvm| {
const fn_data = &decl.fn_link.spirv;
all_buffers.appendAssumeCapacity(wordsToIovConst(fn_data.code.items));
},
.never_succeeded => continue,
}
}
var file_size: u64 = 0;
for (all_buffers.items) |iov| {
file_size += iov.iov_len;
}
const file = self.base.file.?;
try file.seekTo(0);
try file.setEndPos(file_size);
try file.pwritevAll(all_buffers.items, 0);
}
fn writeCapabilities(binary: *std.ArrayList(u32), target: std.Target) !void {
// TODO: Integrate with a hypothetical feature system
const cap: spec.Capability = switch (target.os.tag) {
.opencl => .Kernel,
.glsl450 => .Shader,
.vulkan => .VulkanMemoryModel,
else => unreachable, // TODO
};
try codegen.writeInstruction(binary, .OpCapability, &[_]u32{ @enumToInt(cap) });
}
fn writeMemoryModel(binary: *std.ArrayList(u32), target: std.Target) !void {
const addressing_model = switch (target.os.tag) {
.opencl => switch (target.cpu.arch) {
.spirv32 => spec.AddressingModel.Physical32,
.spirv64 => spec.AddressingModel.Physical64,
else => unreachable, // TODO
},
.glsl450, .vulkan => spec.AddressingModel.Logical,
else => unreachable, // TODO
};
const memory_model: spec.MemoryModel = switch (target.os.tag) {
.opencl => .OpenCL,
.glsl450 => .GLSL450,
.vulkan => .Vulkan,
else => unreachable,
};
try codegen.writeInstruction(binary, .OpMemoryModel, &[_]u32{
@enumToInt(addressing_model), @enumToInt(memory_model)
});
}
fn wordsToIovConst(words: []const u32) std.os.iovec_const {
const bytes = std.mem.sliceAsBytes(words);
return .{
.iov_base = bytes.ptr,
.iov_len = bytes.len,
};
}

View File

@ -312,6 +312,7 @@ const usage_build_generic =
\\ pe Portable Executable (Windows)
\\ coff Common Object File Format (Windows)
\\ macho macOS relocatables
\\ spirv Standard, Portable Intermediate Representation V (SPIR-V)
\\ hex (planned) Intel IHEX
\\ raw (planned) Dump machine code directly
\\ -dirafter [dir] Add directory to AFTER include search path
@ -1588,6 +1589,8 @@ fn buildOutputType(
break :blk .hex;
} else if (mem.eql(u8, ofmt, "raw")) {
break :blk .raw;
} else if (mem.eql(u8, ofmt, "spirv")) {
break :blk .spirv;
} else {
fatal("unsupported object format: {s}", .{ofmt});
}

View File

@ -189,7 +189,7 @@ pub fn supportsStackProbing(target: std.Target) bool {
pub fn osToLLVM(os_tag: std.Target.Os.Tag) llvm.OSType {
return switch (os_tag) {
.freestanding, .other => .UnknownOS,
.freestanding, .other, .opencl, .glsl450, .vulkan => .UnknownOS,
.windows, .uefi => .Win32,
.ananas => .Ananas,
.cloudabi => .CloudABI,
@ -280,7 +280,7 @@ pub fn archToLLVM(arch_tag: std.Target.Cpu.Arch) llvm.ArchType {
.renderscript32 => .renderscript32,
.renderscript64 => .renderscript64,
.ve => .ve,
.spu_2 => .UnknownArch,
.spu_2, .spirv32, .spirv64 => .UnknownArch,
};
}

View File

@ -3597,6 +3597,9 @@ pub const CType = enum {
.amdpal,
.hermit,
.hurd,
.opencl,
.glsl450,
.vulkan,
=> @panic("TODO specify the C integer and float type sizes for this OS"),
}
}

245
tools/gen_spirv_spec.zig Normal file
View File

@ -0,0 +1,245 @@
const std = @import("std");
const Writer = std.ArrayList(u8).Writer;
//! See https://www.khronos.org/registry/spir-v/specs/unified1/MachineReadableGrammar.html
//! and the files in https://github.com/KhronosGroup/SPIRV-Headers/blob/master/include/spirv/unified1/
//! Note: Non-canonical casing in these structs used to match SPIR-V spec json.
const Registry = union(enum) {
core: CoreRegistry,
extension: ExtensionRegistry,
};
const CoreRegistry = struct {
copyright: [][]const u8,
/// Hexadecimal representation of the magic number
magic_number: []const u8,
major_version: u32,
minor_version: u32,
revision: u32,
instruction_printing_class: []InstructionPrintingClass,
instructions: []Instruction,
operand_kinds: []OperandKind,
};
const ExtensionRegistry = struct {
copyright: [][]const u8,
version: u32,
revision: u32,
instructions: []Instruction,
operand_kinds: []OperandKind = &[_]OperandKind{},
};
const InstructionPrintingClass = struct {
tag: []const u8,
heading: ?[]const u8 = null,
};
const Instruction = struct {
opname: []const u8,
class: ?[]const u8 = null, // Note: Only available in the core registry.
opcode: u32,
operands: []Operand = &[_]Operand{},
capabilities: [][]const u8 = &[_][]const u8{},
extensions: [][]const u8 = &[_][]const u8{},
version: ?[]const u8 = null,
lastVersion: ?[]const u8 = null,
};
const Operand = struct {
kind: []const u8,
/// If this field is 'null', the operand is only expected once.
quantifier: ?Quantifier = null,
name: []const u8 = "",
};
const Quantifier = enum {
/// zero or once
@"?",
/// zero or more
@"*",
};
const OperandCategory = enum {
BitEnum,
ValueEnum,
Id,
Literal,
Composite,
};
const OperandKind = struct {
category: OperandCategory,
/// The name
kind: []const u8,
doc: ?[]const u8 = null,
enumerants: ?[]Enumerant = null,
bases: ?[]const []const u8 = null,
};
const Enumerant = struct {
enumerant: []const u8,
value: union(enum) {
bitflag: []const u8, // Hexadecimal representation of the value
int: u31,
},
capabilities: [][]const u8 = &[_][]const u8{},
/// Valid for .ValueEnum and .BitEnum
extensions: [][]const u8 = &[_][]const u8{},
/// `quantifier` will always be `null`.
parameters: []Operand = &[_]Operand{},
version: ?[]const u8 = null,
lastVersion: ?[]const u8 = null,
};
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = &arena.allocator;
const args = try std.process.argsAlloc(allocator);
if (args.len != 2) {
usageAndExit(std.io.getStdErr(), args[0], 1);
}
const spec_path = args[1];
const spec = try std.fs.cwd().readFileAlloc(allocator, spec_path, std.math.maxInt(usize));
var tokens = std.json.TokenStream.init(spec);
var registry = try std.json.parse(Registry, &tokens, .{.allocator = allocator});
var buf = std.ArrayList(u8).init(allocator);
defer buf.deinit();
try render(buf.writer(), registry);
const tree = try std.zig.parse(allocator, buf.items);
_ = try std.zig.render(allocator, std.io.getStdOut().writer(), tree);
}
fn render(writer: Writer, registry: Registry) !void {
switch (registry) {
.core => |core_reg| {
try renderCopyRight(writer, core_reg.copyright);
try writer.print(
\\const Version = @import("builtin").Version;
\\pub const version = Version{{.major = {}, .minor = {}, .patch = {}}};
\\pub const magic_number: u32 = {s};
\\
, .{ core_reg.major_version, core_reg.minor_version, core_reg.revision, core_reg.magic_number },
);
try renderOpcodes(writer, core_reg.instructions);
try renderOperandKinds(writer, core_reg.operand_kinds);
},
.extension => |ext_reg| {
try renderCopyRight(writer, ext_reg.copyright);
try writer.print(
\\const Version = @import("builtin").Version;
\\pub const version = Version{{.major = {}, .minor = 0, .patch = {}}};
\\
, .{ ext_reg.version, ext_reg.revision },
);
try renderOpcodes(writer, ext_reg.instructions);
try renderOperandKinds(writer, ext_reg.operand_kinds);
}
}
}
fn renderCopyRight(writer: Writer, copyright: []const []const u8) !void {
for (copyright) |line| {
try writer.print("// {s}\n", .{ line });
}
}
fn renderOpcodes(writer: Writer, instructions: []const Instruction) !void {
try writer.writeAll("pub const Opcode = extern enum(u16) {\n");
for (instructions) |instr| {
try writer.print("{} = {},\n", .{ std.zig.fmtId(instr.opname), instr.opcode });
}
try writer.writeAll("_,\n};\n");
}
fn renderOperandKinds(writer: Writer, kinds: []const OperandKind) !void {
for (kinds) |kind| {
switch (kind.category) {
.ValueEnum => try renderValueEnum(writer, kind),
.BitEnum => try renderBitEnum(writer, kind),
else => {},
}
}
}
fn renderValueEnum(writer: Writer, enumeration: OperandKind) !void {
try writer.print("pub const {s} = extern enum(u32) {{\n", .{ enumeration.kind });
const enumerants = enumeration.enumerants orelse return error.InvalidRegistry;
for (enumerants) |enumerant| {
if (enumerant.value != .int) return error.InvalidRegistry;
try writer.print("{} = {},\n", .{ std.zig.fmtId(enumerant.enumerant), enumerant.value.int });
}
try writer.writeAll("_,\n};\n");
}
fn renderBitEnum(writer: Writer, enumeration: OperandKind) !void {
try writer.print("pub const {s} = packed struct {{\n", .{ enumeration.kind });
var flags_by_bitpos = [_]?[]const u8{null} ** 32;
const enumerants = enumeration.enumerants orelse return error.InvalidRegistry;
for (enumerants) |enumerant| {
if (enumerant.value != .bitflag) return error.InvalidRegistry;
const value = try parseHexInt(enumerant.value.bitflag);
if (@popCount(u32, value) != 1) {
continue; // Skip combinations and 'none' items
}
var bitpos = std.math.log2_int(u32, value);
if (flags_by_bitpos[bitpos]) |*existing|{
// Keep the shortest
if (enumerant.enumerant.len < existing.len)
existing.* = enumerant.enumerant;
} else {
flags_by_bitpos[bitpos] = enumerant.enumerant;
}
}
for (flags_by_bitpos) |maybe_flag_name, bitpos| {
if (maybe_flag_name) |flag_name| {
try writer.writeAll(flag_name);
} else {
try writer.print("_reserved_bit_{}", .{bitpos});
}
try writer.writeAll(": bool ");
if (bitpos == 0) { // Force alignment to integer boundaries
try writer.writeAll("align(@alignOf(u32)) ");
}
try writer.writeAll("= false, ");
}
try writer.writeAll("};\n");
}
fn parseHexInt(text: []const u8) !u31 {
const prefix = "0x";
if (!std.mem.startsWith(u8, text, prefix))
return error.InvalidHexInt;
return try std.fmt.parseInt(u31, text[prefix.len ..], 16);
}
fn usageAndExit(file: std.fs.File, arg0: []const u8, code: u8) noreturn {
file.writer().print(
\\Usage: {s} <spirv json spec>
\\
\\Generates Zig bindings for a SPIR-V specification .json (either core or
\\extinst versions). The result, printed to stdout, should be used to update
\\files in src/codegen/spirv.
\\
\\The relevant specifications can be obtained from the SPIR-V registry:
\\https://github.com/KhronosGroup/SPIRV-Headers/blob/master/include/spirv/unified1/
\\
, .{arg0}
) catch std.process.exit(1);
std.process.exit(code);
}