SPIR-V: branching

This commit is contained in:
Robin Voetter 2021-05-20 19:15:43 +02:00
parent 5edc5f9730
commit 46184ab85e
2 changed files with 152 additions and 6 deletions

View File

@ -20,6 +20,16 @@ pub const ResultId = u32;
pub const TypeMap = std.HashMap(Type, ResultId, Type.hash, Type.eql, std.hash_map.default_max_load_percentage);
pub const InstMap = std.AutoHashMap(*Inst, ResultId);
const IncomingBlock = struct {
src_label_id: ResultId,
break_value_id: ResultId,
};
pub const BlockMap = std.AutoHashMap(*Inst.Block, struct {
label_id: ResultId,
incoming_blocks: *std.ArrayListUnmanaged(IncomingBlock),
});
pub fn writeOpcode(code: *std.ArrayList(Word), opcode: Opcode, arg_count: u16) !void {
const word_count: Word = arg_count + 1;
try code.append((word_count << 16) | @enumToInt(opcode));
@ -87,6 +97,12 @@ pub const DeclGen = struct {
/// A map keeping track of which instruction generated which result-id.
inst_results: InstMap,
/// We need to keep track of result ids for block labels, as well as the 'incoming' blocks for a block.
blocks: BlockMap,
/// The label of the SPIR-V block we are currently generating.
current_block_label_id: ResultId,
/// The decl we are currently generating code for.
decl: *Decl,
@ -156,6 +172,11 @@ pub const DeclGen = struct {
return self.inst_results.get(inst).?; // Instruction does not dominate all uses!
}
fn beginSPIRVBlock(self: *DeclGen, label_id: ResultId) !void {
try writeInstruction(&self.spv.binary.fn_decls, .OpLabel, &[_]Word{label_id});
self.current_block_label_id = label_id;
}
/// SPIR-V requires enabling specific integer sizes through capabilities, and so if they are not enabled, we need
/// to emulate them in other instructions/types. This function returns, given an integer bit width (signed or unsigned, sign
/// included), the width of the underlying type which represents it, given the enabled features for the current target.
@ -325,7 +346,8 @@ pub const DeclGen = struct {
else => unreachable,
}
},
else => return self.fail(src, "TODO: SPIR-V backend: constant generation of type {s}\n", .{ty.zigTypeTag()}),
.Void => unreachable,
else => return self.fail(src, "TODO: SPIR-V backend: constant generation of type {}", .{ty}),
}
return result_id;
@ -481,7 +503,7 @@ pub const DeclGen = struct {
// TODO: This could probably be done in a better way...
const root_block_id = self.spv.allocResultId();
_ = try writeInstruction(&self.spv.binary.fn_decls, .OpLabel, &[_]Word{root_block_id});
try self.beginSPIRVBlock(root_block_id);
try self.genBody(func_payload.data.body);
try writeInstruction(&self.spv.binary.fn_decls, .OpFunctionEnd, &[_]Word{});
@ -490,7 +512,7 @@ pub const DeclGen = struct {
}
}
fn genBody(self: *DeclGen, body: ir.Body) !void {
fn genBody(self: *DeclGen, body: ir.Body) Error!void {
for (body.instructions) |inst| {
const maybe_result_id = try self.genInst(inst);
if (maybe_result_id) |result_id|
@ -518,16 +540,21 @@ pub const DeclGen = struct {
.not => try self.genUnOp(inst.castTag(.not).?),
.alloc => try self.genAlloc(inst.castTag(.alloc).?),
.arg => self.genArg(),
.block => try self.genBlock(inst.castTag(.block).?),
.br => try self.genBr(inst.castTag(.br).?),
.br_void => try self.genBrVoid(inst.castTag(.br_void).?),
// TODO: Breakpoints won't be supported in SPIR-V, but the compiler seems to insert them
// throughout the IR.
.breakpoint => null,
.condbr => try self.genCondBr(inst.castTag(.condbr).?),
.constant => unreachable,
.dbg_stmt => null,
.load => try self.genLoad(inst.castTag(.load).?),
.ret => self.genRet(inst.castTag(.ret).?),
.retvoid => self.genRetVoid(),
.loop => try self.genLoop(inst.castTag(.loop).?),
.ret => try self.genRet(inst.castTag(.ret).?),
.retvoid => try self.genRetVoid(),
.store => try self.genStore(inst.castTag(.store).?),
.unreach => self.genUnreach(),
.unreach => try self.genUnreach(),
else => self.fail(inst.src, "TODO: SPIR-V backend: implement inst {s}", .{@tagName(inst.tag)}),
};
}
@ -673,6 +700,103 @@ pub const DeclGen = struct {
return self.args.items[self.next_arg_index];
}
fn genBlock(self: *DeclGen, inst: *Inst.Block) !?ResultId {
// In IR, a block doesn't really define an entry point like a block, but more like a scope that breaks can jump out of and
// "return" a value from. This cannot be directly modelled in SPIR-V, so in a block instruction, we're going to split up
// the current block by first generating the code of the block, then a label, and then generate the rest of the current
// ir.Block in a different SPIR-V block.
const label_id = self.spv.allocResultId();
// 4 chosen as arbitrary initial capacity.
var incoming_blocks = try std.ArrayListUnmanaged(IncomingBlock).initCapacity(self.module.gpa, 4);
try self.blocks.putNoClobber(inst, .{
.label_id = label_id,
.incoming_blocks = &incoming_blocks,
});
defer {
self.blocks.removeAssertDiscard(inst);
incoming_blocks.deinit(self.module.gpa);
}
try self.genBody(inst.body);
try self.beginSPIRVBlock(label_id);
// If this block didn't produce a value, simply return here.
if (!inst.base.ty.hasCodeGenBits())
return null;
// Combine the result from the blocks using the Phi instruction.
const result_id = self.spv.allocResultId();
// TODO: OpPhi is limited in the types that it may produce, such as pointers. Figure out which other types
// are not allowed to be created from a phi node, and throw an error for those. For now, genType already throws
// an error for pointers.
const result_type_id = try self.genType(inst.base.src, inst.base.ty);
try writeOpcode(&self.spv.binary.fn_decls, .OpPhi, 2 + @intCast(u16, incoming_blocks.items.len * 2)); // result type + result + variable/parent...
for (incoming_blocks.items) |incoming| {
try self.spv.binary.fn_decls.appendSlice(&[_]Word{ incoming.break_value_id, incoming.src_label_id });
}
return result_id;
}
fn genBr(self: *DeclGen, inst: *Inst.Br) !?ResultId {
// TODO: This instruction needs to be the last in a block. Is that guaranteed?
const target = self.blocks.get(inst.block).?;
// TODO: For some reason, br is emitted with void parameters.
if (inst.operand.ty.hasCodeGenBits()) {
const operand_id = try self.resolve(inst.operand);
// current_block_label_id should not be undefined here, lest there is a br or br_void in the function's body.
try target.incoming_blocks.append(self.module.gpa, .{
.src_label_id = self.current_block_label_id,
.break_value_id = operand_id
});
}
try writeInstruction(&self.spv.binary.fn_decls, .OpBranch, &[_]Word{target.label_id});
return null;
}
fn genBrVoid(self: *DeclGen, inst: *Inst.BrVoid) !?ResultId {
// TODO: This instruction needs to be the last in a block. Is that guaranteed?
const target = self.blocks.get(inst.block).?;
// Don't need to add this to the incoming block list, as there is no value to insert in the phi node anyway.
try writeInstruction(&self.spv.binary.fn_decls, .OpBranch, &[_]Word{target.label_id});
return null;
}
fn genCondBr(self: *DeclGen, inst: *Inst.CondBr) !?ResultId {
// TODO: This instruction needs to be the last in a block. Is that guaranteed?
const condition_id = try self.resolve(inst.condition);
// These will always generate a new SPIR-V block, since they are ir.Body and not ir.Block.
const then_label_id = self.spv.allocResultId();
const else_label_id = self.spv.allocResultId();
// TODO: We can generate OpSelectionMerge here if we know the target block that both of these will resolve to,
// but i don't know if those will always resolve to the same block.
try writeInstruction(&self.spv.binary.fn_decls, .OpBranchConditional, &[_]Word{
condition_id,
then_label_id,
else_label_id,
});
try self.beginSPIRVBlock(then_label_id);
try self.genBody(inst.then_body);
try self.beginSPIRVBlock(else_label_id);
try self.genBody(inst.else_body);
return null;
}
fn genLoad(self: *DeclGen, inst: *Inst.UnOp) !ResultId {
const operand_id = try self.resolve(inst.operand);
@ -689,6 +813,22 @@ pub const DeclGen = struct {
return result_id;
}
fn genLoop(self: *DeclGen, inst: *Inst.Loop) !?ResultId {
// TODO: This instruction needs to be the last in a block. Is that guaranteed?
const loop_label_id = self.spv.allocResultId();
// Jump to the loop entry point
try writeInstruction(&self.spv.binary.fn_decls, .OpBranch, &[_]Word{ loop_label_id });
// TODO: Look into OpLoopMerge.
try self.beginSPIRVBlock(loop_label_id);
try self.genBody(inst.body);
try writeInstruction(&self.spv.binary.fn_decls, .OpBranch, &[_]Word{ loop_label_id });
return null;
}
fn genRet(self: *DeclGen, inst: *Inst.UnOp) !?ResultId {
const operand_id = try self.resolve(inst.operand);
// TODO: This instruction needs to be the last in a block. Is that guaranteed?

View File

@ -161,12 +161,15 @@ pub fn flushModule(self: *SpirV, comp: *Compilation) !void {
.args = std.ArrayList(codegen.Word).init(self.base.allocator),
.next_arg_index = undefined,
.inst_results = codegen.InstMap.init(self.base.allocator),
.blocks = codegen.BlockMap.init(self.base.allocator),
.current_block_label_id = undefined,
.decl = undefined,
.error_msg = undefined,
};
defer decl_gen.inst_results.deinit();
defer decl_gen.args.deinit();
defer decl_gen.blocks.deinit();
for (self.decl_table.items()) |entry| {
const decl = entry.key;
@ -175,6 +178,9 @@ pub fn flushModule(self: *SpirV, comp: *Compilation) !void {
// Reset the decl_gen, but retain allocated resources.
decl_gen.args.items.len = 0;
decl_gen.next_arg_index = 0;
decl_gen.inst_results.clearRetainingCapacity();
decl_gen.blocks.clearRetainingCapacity();
decl_gen.current_block_label_id = undefined;
decl_gen.decl = decl;
decl_gen.error_msg = null;