mirror of
https://github.com/ziglang/zig.git
synced 2026-01-15 20:05:16 +00:00
spirv: introduce SpvModule.Fn to generate function code into spirv: assembler error message setup spirv: runtime spec info spirv: inline assembly tokenizer spirv: inline assembly lhs result/opcode parsing spirv: forgot to fmt spirv: tokenize opcodes and assigned result-ids spirv: operand parsing setup spirv: assembler string literals spirv: assembler integer literals spirv: assembler value enums spirv: assembler bit masks spirv: update assembler to new asm air format spirv: target 1.5 for now Current vulkan sdk version (1.3.204) ships spirv tools targetting 1.5, and so these do not work with binaries targetting 1.6 yet. In the future, this version number should be decided by the target. spirv: store operands in flat arraylist. Instead of having dedicated Operand variants for variadic operands, just flatten them and store them in the normal inst.operands list. This is a little simpler, but is not easily decodable in the operand data representation. spirv: parse variadic assembly operands spirv: improve assembler result-id tokenization spirv: begin instruction processing spirv: only remove decl if it was actually allocated spirv: work around weird miscompilation Seems like there are problems with switch in anonymous struct literals. spirv: begin resolving some types in assembler spirv: improve instruction processing spirv: rename some types + process OpTypeInt spirv: process OpTypeVector spirv: process OpTypeMatrix and OpTypeSampler spirv: add opcode class to spec, remove @exclude'd instructions spirv: process more type instructions spirv: OpTypeFunction spirv: OpTypeOpaque spirv: parse LiteralContextDependentNumber operands spirv: emit assembly instruction into right section spirv: parse OpPhi parameters spirv: inline assembly inputs spirv: also copy air types spirv: inline assembly outputs spirv: spir-v address spaces spirv: basic vector constants/types and shuffle spirv: assembler OpTypeImage spirv: some stuff spirv: remove spirv address spaces for now
328 lines
11 KiB
Zig
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) {
|
|
.arg, .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,
|
|
};
|
|
}
|