Commit 46184ab85e

Robin Voetter <robin@voetter.nl>
2021-05-20 19:15:43
SPIR-V: branching
1 parent 5edc5f9
Changed files (2)
src
codegen
link
src/codegen/spirv.zig
@@ -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?
src/link/SpirV.zig
@@ -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;