matinfo: add SPIR-V analysis functionality

This adds a new option to matinfo that does some very dumb regex-based
analysis on the disassembled SPIR-V, then dumps out transpiled GLSL
that has annotations added to the end of some codelines. For example:

```glsl
float luminance(vec3 linear) // POTENTIAL MIXED PRECISION %49 = OpDot %float %linear %48; relaxed = %49 %linear
{
    return dot(linear, vec3(0.2125999927520751953125, 0.715200006961822509765625, 0.072200000286102294921875));
}
```

The code currently tries to find potential mixed precision but does not
do the right thing yet for pointers and structs. This is just a starting
point to help diagnose problem areas in our generated SPIR-V.
This commit is contained in:
Philip Rideout
2019-08-09 14:21:43 -07:00
parent 829db244f2
commit cf9e87a892

View File

@@ -36,6 +36,10 @@
#include <fstream>
#include <iomanip>
#include <iostream>
#include <map>
#include <regex>
#include <set>
#include <sstream>
using namespace filaflat;
using namespace filamat;
@@ -118,6 +122,7 @@ struct Config {
bool printMetal = false;
bool transpile = false;
bool binary = false;
bool analyze = false;
uint64_t shaderIndex;
};
@@ -132,24 +137,26 @@ static void printUsage(const char* name) {
std::string execName(utils::Path(name).getName());
std::string usage(
"MATINFO prints information about material files compiled with matc\n"
"Usage:\n"
" MATINFO [options] <material file>\n"
"\n"
"Options:\n"
" --help, -h\n"
" Print this message\n\n"
" --print-glsl=[index], -g\n"
" Print GLSL for the nth shader (0 is the first OpenGL shader)\n\n"
" --print-spirv=[index], -s\n"
" Print disasm for the nth shader (0 is the first Vulkan shader)\n\n"
" --print-metal=[index], -m\n"
" Print Metal Shading Language for the nth shader (0 is the first Metal shader)\n\n"
" --print-vkglsl=[index], -v\n"
" Print the nth Vulkan shader transpiled into GLSL\n\n"
" --dump-binary=[index], -b\n"
" Dump binary SPIRV for the nth Vulkan shader to 'out.spv'\n\n"
" --license\n"
" Print copyright and license information\n\n"
"Usage:\n"
" MATINFO [options] <material file>\n"
"\n"
"Options:\n"
" --analyze-spirv=[index], -a\n"
" Print annotated GLSL for the nth shader (0 is the first Vulkan shader)\n\n"
" --help, -h\n"
" Print this message\n\n"
" --print-glsl=[index], -g\n"
" Print GLSL for the nth shader (0 is the first OpenGL shader)\n\n"
" --print-spirv=[index], -s\n"
" Print disasm for the nth shader (0 is the first Vulkan shader)\n\n"
" --print-metal=[index], -m\n"
" Print Metal Shading Language for the nth shader (0 is the first Metal shader)\n\n"
" --print-vkglsl=[index], -v\n"
" Print the nth Vulkan shader transpiled into GLSL\n\n"
" --dump-binary=[index], -b\n"
" Dump binary SPIRV for the nth Vulkan shader to 'out.spv'\n\n"
" --license\n"
" Print copyright and license information\n\n"
);
const std::string from("MATINFO");
@@ -168,13 +175,14 @@ static void license() {
static int handleArguments(int argc, char* argv[], Config* config) {
static constexpr const char* OPTSTR = "hlg:s:v:b:";
static const struct option OPTIONS[] = {
{ "help", no_argument, 0, 'h' },
{ "license", no_argument, 0, 'l' },
{ "print-glsl", required_argument, 0, 'g' },
{ "print-spirv", required_argument, 0, 's' },
{ "print-vkglsl", required_argument, 0, 'v' },
{ "print-metal", required_argument, 0, 'm' },
{ "dump-binary", required_argument, 0, 'b' },
{ "help", no_argument, 0, 'h' },
{ "license", no_argument, 0, 'l' },
{ "analyze-spirv", required_argument, 0, 'a' },
{ "print-glsl", required_argument, 0, 'g' },
{ "print-spirv", required_argument, 0, 's' },
{ "print-vkglsl", required_argument, 0, 'v' },
{ "print-metal", required_argument, 0, 'm' },
{ "dump-binary", required_argument, 0, 'b' },
{ 0, 0, 0, 0 } // termination of the option list
};
@@ -204,6 +212,11 @@ static int handleArguments(int argc, char* argv[], Config* config) {
config->shaderIndex = static_cast<uint64_t>(std::stoi(arg));
config->transpile = true;
break;
case 'a':
config->printSPIRV = true;
config->shaderIndex = static_cast<uint64_t>(std::stoi(arg));
config->analyze = true;
break;
case 'b':
config->printSPIRV = true;
config->shaderIndex = static_cast<uint64_t>(std::stoi(arg));
@@ -852,11 +865,10 @@ static bool printMaterialInfo(const ChunkContainer& container) {
return true;
}
// Consumes SPIRV binary and produces a GLSL-ES string.
static void transpileSpirv(const std::vector<uint32_t>& spirv) {
using namespace spirv_cross;
// We assume that users of the tool are interested in reading GLSL-ES, since our primary
// target platform is Android.
CompilerGLSL::Options emitOptions;
emitOptions.es = true;
emitOptions.vulkan_semantics = true;
@@ -866,15 +878,169 @@ static void transpileSpirv(const std::vector<uint32_t>& spirv) {
std::cout << glslCompiler.compile();
}
static void disassembleSpirv(const std::vector<uint32_t>& spirv) {
// Consumes SPIRV binary and produces an ordered map from "line number" to "GLSL string" where
// the line number is determined by line directives, and the GLSL string is one or more lines of
// transpiled GLSL-ES.
static std::map<int, std::string> transpileSpirvToLines(const std::vector<uint32_t>& spirv) {
using namespace spirv_cross;
CompilerGLSL::Options emitOptions;
emitOptions.es = true;
emitOptions.vulkan_semantics = true;
emitOptions.emit_line_directives = true;
CompilerGLSL glslCompiler(move(spirv));
glslCompiler.set_common_options(emitOptions);
std::string transpiled = glslCompiler.compile();
std::map<int, std::string> result;
const std::regex lineDirectivePattern("\\#line ([0-9]+)");
std::istringstream ss(glslCompiler.compile());
std::string glslCodeline;
int currentLineNumber = -1;
while (std::getline(ss, glslCodeline, '\n')) {
std::smatch matchResult;
if (std::regex_search(glslCodeline, matchResult, lineDirectivePattern)) {
currentLineNumber = stoi(matchResult[1].str());
} else {
result[currentLineNumber] += glslCodeline + "\n";
}
}
return result;
}
static void analyzeSpirv(const std::vector<uint32_t>& spirv, const char* disassembly) {
using namespace std;
const map<int, string> glsl = transpileSpirvToLines(spirv);
const regex globalDecoratorPattern("OpDecorate (\\%[A-Za-z_0-9]+) RelaxedPrecision");
const regex typeDefinitionPattern("(\\%[A-Za-z_0-9]+) = OpType[A-Z][a-z]+");
const regex memberDecoratorPattern("OpMemberDecorate (\\%[A-Za-z_0-9]+) ([0-9]+) RelaxedPrecision");
const regex structDefinitionPattern("(\\%[A-Za-z_0-9]+) = OpTypeStruct");
const regex lineDirectivePattern("OpLine (\\%[A-Za-z_0-9]+) ([0-9]+)");
const regex binaryFunctionPattern("(\\%[A-Za-z_0-9]+).*(\\%[A-Za-z_0-9]+)");
const regex operandPattern("(\\%[A-Za-z_0-9]+)");
const regex operatorPattern("Op[A-Z][A-Za-z]+");
const set<string> ignoredOperators = { "OpStore", "OpLoad", "OpAccessChain" };
string spirvInstruction;
smatch matchResult;
// In the first pass, collect types and variables that are decorated as "relaxed".
//
// NOTE: We also collect struct members but do not use them in any analysis (yet).
// In SPIR-V, struct fields are accessed through pointers, so we would need to:
//
// 1) Create a map of all OpConstant values (integers only, %int and %uint)
// 2) Parse all "OpAccessChain" instructions and dereference their OpConstant arguments
// 3) Follow through to the downstream OpStore / OpLoad
//
// Regarding struct field precision, the SPIR-V specification says:
//
// When applied to a variable or structure member, all loads and stores from the decorated
// object may be treated as though they were decorated with RelaxedPrecision. Loads may also
// be decorated with RelaxedPrecision, in which case they are treated as operating at
// relaxed precision.
set<string> relaxedPrecisionVariables;
set<string> typeIds;
{
istringstream ss(disassembly);
while (getline(ss, spirvInstruction, '\n')) {
if (regex_search(spirvInstruction, matchResult, typeDefinitionPattern)) {
typeIds.insert(matchResult[1].str());
} else if (regex_search(spirvInstruction, matchResult, globalDecoratorPattern)) {
relaxedPrecisionVariables.insert(matchResult[1].str());
} else if (regex_search(spirvInstruction, matchResult, memberDecoratorPattern)) {
string member = matchResult[1].str() + "." + matchResult[2].str();
relaxedPrecisionVariables.insert(member);
}
}
}
// In the second pass, track line numbers and detect potential mixed precision.
map<int, string> mixedPrecisionInfo;
{
istringstream ss(disassembly);
int currentLineNumber = -1;
while (getline(ss, spirvInstruction, '\n')) {
if (regex_search(spirvInstruction, matchResult, lineDirectivePattern)) {
currentLineNumber = stoi(matchResult[2].str());
} else if (regex_search(spirvInstruction, matchResult, binaryFunctionPattern)) {
// Trim out the leftmost whitespace.
const string trimmed = regex_replace(spirvInstruction, regex("^\\s+"), string(""));
// Ignore certain operators.
regex_search(trimmed, matchResult, operatorPattern);
if (ignoredOperators.count(matchResult[0]) || currentLineNumber == -1) {
continue;
}
// Check for mixed precision.
bool mixed = false;
int relaxed = -1;
string remaining = trimmed;
string info = trimmed + "; relaxed = ";
while (regex_search(remaining, matchResult, operandPattern)) {
const string arg = matchResult[1].str();
if (typeIds.count(arg) == 0) {
if (relaxedPrecisionVariables.count(arg) > 0) {
mixed = relaxed == 0 ? true : mixed;
relaxed = 1;
info += arg + " ";
} else {
mixed = relaxed == 1 ? true : mixed;
relaxed = 0;
}
}
remaining = matchResult.suffix();
}
if (mixed) {
mixedPrecisionInfo[currentLineNumber] = info;
}
}
}
}
// Finally, dump out the annotated GLSL.
for (auto keyValue : glsl) {
const int lineNumber = keyValue.first;
istringstream ss(keyValue.second);
string glslCodeline;
bool firstLine = true;
while (getline(ss, glslCodeline, '\n')) {
cout << glslCodeline;
if (firstLine) {
if (mixedPrecisionInfo.count(lineNumber)) {
string info = mixedPrecisionInfo.at(lineNumber);
cout << " // POTENTIAL MIXED PRECISION " + info;
}
firstLine = false;
}
cout << endl;
}
}
}
static void disassembleSpirv(const std::vector<uint32_t>& spirv, bool analyze) {
// If desired feel free to locally replace this with the glslang disassembler (spv::Disassemble)
// but please do not submit. We prefer to use the syntax that the standalone "spirv-dis" tool
// uses, which lets us easily generate test cases for the spirv-cross project.
auto context = spvContextCreate(SPV_ENV_UNIVERSAL_1_1);
spv_text text = nullptr;
const uint32_t options = SPV_BINARY_TO_TEXT_OPTION_INDENT;
const uint32_t options = SPV_BINARY_TO_TEXT_OPTION_INDENT |
SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES;
spvBinaryToText(context, spirv.data(), spirv.size(), options, &text, nullptr);
std::cout << text->str << std::endl;
if (analyze) {
analyzeSpirv(spirv, text->str);
} else {
std::cout << text->str << std::endl;
}
spvTextDestroy(text);
spvContextDestroy(context);
}
@@ -950,7 +1116,7 @@ static bool parseChunks(Config config, void* data, size_t size) {
} else if (config.binary) {
dumpSpirvBinary(spirv, "out.spv");
} else {
disassembleSpirv(spirv);
disassembleSpirv(spirv, config.analyze);
}
return true;