in qrenderdoc/Code/BufferFormatter.cpp [516:2263]
ParsedFormat BufferFormatter::ParseFormatString(const QString &formatString, uint64_t maxLen,
bool cbuffer)
{
ParsedFormat ret;
StructFormatData root;
StructFormatData *cur = &root;
QMap<QString, StructFormatData> structelems;
QString lastStruct;
// regex doesn't account for trailing or preceeding whitespace, or comments
QRegularExpression regExpr(
lit("^" // start of the line
"(?<major>row_major\\s+|column_major\\s+)?" // matrix majorness
"(?<sign>unsigned\\s+|signed\\s+)?" // allow 'signed int' or 'unsigned char'
"(?<rgb>rgb\\s+)?" // rgb element colourising
"(?<type>" // group the options for the type
"uintten|unormten" // R10G10B10A2 types
"|floateleven" // R11G11B10 special type
"|unormh|unormb" // UNORM 16-bit and 8-bit types
"|snormh|snormb" // SNORM 16-bit and 8-bit types
"|bool" // bool is stored as 4-byte int
"|byte|short|int|long|char" // signed ints
"|ubyte|ushort|uint|ulong" // unsigned ints
"|xbyte|xshort|xint|xlong" // hex ints
"|half|float|double" // float types
"|vec|uvec|ivec|dvec" // OpenGL vector types
"|mat|umat|imat|dmat" // OpenGL matrix types
"|int8_t|uint8_t" // C-style sized 8-bit types
"|int16_t|uint16_t" // C-style sized 16-bit types
"|int32_t|uint32_t" // C-style sized 32-bit types
"|int64_t|uint64_t" // C-style sized 64-bit types
"|float16_t|float32_t|float64_t" // C-style sized float types
")" // end of the type group
"(?<vec>[1-9])?" // might be a vector
"(?<mat>x[1-9])?" // or a matrix
"(?<name>\\s+[A-Za-z@_][A-Za-z0-9@_]*)?" // get identifier name
"(?<array>\\s*\\[[0-9]*\\])?" // optional array dimension
"(\\s*:\\s*" // optional specifier after :
"(" // bitfield or semantic
"(?<bitfield>[1-9][0-9]*)|" // bitfield packing
"(?<semantic>[A-Za-z_][A-Za-z0-9_]*)" // semantic to ignore
")" // end bitfield or semantic
")?"
"$"));
bool success = true;
// remove any dos newlines
QString text = formatString;
text.replace(lit("\r\n"), lit("\n"));
QRegularExpression annotationRegex(
lit("^" // start of the line
"\\[\\[" // opening [[
"(?<name>[a-zA-Z0-9_-]+)" // annotation name
"(\\((?<param>[^)]+)\\))?" // optional parameter in ()s
"\\]\\]" // closing ]]
"\\s*"));
QRegularExpression structDeclRegex(
lit("^(struct|enum)\\s+([A-Za-z_][A-Za-z0-9_]*)(\\s*:\\s*([a-z]+))?$"));
QRegularExpression structUseRegex(
lit("^" // start of the line
"([A-Za-z_][A-Za-z0-9_]*)" // struct type name
"([ \\t\\r\\n*]+)" // maybe a pointer, but at least some whitespace
"([A-Za-z@_][A-Za-z0-9@_]*)?" // variable name
"(\\s*\\[[0-9]*\\])?" // optional array dimension
"(\\s*:\\s*([1-9][0-9]*))?" // optional bitfield packing
"$"));
QRegularExpression enumValueRegex(
lit("^" // start of the line
"([A-Za-z_][A-Za-z0-9_]*)" // value name
"\\s*=\\s*" // maybe a pointer
"(-?0x[0-9a-fA-F]+|-?[0-9]+)" // numerical value
"$"));
QRegularExpression bitfieldSkipRegex(
lit("^" // start of the line
"(unsigned\\s+|signed\\s+)?" // allow 'signed int' or 'unsigned char'
"(" // type group
"|bool" // bool is stored as 4-byte int
"|byte|short|int|long|char" // signed ints
"|ubyte|ushort|uint|ulong" // unsigned ints
"|xbyte|xshort|xint|xlong" // hex ints
")" // end of the type group
// no variable name
"\\s*:\\s*([1-9][0-9]*)" // bitfield packing
"$"));
QRegularExpression packingRegex(
lit("^" // start of the line
"#\\s*pack\\s*\\(" // #pack(
"(?<rule>[a-zA-Z0-9_]+)" // packing ruleset or individual rule
"\\)" // )
"$"));
uint32_t bitfieldCurPos = ~0U;
struct Annotation
{
QString name;
QString param;
};
// default to scalar (tight packing) if nothing else is specified at all. The expectation is
// anything that needs a better default will insert that into the format string for the user,
// or be picked up below
Packing::Rules &pack = ret.packing;
pack = Packing::Scalar;
// for D3D and GL we default to the only valid packing for cbuffers and UAVs. The user can still
// override this if they really wish with a #pack, but this makes sense as a sensible default
if(cbuffer)
{
if(IsD3D(m_API))
pack = Packing::D3DCB;
else if(m_API == GraphicsAPI::OpenGL)
pack = Packing::std140;
}
else
{
if(IsD3D(m_API))
pack = Packing::D3DUAV;
else if(m_API == GraphicsAPI::OpenGL)
pack = Packing::std430;
}
// vulkan allows scalar packing in any buffer, so don't wrest control away from the user
int line = 0;
QMap<int, QString> &errors = ret.errors;
auto reportError = [&line, &errors](QString err) { errors[line] = err.trimmed(); };
QList<Annotation> annotations;
QString parseText = text;
int parseLine = 0;
// get each line and parse it to determine the format the user wanted
while(!parseText.isEmpty())
{
// consume up to the next terminator (comma, semicolon, brace, or newline) while ignore C and
// C++ style comments, as well as counting newlines for line numbers
enum parsestate
{
NORMAL,
C_COMMENT,
CPP_COMMENT
} state = NORMAL;
QString decl;
{
int end = 0;
for(; end < parseText.length();)
{
// peek ahead character
QChar c = parseText[end];
// if we have a non-empty declaration and we're about to hit a brace, stop now before
// actually processing it.
const bool brace = (c == QLatin1Char('{') || c == QLatin1Char('}'));
if(brace && !decl.trimmed().isEmpty())
break;
// consume c now, whatever it is, we've read it and will process it below
end++;
// if this is a ; or , we don't bother to include it in the declaration but stop now
if(state == NORMAL && (c == QLatin1Char(';') || c == QLatin1Char(',')))
break;
if(c == QLatin1Char('\n'))
{
parseLine++;
// if we're in a CPP comment, go back to normal
if(state == CPP_COMMENT)
state = NORMAL;
// if we have a preprocessor definition (first non-whitespace character is #) end at the
// end of the line without a ;
if(state == NORMAL && decl.trimmed().startsWith(lit("#")))
break;
}
QChar c2;
if(end + 1 < parseText.length())
c2 = parseText[end];
if(state == NORMAL && c == QLatin1Char('/') && c2 == QLatin1Char('/'))
{
// consume the next character too
end++;
state = CPP_COMMENT;
continue;
}
if(state == NORMAL && c == QLatin1Char('/') && c2 == QLatin1Char('*'))
{
// consume the next character too
end++;
state = C_COMMENT;
continue;
}
if(state == C_COMMENT && c == QLatin1Char('*') && c2 == QLatin1Char('/'))
{
// consume the next character too
end++;
state = NORMAL;
continue;
}
if(state == NORMAL)
{
decl.append(c);
line = parseLine;
// braces should be considered their own declarations
if(brace)
break;
}
}
parseText = parseText.mid(end);
decl = decl.trimmed();
}
if(decl.isEmpty())
continue;
do
{
QRegularExpressionMatch match = annotationRegex.match(decl);
if(!match.hasMatch())
break;
annotations.push_back({match.captured(lit("name")), match.captured(lit("param"))});
decl.remove(match.capturedStart(0), match.capturedLength(0));
decl = decl.trimmed();
} while(true);
if(decl.isEmpty())
continue;
if(decl[0] == QLatin1Char('#'))
{
QRegularExpressionMatch match = packingRegex.match(decl);
if(match.hasMatch())
{
if(cur != &root)
{
reportError(tr("Packing rules can only be changed at global scope."));
success = false;
break;
}
QString packrule = match.captured(lit("rule")).toLower();
// try to pick up common aliases that people might use
if(packrule == lit("d3dcbuffer") || packrule == lit("cbuffer") || packrule == lit("cb"))
pack = Packing::D3DCB;
else if(packrule == lit("d3duav") || packrule == lit("uav") || packrule == lit("structured"))
pack = Packing::D3DUAV;
else if(packrule == lit("std140") || packrule == lit("ubo") || packrule == lit("gl") ||
packrule == lit("gles") || packrule == lit("opengl") || packrule == lit("glsl"))
pack = Packing::std140;
else if(packrule == lit("std430") || packrule == lit("ssbo"))
pack = Packing::std430;
else if(packrule == lit("scalar"))
pack = Packing::Scalar;
else if(packrule == lit("c"))
pack = Packing::C;
// we also allow toggling the individual rules
else if(packrule == lit("vector_align_component"))
pack.vector_align_component = true;
else if(packrule == lit("no_vector_align_component"))
pack.vector_align_component = false;
else if(packrule == lit("tight_arrays"))
pack.tight_arrays = true;
else if(packrule == lit("no_tight_arrays"))
pack.tight_arrays = false;
else if(packrule == lit("vector_straddle_16b"))
pack.vector_straddle_16b = true;
else if(packrule == lit("no_vector_straddle_16b"))
pack.vector_straddle_16b = false;
else if(packrule == lit("trailing_overlap"))
pack.trailing_overlap = true;
else if(packrule == lit("no_trailing_overlap"))
pack.trailing_overlap = false;
else
packrule = QString();
if(packrule.isEmpty())
{
reportError(tr("Unrecognised packing rule specifier '%1'.\n\n"
"Supported rulesets:\n"
" - cbuffer (D3D constant buffer packing)\n"
" - uav (D3D UAV packing)\n"
" - std140 (GL/Vulkan std140 packing)\n"
" - std430 (GL/Vulkan std430 packing)\n"
" - scalar (Tight scalar packing)")
.arg(packrule));
success = false;
break;
}
continue;
}
else
{
reportError(tr("Unrecognised pre-processor command '%1'.\n\n"
"Pre-processor commands must be all on one line.\n")
.arg(decl));
success = false;
break;
}
}
if(cur == &root)
{
// if we're not in a struct, ignore the braces
if(decl == lit("{") || decl == lit("}"))
continue;
}
else
{
// if we're in a struct, ignore the opening brace and revert back to root elements when we hit
// the closing brace. No brace nesting is supported
if(decl == lit("{"))
continue;
if(decl == lit("}"))
{
if(bitfieldCurPos != ~0U)
{
// update final offset to account for any bits consumed by a trailing bitfield, including
// any bits in the last byte that weren't allocated
cur->offset += (bitfieldCurPos + 7) / 8;
// reset bitpacking state.
bitfieldCurPos = ~0U;
}
if(cur->structDef.type.baseType == VarType::Struct)
{
cur->structDef.type.arrayByteStride = cur->offset;
cur->alignment = GetAlignment(pack, cur->structDef);
// if we don't have tight arrays, struct byte strides are always 16-byte aligned
if(!pack.tight_arrays)
{
cur->alignment = 16;
}
cur->structDef.type.arrayByteStride = AlignUp(cur->offset, cur->alignment);
if(cur->paddedStride > 0)
{
// only pad up to the stride, not down
if(cur->paddedStride >= cur->structDef.type.arrayByteStride)
{
cur->structDef.type.arrayByteStride = cur->paddedStride;
}
else
{
reportError(tr("Struct %1 declared size %2 bytes is less than derived structure "
"size %3 bytes.")
.arg(cur->structDef.type.name)
.arg(cur->paddedStride)
.arg(cur->structDef.type.arrayByteStride));
success = false;
break;
}
}
cur->pointerTypeId = PointerTypeRegistry::GetTypeID(cur->structDef.type);
}
cur = &root;
continue;
}
}
if(decl.startsWith(lit("struct")) || decl.startsWith(lit("enum")))
{
QRegularExpressionMatch match = structDeclRegex.match(decl);
if(match.hasMatch())
{
QString typeName = match.captured(1);
QString name = match.captured(2);
if(structelems.contains(name))
{
reportError(tr("type %1 has already been defined.").arg(name));
success = false;
break;
}
cur = &structelems[name];
cur->structDef.type.name = name;
bitfieldCurPos = ~0U;
if(typeName == lit("struct"))
{
lastStruct = name;
cur->structDef.type.baseType = VarType::Struct;
for(const Annotation &annot : annotations)
{
if(annot.name == lit("size") || annot.name == lit("byte_size"))
{
if(annot.param.isEmpty())
{
reportError(tr("Annotation '%1' requires a parameter with the size in bytes.\n\n"
"e.g. [[%1(128)]]")
.arg(annot.name));
success = false;
break;
}
cur->paddedStride = annot.param.toUInt();
}
else if(annot.name == lit("single") || annot.name == lit("fixed"))
{
cur->singleDef = true;
}
else
{
reportError(
tr("Unrecognised annotation '%1' on struct definition.\n\n"
"Supported struct annotations:\n"
" - [[size(x)]] specify the size to pad the struct to.\n"
" - [[single]] specify that this struct is fixed, not array-of-structs.")
.arg(annot.name));
success = false;
break;
}
}
annotations.clear();
if(!success)
break;
}
else
{
cur->structDef.type.baseType = VarType::Enum;
for(const Annotation &annot : annotations)
{
if(false)
{
// no annotations supported currently on enums
}
else
{
reportError(tr("Unrecognised annotation '%1' on enum definition.").arg(annot.name));
success = false;
break;
}
}
annotations.clear();
if(!success)
break;
QString baseType = match.captured(4);
if(baseType.isEmpty())
{
reportError(
tr("Enum declarations require a sized base type. E.g. enum %1 : uint").arg(name));
success = false;
break;
}
ShaderConstant tmp;
bool matched = MatchBaseTypeDeclaration(baseType, false, tmp);
if(!matched ||
(VarTypeCompType(tmp.type.baseType) != CompType::UInt &&
VarTypeCompType(tmp.type.baseType) != CompType::SInt) ||
tmp.type.flags != ShaderVariableFlags::NoFlags)
{
reportError(tr("Invalid enum base type '%1', must be an integer type.").arg(baseType));
success = false;
break;
}
cur->structDef.type.matrixByteStride = VarTypeByteSize(tmp.type.baseType);
cur->signedEnum = (VarTypeCompType(tmp.type.baseType) == CompType::SInt);
}
continue;
}
}
ShaderConstant el;
if(cur->structDef.type.baseType == VarType::Enum)
{
QRegularExpressionMatch enumMatch = enumValueRegex.match(decl);
if(!enumMatch.hasMatch())
{
reportError(tr("Couldn't parse value declaration in enum."));
success = false;
break;
}
QString valueNum = enumMatch.captured(2);
bool ok = false;
if(cur->signedEnum)
{
int64_t val = valueNum.toLongLong(&ok, 0);
if(ok)
{
// convert signed 'literally' to unsigned and truncate
if(cur->structDef.type.matrixByteStride == 1)
{
if(val > INT8_MAX || val < INT8_MIN)
{
reportError(
tr("Enum with 8-bit signed integer type cannot hold value '%1'.").arg(valueNum));
success = false;
break;
}
int8_t truncVal = (int8_t)val;
memcpy(&el.defaultValue, &truncVal, sizeof(truncVal));
}
else if(cur->structDef.type.matrixByteStride == 2)
{
if(val > INT16_MAX || val < INT16_MIN)
{
reportError(
tr("Enum with 16-bit signed integer type cannot hold value '%1'.").arg(valueNum));
success = false;
break;
}
int16_t truncVal = (int16_t)val;
memcpy(&el.defaultValue, &truncVal, sizeof(truncVal));
}
else if(cur->structDef.type.matrixByteStride == 4)
{
if(val > INT32_MAX || val < INT32_MIN)
{
reportError(
tr("Enum with 32-bit signed integer type cannot hold value '%1'.").arg(valueNum));
success = false;
break;
}
int32_t truncVal = (int32_t)val;
memcpy(&el.defaultValue, &truncVal, sizeof(truncVal));
}
else if(cur->structDef.type.matrixByteStride == 8)
{
el.defaultValue = valueNum.toULongLong();
}
}
if(!ok)
{
reportError(tr("Couldn't parse enum numerical value from '%1'.").arg(valueNum));
success = false;
break;
}
}
else
{
if(valueNum[0] == QLatin1Char('-'))
{
reportError(tr("Enum with unsigned base type cannot have signed value."));
success = false;
break;
}
el.defaultValue = valueNum.toULongLong(&ok, 0);
if(el.defaultValue > (UINT64_MAX >> (64 - 8 * cur->structDef.type.matrixByteStride)))
{
reportError(tr("Enum with %1-bit signed integer type cannot hold value '%1'.")
.arg(8 * cur->structDef.type.matrixByteStride)
.arg(valueNum));
success = false;
break;
}
if(!ok)
{
valueNum.toULongLong(&ok, 0);
reportError(tr("Couldn't get enum numerical value from '%1'.").arg(valueNum));
success = false;
break;
}
}
el.name = enumMatch.captured(1);
for(const Annotation &annot : annotations)
{
if(false)
{
// no annotations supported currently on enums
}
else
{
reportError(tr("Unrecognised annotation '%1' on enum value.").arg(annot.name));
success = false;
break;
}
}
annotations.clear();
if(!success)
break;
cur->structDef.type.members.push_back(el);
cur->lineMemberDefs.push_back(line);
continue;
}
QRegularExpressionMatch bitfieldSkipMatch = bitfieldSkipRegex.match(decl);
if(bitfieldSkipMatch.hasMatch())
{
if(bitfieldCurPos == ~0U)
bitfieldCurPos = 0;
bitfieldCurPos += bitfieldSkipMatch.captured(3).toUInt();
for(const Annotation &annot : annotations)
{
if(false)
{
// no annotations supported currently on enums
}
else
{
reportError(tr("Unrecognised annotation '%1' on bitfield skip element.").arg(annot.name));
success = false;
break;
}
}
annotations.clear();
if(!success)
break;
continue;
}
if(cur->singleMember)
{
reportError(
tr("[[single]] can only be used if there is only one variable in the root.\n"
"Consider wrapping the variables in a struct and annotating it as [[single]]."));
success = false;
break;
}
QRegularExpressionMatch structMatch = structUseRegex.match(decl);
bool isPadding = false;
if(structMatch.hasMatch() && structelems.contains(structMatch.captured(1)))
{
StructFormatData &structContext = structelems[structMatch.captured(1)];
QString pointerStars = structMatch.captured(2).trimmed();
bool isPointer = !pointerStars.isEmpty();
if(pointerStars.count() > 1)
{
reportError(tr("Only single pointers are supported."));
success = false;
break;
}
if(structContext.singleDef)
{
reportError(tr("[[single]] annotated structs can't be used, only defined."));
success = false;
break;
}
if(!isPointer && structContext.structDef.type.name == cur->structDef.type.name)
{
reportError(tr("Invalid nested struct declaration, only allowed for pointers."));
success = false;
break;
}
QString varName = structMatch.captured(3).trimmed();
if(varName.isEmpty())
varName = lit("data");
uint32_t specifiedOffset = ~0U;
for(const Annotation &annot : annotations)
{
if(annot.name == lit("offset") || annot.name == lit("byte_offset"))
{
if(annot.param.isEmpty())
{
reportError(tr("Annotation '%1' requires a parameter with the offset in bytes.\n\n"
"e.g. [[%1(128)]]")
.arg(annot.name));
success = false;
break;
}
specifiedOffset = annot.param.toUInt();
}
else if(annot.name == lit("pad") || annot.name == lit("padding"))
{
isPadding = true;
}
else if(annot.name == lit("single") || annot.name == lit("fixed"))
{
if(cur != &root)
{
reportError(tr("[[single]] can only be used on global variables."));
success = false;
break;
}
else if(!cur->structDef.type.members.empty())
{
reportError(
tr("[[single]] can only be used if there is only one variable in the root.\n"
"Consider wrapping the variables in a struct and marking it as [[single]]."));
success = false;
break;
}
else
{
cur->singleMember = true;
}
}
else
{
reportError(tr("Unrecognised annotation '%1' on variable.").arg(annot.name));
success = false;
break;
}
}
if(!success)
break;
annotations.clear();
QString arrayDim = structMatch.captured(4).trimmed();
uint32_t arrayCount = 1;
if(!arrayDim.isEmpty())
{
arrayDim = arrayDim.mid(1, arrayDim.count() - 2);
if(arrayDim.isEmpty())
arrayDim = lit("%1").arg(~0U);
bool ok = false;
arrayCount = arrayDim.toUInt(&ok);
if(!ok)
arrayCount = 1;
}
if(cur->singleMember && arrayCount == ~0U)
{
reportError(tr("[[single]] can't be used on unbounded arrays."));
success = false;
break;
}
QString bitfield = structMatch.captured(6).trimmed();
if(isPointer)
{
if(!bitfield.isEmpty())
{
reportError(tr("Pointers can't be packed into a bitfield."));
success = false;
break;
}
// align to scalar size
cur->offset = AlignUp(cur->offset, 8U);
if(specifiedOffset != ~0U)
{
if(specifiedOffset < cur->offset)
{
reportError(tr("Specified byte offset %1 overlaps with previous data.\n"
"This value must be at byte offset %2 at minimum.")
.arg(specifiedOffset)
.arg(cur->offset));
success = false;
break;
}
cur->offset = specifiedOffset;
}
el.name = varName;
el.byteOffset = cur->offset;
el.type.pointerTypeID = structContext.pointerTypeId;
el.type.baseType = VarType::GPUPointer;
el.type.flags |= ShaderVariableFlags::HexDisplay;
el.type.arrayByteStride = 8;
el.type.elements = arrayCount;
cur->offset += 8 * arrayCount;
if(!isPadding)
{
cur->structDef.type.members.push_back(el);
cur->lineMemberDefs.push_back(line);
}
continue;
}
else if(structContext.structDef.type.baseType == VarType::Enum)
{
if(!bitfield.isEmpty() && !arrayDim.isEmpty())
{
reportError(tr("Arrays can't be packed into a bitfield."));
success = false;
break;
}
// align to scalar size (if not bit packing)
if(bitfieldCurPos == ~0U)
cur->offset = AlignUp(cur->offset, (uint32_t)structContext.structDef.type.matrixByteStride);
if(specifiedOffset != ~0U)
{
uint32_t offs = cur->offset;
if(bitfieldCurPos != ~0U)
offs += (bitfieldCurPos + 7) / 8;
if(specifiedOffset < offs)
{
reportError(tr("Specified byte offset %1 overlaps with previous data.\n"
"This value must be at byte offset %2 at minimum.")
.arg(specifiedOffset)
.arg(offs));
success = false;
break;
}
cur->offset = specifiedOffset;
// reset any bitfield packing to start at 0 at the new location
if(bitfieldCurPos != ~0U)
bitfieldCurPos = 0;
}
el = structContext.structDef;
el.name = varName;
el.byteOffset = cur->offset;
el.type.elements = arrayCount;
bool ok = false;
el.bitFieldSize = qMax(1U, bitfield.toUInt(&ok));
if(!ok)
el.bitFieldSize = 0;
// don't continue here - we will go through and handle bitfield packing like any other
// scalar
}
else
{
if(!bitfield.isEmpty())
{
reportError(tr("Struct variables can't be packed into a bitfield."));
success = false;
break;
}
// all packing rules align structs in the same way as arrays. We already calculated this
// when calculating the struct's alignment which will be padded to 16B for non-tight arrays
cur->offset = AlignUp(cur->offset, structContext.alignment);
if(specifiedOffset != ~0U)
{
if(specifiedOffset < cur->offset)
{
reportError(tr("Specified byte offset %1 overlaps with previous data.\n"
"This value must be at byte offset %2 at minimum.")
.arg(specifiedOffset)
.arg(cur->offset));
success = false;
break;
}
cur->offset = specifiedOffset;
}
el = structContext.structDef;
el.name = varName;
el.byteOffset = cur->offset;
el.type.elements = arrayCount;
if(!isPadding)
{
cur->structDef.type.members.push_back(el);
cur->lineMemberDefs.push_back(line);
}
// advance by the struct including any trailing padding
cur->offset += el.type.elements * el.type.arrayByteStride;
// if we allow trailing overlap, remove the padding
if(pack.trailing_overlap)
cur->offset -= el.type.arrayByteStride - structContext.offset;
continue;
}
}
else
{
QRegularExpressionMatch match = regExpr.match(decl);
if(!match.hasMatch())
{
QString problemGuess;
// try to guess the problem since we don't have a proper parser and are just using regex's,
// so we don't have a parse state to mention
ShaderConstant dummy;
int numRecognisedTypes = 0;
QStringList identifiers = decl.split(QRegularExpression(lit("\\s+")));
for(const QString &identifier : identifiers)
{
bool known = MatchBaseTypeDeclaration(identifier, false, dummy);
if(known)
numRecognisedTypes++;
}
// if we recognised more than one type maybe this is multiple lines that got combined
if(numRecognisedTypes > 1)
{
problemGuess = tr("Did you need a ; between multiple declarations?");
}
else if(identifiers.size() >= 1 && structelems.contains(identifiers[0]))
{
problemGuess = tr("Invalid declaration of struct '%1'.").arg(identifiers[0]);
}
else if(identifiers.size() > 1)
{
problemGuess = tr("Unrecognised type '%1'.").arg(identifiers[0]);
}
reportError(
tr("Failed to parse declaration:\n\n%1\n\n%2").arg(decl).arg(problemGuess).trimmed());
success = false;
break;
}
el.name = !match.captured(lit("name")).isEmpty() ? match.captured(lit("name")).trimmed()
: lit("data");
QString basetype = match.captured(lit("type"));
if(match.captured(lit("major")).trimmed() == lit("row_major"))
el.type.flags |= ShaderVariableFlags::RowMajorMatrix;
if(!match.captured(lit("rgb")).isEmpty())
el.type.flags |= ShaderVariableFlags::RGBDisplay;
QString firstDim =
!match.captured(lit("vec")).isEmpty() ? match.captured(lit("vec")) : lit("1");
QString secondDim =
!match.captured(lit("mat")).isEmpty() ? match.captured(lit("mat")).mid(1) : lit("1");
QString arrayDim = !match.captured(lit("array")).isEmpty()
? match.captured(lit("array")).trimmed()
: lit("[1]");
{
bool isArray = !arrayDim.isEmpty();
arrayDim = arrayDim.mid(1, arrayDim.count() - 2).trimmed();
if(isArray && arrayDim.isEmpty())
arrayDim = lit("%1").arg(~0U);
}
const bool isUnsigned = match.captured(lit("sign")).trimmed() == lit("unsigned");
QString bitfield = match.captured(lit("bitfield"));
QString vecMatSizeSuffix;
// if we have a matrix and it's not GL style, then typeAxB means A rows and B columns
// for GL matAxB that means A columns and B rows. This is in contrast to typeA which means A
// columns for HLSL and A columns for GLSL, hence only the swap for matrices
if(!match.captured(lit("mat")).isEmpty() && basetype != lit("mat"))
{
vecMatSizeSuffix = match.captured(lit("vec")) + match.captured(lit("mat"));
firstDim.swap(secondDim);
}
else
{
if(!match.captured(lit("mat")).isEmpty())
vecMatSizeSuffix = match.captured(lit("mat")).mid(1) + lit("x");
vecMatSizeSuffix += match.captured(lit("vec"));
}
// check for square matrix declarations like 'mat4' and 'mat3'
if(basetype == lit("mat") && match.captured(lit("mat")).isEmpty())
{
secondDim = firstDim;
vecMatSizeSuffix = firstDim + lit("x") + firstDim;
}
// check for square matrix declarations like 'mat4' and 'mat3'
if(basetype == lit("mat") && match.captured(lit("mat")).isEmpty())
secondDim = firstDim;
// calculate format
{
bool ok = false;
el.type.columns = firstDim.toUInt(&ok);
if(!ok)
{
reportError(tr("Invalid vector dimension '%1'.").arg(firstDim));
success = false;
break;
}
el.type.elements = qMax(1U, arrayDim.toUInt(&ok));
if(!ok)
el.type.elements = 1;
if(!bitfield.isEmpty() && el.type.elements > 1)
{
reportError(tr("Arrays can't be packed into a bitfield."));
success = false;
break;
}
el.type.rows = qMax(1U, secondDim.toUInt(&ok));
if(!ok)
{
reportError(tr("Invalid matrix dimension '%1'.").arg(secondDim));
success = false;
break;
}
el.bitFieldSize = qMax(1U, bitfield.toUInt(&ok));
if(!ok)
el.bitFieldSize = 0;
// vectors are marked as row-major by convention
if(el.type.rows == 1)
el.type.flags |= ShaderVariableFlags::RowMajorMatrix;
bool matched = MatchBaseTypeDeclaration(basetype, isUnsigned, el);
if(!matched)
{
reportError(tr("Unrecognised type '%1'.").arg(basetype));
success = false;
break;
}
}
el.type.name = ToStr(el.type.baseType) + vecMatSizeSuffix;
// process packing annotations first, so we have that information to validate e.g. [[unorm]]
for(const Annotation &annot : annotations)
{
if(annot.name == lit("packed"))
{
if(annot.param.toLower() == lit("r11g11b10"))
{
if(el.type.columns != 3 || el.type.baseType != VarType::Float)
{
reportError(tr("R11G11B10 packing must be specified on a 'float3' variable."));
success = false;
break;
}
el.type.flags |= ShaderVariableFlags::R11G11B10;
}
else if(annot.param.toLower() == lit("r10g10b10a2") ||
annot.param.toLower() == lit("r10g10b10a2_uint"))
{
if(el.type.columns != 4 || el.type.baseType != VarType::UInt)
{
reportError(
tr("R10G10B10A2 packing must be specified on a 'uint4' variable "
"(optionally with [[unorm]] or [[snorm]])."));
success = false;
break;
}
el.type.flags |= ShaderVariableFlags::R10G10B10A2;
}
else if(annot.param.toLower() == lit("r10g10b10a2_unorm"))
{
if(el.type.columns != 4 || el.type.baseType != VarType::UInt)
{
reportError(tr("R10G10B10A2_UNORM packing must be specified on a 'uint4' variable."));
success = false;
break;
}
el.type.flags |= ShaderVariableFlags::R10G10B10A2 | ShaderVariableFlags::UNorm;
}
else if(annot.param.toLower() == lit("r10g10b10a2_snorm"))
{
if(el.type.columns != 4 || el.type.baseType != VarType::SInt)
{
reportError(tr("R10G10B10A2_SNORM packing must be specified on a 'int4' variable."));
success = false;
break;
}
el.type.flags |= ShaderVariableFlags::R10G10B10A2 | ShaderVariableFlags::SNorm;
}
else if(annot.param.isEmpty())
{
reportError(tr("Annotation '%1' requires a parameter with the format packing.\n\n"
"e.g. [[%1(r10g10b10a2)]]")
.arg(annot.name));
success = false;
break;
}
else
{
reportError(tr("Unrecognised format packing '%1'.\n").arg(annot.param));
success = false;
break;
}
}
}
if(!success)
break;
for(const Annotation &annot : annotations)
{
if(annot.name == lit("rgb"))
{
el.type.flags |= ShaderVariableFlags::RGBDisplay;
}
else if(annot.name == lit("hex") || annot.name == lit("hexadecimal"))
{
if(VarTypeCompType(el.type.baseType) == CompType::Float)
{
reportError(tr("Hex display is not supported on floating point variables."));
success = false;
break;
}
if(el.type.flags & (ShaderVariableFlags::R10G10B10A2 | ShaderVariableFlags::R11G11B10))
{
reportError(tr("Hex display is not supported on packed formats."));
success = false;
break;
}
el.type.flags |= ShaderVariableFlags::HexDisplay;
if(el.type.baseType == VarType::SLong)
el.type.baseType = VarType::ULong;
else if(el.type.baseType == VarType::SInt)
el.type.baseType = VarType::UInt;
else if(el.type.baseType == VarType::SShort)
el.type.baseType = VarType::UShort;
else if(el.type.baseType == VarType::SByte)
el.type.baseType = VarType::UByte;
}
else if(annot.name == lit("bin") || annot.name == lit("binary"))
{
if(VarTypeCompType(el.type.baseType) == CompType::Float)
{
reportError(tr("Binary display is not supported on floating point variables."));
success = false;
break;
}
if(el.type.flags & (ShaderVariableFlags::R10G10B10A2 | ShaderVariableFlags::R11G11B10))
{
reportError(tr("Binary display is not supported on packed formats."));
success = false;
break;
}
el.type.flags |= ShaderVariableFlags::BinaryDisplay;
if(el.type.baseType == VarType::SLong)
el.type.baseType = VarType::ULong;
else if(el.type.baseType == VarType::SInt)
el.type.baseType = VarType::UInt;
else if(el.type.baseType == VarType::SShort)
el.type.baseType = VarType::UShort;
else if(el.type.baseType == VarType::SByte)
el.type.baseType = VarType::UByte;
}
else if(annot.name == lit("unorm"))
{
if(!(el.type.flags & ShaderVariableFlags::R10G10B10A2))
{
// verify that we're integer typed and 1 or 2 bytes
if(el.type.baseType != VarType::UShort && el.type.baseType != VarType::SShort &&
el.type.baseType != VarType::UByte && el.type.baseType != VarType::SByte)
{
reportError(tr("UNORM packing is only supported on [u]byte and [u]short types."));
success = false;
break;
}
}
el.type.flags |= ShaderVariableFlags::UNorm;
}
else if(annot.name == lit("snorm"))
{
if(!(el.type.flags & ShaderVariableFlags::R10G10B10A2))
{
// verify that we're integer typed and 1 or 2 bytes
if(el.type.baseType != VarType::UShort && el.type.baseType != VarType::SShort &&
el.type.baseType != VarType::UByte && el.type.baseType != VarType::SByte)
{
reportError(tr("SNORM packing is only supported on [u]byte and [u]short types."));
success = false;
break;
}
}
el.type.flags |= ShaderVariableFlags::SNorm;
}
else if(annot.name == lit("row_major"))
{
if(el.type.rows == 1)
{
reportError(tr("Row major can only be specified on matrices."));
success = false;
break;
}
el.type.flags |= ShaderVariableFlags::RowMajorMatrix;
}
else if(annot.name == lit("col_major"))
{
if(el.type.rows == 1)
{
reportError(tr("Column major can only be specified on matrices."));
success = false;
break;
}
el.type.flags &= ~ShaderVariableFlags::RowMajorMatrix;
}
else if(annot.name == lit("packed"))
{
// already processed
}
else if(annot.name == lit("offset") || annot.name == lit("byte_offset"))
{
if(annot.param.isEmpty())
{
reportError(tr("Annotation '%1' requires a parameter with the offset in bytes.\n\n"
"e.g. [[%1(128)]]")
.arg(annot.name));
success = false;
break;
}
uint32_t specifiedOffset = annot.param.toUInt();
if(specifiedOffset < cur->offset)
{
reportError(tr("Specified byte offset %1 overlaps with previous data.\n"
"This value must be at byte offset %2 at minimum.")
.arg(specifiedOffset)
.arg(cur->offset));
success = false;
break;
}
cur->offset = specifiedOffset;
}
else if(annot.name == lit("pad") || annot.name == lit("padding"))
{
isPadding = true;
}
else if(annot.name == lit("single") || annot.name == lit("fixed"))
{
if(cur != &root)
{
reportError(tr("[[single]] can only be used on global variables."));
success = false;
break;
}
else if(!cur->structDef.type.members.empty())
{
reportError(
tr("[[single]] can only be used if there is only one variable in the root.\n"
"Consider wrapping the variables in a struct and marking it as [[single]]."));
success = false;
break;
}
else
{
cur->singleMember = true;
}
}
else
{
reportError(tr("Unrecognised annotation '%1' on variable.").arg(annot.name));
success = false;
break;
}
}
annotations.clear();
if(!success)
break;
// validate that bitfields are only allowed for regular scalars
if(el.bitFieldSize > 0)
{
if(el.type.rows > 1 || el.type.columns > 1)
{
reportError(tr("Vectors and matrices can't be packed into a bitfield."));
success = false;
break;
}
if(el.type.elements > 1)
{
reportError(tr("Arrays can't be packed into a bitfield."));
success = false;
break;
}
if(el.type.flags & (ShaderVariableFlags::R10G10B10A2 | ShaderVariableFlags::R11G11B10 |
ShaderVariableFlags::UNorm | ShaderVariableFlags::SNorm))
{
reportError(tr("Format-packed variables can't be packed into a bitfield."));
success = false;
break;
}
if(VarTypeCompType(el.type.baseType) == CompType::Float)
{
reportError(tr("Floating point variables can't be packed into a bitfield."));
success = false;
break;
}
}
if(basetype == lit("xlong") || basetype == lit("xint") || basetype == lit("xshort") ||
basetype == lit("xbyte"))
el.type.flags |= ShaderVariableFlags::HexDisplay;
}
if(cur->singleMember && el.type.elements == ~0U)
{
reportError(tr("[[single]] can't be used on unbounded arrays."));
success = false;
break;
}
const bool packed32bit =
bool(el.type.flags & (ShaderVariableFlags::R10G10B10A2 | ShaderVariableFlags::R11G11B10));
// normally the array stride is the size of an element
const uint32_t elAlignment = packed32bit ? sizeof(uint32_t) : GetAlignment(pack, el);
const uint8_t vecSize = (el.type.rows > 1 && el.type.ColMajor()) ? el.type.rows : el.type.columns;
const uint32_t elSize =
packed32bit ? sizeof(uint32_t)
: (pack.vector_align_component ? elAlignment * vecSize : elAlignment);
// if we aren't using tight arrays the stride is at least 16 bytes
el.type.arrayByteStride = elAlignment;
if(el.type.rows > 1 || el.type.columns > 1)
el.type.arrayByteStride = elSize;
if(!pack.tight_arrays)
el.type.arrayByteStride = std::max(16U, el.type.arrayByteStride);
// matrices are always aligned like arrays of vectors
if(el.type.rows > 1)
{
// the alignment calculated above is the alignment of a vector, that's our matrix stride
el.type.matrixByteStride = el.type.arrayByteStride;
// the array stride is that alignment times the number of rows/columns
if(el.type.RowMajor())
el.type.arrayByteStride *= el.type.rows;
else
el.type.arrayByteStride *= el.type.columns;
}
if(el.bitFieldSize > 0)
{
// we can use the arrayByteStride since this is a scalar so no vector/arrays, this is just the
// base size. It also works for enums as this is the byte size of the declared underlying type
const uint32_t elemScalarBitSize = el.type.arrayByteStride * 8;
// bitfields can't be larger than the base type
if(el.bitFieldSize > elemScalarBitSize)
{
reportError(tr("Variable type %1 only has %2 bits, can't pack into %3 bits in a bitfield.")
.arg(el.type.name)
.arg(elemScalarBitSize)
.arg(el.bitFieldSize));
success = false;
break;
}
uint32_t start = bitfieldCurPos;
if(start == ~0U)
start = 0;
// if we would end past the current base type size, first roll over and start at the next
// byte
// this could be:
// unsigned int a : 24;
// unsigned byte b : 4;
// unsigned byte c : 4;
// where we just 'rollover' the 3 bytes packed into the unsigned int and start the byte on
// the next byte, there's no extra padding added
// or it could be:
// unsigned int a : 29;
// unsigned byte b : 4;
// unsigned byte c : 4;
// where b would pass through the end of the fourth byte so there ends up being 3 bits of
// padding between a and b when b is rolled onto the next byte
// similarly this can happen if the types are the same:
// unsigned int a : 29;
// unsigned int b : 4;
// since b would still pass through the end of the first dword.
// similarly this allows 'more' padding when the types are bigger:
// unsigned int a : 17;
// unsigned int b : 17;
// which would produce 15 bytes of padding
// Note that if the types are the same and big enough we won't roll over, as in:
// unsigned int a : 24;
// unsigned int b : 4;
// unsigned int c : 4;
if(start + el.bitFieldSize > elemScalarBitSize)
{
// align the offset up to where this bitfield needs to start
cur->offset += ((bitfieldCurPos + (elemScalarBitSize - 1)) / elemScalarBitSize) *
(elemScalarBitSize / 8);
// reset the current bitfield pos
bitfieldCurPos = 0;
}
// if there's no previous bitpacking, nothing much to do
if(bitfieldCurPos == ~0U)
{
// start the next bitfield at our size
bitfieldCurPos = el.bitFieldSize;
}
else
{
// start the next bitfield at the end of the previous
el.bitFieldOffset = bitfieldCurPos;
// update by our size
bitfieldCurPos += el.bitFieldSize;
}
}
else
{
// this element is not bitpacked
if(bitfieldCurPos != ~0U)
{
// update offset to account for any bits consumed by the previous bitfield, which won't have
// happened yet, including any bits in the last byte that weren't allocated
cur->offset += (bitfieldCurPos + 7) / 8;
// reset bitpacking state.
bitfieldCurPos = ~0U;
}
// align to our element's base alignment
cur->offset = AlignUp(cur->offset, elAlignment);
// if we have non-tight arrays, arrays (and matrices) always start on a 16-byte boundary
if(!pack.tight_arrays && (el.type.elements > 1 || el.type.rows > 1))
cur->offset = AlignUp(cur->offset, 16U);
// if vectors can't straddle 16-byte alignment, check to see if we're going to do that
if(!pack.vector_straddle_16b)
{
if(cur->offset / 16 != (cur->offset + elSize - 1) / 16)
{
cur->offset = AlignUp(cur->offset, 16U);
}
}
}
el.byteOffset = cur->offset;
if(!isPadding)
{
cur->structDef.type.members.push_back(el);
cur->lineMemberDefs.push_back(line);
}
// if we're bitfield packing don't advance offset, otherwise advance to the end of this element
if(bitfieldCurPos == ~0U)
cur->offset += GetVarAdvance(pack, el);
}
if(bitfieldCurPos != ~0U)
{
// update final offset to account for any bits consumed by a trailing bitfield, including any
// bits in the last byte that weren't allocated
cur->offset += (bitfieldCurPos + 7) / 8;
// reset bitpacking state.
bitfieldCurPos = ~0U;
}
ShaderConstant &fixed = ret.fixed;
// if we succeeded parsing but didn't get any root elements, use the last defined struct as the
// definition
if(success && root.structDef.type.members.isEmpty() && !lastStruct.isEmpty())
{
root = structelems[lastStruct];
// only pad up to the stride, not down
if(root.paddedStride >= root.offset)
root.offset = cur->paddedStride;
}
fixed = root.structDef;
uint32_t end = root.offset;
if(!fixed.type.members.isEmpty())
end = qMax(
end, fixed.type.members.back().byteOffset + GetVarSizeAndTrail(fixed.type.members.back()));
fixed.type.arrayByteStride = AlignUp(end, GetAlignment(pack, fixed));
if(!fixed.type.members.isEmpty() && fixed.type.members.back().type.elements == ~0U)
{
fixed.type.arrayByteStride =
AlignUp(fixed.type.members.back().type.arrayByteStride, GetAlignment(pack, fixed));
}
if(success)
{
// check that unbounded arrays are only the last member of each struct. Doing this separately
// makes the below check easier since we only have to consider last members
if(!CheckInvalidUnbounded(root, structelems, errors))
success = false;
}
// we only allow one 'infinite' array. You can't have an infinite member inside an already
// infinite struct. E.g. this is invalid:
//
// struct foo {
// uint a;
// float b[];
// };
//
// foo data[];
//
// but it's valid to have either this:
//
// struct foo {
// uint a;
// float b[3];
// };
//
// foo data[];
//
// or this:
//
// struct foo {
// uint a;
// float b[];
// };
//
// foo data;
if(success)
{
ShaderConstant *iter = &fixed;
ShaderConstant *parent = NULL;
bool foundInfinite = false;
int infiniteArrayLine = -1;
while(iter)
{
if(iter->type.elements == ~0U)
{
if(foundInfinite)
{
QString parentName;
if(parent)
parentName = parent->type.name;
success = false;
errors[infiniteArrayLine] = tr("Can't declare an unbounded array when child member %1 of "
"struct %2 is also declared as unbounded.")
.arg(iter->name)
.arg(parentName);
break;
}
foundInfinite = true;
if(parent && parent != &fixed)
infiniteArrayLine = structelems[parent->type.name].lineMemberDefs.back();
else
infiniteArrayLine = root.lineMemberDefs.back();
}
// if there are no more members, stop looking
if(iter->type.members.empty())
break;
parent = iter;
iter = &iter->type.members.back();
}
}
// on D3D if we have an unbounded array it *must* be the root element, as D3D does not support
// some fixed elements before it, structured buffers are strictly just an AoS.
// we do allow specifying cbuffers which are all fixed and not unbounded, so we just check to see
// that if there is an unbounded array that it's the root
if(
// on D3D
IsD3D(m_API) &&
// if the parsing worked
success && !fixed.type.members.empty() &&
// if we have an unbounded array somewhere (we know there's only one, from above)
ContainsUnbounded(fixed) &&
// it must be in the root and it must be alone with no siblings
!(fixed.type.members.size() == 1 && fixed.type.members[0].type.elements == ~0U))
{
errors[root.lineMemberDefs.back()] =
tr("On D3D an unbounded array must be only be used alone as the root element.\n"
"Consider wrapping all the globals in a single struct, or removing the unbounded array "
"declaration.");
success = false;
}
// when not viewing a cbuffer, if the root hasn't been explicitly marked as a single struct and we
// don't have an unbounded array then consider it an AoS definition in all other cases as that is
// very likely what the user expects
if(success && !fixed.type.members.empty() && !ContainsUnbounded(fixed) && !root.singleMember &&
!root.singleDef && !cbuffer)
{
// if there's already only one root member just make it infinite
if(fixed.type.members.size() == 1)
{
fixed.type.members[0].type.elements = ~0U;
}
else
{
// otherwise wrap a struct around the members, to be the infinite AoS
rdcarray<ShaderConstant> inners;
inners.swap(fixed.type.members);
ShaderConstant el;
el.byteOffset = 0;
el.type.baseType = VarType::Struct;
el.type.elements = ~0U;
el.type.arrayByteStride = fixed.type.arrayByteStride;
fixed.type.members.push_back(el);
inners.swap(fixed.type.members[0].type.members);
}
}
if(!success || fixed.type.members.isEmpty())
{
fixed.type.members.clear();
fixed.type.baseType = VarType::Struct;
ShaderConstant el;
el.byteOffset = 0;
el.type.flags = ShaderVariableFlags::RowMajorMatrix | ShaderVariableFlags::HexDisplay;
el.name = "data";
el.type.name = "uint4";
el.type.baseType = VarType::UInt;
el.type.columns = 4;
el.type.elements = ~0U;
if(maxLen > 0 && maxLen < 16)
el.type.columns = 1;
if(maxLen > 0 && maxLen < 4)
el.type.baseType = VarType::UByte;
el.type.arrayByteStride = el.type.columns * VarTypeByteSize(el.type.baseType);
fixed.type.members.push_back(el);
fixed.type.arrayByteStride = el.type.arrayByteStride;
}
// split the struct definition we have now into fixed and repeating. We've enforced above that
// there's only one struct which is unbounded (no other children at any level are unbounded) and
// it's the last member, so we find it, remove it from the hierarchy, and present it separately.
{
ShaderConstant *iter = &fixed;
rdcstr addedprefix;
while(iter)
{
// if there are no more members, stop looking, there's no repeated member
if(iter->type.members.empty())
break;
// add the prefix, so that the repeated element that's a child like buffer { foo { blah[] } }
// shows up as buffer.foo.blah
if(!iter->name.empty())
addedprefix += iter->name + ".";
// we want to search the members so we can remove from the current iter
if(iter->type.members.back().type.elements == ~0U)
{
ret.repeating = iter->type.members.back();
ret.repeating.name = addedprefix + ret.repeating.name;
ret.repeating.type.elements = 1;
iter->type.members.pop_back();
break;
}
iter = &iter->type.members.back();
}
}
return ret;
}