1110 lines
40 KiB
C++
1110 lines
40 KiB
C++
// Copyright (c) 2015-2020 The Khronos Group Inc.
|
|
// Modifications Copyright (C) 2020 Advanced Micro Devices, Inc. All rights
|
|
// reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// This file contains a disassembler: It converts a SPIR-V binary
|
|
// to text.
|
|
|
|
#include "source/disassemble.h"
|
|
|
|
#include <algorithm>
|
|
#include <cassert>
|
|
#include <cstring>
|
|
#include <iomanip>
|
|
#include <memory>
|
|
#include <set>
|
|
#include <stack>
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
|
|
#include "source/assembly_grammar.h"
|
|
#include "source/binary.h"
|
|
#include "source/diagnostic.h"
|
|
#include "source/ext_inst.h"
|
|
#include "source/opcode.h"
|
|
#include "source/parsed_operand.h"
|
|
#include "source/print.h"
|
|
#include "source/spirv_constant.h"
|
|
#include "source/spirv_endian.h"
|
|
#include "source/util/hex_float.h"
|
|
#include "source/util/make_unique.h"
|
|
#include "spirv-tools/libspirv.h"
|
|
|
|
namespace spvtools {
|
|
namespace {
|
|
|
|
// Indices to ControlFlowGraph's list of blocks from one block to its successors
|
|
struct BlockSuccessors {
|
|
// Merge block in OpLoopMerge and OpSelectionMerge
|
|
uint32_t merge_block_id = 0;
|
|
// The continue block in OpLoopMerge
|
|
uint32_t continue_block_id = 0;
|
|
// The true and false blocks in OpBranchConditional
|
|
uint32_t true_block_id = 0;
|
|
uint32_t false_block_id = 0;
|
|
// The body block of a loop, as specified by OpBranch after a merge
|
|
// instruction
|
|
uint32_t body_block_id = 0;
|
|
// The same-nesting-level block that follows this one, indicated by an
|
|
// OpBranch with no merge instruction.
|
|
uint32_t next_block_id = 0;
|
|
// The cases (including default) of an OpSwitch
|
|
std::vector<uint32_t> case_block_ids;
|
|
};
|
|
|
|
class ParsedInstruction {
|
|
public:
|
|
ParsedInstruction(const spv_parsed_instruction_t* instruction) {
|
|
// Make a copy of the parsed instruction, including stable memory for its
|
|
// operands.
|
|
instruction_ = *instruction;
|
|
operands_ =
|
|
std::make_unique<spv_parsed_operand_t[]>(instruction->num_operands);
|
|
memcpy(operands_.get(), instruction->operands,
|
|
instruction->num_operands * sizeof(*instruction->operands));
|
|
instruction_.operands = operands_.get();
|
|
}
|
|
|
|
const spv_parsed_instruction_t* get() const { return &instruction_; }
|
|
|
|
private:
|
|
spv_parsed_instruction_t instruction_;
|
|
std::unique_ptr<spv_parsed_operand_t[]> operands_;
|
|
};
|
|
|
|
// One block in the CFG
|
|
struct SingleBlock {
|
|
// The byte offset in the SPIR-V where the block starts. Used for printing in
|
|
// a comment.
|
|
size_t byte_offset;
|
|
|
|
// Block instructions
|
|
std::vector<ParsedInstruction> instructions;
|
|
|
|
// Successors of this block
|
|
BlockSuccessors successors;
|
|
|
|
// The nesting level for this block.
|
|
uint32_t nest_level = 0;
|
|
bool nest_level_assigned = false;
|
|
|
|
// Whether the block was reachable
|
|
bool reachable = false;
|
|
};
|
|
|
|
// CFG for one function
|
|
struct ControlFlowGraph {
|
|
std::vector<SingleBlock> blocks;
|
|
};
|
|
|
|
// A Disassembler instance converts a SPIR-V binary to its assembly
|
|
// representation.
|
|
class Disassembler {
|
|
public:
|
|
Disassembler(const AssemblyGrammar& grammar, uint32_t options,
|
|
NameMapper name_mapper)
|
|
: print_(spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_PRINT, options)),
|
|
nested_indent_(
|
|
spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_NESTED_INDENT, options)),
|
|
reorder_blocks_(
|
|
spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_REORDER_BLOCKS, options)),
|
|
text_(),
|
|
out_(print_ ? out_stream() : out_stream(text_)),
|
|
instruction_disassembler_(grammar, out_.get(), options, name_mapper),
|
|
header_(!spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_NO_HEADER, options)),
|
|
byte_offset_(0) {}
|
|
|
|
// Emits the assembly header for the module, and sets up internal state
|
|
// so subsequent callbacks can handle the cases where the entire module
|
|
// is either big-endian or little-endian.
|
|
spv_result_t HandleHeader(spv_endianness_t endian, uint32_t version,
|
|
uint32_t generator, uint32_t id_bound,
|
|
uint32_t schema);
|
|
// Emits the assembly text for the given instruction.
|
|
spv_result_t HandleInstruction(const spv_parsed_instruction_t& inst);
|
|
|
|
// If not printing, populates text_result with the accumulated text.
|
|
// Returns SPV_SUCCESS on success.
|
|
spv_result_t SaveTextResult(spv_text* text_result) const;
|
|
|
|
private:
|
|
void EmitCFG();
|
|
|
|
const bool print_; // Should we also print to the standard output stream?
|
|
const bool nested_indent_; // Should the blocks be indented according to the
|
|
// control flow structure?
|
|
const bool
|
|
reorder_blocks_; // Should the blocks be reordered for readability?
|
|
spv_endianness_t endian_; // The detected endianness of the binary.
|
|
std::stringstream text_; // Captures the text, if not printing.
|
|
out_stream out_; // The Output stream. Either to text_ or standard output.
|
|
disassemble::InstructionDisassembler instruction_disassembler_;
|
|
const bool header_; // Should we output header as the leading comment?
|
|
size_t byte_offset_; // The number of bytes processed so far.
|
|
bool inserted_decoration_space_ = false;
|
|
bool inserted_debug_space_ = false;
|
|
bool inserted_type_space_ = false;
|
|
|
|
// The CFG for the current function
|
|
ControlFlowGraph current_function_cfg_;
|
|
};
|
|
|
|
spv_result_t Disassembler::HandleHeader(spv_endianness_t endian,
|
|
uint32_t version, uint32_t generator,
|
|
uint32_t id_bound, uint32_t schema) {
|
|
endian_ = endian;
|
|
|
|
if (header_) {
|
|
instruction_disassembler_.EmitHeaderSpirv();
|
|
instruction_disassembler_.EmitHeaderVersion(version);
|
|
instruction_disassembler_.EmitHeaderGenerator(generator);
|
|
instruction_disassembler_.EmitHeaderIdBound(id_bound);
|
|
instruction_disassembler_.EmitHeaderSchema(schema);
|
|
}
|
|
|
|
byte_offset_ = SPV_INDEX_INSTRUCTION * sizeof(uint32_t);
|
|
|
|
return SPV_SUCCESS;
|
|
}
|
|
|
|
spv_result_t Disassembler::HandleInstruction(
|
|
const spv_parsed_instruction_t& inst) {
|
|
instruction_disassembler_.EmitSectionComment(inst, inserted_decoration_space_,
|
|
inserted_debug_space_,
|
|
inserted_type_space_);
|
|
|
|
// When nesting needs to be calculated or when the blocks are reordered, we
|
|
// have to have the full picture of the CFG first. Defer processing of the
|
|
// instructions until the entire function is visited. This is not done
|
|
// without those options (even if simpler) to improve debuggability; for
|
|
// example to be able to see whatever is parsed so far even if there is a
|
|
// parse error.
|
|
if (nested_indent_ || reorder_blocks_) {
|
|
switch (static_cast<spv::Op>(inst.opcode)) {
|
|
case spv::Op::OpLabel: {
|
|
// Add a new block to the CFG
|
|
SingleBlock new_block;
|
|
new_block.byte_offset = byte_offset_;
|
|
new_block.instructions.emplace_back(&inst);
|
|
current_function_cfg_.blocks.push_back(std::move(new_block));
|
|
break;
|
|
}
|
|
case spv::Op::OpFunctionEnd:
|
|
// Process the CFG and output the instructions
|
|
EmitCFG();
|
|
// Output OpFunctionEnd itself too
|
|
[[fallthrough]];
|
|
default:
|
|
if (!current_function_cfg_.blocks.empty()) {
|
|
// If in a function, stash the instruction for later.
|
|
current_function_cfg_.blocks.back().instructions.emplace_back(&inst);
|
|
} else {
|
|
// Otherwise emit the instruction right away.
|
|
instruction_disassembler_.EmitInstruction(inst, byte_offset_);
|
|
}
|
|
break;
|
|
}
|
|
} else {
|
|
instruction_disassembler_.EmitInstruction(inst, byte_offset_);
|
|
}
|
|
|
|
byte_offset_ += inst.num_words * sizeof(uint32_t);
|
|
|
|
return SPV_SUCCESS;
|
|
}
|
|
|
|
// Helper to get the operand of an instruction as an id.
|
|
uint32_t GetOperand(const spv_parsed_instruction_t* instruction,
|
|
uint32_t operand) {
|
|
return instruction->words[instruction->operands[operand].offset];
|
|
}
|
|
|
|
std::unordered_map<uint32_t, uint32_t> BuildControlFlowGraph(
|
|
ControlFlowGraph& cfg) {
|
|
std::unordered_map<uint32_t, uint32_t> id_to_index;
|
|
|
|
for (size_t index = 0; index < cfg.blocks.size(); ++index) {
|
|
SingleBlock& block = cfg.blocks[index];
|
|
|
|
// For future use, build the ID->index map
|
|
assert(static_cast<spv::Op>(block.instructions[0].get()->opcode) ==
|
|
spv::Op::OpLabel);
|
|
const uint32_t id = block.instructions[0].get()->result_id;
|
|
|
|
id_to_index[id] = static_cast<uint32_t>(index);
|
|
|
|
// Look for a merge instruction first. The function of OpBranch depends on
|
|
// that.
|
|
if (block.instructions.size() >= 3) {
|
|
const spv_parsed_instruction_t* maybe_merge =
|
|
block.instructions[block.instructions.size() - 2].get();
|
|
|
|
switch (static_cast<spv::Op>(maybe_merge->opcode)) {
|
|
case spv::Op::OpLoopMerge:
|
|
block.successors.merge_block_id = GetOperand(maybe_merge, 0);
|
|
block.successors.continue_block_id = GetOperand(maybe_merge, 1);
|
|
break;
|
|
|
|
case spv::Op::OpSelectionMerge:
|
|
block.successors.merge_block_id = GetOperand(maybe_merge, 0);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Then look at the last instruction; it must be a branch
|
|
assert(block.instructions.size() >= 2);
|
|
|
|
const spv_parsed_instruction_t* branch = block.instructions.back().get();
|
|
switch (static_cast<spv::Op>(branch->opcode)) {
|
|
case spv::Op::OpBranch:
|
|
if (block.successors.merge_block_id != 0) {
|
|
block.successors.body_block_id = GetOperand(branch, 0);
|
|
} else {
|
|
block.successors.next_block_id = GetOperand(branch, 0);
|
|
}
|
|
break;
|
|
|
|
case spv::Op::OpBranchConditional:
|
|
block.successors.true_block_id = GetOperand(branch, 1);
|
|
block.successors.false_block_id = GetOperand(branch, 2);
|
|
break;
|
|
|
|
case spv::Op::OpSwitch:
|
|
for (uint32_t case_index = 1; case_index < branch->num_operands;
|
|
case_index += 2) {
|
|
block.successors.case_block_ids.push_back(
|
|
GetOperand(branch, case_index));
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
return id_to_index;
|
|
}
|
|
|
|
// Helper to deal with nesting and non-existing ids / previously-assigned
|
|
// levels. It assigns a given nesting level `level` to the block identified by
|
|
// `id` (unless that block already has a nesting level assigned).
|
|
void Nest(ControlFlowGraph& cfg,
|
|
const std::unordered_map<uint32_t, uint32_t>& id_to_index,
|
|
uint32_t id, uint32_t level) {
|
|
if (id == 0) {
|
|
return;
|
|
}
|
|
|
|
const uint32_t block_index = id_to_index.at(id);
|
|
SingleBlock& block = cfg.blocks[block_index];
|
|
|
|
if (!block.nest_level_assigned) {
|
|
block.nest_level = level;
|
|
block.nest_level_assigned = true;
|
|
}
|
|
}
|
|
|
|
// For a given block, assign nesting level to its successors.
|
|
void NestSuccessors(ControlFlowGraph& cfg, const SingleBlock& block,
|
|
const std::unordered_map<uint32_t, uint32_t>& id_to_index) {
|
|
assert(block.nest_level_assigned);
|
|
|
|
// Nest loops as such:
|
|
//
|
|
// %loop = OpLabel
|
|
// OpLoopMerge %merge %cont ...
|
|
// OpBranch %body
|
|
// %body = OpLabel
|
|
// Op...
|
|
// %cont = OpLabel
|
|
// Op...
|
|
// %merge = OpLabel
|
|
// Op...
|
|
//
|
|
// Nest conditional branches as such:
|
|
//
|
|
// %header = OpLabel
|
|
// OpSelectionMerge %merge ...
|
|
// OpBranchConditional ... %true %false
|
|
// %true = OpLabel
|
|
// Op...
|
|
// %false = OpLabel
|
|
// Op...
|
|
// %merge = OpLabel
|
|
// Op...
|
|
//
|
|
// Nest switch/case as such:
|
|
//
|
|
// %header = OpLabel
|
|
// OpSelectionMerge %merge ...
|
|
// OpSwitch ... %default ... %case0 ... %case1 ...
|
|
// %default = OpLabel
|
|
// Op...
|
|
// %case0 = OpLabel
|
|
// Op...
|
|
// %case1 = OpLabel
|
|
// Op...
|
|
// ...
|
|
// %merge = OpLabel
|
|
// Op...
|
|
//
|
|
// The following can be observed:
|
|
//
|
|
// - In all cases, the merge block has the same nesting as this block
|
|
// - The continue block of loops is nested 1 level deeper
|
|
// - The body/branches/cases are nested 2 levels deeper
|
|
//
|
|
// Back branches to the header block, branches to the merge block, etc
|
|
// are correctly handled by processing the header block first (that is
|
|
// _this_ block, already processed), then following the above rules
|
|
// (in the same order) for any block that is not already processed.
|
|
Nest(cfg, id_to_index, block.successors.merge_block_id, block.nest_level);
|
|
Nest(cfg, id_to_index, block.successors.continue_block_id,
|
|
block.nest_level + 1);
|
|
Nest(cfg, id_to_index, block.successors.true_block_id, block.nest_level + 2);
|
|
Nest(cfg, id_to_index, block.successors.false_block_id, block.nest_level + 2);
|
|
Nest(cfg, id_to_index, block.successors.body_block_id, block.nest_level + 2);
|
|
Nest(cfg, id_to_index, block.successors.next_block_id, block.nest_level);
|
|
for (uint32_t case_block_id : block.successors.case_block_ids) {
|
|
Nest(cfg, id_to_index, case_block_id, block.nest_level + 2);
|
|
}
|
|
}
|
|
|
|
struct StackEntry {
|
|
// The index of the block (in ControlFlowGraph::blocks) to process.
|
|
uint32_t block_index;
|
|
// Whether this is the pre or post visit of the block. Because a post-visit
|
|
// traversal is needed, the same block is pushed back on the stack on
|
|
// pre-visit so it can be visited again on post-visit.
|
|
bool post_visit = false;
|
|
};
|
|
|
|
// Helper to deal with DFS traversal and non-existing ids
|
|
void VisitSuccesor(std::stack<StackEntry>* dfs_stack,
|
|
const std::unordered_map<uint32_t, uint32_t>& id_to_index,
|
|
uint32_t id) {
|
|
if (id != 0) {
|
|
dfs_stack->push({id_to_index.at(id), false});
|
|
}
|
|
}
|
|
|
|
// Given the control flow graph, calculates and returns the reverse post-order
|
|
// ordering of the blocks. The blocks are then disassembled in that order for
|
|
// readability.
|
|
std::vector<uint32_t> OrderBlocks(
|
|
ControlFlowGraph& cfg,
|
|
const std::unordered_map<uint32_t, uint32_t>& id_to_index) {
|
|
std::vector<uint32_t> post_order;
|
|
|
|
// Nest level of a function's first block is 0.
|
|
cfg.blocks[0].nest_level = 0;
|
|
cfg.blocks[0].nest_level_assigned = true;
|
|
|
|
// Stack of block indices as they are visited.
|
|
std::stack<StackEntry> dfs_stack;
|
|
dfs_stack.push({0, false});
|
|
|
|
std::set<uint32_t> visited;
|
|
|
|
while (!dfs_stack.empty()) {
|
|
const uint32_t block_index = dfs_stack.top().block_index;
|
|
const bool post_visit = dfs_stack.top().post_visit;
|
|
dfs_stack.pop();
|
|
|
|
// If this is the second time the block is visited, that's the post-order
|
|
// visit.
|
|
if (post_visit) {
|
|
post_order.push_back(block_index);
|
|
continue;
|
|
}
|
|
|
|
// If already visited, another path got to it first (like a case
|
|
// fallthrough), avoid reprocessing it.
|
|
if (visited.count(block_index) > 0) {
|
|
continue;
|
|
}
|
|
visited.insert(block_index);
|
|
|
|
// Push it back in the stack for post-order visit
|
|
dfs_stack.push({block_index, true});
|
|
|
|
SingleBlock& block = cfg.blocks[block_index];
|
|
|
|
// Assign nest levels of successors right away. The successors are either
|
|
// nested under this block, or are back or forward edges to blocks outside
|
|
// this nesting level (no farther than the merge block), whose nesting
|
|
// levels are already assigned before this block is visited.
|
|
NestSuccessors(cfg, block, id_to_index);
|
|
block.reachable = true;
|
|
|
|
// The post-order visit yields the order in which the blocks are naturally
|
|
// ordered _backwards_. So blocks to be ordered last should be visited
|
|
// first. In other words, they should be pushed to the DFS stack last.
|
|
VisitSuccesor(&dfs_stack, id_to_index, block.successors.true_block_id);
|
|
VisitSuccesor(&dfs_stack, id_to_index, block.successors.false_block_id);
|
|
VisitSuccesor(&dfs_stack, id_to_index, block.successors.body_block_id);
|
|
VisitSuccesor(&dfs_stack, id_to_index, block.successors.next_block_id);
|
|
for (uint32_t case_block_id : block.successors.case_block_ids) {
|
|
VisitSuccesor(&dfs_stack, id_to_index, case_block_id);
|
|
}
|
|
VisitSuccesor(&dfs_stack, id_to_index, block.successors.continue_block_id);
|
|
VisitSuccesor(&dfs_stack, id_to_index, block.successors.merge_block_id);
|
|
}
|
|
|
|
std::vector<uint32_t> order(post_order.rbegin(), post_order.rend());
|
|
|
|
// Finally, dump all unreachable blocks at the end
|
|
for (size_t index = 0; index < cfg.blocks.size(); ++index) {
|
|
SingleBlock& block = cfg.blocks[index];
|
|
|
|
if (!block.reachable) {
|
|
order.push_back(static_cast<uint32_t>(index));
|
|
block.nest_level = 0;
|
|
block.nest_level_assigned = true;
|
|
}
|
|
}
|
|
|
|
return order;
|
|
}
|
|
|
|
void Disassembler::EmitCFG() {
|
|
// Build the CFG edges. At the same time, build an ID->block index map to
|
|
// simplify building the CFG edges.
|
|
const std::unordered_map<uint32_t, uint32_t> id_to_index =
|
|
BuildControlFlowGraph(current_function_cfg_);
|
|
|
|
// Walk the CFG in reverse post-order to find the best ordering of blocks for
|
|
// presentation
|
|
std::vector<uint32_t> block_order =
|
|
OrderBlocks(current_function_cfg_, id_to_index);
|
|
assert(block_order.size() == current_function_cfg_.blocks.size());
|
|
|
|
// Walk the CFG either in block order or input order based on whether the
|
|
// reorder_blocks_ option is given.
|
|
for (uint32_t index = 0; index < current_function_cfg_.blocks.size();
|
|
++index) {
|
|
const uint32_t block_index = reorder_blocks_ ? block_order[index] : index;
|
|
const SingleBlock& block = current_function_cfg_.blocks[block_index];
|
|
|
|
// Emit instructions for this block
|
|
size_t byte_offset = block.byte_offset;
|
|
assert(block.nest_level_assigned);
|
|
|
|
for (const ParsedInstruction& inst : block.instructions) {
|
|
instruction_disassembler_.EmitInstructionInBlock(*inst.get(), byte_offset,
|
|
block.nest_level);
|
|
byte_offset += inst.get()->num_words * sizeof(uint32_t);
|
|
}
|
|
}
|
|
|
|
current_function_cfg_.blocks.clear();
|
|
}
|
|
|
|
spv_result_t Disassembler::SaveTextResult(spv_text* text_result) const {
|
|
if (!print_) {
|
|
size_t length = text_.str().size();
|
|
char* str = new char[length + 1];
|
|
if (!str) return SPV_ERROR_OUT_OF_MEMORY;
|
|
strncpy(str, text_.str().c_str(), length + 1);
|
|
spv_text text = new spv_text_t();
|
|
if (!text) {
|
|
delete[] str;
|
|
return SPV_ERROR_OUT_OF_MEMORY;
|
|
}
|
|
text->str = str;
|
|
text->length = length;
|
|
*text_result = text;
|
|
}
|
|
return SPV_SUCCESS;
|
|
}
|
|
|
|
spv_result_t DisassembleHeader(void* user_data, spv_endianness_t endian,
|
|
uint32_t /* magic */, uint32_t version,
|
|
uint32_t generator, uint32_t id_bound,
|
|
uint32_t schema) {
|
|
assert(user_data);
|
|
auto disassembler = static_cast<Disassembler*>(user_data);
|
|
return disassembler->HandleHeader(endian, version, generator, id_bound,
|
|
schema);
|
|
}
|
|
|
|
spv_result_t DisassembleInstruction(
|
|
void* user_data, const spv_parsed_instruction_t* parsed_instruction) {
|
|
assert(user_data);
|
|
auto disassembler = static_cast<Disassembler*>(user_data);
|
|
return disassembler->HandleInstruction(*parsed_instruction);
|
|
}
|
|
|
|
// Simple wrapper class to provide extra data necessary for targeted
|
|
// instruction disassembly.
|
|
class WrappedDisassembler {
|
|
public:
|
|
WrappedDisassembler(Disassembler* dis, const uint32_t* binary, size_t wc)
|
|
: disassembler_(dis), inst_binary_(binary), word_count_(wc) {}
|
|
|
|
Disassembler* disassembler() { return disassembler_; }
|
|
const uint32_t* inst_binary() const { return inst_binary_; }
|
|
size_t word_count() const { return word_count_; }
|
|
|
|
private:
|
|
Disassembler* disassembler_;
|
|
const uint32_t* inst_binary_;
|
|
const size_t word_count_;
|
|
};
|
|
|
|
spv_result_t DisassembleTargetHeader(void* user_data, spv_endianness_t endian,
|
|
uint32_t /* magic */, uint32_t version,
|
|
uint32_t generator, uint32_t id_bound,
|
|
uint32_t schema) {
|
|
assert(user_data);
|
|
auto wrapped = static_cast<WrappedDisassembler*>(user_data);
|
|
return wrapped->disassembler()->HandleHeader(endian, version, generator,
|
|
id_bound, schema);
|
|
}
|
|
|
|
spv_result_t DisassembleTargetInstruction(
|
|
void* user_data, const spv_parsed_instruction_t* parsed_instruction) {
|
|
assert(user_data);
|
|
auto wrapped = static_cast<WrappedDisassembler*>(user_data);
|
|
// Check if this is the instruction we want to disassemble.
|
|
if (wrapped->word_count() == parsed_instruction->num_words &&
|
|
std::equal(wrapped->inst_binary(),
|
|
wrapped->inst_binary() + wrapped->word_count(),
|
|
parsed_instruction->words)) {
|
|
// Found the target instruction. Disassemble it and signal that we should
|
|
// stop searching so we don't output the same instruction again.
|
|
if (auto error =
|
|
wrapped->disassembler()->HandleInstruction(*parsed_instruction))
|
|
return error;
|
|
return SPV_REQUESTED_TERMINATION;
|
|
}
|
|
return SPV_SUCCESS;
|
|
}
|
|
|
|
uint32_t GetLineLengthWithoutColor(const std::string line) {
|
|
// Currently, every added color is in the form \x1b...m, so instead of doing a
|
|
// lot of string comparisons with spvtools::clr::* strings, we just ignore
|
|
// those ranges.
|
|
uint32_t length = 0;
|
|
for (size_t i = 0; i < line.size(); ++i) {
|
|
if (line[i] == '\x1b') {
|
|
do {
|
|
++i;
|
|
} while (i < line.size() && line[i] != 'm');
|
|
continue;
|
|
}
|
|
|
|
++length;
|
|
}
|
|
|
|
return length;
|
|
}
|
|
|
|
constexpr int kStandardIndent = 15;
|
|
constexpr int kBlockNestIndent = 2;
|
|
constexpr int kBlockBodyIndentOffset = 2;
|
|
constexpr uint32_t kCommentColumn = 50;
|
|
} // namespace
|
|
|
|
namespace disassemble {
|
|
InstructionDisassembler::InstructionDisassembler(const AssemblyGrammar& grammar,
|
|
std::ostream& stream,
|
|
uint32_t options,
|
|
NameMapper name_mapper)
|
|
: grammar_(grammar),
|
|
stream_(stream),
|
|
print_(spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_PRINT, options)),
|
|
color_(spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_COLOR, options)),
|
|
indent_(spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_INDENT, options)
|
|
? kStandardIndent
|
|
: 0),
|
|
nested_indent_(
|
|
spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_NESTED_INDENT, options)),
|
|
comment_(spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_COMMENT, options)),
|
|
show_byte_offset_(
|
|
spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_SHOW_BYTE_OFFSET, options)),
|
|
name_mapper_(std::move(name_mapper)),
|
|
last_instruction_comment_alignment_(0) {}
|
|
|
|
void InstructionDisassembler::EmitHeaderSpirv() { stream_ << "; SPIR-V\n"; }
|
|
|
|
void InstructionDisassembler::EmitHeaderVersion(uint32_t version) {
|
|
stream_ << "; Version: " << SPV_SPIRV_VERSION_MAJOR_PART(version) << "."
|
|
<< SPV_SPIRV_VERSION_MINOR_PART(version) << "\n";
|
|
}
|
|
|
|
void InstructionDisassembler::EmitHeaderGenerator(uint32_t generator) {
|
|
const char* generator_tool =
|
|
spvGeneratorStr(SPV_GENERATOR_TOOL_PART(generator));
|
|
stream_ << "; Generator: " << generator_tool;
|
|
// For unknown tools, print the numeric tool value.
|
|
if (0 == strcmp("Unknown", generator_tool)) {
|
|
stream_ << "(" << SPV_GENERATOR_TOOL_PART(generator) << ")";
|
|
}
|
|
// Print the miscellaneous part of the generator word on the same
|
|
// line as the tool name.
|
|
stream_ << "; " << SPV_GENERATOR_MISC_PART(generator) << "\n";
|
|
}
|
|
|
|
void InstructionDisassembler::EmitHeaderIdBound(uint32_t id_bound) {
|
|
stream_ << "; Bound: " << id_bound << "\n";
|
|
}
|
|
|
|
void InstructionDisassembler::EmitHeaderSchema(uint32_t schema) {
|
|
stream_ << "; Schema: " << schema << "\n";
|
|
}
|
|
|
|
void InstructionDisassembler::EmitInstruction(
|
|
const spv_parsed_instruction_t& inst, size_t inst_byte_offset) {
|
|
EmitInstructionImpl(inst, inst_byte_offset, 0, false);
|
|
}
|
|
|
|
void InstructionDisassembler::EmitInstructionInBlock(
|
|
const spv_parsed_instruction_t& inst, size_t inst_byte_offset,
|
|
uint32_t block_indent) {
|
|
EmitInstructionImpl(inst, inst_byte_offset, block_indent, true);
|
|
}
|
|
|
|
void InstructionDisassembler::EmitInstructionImpl(
|
|
const spv_parsed_instruction_t& inst, size_t inst_byte_offset,
|
|
uint32_t block_indent, bool is_in_block) {
|
|
auto opcode = static_cast<spv::Op>(inst.opcode);
|
|
|
|
// To better align the comments (if any), write the instruction to a line
|
|
// first so its length can be readily available.
|
|
std::ostringstream line;
|
|
|
|
if (nested_indent_ && opcode == spv::Op::OpLabel) {
|
|
// Separate the blocks by an empty line to make them easier to separate
|
|
stream_ << std::endl;
|
|
}
|
|
|
|
if (inst.result_id) {
|
|
SetBlue();
|
|
const std::string id_name = name_mapper_(inst.result_id);
|
|
if (indent_)
|
|
line << std::setw(std::max(0, indent_ - 3 - int(id_name.size())));
|
|
line << "%" << id_name;
|
|
ResetColor();
|
|
line << " = ";
|
|
} else {
|
|
line << std::string(indent_, ' ');
|
|
}
|
|
|
|
if (nested_indent_ && is_in_block) {
|
|
// Output OpLabel at the specified nest level, and instructions inside
|
|
// blocks nested a little more.
|
|
uint32_t indent = block_indent;
|
|
bool body_indent = opcode != spv::Op::OpLabel;
|
|
|
|
line << std::string(
|
|
indent * kBlockNestIndent + (body_indent ? kBlockBodyIndentOffset : 0),
|
|
' ');
|
|
}
|
|
|
|
line << "Op" << spvOpcodeString(opcode);
|
|
|
|
for (uint16_t i = 0; i < inst.num_operands; i++) {
|
|
const spv_operand_type_t type = inst.operands[i].type;
|
|
assert(type != SPV_OPERAND_TYPE_NONE);
|
|
if (type == SPV_OPERAND_TYPE_RESULT_ID) continue;
|
|
line << " ";
|
|
EmitOperand(line, inst, i);
|
|
}
|
|
|
|
// For the sake of comment generation, store information from some
|
|
// instructions for the future.
|
|
if (comment_) {
|
|
GenerateCommentForDecoratedId(inst);
|
|
}
|
|
|
|
std::ostringstream comments;
|
|
const char* comment_separator = "";
|
|
|
|
if (show_byte_offset_) {
|
|
SetGrey(comments);
|
|
auto saved_flags = comments.flags();
|
|
auto saved_fill = comments.fill();
|
|
comments << comment_separator << "0x" << std::setw(8) << std::hex
|
|
<< std::setfill('0') << inst_byte_offset;
|
|
comments.flags(saved_flags);
|
|
comments.fill(saved_fill);
|
|
ResetColor(comments);
|
|
comment_separator = ", ";
|
|
}
|
|
|
|
if (comment_ && opcode == spv::Op::OpName) {
|
|
const spv_parsed_operand_t& operand = inst.operands[0];
|
|
const uint32_t word = inst.words[operand.offset];
|
|
comments << comment_separator << "id %" << word;
|
|
comment_separator = ", ";
|
|
}
|
|
|
|
if (comment_ && inst.result_id && id_comments_.count(inst.result_id) > 0) {
|
|
comments << comment_separator << id_comments_[inst.result_id].str();
|
|
comment_separator = ", ";
|
|
}
|
|
|
|
stream_ << line.str();
|
|
|
|
if (!comments.str().empty()) {
|
|
// Align the comments
|
|
const uint32_t line_length = GetLineLengthWithoutColor(line.str());
|
|
uint32_t align = std::max(
|
|
{line_length + 2, last_instruction_comment_alignment_, kCommentColumn});
|
|
// Round up the alignment to a multiple of 4 for more niceness.
|
|
align = (align + 3) & ~0x3u;
|
|
last_instruction_comment_alignment_ = align;
|
|
|
|
stream_ << std::string(align - line_length, ' ') << "; " << comments.str();
|
|
} else {
|
|
last_instruction_comment_alignment_ = 0;
|
|
}
|
|
|
|
stream_ << "\n";
|
|
}
|
|
|
|
void InstructionDisassembler::GenerateCommentForDecoratedId(
|
|
const spv_parsed_instruction_t& inst) {
|
|
assert(comment_);
|
|
auto opcode = static_cast<spv::Op>(inst.opcode);
|
|
|
|
std::ostringstream partial;
|
|
uint32_t id = 0;
|
|
const char* separator = "";
|
|
|
|
switch (opcode) {
|
|
case spv::Op::OpDecorate:
|
|
// Take everything after `OpDecorate %id` and associate it with id.
|
|
id = inst.words[inst.operands[0].offset];
|
|
for (uint16_t i = 1; i < inst.num_operands; i++) {
|
|
partial << separator;
|
|
separator = " ";
|
|
EmitOperand(partial, inst, i);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (id == 0) {
|
|
return;
|
|
}
|
|
|
|
// Add the new comment to the comments of this id
|
|
std::ostringstream& id_comment = id_comments_[id];
|
|
if (!id_comment.str().empty()) {
|
|
id_comment << ", ";
|
|
}
|
|
id_comment << partial.str();
|
|
}
|
|
|
|
void InstructionDisassembler::EmitSectionComment(
|
|
const spv_parsed_instruction_t& inst, bool& inserted_decoration_space,
|
|
bool& inserted_debug_space, bool& inserted_type_space) {
|
|
auto opcode = static_cast<spv::Op>(inst.opcode);
|
|
if (comment_ && opcode == spv::Op::OpFunction) {
|
|
stream_ << std::endl;
|
|
if (nested_indent_) {
|
|
// Double the empty lines between Function sections since nested_indent_
|
|
// also separates blocks by a blank.
|
|
stream_ << std::endl;
|
|
}
|
|
stream_ << std::string(indent_, ' ');
|
|
stream_ << "; Function " << name_mapper_(inst.result_id) << std::endl;
|
|
}
|
|
if (comment_ && !inserted_decoration_space && spvOpcodeIsDecoration(opcode)) {
|
|
inserted_decoration_space = true;
|
|
stream_ << std::endl;
|
|
stream_ << std::string(indent_, ' ');
|
|
stream_ << "; Annotations" << std::endl;
|
|
}
|
|
if (comment_ && !inserted_debug_space && spvOpcodeIsDebug(opcode)) {
|
|
inserted_debug_space = true;
|
|
stream_ << std::endl;
|
|
stream_ << std::string(indent_, ' ');
|
|
stream_ << "; Debug Information" << std::endl;
|
|
}
|
|
if (comment_ && !inserted_type_space && spvOpcodeGeneratesType(opcode)) {
|
|
inserted_type_space = true;
|
|
stream_ << std::endl;
|
|
stream_ << std::string(indent_, ' ');
|
|
stream_ << "; Types, variables and constants" << std::endl;
|
|
}
|
|
}
|
|
|
|
void InstructionDisassembler::EmitOperand(std::ostream& stream,
|
|
const spv_parsed_instruction_t& inst,
|
|
const uint16_t operand_index) const {
|
|
assert(operand_index < inst.num_operands);
|
|
const spv_parsed_operand_t& operand = inst.operands[operand_index];
|
|
const uint32_t word = inst.words[operand.offset];
|
|
switch (operand.type) {
|
|
case SPV_OPERAND_TYPE_RESULT_ID:
|
|
assert(false && "<result-id> is not supposed to be handled here");
|
|
SetBlue(stream);
|
|
stream << "%" << name_mapper_(word);
|
|
break;
|
|
case SPV_OPERAND_TYPE_ID:
|
|
case SPV_OPERAND_TYPE_TYPE_ID:
|
|
case SPV_OPERAND_TYPE_SCOPE_ID:
|
|
case SPV_OPERAND_TYPE_MEMORY_SEMANTICS_ID:
|
|
SetYellow(stream);
|
|
stream << "%" << name_mapper_(word);
|
|
break;
|
|
case SPV_OPERAND_TYPE_EXTENSION_INSTRUCTION_NUMBER: {
|
|
spv_ext_inst_desc ext_inst;
|
|
SetRed(stream);
|
|
if (grammar_.lookupExtInst(inst.ext_inst_type, word, &ext_inst) ==
|
|
SPV_SUCCESS) {
|
|
stream << ext_inst->name;
|
|
} else {
|
|
if (!spvExtInstIsNonSemantic(inst.ext_inst_type)) {
|
|
assert(false && "should have caught this earlier");
|
|
} else {
|
|
// for non-semantic instruction sets we can just print the number
|
|
stream << word;
|
|
}
|
|
}
|
|
} break;
|
|
case SPV_OPERAND_TYPE_SPEC_CONSTANT_OP_NUMBER: {
|
|
spv_opcode_desc opcode_desc;
|
|
if (grammar_.lookupOpcode(spv::Op(word), &opcode_desc))
|
|
assert(false && "should have caught this earlier");
|
|
SetRed(stream);
|
|
stream << opcode_desc->name;
|
|
} break;
|
|
case SPV_OPERAND_TYPE_LITERAL_INTEGER:
|
|
case SPV_OPERAND_TYPE_TYPED_LITERAL_NUMBER:
|
|
case SPV_OPERAND_TYPE_LITERAL_FLOAT: {
|
|
SetRed(stream);
|
|
EmitNumericLiteral(&stream, inst, operand);
|
|
ResetColor(stream);
|
|
} break;
|
|
case SPV_OPERAND_TYPE_LITERAL_STRING: {
|
|
stream << "\"";
|
|
SetGreen(stream);
|
|
|
|
std::string str = spvDecodeLiteralStringOperand(inst, operand_index);
|
|
for (char const& c : str) {
|
|
if (c == '"' || c == '\\') stream << '\\';
|
|
stream << c;
|
|
}
|
|
ResetColor(stream);
|
|
stream << '"';
|
|
} break;
|
|
case SPV_OPERAND_TYPE_CAPABILITY:
|
|
case SPV_OPERAND_TYPE_SOURCE_LANGUAGE:
|
|
case SPV_OPERAND_TYPE_EXECUTION_MODEL:
|
|
case SPV_OPERAND_TYPE_ADDRESSING_MODEL:
|
|
case SPV_OPERAND_TYPE_MEMORY_MODEL:
|
|
case SPV_OPERAND_TYPE_EXECUTION_MODE:
|
|
case SPV_OPERAND_TYPE_STORAGE_CLASS:
|
|
case SPV_OPERAND_TYPE_DIMENSIONALITY:
|
|
case SPV_OPERAND_TYPE_SAMPLER_ADDRESSING_MODE:
|
|
case SPV_OPERAND_TYPE_SAMPLER_FILTER_MODE:
|
|
case SPV_OPERAND_TYPE_SAMPLER_IMAGE_FORMAT:
|
|
case SPV_OPERAND_TYPE_FP_ROUNDING_MODE:
|
|
case SPV_OPERAND_TYPE_LINKAGE_TYPE:
|
|
case SPV_OPERAND_TYPE_ACCESS_QUALIFIER:
|
|
case SPV_OPERAND_TYPE_FUNCTION_PARAMETER_ATTRIBUTE:
|
|
case SPV_OPERAND_TYPE_DECORATION:
|
|
case SPV_OPERAND_TYPE_BUILT_IN:
|
|
case SPV_OPERAND_TYPE_GROUP_OPERATION:
|
|
case SPV_OPERAND_TYPE_KERNEL_ENQ_FLAGS:
|
|
case SPV_OPERAND_TYPE_KERNEL_PROFILING_INFO:
|
|
case SPV_OPERAND_TYPE_RAY_FLAGS:
|
|
case SPV_OPERAND_TYPE_RAY_QUERY_INTERSECTION:
|
|
case SPV_OPERAND_TYPE_RAY_QUERY_COMMITTED_INTERSECTION_TYPE:
|
|
case SPV_OPERAND_TYPE_RAY_QUERY_CANDIDATE_INTERSECTION_TYPE:
|
|
case SPV_OPERAND_TYPE_DEBUG_BASE_TYPE_ATTRIBUTE_ENCODING:
|
|
case SPV_OPERAND_TYPE_DEBUG_COMPOSITE_TYPE:
|
|
case SPV_OPERAND_TYPE_DEBUG_TYPE_QUALIFIER:
|
|
case SPV_OPERAND_TYPE_DEBUG_OPERATION:
|
|
case SPV_OPERAND_TYPE_CLDEBUG100_DEBUG_BASE_TYPE_ATTRIBUTE_ENCODING:
|
|
case SPV_OPERAND_TYPE_CLDEBUG100_DEBUG_COMPOSITE_TYPE:
|
|
case SPV_OPERAND_TYPE_CLDEBUG100_DEBUG_TYPE_QUALIFIER:
|
|
case SPV_OPERAND_TYPE_CLDEBUG100_DEBUG_OPERATION:
|
|
case SPV_OPERAND_TYPE_CLDEBUG100_DEBUG_IMPORTED_ENTITY:
|
|
case SPV_OPERAND_TYPE_FPDENORM_MODE:
|
|
case SPV_OPERAND_TYPE_FPOPERATION_MODE:
|
|
case SPV_OPERAND_TYPE_QUANTIZATION_MODES:
|
|
case SPV_OPERAND_TYPE_FPENCODING:
|
|
case SPV_OPERAND_TYPE_OVERFLOW_MODES: {
|
|
spv_operand_desc entry;
|
|
if (grammar_.lookupOperand(operand.type, word, &entry))
|
|
assert(false && "should have caught this earlier");
|
|
stream << entry->name;
|
|
} break;
|
|
case SPV_OPERAND_TYPE_FP_FAST_MATH_MODE:
|
|
case SPV_OPERAND_TYPE_FUNCTION_CONTROL:
|
|
case SPV_OPERAND_TYPE_LOOP_CONTROL:
|
|
case SPV_OPERAND_TYPE_IMAGE:
|
|
case SPV_OPERAND_TYPE_MEMORY_ACCESS:
|
|
case SPV_OPERAND_TYPE_SELECTION_CONTROL:
|
|
case SPV_OPERAND_TYPE_DEBUG_INFO_FLAGS:
|
|
case SPV_OPERAND_TYPE_CLDEBUG100_DEBUG_INFO_FLAGS:
|
|
case SPV_OPERAND_TYPE_RAW_ACCESS_CHAIN_OPERANDS:
|
|
EmitMaskOperand(stream, operand.type, word);
|
|
break;
|
|
default:
|
|
if (spvOperandIsConcreteMask(operand.type)) {
|
|
EmitMaskOperand(stream, operand.type, word);
|
|
} else if (spvOperandIsConcrete(operand.type)) {
|
|
spv_operand_desc entry;
|
|
if (grammar_.lookupOperand(operand.type, word, &entry))
|
|
assert(false && "should have caught this earlier");
|
|
stream << entry->name;
|
|
} else {
|
|
assert(false && "unhandled or invalid case");
|
|
}
|
|
break;
|
|
}
|
|
ResetColor(stream);
|
|
}
|
|
|
|
void InstructionDisassembler::EmitMaskOperand(std::ostream& stream,
|
|
const spv_operand_type_t type,
|
|
const uint32_t word) const {
|
|
// Scan the mask from least significant bit to most significant bit. For each
|
|
// set bit, emit the name of that bit. Separate multiple names with '|'.
|
|
uint32_t remaining_word = word;
|
|
uint32_t mask;
|
|
int num_emitted = 0;
|
|
for (mask = 1; remaining_word; mask <<= 1) {
|
|
if (remaining_word & mask) {
|
|
remaining_word ^= mask;
|
|
spv_operand_desc entry;
|
|
if (grammar_.lookupOperand(type, mask, &entry))
|
|
assert(false && "should have caught this earlier");
|
|
if (num_emitted) stream << "|";
|
|
stream << entry->name;
|
|
num_emitted++;
|
|
}
|
|
}
|
|
if (!num_emitted) {
|
|
// An operand value of 0 was provided, so represent it by the name
|
|
// of the 0 value. In many cases, that's "None".
|
|
spv_operand_desc entry;
|
|
if (SPV_SUCCESS == grammar_.lookupOperand(type, 0, &entry))
|
|
stream << entry->name;
|
|
}
|
|
}
|
|
|
|
void InstructionDisassembler::ResetColor(std::ostream& stream) const {
|
|
if (color_) stream << spvtools::clr::reset{print_};
|
|
}
|
|
void InstructionDisassembler::SetGrey(std::ostream& stream) const {
|
|
if (color_) stream << spvtools::clr::grey{print_};
|
|
}
|
|
void InstructionDisassembler::SetBlue(std::ostream& stream) const {
|
|
if (color_) stream << spvtools::clr::blue{print_};
|
|
}
|
|
void InstructionDisassembler::SetYellow(std::ostream& stream) const {
|
|
if (color_) stream << spvtools::clr::yellow{print_};
|
|
}
|
|
void InstructionDisassembler::SetRed(std::ostream& stream) const {
|
|
if (color_) stream << spvtools::clr::red{print_};
|
|
}
|
|
void InstructionDisassembler::SetGreen(std::ostream& stream) const {
|
|
if (color_) stream << spvtools::clr::green{print_};
|
|
}
|
|
|
|
void InstructionDisassembler::ResetColor() { ResetColor(stream_); }
|
|
void InstructionDisassembler::SetGrey() { SetGrey(stream_); }
|
|
void InstructionDisassembler::SetBlue() { SetBlue(stream_); }
|
|
void InstructionDisassembler::SetYellow() { SetYellow(stream_); }
|
|
void InstructionDisassembler::SetRed() { SetRed(stream_); }
|
|
void InstructionDisassembler::SetGreen() { SetGreen(stream_); }
|
|
} // namespace disassemble
|
|
|
|
std::string spvInstructionBinaryToText(const spv_target_env env,
|
|
const uint32_t* instCode,
|
|
const size_t instWordCount,
|
|
const uint32_t* code,
|
|
const size_t wordCount,
|
|
const uint32_t options) {
|
|
spv_context context = spvContextCreate(env);
|
|
const AssemblyGrammar grammar(context);
|
|
if (!grammar.isValid()) {
|
|
spvContextDestroy(context);
|
|
return "";
|
|
}
|
|
|
|
// Generate friendly names for Ids if requested.
|
|
std::unique_ptr<FriendlyNameMapper> friendly_mapper;
|
|
NameMapper name_mapper = GetTrivialNameMapper();
|
|
if (options & SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES) {
|
|
friendly_mapper = MakeUnique<FriendlyNameMapper>(context, code, wordCount);
|
|
name_mapper = friendly_mapper->GetNameMapper();
|
|
}
|
|
|
|
// Now disassemble!
|
|
Disassembler disassembler(grammar, options, name_mapper);
|
|
WrappedDisassembler wrapped(&disassembler, instCode, instWordCount);
|
|
spvBinaryParse(context, &wrapped, code, wordCount, DisassembleTargetHeader,
|
|
DisassembleTargetInstruction, nullptr);
|
|
|
|
spv_text text = nullptr;
|
|
std::string output;
|
|
if (disassembler.SaveTextResult(&text) == SPV_SUCCESS) {
|
|
output.assign(text->str, text->str + text->length);
|
|
// Drop trailing newline characters.
|
|
while (!output.empty() && output.back() == '\n') output.pop_back();
|
|
}
|
|
spvTextDestroy(text);
|
|
spvContextDestroy(context);
|
|
|
|
return output;
|
|
}
|
|
} // namespace spvtools
|
|
|
|
spv_result_t spvBinaryToText(const spv_const_context context,
|
|
const uint32_t* code, const size_t wordCount,
|
|
const uint32_t options, spv_text* pText,
|
|
spv_diagnostic* pDiagnostic) {
|
|
spv_context_t hijack_context = *context;
|
|
if (pDiagnostic) {
|
|
*pDiagnostic = nullptr;
|
|
spvtools::UseDiagnosticAsMessageConsumer(&hijack_context, pDiagnostic);
|
|
}
|
|
|
|
const spvtools::AssemblyGrammar grammar(&hijack_context);
|
|
if (!grammar.isValid()) return SPV_ERROR_INVALID_TABLE;
|
|
|
|
// Generate friendly names for Ids if requested.
|
|
std::unique_ptr<spvtools::FriendlyNameMapper> friendly_mapper;
|
|
spvtools::NameMapper name_mapper = spvtools::GetTrivialNameMapper();
|
|
if (options & SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES) {
|
|
friendly_mapper = spvtools::MakeUnique<spvtools::FriendlyNameMapper>(
|
|
&hijack_context, code, wordCount);
|
|
name_mapper = friendly_mapper->GetNameMapper();
|
|
}
|
|
|
|
// Now disassemble!
|
|
spvtools::Disassembler disassembler(grammar, options, name_mapper);
|
|
if (auto error =
|
|
spvBinaryParse(&hijack_context, &disassembler, code, wordCount,
|
|
spvtools::DisassembleHeader,
|
|
spvtools::DisassembleInstruction, pDiagnostic)) {
|
|
return error;
|
|
}
|
|
|
|
return disassembler.SaveTextResult(pText);
|
|
}
|