| 1 | const std = @import("std"); |
| 2 | const assert = std.debug.assert; |
| 3 | const Allocator = std.mem.Allocator; |
| 4 | const log = std.log.scoped(.spirv_parse); |
| 5 | |
| 6 | const spec = @import("../../codegen/spirv/spec.zig"); |
| 7 | const Opcode = spec.Opcode; |
| 8 | const Word = spec.Word; |
| 9 | const InstructionSet = spec.InstructionSet; |
| 10 | const ResultId = spec.Id; |
| 11 | |
| 12 | const BinaryModule = @This(); |
| 13 | |
| 14 | /// The result-id bound of this SPIR-V module. |
| 15 | id_bound: u32, |
| 16 | |
| 17 | /// The instructions of this module (no header). |
| 18 | instructions: []const Word, |
| 19 | |
| 20 | /// Maps OpExtInstImport result-ids to their InstructionSet. |
| 21 | ext_inst_map: std.AutoHashMapUnmanaged(ResultId, InstructionSet), |
| 22 | |
| 23 | /// Width of arithmetic types (OpTypeInt/OpTypeFloat). Needed to correctly |
| 24 | /// parse operands of Op(Spec)Constant and OpSwitch. |
| 25 | arith_type_width: std.AutoHashMapUnmanaged(ResultId, u16), |
| 26 | |
| 27 | functions_start: usize, |
| 28 | |
| 29 | pub fn deinit(bm: *BinaryModule, gpa: Allocator) void { |
| 30 | bm.ext_inst_map.deinit(gpa); |
| 31 | bm.arith_type_width.deinit(gpa); |
| 32 | bm.* = undefined; |
| 33 | } |
| 34 | |
| 35 | pub fn iterateInstructions(bm: BinaryModule) Instruction.Iterator { |
| 36 | return Instruction.Iterator.init(bm.instructions, 0); |
| 37 | } |
| 38 | |
| 39 | pub fn iterateInstructionsFrom(bm: BinaryModule, offset: usize) Instruction.Iterator { |
| 40 | return Instruction.Iterator.init(bm.instructions, offset); |
| 41 | } |
| 42 | |
| 43 | pub const Instruction = struct { |
| 44 | pub const Iterator = struct { |
| 45 | words: []const Word, |
| 46 | offset: usize = 0, |
| 47 | |
| 48 | pub fn init(words: []const Word, start_offset: usize) Iterator { |
| 49 | return .{ .words = words, .offset = start_offset }; |
| 50 | } |
| 51 | |
| 52 | pub fn next(it: *Iterator) ?Instruction { |
| 53 | if (it.offset >= it.words.len) return null; |
| 54 | |
| 55 | const instruction_len = it.words[it.offset] >> 16; |
| 56 | defer it.offset += instruction_len; |
| 57 | assert(instruction_len != 0); |
| 58 | assert(it.offset < it.words.len); |
| 59 | |
| 60 | return Instruction{ |
| 61 | .opcode = @fromBackingInt(@intCast(it.words[it.offset] & 0xFFFF)), |
| 62 | .offset = it.offset, |
| 63 | .operands = it.words[it.offset..][1..instruction_len], |
| 64 | }; |
| 65 | } |
| 66 | }; |
| 67 | |
| 68 | opcode: Opcode, |
| 69 | offset: usize, |
| 70 | operands: []const Word, |
| 71 | }; |
| 72 | |
| 73 | pub const Parser = struct { |
| 74 | gpa: Allocator, |
| 75 | opcode_table: std.AutoHashMapUnmanaged(u32, u16) = .empty, |
| 76 | |
| 77 | pub fn init(gpa: Allocator) !Parser { |
| 78 | var parser = Parser{ .gpa = gpa }; |
| 79 | errdefer parser.deinit(); |
| 80 | |
| 81 | inline for (std.meta.tags(InstructionSet)) |set| { |
| 82 | const instructions = set.instructions(); |
| 83 | try parser.opcode_table.ensureUnusedCapacity(gpa, @intCast(instructions.len)); |
| 84 | for (instructions, 0..) |inst, i| { |
| 85 | const entry = parser.opcode_table.getOrPutAssumeCapacity(mapSetAndOpcode(set, @intCast(inst.opcode))); |
| 86 | if (!entry.found_existing) { |
| 87 | entry.value_ptr.* = @intCast(i); |
| 88 | } |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | return parser; |
| 93 | } |
| 94 | |
| 95 | pub fn deinit(parser: *Parser) void { |
| 96 | parser.opcode_table.deinit(parser.gpa); |
| 97 | } |
| 98 | |
| 99 | fn mapSetAndOpcode(set: InstructionSet, opcode: u16) u32 { |
| 100 | return (@as(u32, @backingInt(set)) << 16) | opcode; |
| 101 | } |
| 102 | |
| 103 | pub fn getInstSpec(parser: Parser, opcode: Opcode) ?spec.Instruction { |
| 104 | const index = parser.opcode_table.get(mapSetAndOpcode(.core, @backingInt(opcode))) orelse return null; |
| 105 | return InstructionSet.core.instructions()[index]; |
| 106 | } |
| 107 | |
| 108 | /// Build a BinaryModule from raw instruction words (no header). |
| 109 | /// Scans for ext_inst_map, arith_type_width, and the functions section offset. |
| 110 | pub fn initFromWords(parser: *Parser, words: []const Word, id_bound: u32) !BinaryModule { |
| 111 | var binary = BinaryModule{ |
| 112 | .id_bound = id_bound, |
| 113 | .instructions = words, |
| 114 | .ext_inst_map = .{}, |
| 115 | .arith_type_width = .{}, |
| 116 | .functions_start = undefined, |
| 117 | }; |
| 118 | |
| 119 | var maybe_function_section: ?usize = null; |
| 120 | var it = binary.iterateInstructions(); |
| 121 | while (it.next()) |inst| { |
| 122 | const inst_spec = parser.getInstSpec(inst.opcode) orelse continue; |
| 123 | const operands = inst.operands; |
| 124 | |
| 125 | switch (inst.opcode) { |
| 126 | .OpExtInstImport => { |
| 127 | const set_name = std.mem.sliceTo(std.mem.sliceAsBytes(operands[1..]), 0); |
| 128 | const set = std.meta.stringToEnum(InstructionSet, set_name) orelse continue; |
| 129 | if (set == .core) continue; |
| 130 | try binary.ext_inst_map.put(parser.gpa, @fromBackingInt(@intCast(operands[0])), set); |
| 131 | }, |
| 132 | .OpTypeInt, .OpTypeFloat => { |
| 133 | try binary.arith_type_width.put(parser.gpa, @fromBackingInt(@intCast(operands[0])), @intCast(operands[1])); |
| 134 | }, |
| 135 | .OpFunction => if (maybe_function_section == null) { |
| 136 | maybe_function_section = inst.offset; |
| 137 | }, |
| 138 | else => {}, |
| 139 | } |
| 140 | |
| 141 | // propagate arith type widths through instructions that return int/float |
| 142 | const spec_operands = inst_spec.operands; |
| 143 | if (spec_operands.len >= 2 and |
| 144 | spec_operands[0].kind == .id_result_type and |
| 145 | spec_operands[1].kind == .id_result) |
| 146 | { |
| 147 | if (operands.len >= 2) { |
| 148 | if (binary.arith_type_width.get(@fromBackingInt(@intCast(operands[0])))) |width| { |
| 149 | try binary.arith_type_width.put(parser.gpa, @fromBackingInt(@intCast(operands[1])), width); |
| 150 | } |
| 151 | } |
| 152 | } |
| 153 | } |
| 154 | |
| 155 | binary.functions_start = maybe_function_section orelse binary.instructions.len; |
| 156 | |
| 157 | return binary; |
| 158 | } |
| 159 | |
| 160 | /// Parse offsets in the instruction that contain result-ids. |
| 161 | /// Returned offsets are relative to inst.operands. |
| 162 | pub fn parseInstructionResultIds( |
| 163 | parser: *Parser, |
| 164 | binary: BinaryModule, |
| 165 | inst: Instruction, |
| 166 | offsets: *std.ArrayList(u16), |
| 167 | ) !void { |
| 168 | const index = parser.opcode_table.get(mapSetAndOpcode(.core, @backingInt(inst.opcode))).?; |
| 169 | const operands = InstructionSet.core.instructions()[index].operands; |
| 170 | |
| 171 | var offset: usize = 0; |
| 172 | switch (inst.opcode) { |
| 173 | .OpSpecConstantOp => { |
| 174 | assert(operands[0].kind == .id_result_type); |
| 175 | assert(operands[1].kind == .id_result); |
| 176 | offset = try parser.parseOperandsResultIds(binary, inst, operands[0..2], offset, offsets); |
| 177 | |
| 178 | if (offset >= inst.operands.len) return error.InvalidPhysicalFormat; |
| 179 | const spec_opcode = std.math.cast(u16, inst.operands[offset]) orelse return error.InvalidPhysicalFormat; |
| 180 | const spec_index = parser.opcode_table.get(mapSetAndOpcode(.core, spec_opcode)) orelse |
| 181 | return error.InvalidPhysicalFormat; |
| 182 | const spec_operands = InstructionSet.core.instructions()[spec_index].operands; |
| 183 | assert(spec_operands[0].kind == .id_result_type); |
| 184 | assert(spec_operands[1].kind == .id_result); |
| 185 | offset = try parser.parseOperandsResultIds(binary, inst, spec_operands[2..], offset + 1, offsets); |
| 186 | }, |
| 187 | .OpExtInst => { |
| 188 | assert(operands[0].kind == .id_result_type); |
| 189 | assert(operands[1].kind == .id_result); |
| 190 | offset = try parser.parseOperandsResultIds(binary, inst, operands[0..2], offset, offsets); |
| 191 | |
| 192 | if (offset + 1 >= inst.operands.len) return error.InvalidPhysicalFormat; |
| 193 | const set_id: ResultId = @fromBackingInt(@intCast(inst.operands[offset])); |
| 194 | try offsets.append(parser.gpa, @intCast(offset)); |
| 195 | const set = binary.ext_inst_map.get(set_id) orelse { |
| 196 | log.err("invalid instruction set {}", .{@backingInt(set_id)}); |
| 197 | return error.InvalidId; |
| 198 | }; |
| 199 | const ext_opcode = std.math.cast(u16, inst.operands[offset + 1]) orelse return error.InvalidPhysicalFormat; |
| 200 | const ext_index = parser.opcode_table.get(mapSetAndOpcode(set, ext_opcode)) orelse |
| 201 | return error.InvalidPhysicalFormat; |
| 202 | const ext_operands = set.instructions()[ext_index].operands; |
| 203 | offset = try parser.parseOperandsResultIds(binary, inst, ext_operands, offset + 2, offsets); |
| 204 | }, |
| 205 | else => { |
| 206 | offset = try parser.parseOperandsResultIds(binary, inst, operands, offset, offsets); |
| 207 | }, |
| 208 | } |
| 209 | |
| 210 | if (offset != inst.operands.len) return error.InvalidPhysicalFormat; |
| 211 | } |
| 212 | |
| 213 | fn parseOperandsResultIds( |
| 214 | parser: *Parser, |
| 215 | binary: BinaryModule, |
| 216 | inst: Instruction, |
| 217 | operands: []const spec.Operand, |
| 218 | start_offset: usize, |
| 219 | offsets: *std.ArrayList(u16), |
| 220 | ) !usize { |
| 221 | var offset = start_offset; |
| 222 | for (operands) |operand| { |
| 223 | offset = try parser.parseOperandResultIds(binary, inst, operand, offset, offsets); |
| 224 | } |
| 225 | return offset; |
| 226 | } |
| 227 | |
| 228 | fn parseOperandResultIds( |
| 229 | parser: *Parser, |
| 230 | binary: BinaryModule, |
| 231 | inst: Instruction, |
| 232 | operand: spec.Operand, |
| 233 | start_offset: usize, |
| 234 | offsets: *std.ArrayList(u16), |
| 235 | ) !usize { |
| 236 | var offset = start_offset; |
| 237 | switch (operand.quantifier) { |
| 238 | .variadic => while (offset < inst.operands.len) { |
| 239 | offset = try parser.parseOperandKindResultIds(binary, inst, operand.kind, offset, offsets); |
| 240 | }, |
| 241 | .optional => if (offset < inst.operands.len) { |
| 242 | offset = try parser.parseOperandKindResultIds(binary, inst, operand.kind, offset, offsets); |
| 243 | }, |
| 244 | .required => { |
| 245 | offset = try parser.parseOperandKindResultIds(binary, inst, operand.kind, offset, offsets); |
| 246 | }, |
| 247 | } |
| 248 | return offset; |
| 249 | } |
| 250 | |
| 251 | fn parseOperandKindResultIds( |
| 252 | parser: *Parser, |
| 253 | binary: BinaryModule, |
| 254 | inst: Instruction, |
| 255 | kind: spec.OperandKind, |
| 256 | start_offset: usize, |
| 257 | offsets: *std.ArrayList(u16), |
| 258 | ) !usize { |
| 259 | var offset = start_offset; |
| 260 | if (offset >= inst.operands.len) return error.InvalidPhysicalFormat; |
| 261 | |
| 262 | switch (kind.category()) { |
| 263 | .bit_enum => { |
| 264 | const mask = inst.operands[offset]; |
| 265 | offset += 1; |
| 266 | for (kind.enumerants()) |enumerant| { |
| 267 | if ((mask & enumerant.value) != 0) { |
| 268 | for (enumerant.parameters) |param_kind| { |
| 269 | offset = try parser.parseOperandKindResultIds(binary, inst, param_kind, offset, offsets); |
| 270 | } |
| 271 | } |
| 272 | } |
| 273 | }, |
| 274 | .value_enum => { |
| 275 | const value = inst.operands[offset]; |
| 276 | offset += 1; |
| 277 | for (kind.enumerants()) |enumerant| { |
| 278 | if (value == enumerant.value) { |
| 279 | for (enumerant.parameters) |param_kind| { |
| 280 | offset = try parser.parseOperandKindResultIds(binary, inst, param_kind, offset, offsets); |
| 281 | } |
| 282 | break; |
| 283 | } |
| 284 | } |
| 285 | }, |
| 286 | .id => { |
| 287 | try offsets.append(parser.gpa, @intCast(offset)); |
| 288 | offset += 1; |
| 289 | }, |
| 290 | else => switch (kind) { |
| 291 | .literal_integer, .literal_float => offset += 1, |
| 292 | .literal_string => while (true) { |
| 293 | if (offset >= inst.operands.len) return error.InvalidPhysicalFormat; |
| 294 | const word = inst.operands[offset]; |
| 295 | offset += 1; |
| 296 | |
| 297 | if (word & 0xFF000000 == 0 or |
| 298 | word & 0x00FF0000 == 0 or |
| 299 | word & 0x0000FF00 == 0 or |
| 300 | word & 0x000000FF == 0) |
| 301 | { |
| 302 | break; |
| 303 | } |
| 304 | }, |
| 305 | .literal_context_dependent_number => { |
| 306 | assert(inst.opcode == .OpConstant or inst.opcode == .OpSpecConstant); |
| 307 | const bit_width = binary.arith_type_width.get(@fromBackingInt(@intCast(inst.operands[0]))) orelse { |
| 308 | log.err("invalid LiteralContextDependentNumber type {}", .{inst.operands[0]}); |
| 309 | return error.InvalidId; |
| 310 | }; |
| 311 | offset += switch (bit_width) { |
| 312 | 1...32 => 1, |
| 313 | 33...64 => 2, |
| 314 | else => unreachable, |
| 315 | }; |
| 316 | }, |
| 317 | .literal_ext_inst_integer => unreachable, |
| 318 | .literal_spec_constant_op_integer => unreachable, |
| 319 | .pair_literal_integer_id_ref => { |
| 320 | assert(inst.opcode == .OpSwitch); |
| 321 | const bit_width = binary.arith_type_width.get(@fromBackingInt(@intCast(inst.operands[0]))) orelse { |
| 322 | log.err("invalid OpSwitch type {}", .{inst.operands[0]}); |
| 323 | return error.InvalidId; |
| 324 | }; |
| 325 | offset += switch (bit_width) { |
| 326 | 1...32 => 1, |
| 327 | 33...64 => 2, |
| 328 | else => unreachable, |
| 329 | }; |
| 330 | try offsets.append(parser.gpa, @intCast(offset)); |
| 331 | offset += 1; |
| 332 | }, |
| 333 | .pair_id_ref_literal_integer => { |
| 334 | try offsets.append(parser.gpa, @intCast(offset)); |
| 335 | offset += 2; |
| 336 | }, |
| 337 | .pair_id_ref_id_ref => { |
| 338 | try offsets.append(parser.gpa, @intCast(offset)); |
| 339 | try offsets.append(parser.gpa, @intCast(offset + 1)); |
| 340 | offset += 2; |
| 341 | }, |
| 342 | else => unreachable, |
| 343 | }, |
| 344 | } |
| 345 | return offset; |
| 346 | } |
| 347 | }; |