ParsedFormat BufferFormatter::ParseFormatString()

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;
}