zig/src/link/SpirV.zig
2022-12-30 17:00:50 +02:00

328 lines
11 KiB
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, (preferably) OpUndef instructions.
//! All function declarations without a body (extern functions presumably).
//! All regular functions.
// Because SPIR-V requires re-compilation anyway, and so hot swapping will not work
// anyway, we simply generate all the code in flushModule. This keeps
// things considerably simpler.
const SpirV = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const assert = std.debug.assert;
const log = std.log.scoped(.link);
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 Air = @import("../Air.zig");
const Liveness = @import("../Liveness.zig");
const Value = @import("../value.zig").Value;
const SpvModule = @import("../codegen/spirv/Module.zig");
const spec = @import("../codegen/spirv/spec.zig");
const IdResult = spec.IdResult;
// TODO: Should this struct be used at all rather than just a hashmap of aux data for every decl?
pub const FnData = struct {
// We're going to fill these in flushModule, and we're going to fill them unconditionally,
// so just set it to undefined.
id: IdResult = undefined,
};
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;
}
};
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,
},
};
// 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.target.ofmt == .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.
const spirv = try createEmpty(allocator, options);
errdefer spirv.base.destroy();
// TODO: read the file and keep valid parts instead of truncating
const file = try options.emit.?.directory.handle.createFile(sub_path, .{ .truncate = true, .read = true });
spirv.base.file = file;
return spirv;
}
pub fn deinit(self: *SpirV) void {
self.decl_table.deinit(self.base.allocator);
}
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 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);
}
pub fn updateDeclExports(
self: *SpirV,
module: *Module,
decl_index: Module.Decl.Index,
exports: []const *Module.Export,
) !void {
_ = self;
_ = module;
_ = decl_index;
_ = exports;
}
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);
}
}
}
pub fn flush(self: *SpirV, comp: *Compilation, prog_node: *std.Progress.Node) link.File.FlushError!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, prog_node);
}
}
pub fn flushModule(self: *SpirV, comp: *Compilation, prog_node: *std.Progress.Node) link.File.FlushError!void {
if (build_options.skip_non_native) {
@panic("Attempted to compile for architecture that was disabled by build configuration");
}
const tracy = trace(@src());
defer tracy.end();
var sub_prog_node = prog_node.start("Flush Module", 0);
sub_prog_node.activate();
defer sub_prog_node.end();
const module = self.base.options.module.?;
const target = comp.getTarget();
var arena = std.heap.ArenaAllocator.init(self.base.allocator);
defer arena.deinit();
var spv = SpvModule.init(self.base.allocator, arena.allocator());
defer spv.deinit();
// 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?
// TODO: fn_link is used here, but thats probably not the right field. It will work anyway though.
for (self.decl_table.keys()) |decl_index| {
const decl = module.declPtr(decl_index);
if (decl.has_tv) {
decl.fn_link.spirv.id = spv.allocId();
}
}
// Now, actually generate the code for all declarations.
var decl_gen = codegen.DeclGen.init(self.base.allocator, module, &spv);
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, 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.?);
}
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},
.glsl450 => &.{.Shader},
.vulkan => &.{.Shader},
else => unreachable, // TODO
};
for (caps) |cap| {
try spv.sections.capabilities.emit(spv.gpa, .OpCapability, .{
.capability = cap,
});
}
}
fn writeMemoryModel(spv: *SpvModule, 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 => .GLSL450,
else => unreachable,
};
// TODO: Put this in a proper section.
try spv.sections.capabilities.emit(spv.gpa, .OpMemoryModel, .{
.addressing_model = addressing_model,
.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) |*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) |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,
};
}