void D3D12PipelineStateViewer::setState()

in qrenderdoc/Windows/PipelineState/D3D12PipelineStateViewer.cpp [1187:2253]


void D3D12PipelineStateViewer::setState()
{
  if(!m_Ctx.IsCaptureLoaded())
  {
    clearState();
    return;
  }

  // cache latest state of these checkboxes
  m_ShowUnused = ui->showUnused->isChecked();
  m_ShowEmpty = ui->showEmpty->isChecked();

  const D3D12Pipe::State &state = *m_Ctx.CurD3D12PipelineState();
  const ActionDescription *action = m_Ctx.CurAction();

  const QPixmap &tick = Pixmaps::tick(this);
  const QPixmap &cross = Pixmaps::cross(this);

  // highlight the appropriate stages in the flowchart
  if(action == NULL)
  {
    QList<bool> allOn;
    for(int i = 0; i < ui->pipeFlow->stageNames().count(); i++)
      allOn.append(true);
    ui->pipeFlow->setStagesEnabled(allOn);
  }
  else if(action->flags & ActionFlags::Dispatch)
  {
    QList<bool> computeOnly;
    for(int i = 0; i < ui->pipeFlow->stageNames().count(); i++)
      computeOnly.append(false);
    computeOnly.back() = true;
    ui->pipeFlow->setStagesEnabled(computeOnly);
  }
  else if(action->flags & ActionFlags::MeshDispatch)
  {
    setNewMeshPipeFlow();
    ui->pipeFlow->setStagesEnabled(
        {state.ampShader.resourceId != ResourceId(), true, true, true, true, false});
  }
  else
  {
    bool streamOutActive = false;

    for(const D3D12Pipe::StreamOutBind &o : state.streamOut.outputs)
    {
      if(o.resourceId != ResourceId())
      {
        streamOutActive = true;
        break;
      }
    }

    if(state.geometryShader.resourceId == ResourceId() && streamOutActive)
    {
      ui->pipeFlow->setStageName(4, lit("SO"), tr("Stream Out"));
    }
    else
    {
      ui->pipeFlow->setStageName(4, lit("GS"), tr("Geometry Shader"));
    }

    setOldMeshPipeFlow();
    ui->pipeFlow->setStagesEnabled(
        {true, true, state.hullShader.resourceId != ResourceId(),
         state.domainShader.resourceId != ResourceId(),
         state.geometryShader.resourceId != ResourceId() || streamOutActive, true,
         state.pixelShader.resourceId != ResourceId(), true, false});
  }

  ////////////////////////////////////////////////
  // Vertex Input

  int vs = 0;

  bool usedVBuffers[128] = {};
  uint32_t layoutOffs[128] = {};

  if(m_MeshPipe)
  {
    setShaderState(state.ampShader, ui->asShader, ui->asRootSig);
    setShaderState(state.meshShader, ui->msShader, ui->msRootSig);

    if(state.meshShader.reflection)
      ui->msTopology->setText(ToQStr(state.meshShader.reflection->outputTopology));
    else
      ui->msTopology->setText(QString());
  }
  else
  {
    vs = ui->iaLayouts->verticalScrollBar()->value();
    ui->iaLayouts->beginUpdate();
    ui->iaLayouts->clear();
    {
      int i = 0;
      for(const D3D12Pipe::Layout &l : state.inputAssembly.layouts)
      {
        QString byteOffs = Formatter::HumanFormat(l.byteOffset, Formatter::OffsetSize);

        // D3D12 specific value
        if(l.byteOffset == ~0U)
        {
          byteOffs = lit("APPEND_ALIGNED (%1)").arg(layoutOffs[l.inputSlot]);
        }
        else
        {
          layoutOffs[l.inputSlot] = l.byteOffset;
        }

        layoutOffs[l.inputSlot] += l.format.compByteWidth * l.format.compCount;

        bool filledSlot = true;
        bool usedSlot = false;

        for(int ia = 0; state.vertexShader.reflection &&
                        ia < state.vertexShader.reflection->inputSignature.count();
            ia++)
        {
          if(!QString(state.vertexShader.reflection->inputSignature[ia].semanticName)
                  .compare(l.semanticName, Qt::CaseInsensitive) &&
             state.vertexShader.reflection->inputSignature[ia].semanticIndex == l.semanticIndex)
          {
            usedSlot = true;
            break;
          }
        }

        if(showNode(usedSlot, filledSlot))
        {
          RDTreeWidgetItem *node = new RDTreeWidgetItem(
              {i, l.semanticName, l.semanticIndex, l.format.Name(), l.inputSlot, byteOffs,
               l.perInstance ? lit("PER_INSTANCE") : lit("PER_VERTEX"), l.instanceDataStepRate,
               QString()});

          node->setTag(i);

          if(usedSlot)
            usedVBuffers[l.inputSlot] = true;

          if(!usedSlot)
            setInactiveRow(node);

          ui->iaLayouts->addTopLevelItem(node);
        }

        i++;
      }
    }
    ui->iaLayouts->clearSelection();
    ui->iaLayouts->endUpdate();
    ui->iaLayouts->verticalScrollBar()->setValue(vs);

    int numCPs = PatchList_Count(state.inputAssembly.topology);
    if(numCPs > 0)
    {
      ui->topology->setText(tr("PatchList (%1 Control Points)").arg(numCPs));
    }
    else
    {
      ui->topology->setText(ToQStr(state.inputAssembly.topology));
    }

    m_Common.setTopologyDiagram(ui->topologyDiagram, state.inputAssembly.topology);

    bool ibufferUsed = action && (action->flags & ActionFlags::Indexed);

    m_VBNodes.clear();
    m_EmptyNodes.clear();

    vs = ui->iaBuffers->verticalScrollBar()->value();
    ui->iaBuffers->beginUpdate();
    ui->iaBuffers->clear();

    if(state.inputAssembly.indexBuffer.resourceId != ResourceId())
    {
      if(ibufferUsed || m_ShowUnused)
      {
        uint64_t length = state.inputAssembly.indexBuffer.byteSize;

        BufferDescription *buf = m_Ctx.GetBuffer(state.inputAssembly.indexBuffer.resourceId);

        RDTreeWidgetItem *node = new RDTreeWidgetItem(
            {tr("Index"), state.inputAssembly.indexBuffer.resourceId,
             (qulonglong)state.inputAssembly.indexBuffer.byteStride,
             (qulonglong)state.inputAssembly.indexBuffer.byteOffset, (qulonglong)length, QString()});

        QString iformat;

        if(state.inputAssembly.indexBuffer.byteStride == 1)
          iformat = lit("ubyte");
        else if(state.inputAssembly.indexBuffer.byteStride == 2)
          iformat = lit("ushort");
        else if(state.inputAssembly.indexBuffer.byteStride == 4)
          iformat = lit("uint");

        iformat +=
            lit(" indices[%1]").arg(RENDERDOC_NumVerticesPerPrimitive(state.inputAssembly.topology));

        uint32_t drawOffset =
            (action ? action->indexOffset * state.inputAssembly.indexBuffer.byteStride : 0);

        node->setTag(QVariant::fromValue(
            D3D12VBIBTag(state.inputAssembly.indexBuffer.resourceId,
                         state.inputAssembly.indexBuffer.byteOffset + drawOffset,
                         drawOffset > state.inputAssembly.indexBuffer.byteSize
                             ? 0
                             : state.inputAssembly.indexBuffer.byteSize - drawOffset,
                         iformat)));

        for(const D3D12Pipe::ResourceData &res : m_Ctx.CurD3D12PipelineState()->resourceStates)
        {
          if(res.resourceId == state.inputAssembly.indexBuffer.resourceId)
          {
            node->setToolTip(tr("Buffer is in the '%1' state").arg(res.states[0].name));
            break;
          }
        }

        if(!ibufferUsed)
          setInactiveRow(node);

        if(state.inputAssembly.indexBuffer.resourceId == ResourceId())
        {
          setEmptyRow(node);
          m_EmptyNodes.push_back(node);
        }

        ui->iaBuffers->addTopLevelItem(node);
      }
    }
    else
    {
      if(ibufferUsed || m_ShowEmpty)
      {
        RDTreeWidgetItem *node = new RDTreeWidgetItem(
            {tr("Index"), tr("No Buffer Set"), lit("-"), lit("-"), lit("-"), QString()});

        QString iformat;

        if(state.inputAssembly.indexBuffer.byteStride == 1)
          iformat = lit("ubyte");
        else if(state.inputAssembly.indexBuffer.byteStride == 2)
          iformat = lit("ushort");
        else if(state.inputAssembly.indexBuffer.byteStride == 4)
          iformat = lit("uint");

        iformat +=
            lit(" indices[%1]").arg(RENDERDOC_NumVerticesPerPrimitive(state.inputAssembly.topology));

        uint32_t drawOffset =
            (action ? action->indexOffset * state.inputAssembly.indexBuffer.byteStride : 0);

        node->setTag(QVariant::fromValue(
            D3D12VBIBTag(state.inputAssembly.indexBuffer.resourceId,
                         state.inputAssembly.indexBuffer.byteOffset + drawOffset,
                         drawOffset > state.inputAssembly.indexBuffer.byteSize
                             ? 0
                             : state.inputAssembly.indexBuffer.byteSize - drawOffset,
                         iformat)));

        for(const D3D12Pipe::ResourceData &res : m_Ctx.CurD3D12PipelineState()->resourceStates)
        {
          if(res.resourceId == state.inputAssembly.indexBuffer.resourceId)
          {
            node->setToolTip(tr("Buffer is in the '%1' state").arg(res.states[0].name));
            break;
          }
        }

        setEmptyRow(node);
        m_EmptyNodes.push_back(node);

        if(!ibufferUsed)
          setInactiveRow(node);

        ui->iaBuffers->addTopLevelItem(node);
      }
    }

    for(int i = 0; i < 128; i++)
    {
      if(i >= state.inputAssembly.vertexBuffers.count())
      {
        // for vbuffers that are referenced but not bound, make sure we add an empty row
        if(usedVBuffers[i])
        {
          RDTreeWidgetItem *node =
              new RDTreeWidgetItem({i, tr("No Buffer Set"), lit("-"), lit("-"), lit("-"), QString()});
          node->setTag(QVariant::fromValue(D3D12VBIBTag(ResourceId(), 0, 0)));

          setEmptyRow(node);
          m_EmptyNodes.push_back(node);

          m_VBNodes.push_back(node);

          ui->iaBuffers->addTopLevelItem(node);
        }
        else
        {
          m_VBNodes.push_back(NULL);
        }

        continue;
      }

      const D3D12Pipe::VertexBuffer &v = state.inputAssembly.vertexBuffers[i];

      bool filledSlot = (v.resourceId != ResourceId());
      bool usedSlot = (usedVBuffers[i]);

      if(showNode(usedSlot, filledSlot))
      {
        qulonglong length = v.byteSize;

        BufferDescription *buf = m_Ctx.GetBuffer(v.resourceId);

        RDTreeWidgetItem *node = NULL;

        if(filledSlot)
          node = new RDTreeWidgetItem(
              {i, v.resourceId, v.byteStride, (qulonglong)v.byteOffset, length, QString()});
        else
          node =
              new RDTreeWidgetItem({i, tr("No Buffer Set"), lit("-"), lit("-"), lit("-"), QString()});

        node->setTag(QVariant::fromValue(D3D12VBIBTag(v.resourceId, v.byteOffset, v.byteSize,
                                                      m_Common.GetVBufferFormatString(i))));

        for(const D3D12Pipe::ResourceData &res : m_Ctx.CurD3D12PipelineState()->resourceStates)
        {
          if(res.resourceId == v.resourceId)
          {
            node->setToolTip(tr("Buffer is in the '%1' state").arg(res.states[0].name));
            break;
          }
        }

        if(!filledSlot)
        {
          setEmptyRow(node);
          m_EmptyNodes.push_back(node);
        }

        if(!usedSlot)
          setInactiveRow(node);

        m_VBNodes.push_back(node);

        ui->iaBuffers->addTopLevelItem(node);
      }
      else
      {
        m_VBNodes.push_back(NULL);
      }
    }
    ui->iaBuffers->clearSelection();
    ui->iaBuffers->endUpdate();
    ui->iaBuffers->verticalScrollBar()->setValue(vs);

    setShaderState(state.vertexShader, ui->vsShader, ui->vsRootSig);
    setShaderState(state.geometryShader, ui->gsShader, ui->gsRootSig);
    setShaderState(state.hullShader, ui->hsShader, ui->hsRootSig);
    setShaderState(state.domainShader, ui->dsShader, ui->dsRootSig);
  }

  setShaderState(state.pixelShader, ui->psShader, ui->psRootSig);
  setShaderState(state.computeShader, ui->csShader, ui->csRootSig);

  // fill in descriptor access
  {
    RDTreeWidget *resources[] = {
        ui->vsResources, ui->hsResources, ui->dsResources, ui->gsResources,
        ui->psResources, ui->csResources, ui->asResources, ui->msResources,
    };

    RDTreeWidget *uavs[] = {
        ui->vsUAVs, ui->hsUAVs, ui->dsUAVs, ui->gsUAVs,
        ui->psUAVs, ui->csUAVs, ui->asUAVs, ui->msUAVs,
    };

    RDTreeWidget *samplers[] = {
        ui->vsSamplers, ui->hsSamplers, ui->dsSamplers, ui->gsSamplers,
        ui->psSamplers, ui->csSamplers, ui->asSamplers, ui->msSamplers,
    };

    RDTreeWidget *cbuffers[] = {
        ui->vsCBuffers, ui->hsCBuffers, ui->dsCBuffers, ui->gsCBuffers,
        ui->psCBuffers, ui->csCBuffers, ui->asCBuffers, ui->msCBuffers,
    };

    ScopedTreeUpdater restorers[] = {
        ui->vsResources, ui->hsResources, ui->dsResources, ui->gsResources, ui->psResources,
        ui->csResources, ui->asResources, ui->msResources, ui->vsUAVs,      ui->hsUAVs,
        ui->dsUAVs,      ui->gsUAVs,      ui->psUAVs,      ui->csUAVs,      ui->asUAVs,
        ui->msUAVs,      ui->vsSamplers,  ui->hsSamplers,  ui->dsSamplers,  ui->gsSamplers,
        ui->psSamplers,  ui->csSamplers,  ui->asSamplers,  ui->msSamplers,  ui->vsCBuffers,
        ui->hsCBuffers,  ui->dsCBuffers,  ui->gsCBuffers,  ui->psCBuffers,  ui->csCBuffers,
        ui->asCBuffers,  ui->msCBuffers,
    };

    const ShaderReflection *shaderRefls[NumShaderStages];
    // this is a simple flag to see if any non-zero spaces are used. If not, we can be more concise
    // and omit the space when listing the binding for a particular register.
    bool spacesUsed[NumShaderStages] = {};

    for(ShaderStage stage : values<ShaderStage>())
    {
      shaderRefls[(uint32_t)stage] = m_Ctx.CurPipelineState().GetShaderReflection(stage);

      if(!shaderRefls[(uint32_t)stage])
        continue;

      for(const ConstantBlock &bind : shaderRefls[(uint32_t)stage]->constantBlocks)
        spacesUsed[(uint32_t)stage] |= bind.fixedBindSetOrSpace > 0;
      for(const ShaderSampler &bind : shaderRefls[(uint32_t)stage]->samplers)
        spacesUsed[(uint32_t)stage] |= bind.fixedBindSetOrSpace > 0;
      for(const ShaderResource &bind : shaderRefls[(uint32_t)stage]->readOnlyResources)
        spacesUsed[(uint32_t)stage] |= bind.fixedBindSetOrSpace > 0;
      for(const ShaderResource &bind : shaderRefls[(uint32_t)stage]->readWriteResources)
        spacesUsed[(uint32_t)stage] |= bind.fixedBindSetOrSpace > 0;
    }

    rdcarray<UsedDescriptor> descriptors = m_Ctx.CurPipelineState().GetAllUsedDescriptors();

    std::sort(descriptors.begin(), descriptors.end(),
              [](const UsedDescriptor &a, const UsedDescriptor &b) {
                // sort by declared shader interface resource order
                if(a.access.index != b.access.index)
                  return a.access.index < b.access.index;

                return a.access.arrayElement < b.access.arrayElement;
              });

    for(const UsedDescriptor &used : descriptors)
    {
      const ShaderReflection *refl = shaderRefls[(uint32_t)used.access.stage];

      if(IsConstantBlockDescriptor(used.access.type))
      {
        const Descriptor &descriptor = used.descriptor;

        QVariant cbuftag;

        const ConstantBlock *shaderBind = NULL;

        if(used.access.index == DescriptorAccess::NoShaderBinding)
        {
          cbuftag = QVariant::fromValue(D3D12CBufTag(descriptor));
        }
        else
        {
          if(refl && used.access.index < refl->constantBlocks.size())
            shaderBind = &refl->constantBlocks[used.access.index];

          cbuftag = QVariant::fromValue(D3D12CBufTag(used.access.index, used.access.arrayElement));
        }

        bool filledSlot = (descriptor.resource != ResourceId());
        // D3D12 does not report unused elements at all because we enumerate exclusively from the
        // perspective of which descriptors are used
        bool usedSlot = true;

        if(showNode(usedSlot, filledSlot))
        {
          ulong length = descriptor.byteSize;
          uint64_t offset = descriptor.byteOffset;
          int numvars = shaderBind ? shaderBind->variables.count() : 0;
          uint32_t bytesize = shaderBind ? shaderBind->byteSize : 0;

          QString regname;
          if(used.access.index == DescriptorAccess::NoShaderBinding)
          {
            regname =
                m_Locations[{used.access.descriptorStore, used.access.byteOffset}].logicalBindName;
          }
          else if(shaderBind)
          {
            if(!spacesUsed[(uint32_t)used.access.stage])
              regname = QFormatStr("%1").arg(shaderBind->fixedBindNumber);
            else
              regname = QFormatStr("space%1, %2")
                            .arg(shaderBind->fixedBindSetOrSpace)
                            .arg(shaderBind->fixedBindNumber);

            if(!shaderBind->name.empty())
              regname += lit(": ") + shaderBind->name;

            if(shaderBind->bindArraySize > 1)
              regname += QFormatStr("[%1]").arg(used.access.arrayElement);
          }

          QString sizestr;
          if(bytesize == (uint32_t)length)
            sizestr = tr("%1 Variables, %2 bytes")
                          .arg(numvars)
                          .arg(Formatter::HumanFormat(length, Formatter::OffsetSize));
          else
            sizestr = tr("%1 Variables, %2 bytes needed, %3 provided")
                          .arg(numvars)
                          .arg(Formatter::HumanFormat(bytesize, Formatter::OffsetSize))
                          .arg(Formatter::HumanFormat(length, Formatter::OffsetSize));

          if(length < bytesize)
            filledSlot = false;

          // ignore offset into virtualised data storage on root constants, display as 0-based
          if(descriptor.flags & DescriptorFlags::InlineData)
            offset = 0;

          RDTreeWidgetItem *node = new RDTreeWidgetItem({
              regname,
              (descriptor.flags & DescriptorFlags::InlineData) ? ResourceId() : descriptor.resource,
              QFormatStr("%1 - %2")
                  .arg(Formatter::HumanFormat(offset, Formatter::OffsetSize))
                  .arg(Formatter::HumanFormat(offset + bytesize, Formatter::OffsetSize)),
              sizestr,
              QString(),
          });

          node->setTag(cbuftag);

          if(!filledSlot)
            setEmptyRow(node);

          if(!usedSlot)
            setInactiveRow(node);

          cbuffers[(uint32_t)used.access.stage]->addTopLevelItem(node);
        }
      }
      else if(IsSamplerDescriptor(used.access.type))
      {
        const SamplerDescriptor &samplerDescriptor = used.sampler;

        const ShaderSampler *shaderBind = NULL;

        if(refl && used.access.index < refl->samplers.size())
          shaderBind = &refl->samplers[used.access.index];

        bool filledSlot = samplerDescriptor.filter.minify != FilterMode::NoFilter;
        // D3D12 does not report unused elements at all because we enumerate exclusively from the
        // perspective of which descriptors are used
        bool usedSlot = true;

        if(showNode(usedSlot, filledSlot))
        {
          QString regname;
          if(used.access.index == DescriptorAccess::NoShaderBinding)
          {
            regname =
                m_Locations[{used.access.descriptorStore, used.access.byteOffset}].logicalBindName;
          }
          else if(shaderBind)
          {
            if(!spacesUsed[(uint32_t)used.access.stage])
              regname = QFormatStr("%1").arg(shaderBind->fixedBindNumber);
            else
              regname = QFormatStr("space%1, %2")
                            .arg(shaderBind->fixedBindSetOrSpace)
                            .arg(shaderBind->fixedBindNumber);

            if(!shaderBind->name.empty())
              regname += lit(": ") + shaderBind->name;

            if(shaderBind->bindArraySize > 1)
              regname += QFormatStr("[%1]").arg(used.access.arrayElement);
          }

          QString borderColor;

          if(samplerDescriptor.borderColorType == CompType::Float)
            borderColor = QFormatStr("%1, %2, %3, %4")
                              .arg(samplerDescriptor.borderColorValue.floatValue[0])
                              .arg(samplerDescriptor.borderColorValue.floatValue[1])
                              .arg(samplerDescriptor.borderColorValue.floatValue[2])
                              .arg(samplerDescriptor.borderColorValue.floatValue[3]);
          else
            borderColor = QFormatStr("%1, %2, %3, %4")
                              .arg(samplerDescriptor.borderColorValue.uintValue[0])
                              .arg(samplerDescriptor.borderColorValue.uintValue[1])
                              .arg(samplerDescriptor.borderColorValue.uintValue[2])
                              .arg(samplerDescriptor.borderColorValue.uintValue[3]);

          QString addressing;

          QString addPrefix;
          QString addVal;

          QString addr[] = {ToQStr(samplerDescriptor.addressU, GraphicsAPI::D3D12),
                            ToQStr(samplerDescriptor.addressV, GraphicsAPI::D3D12),
                            ToQStr(samplerDescriptor.addressW, GraphicsAPI::D3D12)};

          // arrange like either UVW: WRAP or UV: WRAP, W: CLAMP
          for(int a = 0; a < 3; a++)
          {
            const QString str[] = {lit("U"), lit("V"), lit("W")};
            QString prefix = str[a];

            if(a == 0 || addr[a] == addr[a - 1])
            {
              addPrefix += prefix;
            }
            else
            {
              addressing += QFormatStr("%1: %2, ").arg(addPrefix).arg(addVal);

              addPrefix = prefix;
            }
            addVal = addr[a];
          }

          addressing += addPrefix + lit(": ") + addVal;

          if(samplerDescriptor.UseBorder())
            addressing += QFormatStr("<%1>").arg(borderColor);

          if(samplerDescriptor.unnormalized)
            addressing += lit(" (Non-norm)");

          QString filter = ToQStr(samplerDescriptor.filter);

          if(samplerDescriptor.maxAnisotropy > 1)
            filter += QFormatStr(" %1x").arg(samplerDescriptor.maxAnisotropy);

          if(samplerDescriptor.filter.filter == FilterFunction::Comparison)
            filter += QFormatStr(" (%1)").arg(ToQStr(samplerDescriptor.compareFunction));
          else if(samplerDescriptor.filter.filter != FilterFunction::Normal)
            filter += QFormatStr(" (%1)").arg(ToQStr(samplerDescriptor.filter.filter));

          RDTreeWidgetItem *node =
              new RDTreeWidgetItem({regname, addressing, filter,
                                    QFormatStr("%1 - %2")
                                        .arg(samplerDescriptor.minLOD == -FLT_MAX
                                                 ? lit("0")
                                                 : QString::number(samplerDescriptor.minLOD))
                                        .arg(samplerDescriptor.maxLOD == FLT_MAX
                                                 ? lit("FLT_MAX")
                                                 : QString::number(samplerDescriptor.maxLOD)),
                                    samplerDescriptor.mipBias});

          if(!filledSlot)
            setEmptyRow(node);

          if(!usedSlot)
            setInactiveRow(node);

          samplers[(uint32_t)used.access.stage]->addTopLevelItem(node);
        }
      }
      else
      {
        const bool srv = IsReadOnlyDescriptor(used.access.type);

        RDTreeWidget *tree =
            srv ? resources[(uint32_t)used.access.stage] : uavs[(uint32_t)used.access.stage];

        const Descriptor &view = used.descriptor;

        D3D12ViewTag tag;
        tag.type = srv ? D3D12ViewTag::SRV : D3D12ViewTag::UAV;
        tag.access = used.access;
        tag.descriptor = view;

        const rdcarray<ShaderResource> &resArray =
            srv ? refl->readOnlyResources : refl->readWriteResources;

        const ShaderResource *shaderBind = NULL;

        if(refl && used.access.index < resArray.size())
          shaderBind = &resArray[used.access.index];

        bool filledSlot = view.resource != ResourceId();
        // D3D12 does not report unused elements at all because we enumerate exclusively from the
        // perspective of which descriptors are used
        bool usedSlot = true;

        if(showNode(usedSlot, filledSlot))
        {
          addResourceRow(tag, shaderBind, spacesUsed[(uint32_t)used.access.stage], tree);
        }
      }
    }
  }

  QToolButton *shaderButtons[] = {
      // view buttons
      ui->asShaderViewButton,
      ui->msShaderViewButton,
      ui->vsShaderViewButton,
      ui->hsShaderViewButton,
      ui->dsShaderViewButton,
      ui->gsShaderViewButton,
      ui->psShaderViewButton,
      ui->csShaderViewButton,
      // edit buttons
      ui->asShaderEditButton,
      ui->msShaderEditButton,
      ui->vsShaderEditButton,
      ui->hsShaderEditButton,
      ui->dsShaderEditButton,
      ui->gsShaderEditButton,
      ui->psShaderEditButton,
      ui->csShaderEditButton,
      // save buttons
      ui->asShaderSaveButton,
      ui->msShaderSaveButton,
      ui->vsShaderSaveButton,
      ui->hsShaderSaveButton,
      ui->dsShaderSaveButton,
      ui->gsShaderSaveButton,
      ui->psShaderSaveButton,
      ui->csShaderSaveButton,
  };

  for(QToolButton *b : shaderButtons)
  {
    const D3D12Pipe::Shader *stage = stageForSender(b);

    if(stage == NULL || stage->resourceId == ResourceId())
      continue;

    b->setEnabled(stage->reflection && state.pipelineResourceId != ResourceId());

    m_Common.SetupShaderEditButton(b, state.pipelineResourceId, stage->resourceId, stage->reflection);
  }

  QToolButton *sigButtons[] = {
      ui->vsRootSigButton, ui->hsRootSigButton, ui->dsRootSigButton, ui->gsRootSigButton,
      ui->psRootSigButton, ui->csRootSigButton, ui->asRootSigButton, ui->msRootSigButton,
  };

  for(QToolButton *b : sigButtons)
    b->setEnabled(state.rootSignature.resourceId != ResourceId());

  bool streamoutSet = false;
  vs = ui->gsStreamOut->verticalScrollBar()->value();
  ui->gsStreamOut->beginUpdate();
  ui->gsStreamOut->clear();
  for(int i = 0; i < state.streamOut.outputs.count(); i++)
  {
    const D3D12Pipe::StreamOutBind &s = state.streamOut.outputs[i];

    bool filledSlot = (s.resourceId != ResourceId());
    bool usedSlot = (filledSlot);

    if(showNode(usedSlot, filledSlot))
    {
      qulonglong length = s.byteSize;

      BufferDescription *buf = m_Ctx.GetBuffer(s.resourceId);

      RDTreeWidgetItem *node = new RDTreeWidgetItem({
          i,
          s.resourceId,
          Formatter::HumanFormat(s.byteOffset, Formatter::OffsetSize),
          Formatter::HumanFormat(length, Formatter::OffsetSize),
          s.writtenCountResourceId,
          Formatter::HumanFormat(s.writtenCountByteOffset, Formatter::OffsetSize),
          QString(),
      });

      node->setTag(QVariant::fromValue(s.resourceId));

      if(!filledSlot)
        setEmptyRow(node);

      if(!usedSlot)
        setInactiveRow(node);

      streamoutSet = true;

      ui->gsStreamOut->addTopLevelItem(node);
    }
  }
  ui->gsStreamOut->verticalScrollBar()->setValue(vs);
  ui->gsStreamOut->clearSelection();
  ui->gsStreamOut->endUpdate();

  ui->gsStreamOut->setVisible(streamoutSet);
  ui->soGroup->setVisible(streamoutSet);

  ////////////////////////////////////////////////
  // Rasterizer

  vs = ui->viewports->verticalScrollBar()->value();
  ui->viewports->beginUpdate();
  ui->viewports->clear();
  for(int i = 0; i < state.rasterizer.viewports.count(); i++)
  {
    const Viewport &v = state.rasterizer.viewports[i];

    RDTreeWidgetItem *node =
        new RDTreeWidgetItem({i, v.x, v.y, v.width, v.height, v.minDepth, v.maxDepth});

    if(v.width == 0 || v.height == 0 || v.minDepth == v.maxDepth)
      setEmptyRow(node);

    ui->viewports->addTopLevelItem(node);
  }
  ui->viewports->verticalScrollBar()->setValue(vs);
  ui->viewports->clearSelection();
  ui->viewports->endUpdate();

  vs = ui->scissors->verticalScrollBar()->value();
  ui->scissors->beginUpdate();
  ui->scissors->clear();
  for(int i = 0; i < state.rasterizer.scissors.count(); i++)
  {
    const Scissor &s = state.rasterizer.scissors[i];

    RDTreeWidgetItem *node = new RDTreeWidgetItem({i, s.x, s.y, s.width, s.height});

    if(s.width == 0 || s.height == 0)
      setEmptyRow(node);

    ui->scissors->addTopLevelItem(node);
  }
  ui->scissors->clearSelection();
  ui->scissors->verticalScrollBar()->setValue(vs);
  ui->scissors->endUpdate();

  ui->fillMode->setText(ToQStr(state.rasterizer.state.fillMode));
  ui->cullMode->setText(ToQStr(state.rasterizer.state.cullMode));
  ui->frontCCW->setPixmap(state.rasterizer.state.frontCCW ? tick : cross);

  switch(state.rasterizer.state.lineRasterMode)
  {
    case LineRaster::Default: ui->lineAA->setText(lit("Default")); break;
    case LineRaster::Bresenham: ui->lineAA->setText(lit("Aliased")); break;
    case LineRaster::RectangularSmooth: ui->lineAA->setText(lit("Alpha antialiased")); break;
    case LineRaster::Rectangular: ui->lineAA->setText(lit("Quadrilateral (narrow)")); break;
    case LineRaster::RectangularD3D: ui->lineAA->setText(lit("Quadrilateral")); break;
  }
  ui->sampleMask->setText(Formatter::Format(state.rasterizer.sampleMask, true));

  ui->depthClip->setPixmap(state.rasterizer.state.depthClip ? tick : cross);
  ui->depthBias->setText(Formatter::Format(state.rasterizer.state.depthBias));
  ui->depthBiasClamp->setText(Formatter::Format(state.rasterizer.state.depthBiasClamp));
  ui->slopeScaledBias->setText(Formatter::Format(state.rasterizer.state.slopeScaledDepthBias));
  ui->forcedSampleCount->setText(QString::number(state.rasterizer.state.forcedSampleCount));
  ui->conservativeRaster->setPixmap(
      state.rasterizer.state.conservativeRasterization != ConservativeRaster::Disabled ? tick
                                                                                       : cross);

  ui->baseShadingRate->setText(QFormatStr("%1x%2")
                                   .arg(state.rasterizer.state.baseShadingRate.first)
                                   .arg(state.rasterizer.state.baseShadingRate.second));
  ui->shadingRateCombiners->setText(
      QFormatStr("%1, %2")
          .arg(ToQStr(state.rasterizer.state.shadingRateCombiners.first, GraphicsAPI::D3D12))
          .arg(ToQStr(state.rasterizer.state.shadingRateCombiners.second, GraphicsAPI::D3D12)));
  ui->shadingRateImage->setText(ToQStr(state.rasterizer.state.shadingRateImage));

  ////////////////////////////////////////////////
  // Output Merger

  bool targets[32] = {};

  vs = ui->targetOutputs->verticalScrollBar()->value();
  ui->targetOutputs->beginUpdate();
  ui->targetOutputs->clear();
  {
    rdcarray<Descriptor> rts = m_Ctx.CurPipelineState().GetOutputTargets();
    for(uint32_t i = 0; i < rts.size(); i++)
    {
      DescriptorAccess access;
      access.index = i;
      addResourceRow(D3D12ViewTag(D3D12ViewTag::OMTarget, access, rts[i]), NULL, false,
                     ui->targetOutputs);

      if(rts[i].resource != ResourceId())
        targets[i] = true;
    }

    Descriptor depth = m_Ctx.CurPipelineState().GetDepthTarget();
    addResourceRow(D3D12ViewTag(D3D12ViewTag::OMDepth, DescriptorAccess(), depth), NULL, false,
                   ui->targetOutputs);
  }
  ui->targetOutputs->clearSelection();
  ui->targetOutputs->endUpdate();
  ui->targetOutputs->verticalScrollBar()->setValue(vs);

  vs = ui->blends->verticalScrollBar()->value();
  ui->blends->beginUpdate();
  ui->blends->clear();
  {
    int i = 0;
    for(const ColorBlend &blend : state.outputMerger.blendState.blends)
    {
      bool filledSlot = (blend.enabled || targets[i]);
      bool usedSlot = (targets[i]);

      if(showNode(usedSlot, filledSlot))
      {
        RDTreeWidgetItem *node = NULL;

        node = new RDTreeWidgetItem(
            {i, blend.enabled ? tr("True") : tr("False"),

             ToQStr(blend.colorBlend.source), ToQStr(blend.colorBlend.destination),
             ToQStr(blend.colorBlend.operation),

             ToQStr(blend.alphaBlend.source), ToQStr(blend.alphaBlend.destination),
             ToQStr(blend.alphaBlend.operation),

             blend.logicOperationEnabled ? ToQStr(blend.logicOperation) : tr("Disabled"),

             QFormatStr("%1%2%3%4")
                 .arg((blend.writeMask & 0x1) == 0 ? lit("_") : lit("R"))
                 .arg((blend.writeMask & 0x2) == 0 ? lit("_") : lit("G"))
                 .arg((blend.writeMask & 0x4) == 0 ? lit("_") : lit("B"))
                 .arg((blend.writeMask & 0x8) == 0 ? lit("_") : lit("A"))});

        if(!filledSlot)
          setEmptyRow(node);

        if(!usedSlot)
          setInactiveRow(node);

        ui->blends->addTopLevelItem(node);
      }

      i++;
    }
  }
  ui->blends->clearSelection();
  ui->blends->endUpdate();
  ui->blends->verticalScrollBar()->setValue(vs);

  ui->alphaToCoverage->setPixmap(state.outputMerger.blendState.alphaToCoverage ? tick : cross);
  ui->independentBlend->setPixmap(state.outputMerger.blendState.independentBlend ? tick : cross);

  ui->blendFactor->setText(QFormatStr("%1, %2, %3, %4")
                               .arg(state.outputMerger.blendState.blendFactor[0], 0, 'f', 2)
                               .arg(state.outputMerger.blendState.blendFactor[1], 0, 'f', 2)
                               .arg(state.outputMerger.blendState.blendFactor[2], 0, 'f', 2)
                               .arg(state.outputMerger.blendState.blendFactor[3], 0, 'f', 2));

  if(state.outputMerger.depthStencilState.depthEnable)
  {
    ui->depthEnabled->setPixmap(tick);
    ui->depthFunc->setText(ToQStr(state.outputMerger.depthStencilState.depthFunction));
    ui->depthWrite->setPixmap(state.outputMerger.depthStencilState.depthWrites ? tick : cross);
    ui->depthWrite->setText(QString());
  }
  else
  {
    ui->depthEnabled->setPixmap(cross);
    ui->depthFunc->setText(tr("Disabled"));
    ui->depthWrite->setPixmap(QPixmap());
    ui->depthWrite->setText(tr("Disabled"));
  }

  if(state.outputMerger.depthStencilState.depthBoundsEnable)
  {
    ui->depthBounds->setPixmap(QPixmap());
    ui->depthBounds->setText(Formatter::Format(state.outputMerger.depthStencilState.minDepthBounds) +
                             lit("-") +
                             Formatter::Format(state.outputMerger.depthStencilState.maxDepthBounds));
  }
  else
  {
    ui->depthBounds->setText(QString());
    ui->depthBounds->setPixmap(cross);
  }

  ui->stencils->beginUpdate();
  ui->stencils->clear();
  if(state.outputMerger.depthStencilState.stencilEnable)
  {
    ui->stencils->addTopLevelItem(new RDTreeWidgetItem({
        tr("Front"),
        ToQStr(state.outputMerger.depthStencilState.frontFace.function),
        ToQStr(state.outputMerger.depthStencilState.frontFace.failOperation),
        ToQStr(state.outputMerger.depthStencilState.frontFace.depthFailOperation),
        ToQStr(state.outputMerger.depthStencilState.frontFace.passOperation),
        QVariant(),
        QVariant(),
        QVariant(),
    }));

    m_Common.SetStencilTreeItemValue(ui->stencils->topLevelItem(0), 5,
                                     state.outputMerger.depthStencilState.frontFace.writeMask);
    m_Common.SetStencilTreeItemValue(ui->stencils->topLevelItem(0), 6,
                                     state.outputMerger.depthStencilState.frontFace.compareMask);
    m_Common.SetStencilTreeItemValue(ui->stencils->topLevelItem(0), 7,
                                     state.outputMerger.depthStencilState.frontFace.reference);

    ui->stencils->addTopLevelItem(new RDTreeWidgetItem({
        tr("Back"),
        ToQStr(state.outputMerger.depthStencilState.backFace.function),
        ToQStr(state.outputMerger.depthStencilState.backFace.failOperation),
        ToQStr(state.outputMerger.depthStencilState.backFace.depthFailOperation),
        ToQStr(state.outputMerger.depthStencilState.backFace.passOperation),
        QVariant(),
        QVariant(),
        QVariant(),
    }));

    m_Common.SetStencilTreeItemValue(ui->stencils->topLevelItem(1), 5,
                                     state.outputMerger.depthStencilState.backFace.writeMask);
    m_Common.SetStencilTreeItemValue(ui->stencils->topLevelItem(1), 6,
                                     state.outputMerger.depthStencilState.backFace.compareMask);
    m_Common.SetStencilTreeItemValue(ui->stencils->topLevelItem(1), 7,
                                     state.outputMerger.depthStencilState.backFace.reference);
  }
  else
  {
    ui->stencils->addTopLevelItem(new RDTreeWidgetItem(
        {tr("Front"), lit("-"), lit("-"), lit("-"), lit("-"), lit("-"), lit("-"), lit("-")}));
    ui->stencils->addTopLevelItem(new RDTreeWidgetItem(
        {tr("Back"), lit("-"), lit("-"), lit("-"), lit("-"), lit("-"), lit("-"), lit("-")}));
  }
  ui->stencils->clearSelection();
  ui->stencils->endUpdate();

  // set up thread debugging inputs
  bool enableDebug = m_Ctx.APIProps().shaderDebugging && state.computeShader.reflection &&
                     state.computeShader.reflection->debugInfo.debuggable && action &&
                     (action->flags & ActionFlags::Dispatch);
  if(enableDebug)
  {
    // Validate dispatch/threadgroup dimensions
    enableDebug &= action->dispatchDimension[0] > 0;
    enableDebug &= action->dispatchDimension[1] > 0;
    enableDebug &= action->dispatchDimension[2] > 0;

    const rdcfixedarray<uint32_t, 3> &threadDims =
        (action->dispatchThreadsDimension[0] == 0)
            ? state.computeShader.reflection->dispatchThreadsDimension
            : action->dispatchThreadsDimension;
    enableDebug &= threadDims[0] > 0;
    enableDebug &= threadDims[1] > 0;
    enableDebug &= threadDims[2] > 0;
  }

  if(enableDebug)
  {
    ui->computeDebugSelector->setEnabled(true);

    // set maximums for CS debugging
    m_ComputeDebugSelector->SetThreadBounds(
        action->dispatchDimension, (action->dispatchThreadsDimension[0] == 0)
                                       ? state.computeShader.reflection->dispatchThreadsDimension
                                       : action->dispatchThreadsDimension);

    ui->computeDebugSelector->setToolTip(
        tr("Debug this compute shader by specifying group/thread ID or dispatch ID"));
  }
  else
  {
    ui->computeDebugSelector->setEnabled(false);

    if(!m_Ctx.APIProps().shaderDebugging)
      ui->computeDebugSelector->setToolTip(tr("This API does not support shader debugging"));
    else if(!action || !(action->flags & ActionFlags::Dispatch))
      ui->computeDebugSelector->setToolTip(tr("No dispatch selected"));
    else if(!state.computeShader.reflection)
      ui->computeDebugSelector->setToolTip(tr("No compute shader bound"));
    else if(!state.computeShader.reflection->debugInfo.debuggable)
      ui->computeDebugSelector->setToolTip(
          tr("This shader doesn't support debugging: %1")
              .arg(state.computeShader.reflection->debugInfo.debugStatus));
    else
      ui->computeDebugSelector->setToolTip(tr("Invalid dispatch/threadgroup dimensions."));
  }
}