Files
zig/src/codegen/spirv/Section.zig
Robin Voetter 074ba69ba6 spirv: assembler
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
2022-11-23 19:17:58 +01:00

439 lines
14 KiB
Zig

//! Represents a section or subsection of instructions in a SPIR-V binary. Instructions can be append
//! to separate sections, which can then later be merged into the final binary.
const Section = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const testing = std.testing;
const spec = @import("spec.zig");
const Word = spec.Word;
const DoubleWord = std.meta.Int(.unsigned, @bitSizeOf(Word) * 2);
const Log2Word = std.math.Log2Int(Word);
const Opcode = spec.Opcode;
/// The instructions in this section. Memory is owned by the Module
/// externally associated to this Section.
instructions: std.ArrayListUnmanaged(Word) = .{},
pub fn deinit(section: *Section, allocator: Allocator) void {
section.instructions.deinit(allocator);
section.* = undefined;
}
/// Clear the instructions in this section
pub fn reset(section: *Section) void {
section.instructions.items.len = 0;
}
pub fn toWords(section: Section) []Word {
return section.instructions.items;
}
/// Append the instructions from another section into this section.
pub fn append(section: *Section, allocator: Allocator, other_section: Section) !void {
try section.instructions.appendSlice(allocator, other_section.instructions.items);
}
/// Ensure capacity of at least `capacity` more words in this section.
pub fn ensureUnusedCapacity(section: *Section, allocator: Allocator, capacity: usize) !void {
try section.instructions.ensureUnusedCapacity(allocator, capacity);
}
/// Write an instruction and size, operands are to be inserted manually.
pub fn emitRaw(
section: *Section,
allocator: Allocator,
opcode: Opcode,
operand_words: usize, // opcode itself not included
) !void {
const word_count = 1 + operand_words;
try section.instructions.ensureUnusedCapacity(allocator, word_count);
section.writeWord((@intCast(Word, word_count << 16)) | @enumToInt(opcode));
}
pub fn emit(
section: *Section,
allocator: Allocator,
comptime opcode: spec.Opcode,
operands: opcode.Operands(),
) !void {
const word_count = instructionSize(opcode, operands);
try section.instructions.ensureUnusedCapacity(allocator, word_count);
section.writeWord(@intCast(Word, word_count << 16) | @enumToInt(opcode));
section.writeOperands(opcode.Operands(), operands);
}
/// Decorate a result-id.
pub fn decorate(
section: *Section,
allocator: Allocator,
target: spec.IdRef,
decoration: spec.Decoration.Extended,
) !void {
try section.emit(allocator, .OpDecorate, .{
.target = target,
.decoration = decoration,
});
}
/// 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,
});
}
pub fn writeWord(section: *Section, word: Word) void {
section.instructions.appendAssumeCapacity(word);
}
pub fn writeWords(section: *Section, words: []const Word) void {
section.instructions.appendSliceAssumeCapacity(words);
}
pub fn writeDoubleWord(section: *Section, dword: DoubleWord) void {
section.writeWords(&.{
@truncate(Word, dword),
@truncate(Word, dword >> @bitSizeOf(Word)),
});
}
fn writeOperands(section: *Section, comptime Operands: type, operands: Operands) void {
const fields = switch (@typeInfo(Operands)) {
.Struct => |info| info.fields,
.Void => return,
else => unreachable,
};
inline for (fields) |field| {
section.writeOperand(field.field_type, @field(operands, field.name));
}
}
pub fn writeOperand(section: *Section, comptime Operand: type, operand: Operand) void {
switch (Operand) {
spec.IdResultType, spec.IdResult, spec.IdRef => section.writeWord(operand.id),
spec.LiteralInteger => section.writeWord(operand),
spec.LiteralString => section.writeString(operand),
spec.LiteralContextDependentNumber => section.writeContextDependentNumber(operand),
spec.LiteralExtInstInteger => section.writeWord(operand.inst),
// TODO: Where this type is used (OpSpecConstantOp) is currently not correct in the spec json,
// so it most likely needs to be altered into something that can actually describe the entire
// instruction in which it is used.
spec.LiteralSpecConstantOpInteger => section.writeWord(@enumToInt(operand.opcode)),
spec.PairLiteralIntegerIdRef => section.writeWords(&.{ operand.value, operand.label.id }),
spec.PairIdRefLiteralInteger => section.writeWords(&.{ operand.target.id, operand.member }),
spec.PairIdRefIdRef => section.writeWords(&.{ operand[0].id, operand[1].id }),
else => switch (@typeInfo(Operand)) {
.Enum => section.writeWord(@enumToInt(operand)),
.Optional => |info| if (operand) |child| {
section.writeOperand(info.child, child);
},
.Pointer => |info| {
std.debug.assert(info.size == .Slice); // Should be no other pointer types in the spec.
for (operand) |item| {
section.writeOperand(info.child, item);
}
},
.Struct => |info| {
if (info.layout == .Packed) {
section.writeWord(@bitCast(Word, operand));
} else {
section.writeExtendedMask(Operand, operand);
}
},
.Union => section.writeExtendedUnion(Operand, operand),
else => unreachable,
},
}
}
fn writeString(section: *Section, str: []const u8) void {
// TODO: Not actually sure whether this is correct for big-endian.
// See https://www.khronos.org/registry/spir-v/specs/unified1/SPIRV.html#Literal
const zero_terminated_len = str.len + 1;
var i: usize = 0;
while (i < zero_terminated_len) : (i += @sizeOf(Word)) {
var word: Word = 0;
var j: usize = 0;
while (j < @sizeOf(Word) and i + j < str.len) : (j += 1) {
word |= @as(Word, str[i + j]) << @intCast(Log2Word, j * @bitSizeOf(u8));
}
section.instructions.appendAssumeCapacity(word);
}
}
fn writeContextDependentNumber(section: *Section, operand: spec.LiteralContextDependentNumber) void {
switch (operand) {
.int32 => |int| section.writeWord(@bitCast(Word, int)),
.uint32 => |int| section.writeWord(@bitCast(Word, int)),
.int64 => |int| section.writeDoubleWord(@bitCast(DoubleWord, int)),
.uint64 => |int| section.writeDoubleWord(@bitCast(DoubleWord, int)),
.float32 => |float| section.writeWord(@bitCast(Word, float)),
.float64 => |float| section.writeDoubleWord(@bitCast(DoubleWord, float)),
}
}
fn writeExtendedMask(section: *Section, comptime Operand: type, operand: Operand) void {
var mask: Word = 0;
inline for (@typeInfo(Operand).Struct.fields) |field, bit| {
switch (@typeInfo(field.field_type)) {
.Optional => if (@field(operand, field.name) != null) {
mask |= 1 << @intCast(u5, bit);
},
.Bool => if (@field(operand, field.name)) {
mask |= 1 << @intCast(u5, bit);
},
else => unreachable,
}
}
if (mask == 0) {
return;
}
section.writeWord(mask);
inline for (@typeInfo(Operand).Struct.fields) |field| {
switch (@typeInfo(field.field_type)) {
.Optional => |info| if (@field(operand, field.name)) |child| {
section.writeOperands(info.child, child);
},
.Bool => {},
else => unreachable,
}
}
}
fn writeExtendedUnion(section: *Section, comptime Operand: type, operand: Operand) void {
const tag = std.meta.activeTag(operand);
section.writeWord(@enumToInt(tag));
inline for (@typeInfo(Operand).Union.fields) |field| {
if (@field(Operand, field.name) == tag) {
section.writeOperands(field.field_type, @field(operand, field.name));
return;
}
}
unreachable;
}
fn instructionSize(comptime opcode: spec.Opcode, operands: opcode.Operands()) usize {
return 1 + operandsSize(opcode.Operands(), operands);
}
fn operandsSize(comptime Operands: type, operands: Operands) usize {
const fields = switch (@typeInfo(Operands)) {
.Struct => |info| info.fields,
.Void => return 0,
else => unreachable,
};
var total: usize = 0;
inline for (fields) |field| {
total += operandSize(field.field_type, @field(operands, field.name));
}
return total;
}
fn operandSize(comptime Operand: type, operand: Operand) usize {
return switch (Operand) {
spec.IdResultType,
spec.IdResult,
spec.IdRef,
spec.LiteralInteger,
spec.LiteralExtInstInteger,
=> 1,
spec.LiteralString => std.math.divCeil(usize, operand.len + 1, @sizeOf(Word)) catch unreachable, // Add one for zero-terminator
spec.LiteralContextDependentNumber => switch (operand) {
.int32, .uint32, .float32 => @as(usize, 1),
.int64, .uint64, .float64 => @as(usize, 2),
},
// TODO: Where this type is used (OpSpecConstantOp) is currently not correct in the spec
// json, so it most likely needs to be altered into something that can actually
// describe the entire insturction in which it is used.
spec.LiteralSpecConstantOpInteger => 1,
spec.PairLiteralIntegerIdRef,
spec.PairIdRefLiteralInteger,
spec.PairIdRefIdRef,
=> 2,
else => switch (@typeInfo(Operand)) {
.Enum => 1,
.Optional => |info| if (operand) |child| operandSize(info.child, child) else 0,
.Pointer => |info| blk: {
std.debug.assert(info.size == .Slice); // Should be no other pointer types in the spec.
var total: usize = 0;
for (operand) |item| {
total += operandSize(info.child, item);
}
break :blk total;
},
.Struct => |info| if (info.layout == .Packed) 1 else extendedMaskSize(Operand, operand),
.Union => extendedUnionSize(Operand, operand),
else => unreachable,
},
};
}
fn extendedMaskSize(comptime Operand: type, operand: Operand) usize {
var total: usize = 0;
var any_set = false;
inline for (@typeInfo(Operand).Struct.fields) |field| {
switch (@typeInfo(field.field_type)) {
.Optional => |info| if (@field(operand, field.name)) |child| {
total += operandsSize(info.child, child);
any_set = true;
},
.Bool => if (@field(operand, field.name)) {
any_set = true;
},
else => unreachable,
}
}
if (!any_set) {
return 0;
}
return total + 1; // Add one for the mask itself.
}
fn extendedUnionSize(comptime Operand: type, operand: Operand) usize {
const tag = std.meta.activeTag(operand);
inline for (@typeInfo(Operand).Union.fields) |field| {
if (@field(Operand, field.name) == tag) {
// Add one for the tag itself.
return 1 + operandsSize(field.field_type, @field(operand, field.name));
}
}
unreachable;
}
test "SPIR-V Section emit() - no operands" {
if (@import("builtin").zig_backend == .stage1) return error.SkipZigTest;
var section = Section{};
defer section.deinit(std.testing.allocator);
try section.emit(std.testing.allocator, .OpNop, {});
try testing.expect(section.instructions.items[0] == (@as(Word, 1) << 16) | @enumToInt(Opcode.OpNop));
}
test "SPIR-V Section emit() - simple" {
if (@import("builtin").zig_backend == .stage1) return error.SkipZigTest;
var section = Section{};
defer section.deinit(std.testing.allocator);
try section.emit(std.testing.allocator, .OpUndef, .{
.id_result_type = .{ .id = 0 },
.id_result = .{ .id = 1 },
});
try testing.expectEqualSlices(Word, &.{
(@as(Word, 3) << 16) | @enumToInt(Opcode.OpUndef),
0,
1,
}, section.instructions.items);
}
test "SPIR-V Section emit() - string" {
if (@import("builtin").zig_backend == .stage1) return error.SkipZigTest;
var section = Section{};
defer section.deinit(std.testing.allocator);
try section.emit(std.testing.allocator, .OpSource, .{
.source_language = .Unknown,
.version = 123,
.file = .{ .id = 456 },
.source = "pub fn main() void {}",
});
try testing.expectEqualSlices(Word, &.{
(@as(Word, 10) << 16) | @enumToInt(Opcode.OpSource),
@enumToInt(spec.SourceLanguage.Unknown),
123,
456,
std.mem.bytesToValue(Word, "pub "),
std.mem.bytesToValue(Word, "fn m"),
std.mem.bytesToValue(Word, "ain("),
std.mem.bytesToValue(Word, ") vo"),
std.mem.bytesToValue(Word, "id {"),
std.mem.bytesToValue(Word, "}\x00\x00\x00"),
}, section.instructions.items);
}
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);
try section.emit(std.testing.allocator, .OpLoopMerge, .{
.merge_block = .{ .id = 10 },
.continue_target = .{ .id = 20 },
.loop_control = .{
.Unroll = true,
.DependencyLength = .{
.literal_integer = 2,
},
},
});
try testing.expectEqualSlices(Word, &.{
(@as(Word, 5) << 16) | @enumToInt(Opcode.OpLoopMerge),
10,
20,
@bitCast(Word, spec.LoopControl{ .Unroll = true, .DependencyLength = true }),
2,
}, section.instructions.items);
}
test "SPIR-V Section emit() - extended union" {
if (@import("builtin").zig_backend == .stage1) return error.SkipZigTest;
var section = Section{};
defer section.deinit(std.testing.allocator);
try section.emit(std.testing.allocator, .OpExecutionMode, .{
.entry_point = .{ .id = 888 },
.mode = .{
.LocalSize = .{ .x_size = 4, .y_size = 8, .z_size = 16 },
},
});
try testing.expectEqualSlices(Word, &.{
(@as(Word, 6) << 16) | @enumToInt(Opcode.OpExecutionMode),
888,
@enumToInt(spec.ExecutionMode.LocalSize),
4,
8,
16,
}, section.instructions.items);
}