From 3747a5ef4fb7a97ded14509c6d62dd199f2bc81a Mon Sep 17 00:00:00 2001 From: baldurk Date: Wed, 8 Jul 2015 23:42:07 +0200 Subject: [PATCH] Add enough SPIR-V disassembly for the example shader in the spec * It's quite hacky at the moment since it doesn't read out the data in a structured way, just enough to disassemble. For now it's a proof of concept kind of thing. --- .../shaders/spirv/spirv_disassemble.cpp | 706 +++++++++++++++++- 1 file changed, 683 insertions(+), 23 deletions(-) diff --git a/renderdoc/driver/shaders/spirv/spirv_disassemble.cpp b/renderdoc/driver/shaders/spirv/spirv_disassemble.cpp index 7cf31f129..ce6752a89 100644 --- a/renderdoc/driver/shaders/spirv/spirv_disassemble.cpp +++ b/renderdoc/driver/shaders/spirv/spirv_disassemble.cpp @@ -28,12 +28,25 @@ #include "spirv_common.h" +#include +using std::pair; +using std::make_pair; + #undef min #undef max #include "3rdparty/glslang/SPIRV/spirv.h" +#include "3rdparty/glslang/SPIRV/GLSL450Lib.h" #include "3rdparty/glslang/glslang/Public/ShaderLang.h" +const char *GLSL_std_450_names[GLSL_STD_450::Count] = {0}; + +template +static string OptionalFlagString(EnumType e) +{ + return (int)e ? "[" + ToStr::Get(e) + "]" : ""; +} + void DisassembleSPIRV(SPIRVShaderStage shadType, const vector &spirv, string &disasm) { if(shadType >= eSPIRVInvalid) @@ -68,7 +81,7 @@ void DisassembleSPIRV(SPIRVShaderStage shadType, const vector &spirv, for(size_t i=0; i < ARRAY_COUNT(gens); i++) if(gens[i].magic == spirv[2]) gen = gens[i].name; disasm += StringFormat::Fmt("Version %u, Generator %08x (%s)\n", spirv[1], spirv[2], gen); - disasm += StringFormat::Fmt("IDs up to <%u>\n", spirv[3]); + disasm += StringFormat::Fmt("IDs up to {%u}\n", spirv[3]); uint32_t idbound = spirv[3]; @@ -76,13 +89,38 @@ void DisassembleSPIRV(SPIRVShaderStage shadType, const vector &spirv, disasm += "\n"; - uint32_t opidx = 0; - bool infunc = false; - vector resultnames; resultnames.resize(idbound); + + vector< pair > extensionSets; + extensionSets.reserve(2); + + vector< pair > decorations; - // fetch names and things to be used in the second pass + // needs to be fleshed out, but this is enough for now + enum BaseType + { + eSPIRVTypeVoid, + eSPIRVTypeBool, + eSPIRVTypeFloat, // assuming floats are all 32-bit + eSPIRVTypeSInt32, // assuming ints are signed or unsigned 32-bit + eSPIRVTypeUInt32, + }; + + vector typeinfo; + typeinfo.resize(idbound); + + vector values; + values.resize(idbound); + + // complete hack + vector membernames; + + // fetch names and things to be used in the second pass. + // could get away with one pass, just need to detect when function + // declarations/definitions start to fill out unnamed IDs. Or wrap + // it all up and give them anonymous names on the fly (more + // likely). size_t it = 5; while(it < spirv.size()) { @@ -90,14 +128,164 @@ void DisassembleSPIRV(SPIRVShaderStage shadType, const vector &spirv, spv::Op OpCode = spv::Op(spirv[it]&0xffff); if(OpCode == spv::OpName) + { resultnames[ spirv[it+1] ] = (const char *)&spirv[it+2]; + } + else if(OpCode == spv::OpLabel) + { + resultnames[spirv[it+1]] = StringFormat::Fmt("Label%u", spirv[it+1]); + } + else if(OpCode == spv::OpMemberName) + { + uint32_t id = spirv[it+1]; + uint32_t memberIdx = spirv[it+2]; + const char *memberName = (const char *)&spirv[it+3]; + + // COMPLETE hack + membernames.resize( RDCMAX(membernames.size(), memberIdx+1) ); + membernames[memberIdx] = memberName; + } + else if(OpCode == spv::OpDecorate) + { + uint32_t target = spirv[it+1]; + spv::Decoration decoration = spv::Decoration(spirv[it+2]); + + // TODO: decoration parameters here... + + decorations.push_back( make_pair(target, ToStr::Get(decoration)) ); + } + else if(OpCode == spv::OpTypeVoid) + { + resultnames[ spirv[it+1] ] = "void"; + typeinfo[ spirv[it+1] ] = eSPIRVTypeVoid; + } + else if(OpCode == spv::OpTypeBool) + { + resultnames[ spirv[it+1] ] = "bool"; + typeinfo[ spirv[it+1] ] = eSPIRVTypeBool; + } + else if(OpCode == spv::OpTypeInt) + { + resultnames[ spirv[it+1] ] = "int"; + RDCASSERT( spirv[it+2] == 32 ); + typeinfo[ spirv[it+1] ] = spirv[it+3] ? eSPIRVTypeSInt32 : eSPIRVTypeUInt32; + } + else if(OpCode == spv::OpTypeFloat) + { + resultnames[ spirv[it+1] ] = "float"; + RDCASSERT( spirv[it+2] == 32 ); + typeinfo[ spirv[it+1] ] = eSPIRVTypeFloat; + } + else if(OpCode == spv::OpTypeVector) + { + resultnames[ spirv[it+1] ] = StringFormat::Fmt("%s%u", resultnames[ spirv[it+2] ].c_str(), spirv[it+3]); + typeinfo[ spirv[it+1] ] = typeinfo[ spirv[it+2] ]; + } + else if(OpCode == spv::OpTypeArray) + { + resultnames[ spirv[it+1] ] = StringFormat::Fmt("%s[%u]", resultnames[ spirv[it+2] ].c_str(), values[ spirv[it+3] ]); + typeinfo[ spirv[it+1] ] = typeinfo[ spirv[it+2] ]; + } + else if(OpCode == spv::OpTypeStruct) + { + resultnames[ spirv[it+1] ] = "struct"; // don't need to decode this at all, we're not going to use the type info + } + else if(OpCode == spv::OpTypePointer) + { + uint32_t id = spirv[it+1]; + spv::StorageClass storage = spv::StorageClass(spirv[it+2]); + uint32_t baseType = spirv[it+3]; + + // bit specific for where we need it (variable declarations), but all this data will be properly parsed & stored + // so each instruction can use it as it wishes + resultnames[id] = resultnames[baseType] + "*"; + } + else if(OpCode == spv::OpTypeFunction) + { + // this name will just be used for the arguments in the function definition string, don't need to keep the type info + // or print the return type anywhere (as it must match the return type in the function definition opcode) + string args = ""; + + for(int i=3; i < WordCount; i++) + { + uint32_t typeId = spirv[it+i]; + + args += resultnames[typeId]; + + if(i+1 < WordCount) + args += ", "; + } + + if(args.empty()) args = "void"; + + resultnames[ spirv[it+1] ] = args; + } + else if(OpCode == spv::OpConstant) + { + uint32_t typeId = spirv[it+1]; + uint32_t id = spirv[it+2]; + + // hack - assuming only up to 32-bit values + values[id] = spirv[it+3]; + + BaseType type = typeinfo[typeId]; + string lit = ""; + + if(type == eSPIRVTypeBool) + lit += values[id] ? "true" : "false"; + else if(type == eSPIRVTypeFloat) + lit += StringFormat::Fmt("%f", *(float*)&values[id]); + else if(type == eSPIRVTypeSInt32) + lit += StringFormat::Fmt("%d", *(int32_t*)&values[id]); + else if(type == eSPIRVTypeUInt32) + lit += StringFormat::Fmt("%u", values[id]); + + resultnames[id] = StringFormat::Fmt("%s(%s)", resultnames[typeId].c_str(), lit.c_str()); + } + else if(OpCode == spv::OpConstantComposite) + { + uint32_t typeId = spirv[it+1]; + uint32_t id = spirv[it+2]; + + BaseType type = typeinfo[typeId]; + string lits = ""; + + for(int i=3; i < WordCount; i++) + { + uint32_t val = spirv[it+i]; + + if(type == eSPIRVTypeBool) + lits += values[val] ? "true" : "false"; + else if(type == eSPIRVTypeFloat) + lits += StringFormat::Fmt("%f", *(float*)&values[val]); + else if(type == eSPIRVTypeSInt32) + lits += StringFormat::Fmt("%d", *(int32_t*)&values[val]); + else if(type == eSPIRVTypeUInt32) + lits += StringFormat::Fmt("%u", values[val]); + + if(i+1 < WordCount) + lits += ", "; + } + + resultnames[id] = StringFormat::Fmt("%s(%s)", resultnames[typeId].c_str(), lits.c_str()); + } it += WordCount; } for(size_t i=0; i < resultnames.size(); i++) if(resultnames[i].empty()) - resultnames[i] = StringFormat::Fmt("<%d>", i); + resultnames[i] = StringFormat::Fmt("{%d}", i); + + const size_t tabSize = 2; + + string indent; + indent.reserve(tabSize*6); + + string funcname; + vector flowstack; + + bool variables = false; it = 5; while(it < spirv.size()) @@ -111,47 +299,383 @@ void DisassembleSPIRV(SPIRVShaderStage shadType, const vector &spirv, switch(OpCode) { case spv::OpSource: - body = StringFormat::Fmt("%s %d", ToStr::Get(spv::SourceLanguage(spirv[it+1])).c_str(), spirv[it+2]); + { + body = StringFormat::Fmt("Source %s %d", ToStr::Get(spv::SourceLanguage(spirv[it+1])).c_str(), spirv[it+2]); break; + } case spv::OpExtInstImport: + { resultnames[ spirv[it+1] ] = (char *)&spirv[it+2]; - body = StringFormat::Fmt("%s", (char *)&spirv[it+2]); + body = StringFormat::Fmt("ExtInstImport %s", (char *)&spirv[it+2]); + + if(resultnames[ spirv[it+1] ] == "GLSL.std.450") + { + extensionSets.push_back( make_pair(spirv[it+1], GLSL_std_450_names) ); + + if(GLSL_std_450_names[0] == NULL) + GLSL_STD_450::GetDebugNames(GLSL_std_450_names); + } + break; + } case spv::OpMemoryModel: - body = StringFormat::Fmt("%s Addressing, %s Memory model", + { + body = StringFormat::Fmt("MemoryModel %s Addressing, %s Memory model", ToStr::Get(spv::AddressingModel(spirv[it+1])).c_str(), ToStr::Get(spv::MemoryModel(spirv[it+2])).c_str()); break; + } case spv::OpEntryPoint: - body = StringFormat::Fmt("%s (%s)", + { + body = StringFormat::Fmt("EntryPoint = %s (%s)", resultnames[ spirv[it+2] ].c_str(), ToStr::Get(spv::ExecutionModel(spirv[it+1])).c_str()); break; + } + case spv::OpVariable: + { + if(!variables) + { + variables = true; + disasm += "\n"; + } + + uint32_t retType = spirv[it+1]; + uint32_t resultId = spirv[it+2]; + spv::StorageClass control = spv::StorageClass(spirv[it+3]); + + uint32_t initializer = ~0U; + if(WordCount > 4) + initializer = spirv[it+4]; + + string decorationsStr = ""; + + for(auto it=decorations.begin(); it != decorations.end(); ++it) + { + if(it->first == resultId) + { + decorationsStr += it->second + " "; + } + } + + body = StringFormat::Fmt("%s%s %s %s", + decorationsStr.c_str(), + ToStr::Get(control).c_str(), + resultnames[ retType ].c_str(), + resultnames[ resultId ].c_str()); + + if(initializer < idbound) + body += StringFormat::Fmt(" = %s", resultnames[initializer].c_str()); + break; + } case spv::OpFunction: - infunc = true; + { + uint32_t retType = spirv[it+1]; + uint32_t resultId = spirv[it+2]; + spv::FunctionControlMask control = spv::FunctionControlMask(spirv[it+3]); + uint32_t funcType = spirv[it+4]; + + // add an extra newline + disasm += "\n"; + body = StringFormat::Fmt("%s %s(%s) %s {", + resultnames[retType].c_str(), + resultnames[resultId].c_str(), + resultnames[funcType].c_str(), + OptionalFlagString(control).c_str()); + + funcname = resultnames[resultId]; + break; + } case spv::OpFunctionEnd: - infunc = false; + { + body = StringFormat::Fmt("} // end of %s", funcname.c_str()); + funcname = ""; + indent.resize(indent.size() - tabSize); break; + } + case spv::OpAccessChain: + { + uint32_t retType = spirv[it+1]; + uint32_t resultId = spirv[it+2]; + uint32_t base = spirv[it+3]; + + body = StringFormat::Fmt("%s %s = %s", + resultnames[retType].c_str(), + resultnames[resultId].c_str(), + resultnames[base].c_str()); + + // this is a complete and utter hack + for(int i=4; i < WordCount; i++) + { + if(i == 4 && values[spirv[it+4]] < membernames.size()) + body += StringFormat::Fmt(".%s", membernames[ values[spirv[it+4]] ]); + else + body += StringFormat::Fmt("[%s]", resultnames[ spirv[it+i] ].c_str()); + } + + break; + } + case spv::OpLoad: + { + uint32_t retType = spirv[it+1]; + uint32_t resultId = spirv[it+2]; + uint32_t pointer = spirv[it+3]; + + spv::MemoryAccessMask access = spv::MemoryAccessMaskNone; + + for(int i=4; i < WordCount; i++) + { + if(i == WordCount-1) + { + access = spv::MemoryAccessMask(spirv[it+i]); + } + else + { + uint32_t lit = spirv[it+i]; + // don't understand what these literals are - seems like OpAccessChain handles + // struct member/array access so it doesn't seem to be for array indices + RDCBREAK(); + } + } + + body = StringFormat::Fmt("%s %s = Load(%s) %s", + resultnames[retType].c_str(), + resultnames[resultId].c_str(), + resultnames[pointer].c_str(), + OptionalFlagString(access).c_str()); + break; + } + case spv::OpStore: + case spv::OpCopyMemory: + { + uint32_t pointer = spirv[it+1]; + uint32_t object = spirv[it+2]; + + spv::MemoryAccessMask access = spv::MemoryAccessMaskNone; + + for(int i=3; i < WordCount; i++) + { + if(i == WordCount-1) + { + access = spv::MemoryAccessMask(spirv[it+i]); + } + else + { + uint32_t lit = spirv[it+i]; + // don't understand what these literals are - seems like OpAccessChain handles + // struct member/array access so it doesn't seem to be for array indices + RDCBREAK(); + } + } + + if(OpCode == spv::OpStore) + body = StringFormat::Fmt("Store(%s) = %s %s", + resultnames[ pointer ].c_str(), + resultnames[ object ].c_str(), + OptionalFlagString(access).c_str()); + if(OpCode == spv::OpCopyMemory) + body = StringFormat::Fmt("Copy(%s) = Load(%s) %s", + resultnames[ pointer ].c_str(), + resultnames[ object ].c_str(), + OptionalFlagString(access).c_str()); + break; + } case spv::OpName: + case spv::OpMemberName: + case spv::OpDecorate: + case spv::OpConstant: + case spv::OpConstantComposite: + case spv::OpTypeVoid: + case spv::OpTypeBool: + case spv::OpTypeInt: + case spv::OpTypeFloat: + case spv::OpTypeVector: + case spv::OpTypePointer: + case spv::OpTypeArray: + case spv::OpTypeStruct: + case spv::OpTypeFunction: + { silent = true; break; + } + case spv::OpIAdd: + case spv::OpIMul: + case spv::OpFAdd: + case spv::OpFMul: + case spv::OpSLessThan: + { + char op = '?'; + switch(OpCode) + { + case spv::OpIAdd: + case spv::OpFAdd: + op = '+'; + break; + case spv::OpIMul: + case spv::OpFMul: + op = '*'; + break; + case spv::OpSLessThan: + op = '<'; + break; + } + + uint32_t retType = spirv[it+1]; + uint32_t result = spirv[it+2]; + uint32_t a = spirv[it+3]; + uint32_t b = spirv[it+4]; + + body = StringFormat::Fmt("%s %s = %s %c %s", + resultnames[ retType ].c_str(), + resultnames[ result ].c_str(), + resultnames[ a ].c_str(), + op, + resultnames[ b ].c_str()); + break; + } + case spv::OpExtInst: + { + uint32_t retType = spirv[it+1]; + uint32_t result = spirv[it+2]; + uint32_t extset = spirv[it+3]; + uint32_t instruction = spirv[it+4]; + + string instructionName = ""; + + for(auto ext = extensionSets.begin(); ext != extensionSets.end(); ++ext) + if(ext->first == extset) + instructionName = ext->second[instruction]; + + if(instructionName.empty()) + instructionName = StringFormat::Fmt("Unknown%u", instruction); + + string args = ""; + + for(int i=5; i < WordCount; i++) + { + args += resultnames[ spirv[it+i] ]; + + if(i+1 < WordCount) + args += ", "; + } + + body = StringFormat::Fmt("%s %s = %s::%s(%s)", + resultnames[ retType ].c_str(), + resultnames[ result ].c_str(), + resultnames[ extset ].c_str(), + instructionName.c_str(), + args.c_str()); + + break; + } + case spv::OpReturn: + { + body = "Return"; + break; + } + case spv::OpSelectionMerge: + { + uint32_t mergeLabel = spirv[it+1]; + spv::SelectionControlMask control = spv::SelectionControlMask(spirv[it+2]); + + flowstack.push_back(mergeLabel); + + body = StringFormat::Fmt("SelectionMerge %s %s", resultnames[ mergeLabel ].c_str(), OptionalFlagString(control).c_str()); + break; + } + case spv::OpLoopMerge: + { + uint32_t mergeLabel = spirv[it+1]; + spv::LoopControlMask control = spv::LoopControlMask(spirv[it+2]); + + flowstack.push_back(mergeLabel); + + body = StringFormat::Fmt("LoopMerge %s %s", resultnames[ mergeLabel ].c_str(), OptionalFlagString(control).c_str()); + break; + } + case spv::OpBranch: + { + body = StringFormat::Fmt("goto %s", resultnames[ spirv[it+1] ].c_str()); + break; + } + case spv::OpBranchConditional: + { + uint32_t condition = spirv[it+1]; + uint32_t truelabel = spirv[it+2]; + uint32_t falselabel = spirv[it+3]; + + if(WordCount == 4) + { + body = StringFormat::Fmt("if(%s) goto %s, else goto %s", + resultnames[condition].c_str(), + resultnames[truelabel].c_str(), + resultnames[falselabel].c_str()); + } + else + { + uint32_t weightA = spirv[it+4]; + uint32_t weightB = spirv[it+5]; + + float a = float(weightA)/float(weightA+weightB); + float b = float(weightB)/float(weightA+weightB); + + a *= 100.0f; + b *= 100.0f; + + body = StringFormat::Fmt("if(%s) goto %.2f%% %s, else goto %.2f%% %s", + a, + resultnames[condition].c_str(), + resultnames[truelabel].c_str(), + b, + resultnames[falselabel].c_str()); + } + + break; + } + case spv::OpLabel: + { + body = resultnames[spirv[it+1]] + ":"; + + if(!flowstack.empty() && flowstack.back() == spirv[it+1]) + indent.resize(indent.size() - tabSize); + break; + } + default: + { + body = "!" + ToStr::Get(OpCode); + for(uint16_t i=1; i < WordCount; i++) + { + if(spirv[it+i] <= idbound) + body += StringFormat::Fmt(" %u%s", spirv[it+i], i+1 < WordCount ? "," : ""); + else + body += StringFormat::Fmt(" %#x%s", spirv[it+i], i+1 < WordCount ? "," : ""); + } + break; + } + } + + if(!silent) + disasm += StringFormat::Fmt("%s%s\n", indent.c_str(), body.c_str()); + + // post printing operations + switch(OpCode) + { + case spv::OpFunction: + indent.insert(indent.end(), tabSize, ' '); + break; + case spv::OpSelectionMerge: + case spv::OpLoopMerge: + indent.insert(indent.end(), tabSize, ' '); + break; default: break; } - - if(infunc) - { - disasm += StringFormat::Fmt("% 4u: %s %s\n", opidx, ToStr::Get(OpCode).c_str(), body.c_str()); - opidx++; - } - else if(!silent) - { - disasm += StringFormat::Fmt(" %s %s\n", ToStr::Get(OpCode).c_str(), body.c_str()); - } it += WordCount; } + + return; } template<> @@ -429,7 +953,7 @@ string ToStrHelper::Get(const spv::Op &el) default: break; } - return "Unrecognised"; + return StringFormat::Fmt("Unrecognised{%u}", (uint32_t)el); } template<> @@ -494,3 +1018,139 @@ string ToStrHelper::Get(const spv::ExecutionModel &e return "Unrecognised"; } + +template<> +string ToStrHelper::Get(const spv::Decoration &el) +{ + switch(el) + { + case spv::DecorationPrecisionLow: return "PrecisionLow"; + case spv::DecorationPrecisionMedium: return "PrecisionMedium"; + case spv::DecorationPrecisionHigh: return "PrecisionHigh"; + case spv::DecorationBlock: return "Block"; + case spv::DecorationBufferBlock: return "BufferBlock"; + case spv::DecorationRowMajor: return "RowMajor"; + case spv::DecorationColMajor: return "ColMajor"; + case spv::DecorationGLSLShared: return "GLSLShared"; + case spv::DecorationGLSLStd140: return "GLSLStd140"; + case spv::DecorationGLSLStd430: return "GLSLStd430"; + case spv::DecorationGLSLPacked: return "GLSLPacked"; + case spv::DecorationSmooth: return "Smooth"; + case spv::DecorationNoperspective: return "Noperspective"; + case spv::DecorationFlat: return "Flat"; + case spv::DecorationPatch: return "Patch"; + case spv::DecorationCentroid: return "Centroid"; + case spv::DecorationSample: return "Sample"; + case spv::DecorationInvariant: return "Invariant"; + case spv::DecorationRestrict: return "Restrict"; + case spv::DecorationAliased: return "Aliased"; + case spv::DecorationVolatile: return "Volatile"; + case spv::DecorationConstant: return "Constant"; + case spv::DecorationCoherent: return "Coherent"; + case spv::DecorationNonwritable: return "Nonwritable"; + case spv::DecorationNonreadable: return "Nonreadable"; + case spv::DecorationUniform: return "Uniform"; + case spv::DecorationNoStaticUse: return "NoStaticUse"; + case spv::DecorationCPacked: return "CPacked"; + case spv::DecorationSaturatedConversion: return "SaturatedConversion"; + case spv::DecorationStream: return "Stream"; + case spv::DecorationLocation: return "Location"; + case spv::DecorationComponent: return "Component"; + case spv::DecorationIndex: return "Index"; + case spv::DecorationBinding: return "Binding"; + case spv::DecorationDescriptorSet: return "DescriptorSet"; + case spv::DecorationOffset: return "Offset"; + case spv::DecorationAlignment: return "Alignment"; + case spv::DecorationXfbBuffer: return "XfbBuffer"; + case spv::DecorationStride: return "Stride"; + case spv::DecorationBuiltIn: return "BuiltIn"; + case spv::DecorationFuncParamAttr: return "FuncParamAttr"; + case spv::DecorationFPRoundingMode: return "FPRoundingMode"; + case spv::DecorationFPFastMathMode: return "FPFastMathMode"; + case spv::DecorationLinkageAttributes: return "LinkageAttributes"; + case spv::DecorationSpecId: return "SpecId"; + default: break; + } + + return "Unrecognised"; +} + +template<> +string ToStrHelper::Get(const spv::StorageClass &el) +{ + switch(el) + { + case spv::StorageClassUniformConstant: return "UniformConstant"; + case spv::StorageClassInput: return "Input"; + case spv::StorageClassUniform: return "Uniform"; + case spv::StorageClassOutput: return "Output"; + case spv::StorageClassWorkgroupLocal: return "WorkgroupLocal"; + case spv::StorageClassWorkgroupGlobal: return "WorkgroupGlobal"; + case spv::StorageClassPrivateGlobal: return "PrivateGlobal"; + case spv::StorageClassFunction: return "Function"; + case spv::StorageClassGeneric: return "Generic"; + case spv::StorageClassPrivate: return "Private"; + case spv::StorageClassAtomicCounter: return "AtomicCounter"; + default: break; + } + + return "Unrecognised"; +} + +template<> +string ToStrHelper::Get(const spv::FunctionControlMask &el) +{ + string ret; + + if(el & spv::FunctionControlInlineMask) ret += ", Inline"; + if(el & spv::FunctionControlDontInlineMask) ret += ", DontInline"; + if(el & spv::FunctionControlPureMask) ret += ", Pure"; + if(el & spv::FunctionControlConstMask) ret += ", Const"; + + if(!ret.empty()) + ret = ret.substr(2); + + return ret; +} + +template<> +string ToStrHelper::Get(const spv::SelectionControlMask &el) +{ + string ret; + + if(el & spv::SelectionControlFlattenMask) ret += ", Flatten"; + if(el & spv::SelectionControlDontFlattenMask) ret += ", DontFlatten"; + + if(!ret.empty()) + ret = ret.substr(2); + + return ret; +} + +template<> +string ToStrHelper::Get(const spv::LoopControlMask &el) +{ + string ret; + + if(el & spv::LoopControlUnrollMask) ret += ", Unroll"; + if(el & spv::LoopControlDontUnrollMask) ret += ", DontUnroll"; + + if(!ret.empty()) + ret = ret.substr(2); + + return ret; +} + +template<> +string ToStrHelper::Get(const spv::MemoryAccessMask &el) +{ + string ret; + + if(el & spv::MemoryAccessVolatileMask) ret += ", Volatile"; + if(el & spv::MemoryAccessAlignedMask) ret += ", Aligned"; + + if(!ret.empty()) + ret = ret.substr(2); + + return ret; +}