Files
renderdoc/qrenderdoc/Windows/DescriptorViewer.cpp
T
baldurk ceb062b658 Add a DescriptorType to GetDescriptors query
* This will be optional in many cases but for some situations might be required
  when type information is not implicitly available in the descriptor store.
  Generally it should always be available unless the descriptor store is being
  viewed 'blank' purely from its contents with no other context.
2025-07-30 22:10:23 +01:00

1601 lines
53 KiB
C++

/******************************************************************************
* The MIT License (MIT)
*
* Copyright (c) 2019-2025 Baldur Karlsson
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
******************************************************************************/
#include "DescriptorViewer.h"
#include <float.h>
#include <QFontDatabase>
#include "Code/QRDUtils.h"
#include "Code/Resources.h"
#include "toolwindowmanager/ToolWindowManager.h"
#include "ui_DescriptorViewer.h"
struct ButtonTag
{
ButtonTag() = default;
ButtonTag(bool buffer, const Descriptor &descriptor)
: valid(true), buffer(buffer), descriptor(descriptor)
{
}
ButtonTag(ResourceId heap) : valid(true), buffer(false), heap(heap) {}
// all constructe tags compare equal so this value can contain data but still be used to enable buttons
bool operator==(const ButtonTag &o) const { return valid && o.valid; }
bool operator<(const ButtonTag &o) const { return valid < o.valid; }
bool valid = false;
bool buffer = false;
Descriptor descriptor;
ResourceId heap;
};
Q_DECLARE_METATYPE(ButtonTag);
class DescriptorItemModel : public QAbstractItemModel
{
public:
DescriptorItemModel(ICaptureContext &ctx, DescriptorViewer &view, QObject *parent)
: QAbstractItemModel(parent), m_Ctx(ctx), m_View(view)
{
m_API = m_Ctx.APIProps().pipelineType;
}
void refresh()
{
emit beginResetModel();
if(m_View.m_D3D12RootSig.parameters.size() + 1 >= ParameterIndexMask)
{
qCritical() << "Too many root signature parameters, will be clipped";
for(const D3D12Pipe::RootParam &param : m_View.m_D3D12RootSig.parameters)
if(param.tableRanges.size() + 1 >= TableIndexMask)
qCritical() << "Too many tables in parameter, will be clipped";
}
emit endResetModel();
}
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override
{
if(row < 0 || row >= rowCount(parent) || column < 0 || column >= columnCount())
return QModelIndex();
// root signature has more levels of nesting so is more complex
//
// for ease, each (x, y) tuple is a QModelIndex(row=x, column, id=y). Column is omitted as it's
// not important. We encode the levels of array indexing in the id assuming reasonable packing.
//
// For packing:
// param is at very most 63 because of root signature limits so it needs 6 bits
// tables can be at most 1 million long because of descriptor heap limits
// tables could be large but it's more likely to have a few tables that are very large
//
// With 64-bit ids we have plenty of bits.
// For 32-bit we take 1 bit for parameter or sampler, 6 bits for param, 20 bits for descriptor,
// and the remaining 5 bits for table index. We don't expect this code to run in 32-bit typically
// (or ever) so it's a best-effort. For this case we also set the 'ParameterFlag' id specially to
// avoid needing to take more bits. We use 1-based indices to be able to distinguish parent from
// child.
//
// QModelIndex() <= root
// (ParametersRootNode, FixedNode) <= "Parameters"
// (param, ParameterFlag) <= params[param]
// (0, ParameterData | param+1) <= params[param].data[0]
// (1..., ParameterData | param+1) <= params[param].data[1]... etc
// more for root consts/desc
//
// (table, ParameterData | param+1) <= param.tables[table]
// with table >= fixedDataCount
// (0, ParameterData | param+1 | table+1) <= table.data[0]
// (1..., ParameterData | param+1 | table+1) <= table.data[1]...
// (desc, ParameterData | param+1 | table+1) <= table.descriptor[desc]
// (dataIdx, ParameterData | param+1 | table+1 | desc+1) <= desc.data[dataIdx]
//
// (StaticSamplersRootNode, FixedNode) <= "StaticSamplers"
// (sampIdx, StaticSamplerData) <= samplers[sampIdx]
// (dataIdx, StaticSamplerData | sampIdx+1) <= samplers[sampIdx].data[dataIdx]
// (DescriptorHeapNode, FixedNode) <= "Descriptor Heap 1234"
if(m_View.m_D3D12RootSig.resourceId != ResourceId())
{
// children of the root are the two fixed nodes
if(parent == QModelIndex())
{
return createIndex(row, column, FixedNode);
}
// children of the fixed nodes are parameters or static samplers
if(parent.internalId() == FixedNode)
{
if(parent.row() == ParametersRootNode)
return createIndex(row, column, ParameterFlag);
if(parent.row() == StaticSamplersRootNode)
return createIndex(row, column, StaticSamplerData);
// other root entries are descriptor heaps
return QModelIndex();
}
// children of static samplers just add on their index (+1 to distinguish from the plain node)
if(parent.internalId() == StaticSamplerData)
{
return createIndex(row, column, StaticSamplerData | (parent.row() + 1));
}
// other rows that aren't parameter data are static sampler properties themselves and have no children
if((parent.internalId() & ParameterData) == 0)
return QModelIndex();
// children of a parameter node mask on the index into their id
if(parent.internalId() == ParameterFlag)
{
return createIndex(row, column, Encode({uint8_t(parent.row() + 1), 0, 0}));
}
RootIdx parentIdx = Decode(parent.internalId());
// should not be possible, the root is ParameterFlag and then after that we encode with
// 1-based indexing so the values are not 0.
if(parentIdx.parameter == 0)
return QModelIndex();
// this is a child of a parameter node, encode the range index from the parent's row
if(parentIdx.range == 0 && parentIdx.descriptor == 0)
{
// the fixed parameters do not have children
if(parent.row() < TableParameterFixedRowCount)
return QModelIndex();
return createIndex(row, column,
Encode({parentIdx.parameter,
uint16_t(parent.row() - TableParameterFixedRowCount + 1), 0}));
}
// this is the child of a table node, encode the descriptor index from the parent's row
if(parentIdx.descriptor == 0)
{
// the fixed parameters do not have children
if(parent.row() < RangeFixedRowCount)
return QModelIndex();
return createIndex(row, column,
Encode({parentIdx.parameter, parentIdx.range,
uint32_t(parent.row() - RangeFixedRowCount + 1)}));
}
// children of descriptors are data entries, and do not have children themselves
return QModelIndex();
}
// otherwise it's a plain list of descriptors
//
// QModelIndex() <= root
// (descIdx, DescriptorFlag) <= descriptor[descIdx]
// (dataIdx, DescriptorDataFlag | descIdx) <= descriptor[descIdx].data[dataIdx]
if(parent == QModelIndex())
return createIndex(row, column, DescriptorFlag);
if(parent.internalId() & DescriptorFlag)
return createIndex(row, column, parent.row() | DescriptorDataFlag);
// invalid, this would be a child of the data elements
return QModelIndex();
}
QModelIndex parent(const QModelIndex &index) const override
{
if(index == QModelIndex())
return QModelIndex();
if(m_View.m_D3D12RootSig.resourceId != ResourceId())
{
quintptr id = index.internalId();
// the fixed nodes are under the root
if(id == FixedNode)
return QModelIndex();
// parameter nodes have a specific id, they are under the fixed node
if(id == ParameterFlag)
return createIndex(ParametersRootNode, 0, FixedNode);
// a static sampler node, parented under the other fixed node
if(id == StaticSamplerData)
return createIndex(StaticSamplersRootNode, 0, FixedNode);
// other rows that aren't parameter data are static sampler properties and are parented under
// their static sampler
if((id & ParameterData) == 0)
{
// this is the row index + 1
uint32_t sampIndex = id & ~StaticSamplerData;
if(sampIndex == 0)
return QModelIndex();
return createIndex(sampIndex - 1, 0, StaticSamplerData);
}
// at this point the index should either be a range, a descriptor, or a descriptor
// data row.
RootIdx decodedIndex = Decode(id);
// we basically knock the finest grained index out of the parentIdx and turn it into a row.
// The only special case is ranges since their parent needs the magic ParameterFlag id
// descriptor data node - parent is the descriptor node
if(decodedIndex.descriptor != 0)
{
int row = decodedIndex.descriptor - 1;
decodedIndex.descriptor = 0;
return createIndex(RangeFixedRowCount + row, 0, Encode(decodedIndex));
}
// descriptor node - parent is the range
if(decodedIndex.range != 0)
{
int row = decodedIndex.range - 1;
decodedIndex.range = 0;
return createIndex(TableParameterFixedRowCount + row, 0, Encode(decodedIndex));
}
// should not be possible here
if(decodedIndex.parameter == 0)
return QModelIndex();
// range node - parent is the parameter which has a different index
int row = decodedIndex.parameter - 1;
return createIndex(row, 0, ParameterFlag);
}
else
{
// the descriptors are parented directly under the root
if(index.internalId() & DescriptorFlag)
return QModelIndex();
// the children of the descriptor itself are under the descriptor
if(index.internalId() & DescriptorDataFlag)
{
int row = index.internalId() & ~DescriptorDataFlag;
return createIndex(row, 0, DescriptorFlag);
}
}
return QModelIndex();
}
int rowCount(const QModelIndex &parent = QModelIndex()) const override
{
if(m_View.m_D3D12RootSig.resourceId != ResourceId())
{
// for root signature the root node has 2 children for parameters and static samplers
if(parent == QModelIndex())
return FirstHeapRootNode + m_View.m_D3D12Heaps.count();
// those fixed nodes have a simple row count
if(parent.internalId() == FixedNode)
{
if(parent.row() == ParametersRootNode)
return m_View.m_D3D12RootSig.parameters.count();
if(parent.row() == StaticSamplersRootNode)
return m_View.m_D3D12RootSig.staticSamplers.count();
// other members are descriptor heaps which have no members
return 0;
}
// parameter nodes have a specific id
if(parent.internalId() == ParameterFlag)
{
const D3D12Pipe::RootParam &param = m_View.m_D3D12RootSig.parameters[parent.row()];
if(!param.constants.empty())
return ConstParameterFixedRowCount;
if(param.descriptor.type != DescriptorType::Unknown)
return DescParameterFixedRowCount + rowCount(param.descriptor);
return TableParameterFixedRowCount + param.tableRanges.count();
}
// static sampler node
if(parent.internalId() == StaticSamplerData)
{
return StaticSamplerFixedRowCount + samplerRowCount();
}
// other rows that aren't parameter data are static sampler properties themselves and have no children
if((parent.internalId() & ParameterData) == 0)
return 0;
// at this point the parent node should either be a range, a descriptor, or a descriptor
// data row. Parameter node parents are handled above as the magic ParameterFlag id
RootIdx parentIdx = Decode(parent.internalId());
// 0 should be impossible, all parent nodes at this level should have at least a parameter encoded
if(parentIdx.parameter == 0 ||
parentIdx.parameter - 1 >= m_View.m_D3D12RootSig.parameters.count())
return 0;
const D3D12Pipe::RootParam &param = m_View.m_D3D12RootSig.parameters[parentIdx.parameter - 1];
if(parentIdx.range > 0 && parentIdx.range - 1 >= param.tableRanges.count())
return 0;
// parameters with no tables don't have any more children
if(!param.constants.empty() || param.descriptor.type != DescriptorType::Unknown)
return 0;
// if range is 0 on the parent's ID then this is a range node, so take the index from the parent's row
const D3D12Pipe::RootTableRange &range =
param.tableRanges[parentIdx.range == 0 ? parent.row() - TableParameterFixedRowCount
: parentIdx.range - 1];
// if this is a range node, parent's range will be 0. We return the number of descriptors (plus fixed rows)
if(parentIdx.range == 0)
{
// fixed rows in range nodes have no children
if(parent.row() < TableParameterFixedRowCount)
return 0;
// Do a clamp here if we have descriptors to display
if(!m_View.m_Descriptors.empty())
{
uint32_t fullOffset = param.heapByteOffset + range.tableByteOffset;
uint32_t maxDescriptors;
if(range.category == DescriptorCategory::Sampler)
maxDescriptors = uint32_t(m_View.m_SamplerDescriptors.size() - fullOffset);
else
maxDescriptors = uint32_t(m_View.m_Descriptors.size() - fullOffset);
return RangeFixedRowCount + qMin(maxDescriptors, range.count);
}
// otherwise we'll have no descriptor rows but we will have two extras to show the space and
// register that would normally be listed in the descriptor names
return RangeFixedRowCount + 2;
}
// if the *parent* has a descriptor index then this must be a descriptor data row, it has no children.
if(parentIdx.descriptor != 0)
return 0;
// otherwise it is a descriptor node
// fixed rows in a range have no children
if(parent.row() < RangeFixedRowCount)
return 0;
if(range.category == DescriptorCategory::Sampler)
return samplerRowCount();
// out of bounds descriptor index shouldn't happen as we clamped the count above
if(param.heapByteOffset + range.tableByteOffset + parent.row() - RangeFixedRowCount >=
m_View.m_Descriptors.size())
return 0;
return RootSigDescriptorFixedRows +
rowCount(m_View.m_Descriptors[param.heapByteOffset + range.tableByteOffset +
parent.row() - RangeFixedRowCount]);
}
else
{
if(parent == QModelIndex())
return m_View.m_Descriptors.count();
// the children of the descriptor itself don't have any children
if(parent.internalId() & DescriptorDataFlag)
return 0;
uint32_t sampIndex = parent.row();
if(!m_View.m_DescriptorToSamplerLookup.empty())
sampIndex = m_View.m_DescriptorToSamplerLookup[parent.row()];
if(sampIndex != ~0U && m_View.m_SamplerDescriptors[sampIndex].type == DescriptorType::Sampler)
return samplerRowCount();
int ret = rowCount(m_View.m_Descriptors[parent.row()]);
if(parent.row() < m_View.m_Locations.count())
ret += DescriptorLocationFixedRowCount;
return ret;
}
}
int columnCount(const QModelIndex &parent = QModelIndex()) const override { return 2; }
Qt::ItemFlags flags(const QModelIndex &index) const override
{
if(!index.isValid())
return 0;
return QAbstractItemModel::flags(index);
}
QVariant headerData(int section, Qt::Orientation orientation, int role) const override
{
if(orientation == Qt::Horizontal && role == Qt::DisplayRole)
{
if(section == 0)
return tr("Index");
else if(section == 1)
return tr("Contents");
}
return QVariant();
}
int rowCount(const Descriptor &desc, bool includeSampler = true) const
{
int ret = 1;
switch(desc.type)
{
case DescriptorType::ConstantBuffer:
case DescriptorType::Buffer:
case DescriptorType::ReadWriteBuffer:
case DescriptorType::TypedBuffer:
case DescriptorType::ReadWriteTypedBuffer:
{
// type, resource, offset, size, button to view
ret = 5;
if(desc.flags != DescriptorFlags::NoFlags)
ret++;
// format or structure size
if(desc.type == DescriptorType::TypedBuffer ||
desc.type == DescriptorType::ReadWriteTypedBuffer)
ret++;
else if(desc.elementByteSize != 0)
ret++;
// counter buffer, offset, value
if(desc.secondary != ResourceId())
ret += 3;
break;
}
case DescriptorType::AccelerationStructure:
{
// type, resource, size
ret = 3;
break;
}
case DescriptorType::Image:
case DescriptorType::ImageSampler:
case DescriptorType::ReadWriteImage:
{
// type, texture type, resource, format, min LOD, button to view
ret = 6;
// first/num slices
ret++;
// first/num mips
ret++;
// swizzle
ret++;
if(desc.flags != DescriptorFlags::NoFlags)
ret++;
if(desc.view != ResourceId())
ret++;
if(desc.type == DescriptorType::ImageSampler && includeSampler)
ret += samplerRowCount(true);
break;
}
case DescriptorType::Sampler: ret = samplerRowCount(); break;
case DescriptorType::Unknown: break;
}
return ret;
}
QVariant data(const Descriptor &desc, int row, int col) const
{
if(row == 0)
{
if(col == 0)
return lit("Type");
if(m_API == GraphicsAPI::Vulkan)
{
switch(desc.type)
{
case DescriptorType::ConstantBuffer: return lit("Uniform Buffer");
case DescriptorType::Buffer: // no such type on vulkan
case DescriptorType::ReadWriteBuffer: return lit("Storage Buffer");
case DescriptorType::TypedBuffer: return lit("Texel Buffer");
case DescriptorType::ReadWriteTypedBuffer: return lit("Storage Texel Buffer");
case DescriptorType::AccelerationStructure: return lit("Acceleration Structure");
case DescriptorType::Image: return lit("Sampled Image");
case DescriptorType::ImageSampler: return lit("Combined Image/Sampler");
case DescriptorType::ReadWriteImage: return lit("Storage Image");
case DescriptorType::Sampler: return lit("Sampler");
case DescriptorType::Unknown: return lit("Uninitialised");
}
}
else
{
switch(desc.type)
{
case DescriptorType::ConstantBuffer: return lit("Constant Buffer");
case DescriptorType::ImageSampler: // no such type on D3D12
case DescriptorType::Buffer:
case DescriptorType::Image:
case DescriptorType::TypedBuffer:
case DescriptorType::AccelerationStructure: return lit("Shader Resource View");
case DescriptorType::ReadWriteBuffer:
case DescriptorType::ReadWriteTypedBuffer:
case DescriptorType::ReadWriteImage: return lit("Unordered Resource View");
case DescriptorType::Sampler: return lit("Sampler");
case DescriptorType::Unknown: return lit("Uninitialised");
}
}
return QVariant();
}
switch(desc.type)
{
case DescriptorType::ConstantBuffer:
case DescriptorType::Buffer:
case DescriptorType::ReadWriteBuffer:
case DescriptorType::TypedBuffer:
case DescriptorType::ReadWriteTypedBuffer:
{
if(row == 1)
return col == 0 ? lit("Buffer") : QVariant::fromValue(desc.resource);
if(row == 2)
return col == 0 ? lit("Byte Offset")
: Formatter::HumanFormat(desc.byteOffset, Formatter::OffsetSize);
if(row == 3)
return col == 0 ? lit("Byte Size")
: Formatter::HumanFormat(desc.byteSize, Formatter::OffsetSize);
row -= 4;
if(desc.flags != DescriptorFlags::NoFlags)
{
if(row == 0)
return col == 0 ? lit("Flags") : ToQStr(desc.flags);
row--;
}
// format or structure size
if(desc.type == DescriptorType::TypedBuffer ||
desc.type == DescriptorType::ReadWriteTypedBuffer)
{
if(row == 0)
return col == 0 ? lit("Format") : QString(desc.format.Name());
row--;
}
else if(desc.elementByteSize != 0)
{
if(row == 0)
return col == 0 ? lit("Element Size")
: Formatter::HumanFormat(desc.elementByteSize, Formatter::OffsetSize);
row--;
}
// counter buffer, offset, value
if(desc.secondary != ResourceId())
{
if(row == 0)
return col == 0 ? lit("Counter Buffer") : QVariant::fromValue(desc.secondary);
if(row == 1)
return col == 0 ? lit("Counter Byte Offset")
: Formatter::HumanFormat(desc.counterByteOffset, Formatter::OffsetSize);
if(row == 2)
return col == 0 ? lit("Counter Value") : Formatter::Format(desc.bufferStructCount);
row -= 3;
}
if(row == 0)
return col == 0 ? lit("Show Contents") : QVariant::fromValue(ButtonTag(true, desc));
break;
}
case DescriptorType::AccelerationStructure:
{
if(row == 1)
return col == 0 ? lit("Acceleration Structure") : QVariant::fromValue(desc.resource);
if(row == 2)
return col == 0 ? lit("Byte Size")
: Formatter::HumanFormat(desc.byteSize, Formatter::OffsetSize);
break;
}
case DescriptorType::Image:
case DescriptorType::ImageSampler:
case DescriptorType::ReadWriteImage:
{
if(row == 1)
return col == 0 ? lit("Texture Type") : ToQStr(desc.textureType);
if(row == 2)
return col == 0 ? lit("Texture") : QVariant::fromValue(desc.resource);
if(row == 3)
return col == 0 ? lit("Format") : QString(desc.format.Name());
if(row == 4)
return col == 0 ? lit("Min LOD") : Formatter::Format(desc.minLODClamp);
row -= 5;
// first/num slices
if(row == 0)
return col == 0
? lit("Slice Range")
: QFormatStr("%1 - %2")
.arg(desc.firstSlice)
.arg(desc.numSlices == UINT16_MAX ? desc.numSlices
: desc.firstSlice + desc.numSlices);
// first/num mips
if(row == 1)
return col == 0 ? lit("Mip Range")
: QFormatStr("%1 - %2")
.arg(desc.firstMip)
.arg(desc.numMips == UINT8_MAX ? desc.numMips
: desc.firstMip + desc.numMips);
// swizzle
if(row == 2)
return col == 0 ? lit("Swizzle")
: QFormatStr("%1%2%3%4")
.arg(ToQStr(desc.swizzle.red))
.arg(ToQStr(desc.swizzle.green))
.arg(ToQStr(desc.swizzle.blue))
.arg(ToQStr(desc.swizzle.alpha));
row -= 3;
if(desc.flags != DescriptorFlags::NoFlags)
{
if(row == 0)
return col == 0 ? lit("Flags") : ToQStr(desc.flags);
row--;
}
if(desc.view != ResourceId())
{
if(row == 0)
return col == 0 ? lit("View") : QVariant::fromValue(desc.view);
row--;
}
if(row == 0)
return col == 0 ? lit("Show Contents") : QVariant::fromValue(ButtonTag(false, desc));
break;
}
case DescriptorType::Sampler: break;
case DescriptorType::Unknown: break;
}
return QVariant();
}
int samplerRowCount(bool combinedSampler = false) const
{
// type, address U/V/W, filter min/mag/mip, filter function
int ret = 8;
// omit the type for combined samplers
if(combinedSampler)
ret--;
// min/max LOD
ret++;
// mip bias
ret++;
if(m_API == GraphicsAPI::Vulkan)
{
// immutable
ret++;
// object
ret++;
// seamless and unnormalized
ret += 2;
// sRGB border
ret++;
// munged ycbcr stuff
ret++;
}
return ret;
}
QString filter(FilterMode mode, float aniso) const
{
QString ret = ToQStr(mode);
if(mode == FilterMode::Anisotropic)
ret += QFormatStr(" %1x").arg(aniso);
return ret;
}
QString filter(FilterFunction func, CompareFunction compare) const
{
QString ret = ToQStr(func);
if(func == FilterFunction::Comparison)
ret += QFormatStr(" %1").arg(ToStr(compare));
return ret;
}
QVariant data(const SamplerDescriptor &desc, int row, int col, bool combinedSampler = false) const
{
if(combinedSampler)
{ // omit the type
}
else
{
if(row == 0)
return col == 0 ? lit("Type") : ToQStr(desc.type);
row--;
}
if(m_Ctx.APIProps().pipelineType == GraphicsAPI::Vulkan)
{
if(row == 0)
return col == 0 ? lit("Immutable") : Formatter::Format(desc.creationTimeConstant);
row--;
}
if(row == 0)
return col == 0 ? lit("U Addressing") : ToQStr(desc.addressU);
if(row == 1)
return col == 0 ? lit("V Addressing") : ToQStr(desc.addressV);
if(row == 2)
return col == 0 ? lit("W Addressing") : ToQStr(desc.addressW);
if(row == 3)
return col == 0 ? lit("Minify") : filter(desc.filter.minify, desc.maxAnisotropy);
if(row == 4)
return col == 0 ? lit("Magnify") : filter(desc.filter.magnify, desc.maxAnisotropy);
if(row == 5)
return col == 0 ? lit("Mip") : filter(desc.filter.mip, desc.maxAnisotropy);
if(row == 6)
return col == 0 ? lit("Filter") : filter(desc.filter.filter, desc.compareFunction);
if(row == 7)
{
QString minLOD = Formatter::Format(desc.minLOD);
QString maxLOD = Formatter::Format(desc.maxLOD);
if(desc.minLOD == -FLT_MAX)
minLOD = lit("0");
if(desc.minLOD == -1000.0)
minLOD = lit("VK_LOD_CLAMP_NONE");
if(desc.maxLOD == FLT_MAX)
maxLOD = lit("FLT_MAX");
if(desc.maxLOD == 1000.0)
maxLOD = lit("VK_LOD_CLAMP_NONE");
return col == 0 ? lit("LOD Range") : QFormatStr("%1 - %2").arg(minLOD).arg(maxLOD);
}
if(row == 8)
return col == 0 ? lit("Mip Bias") : Formatter::Format(desc.mipBias);
if(m_Ctx.APIProps().pipelineType == GraphicsAPI::Vulkan)
{
if(row == 9)
return col == 0 ? lit("Sampler") : QVariant::fromValue(desc.object);
if(row == 10)
return col == 0 ? lit("Seamless Cubemaps") : Formatter::Format(desc.seamlessCubemaps);
if(row == 11)
return col == 0 ? lit("Unnormalised") : Formatter::Format(desc.unnormalized);
if(row == 12)
return col == 0 ? lit("sRGB Border") : Formatter::Format(desc.srgbBorder);
if(row == 13)
{
if(col == 0)
return lit("yCbCr Sampling");
QString data;
if(desc.ycbcrSampler != ResourceId())
{
data = m_Ctx.GetResourceName(desc.ycbcrSampler);
data += QFormatStr(", %1 %2").arg(ToQStr(desc.ycbcrModel)).arg(ToQStr(desc.ycbcrRange));
data += tr(", Chroma %1 [%2,%3]")
.arg(ToQStr(desc.chromaFilter))
.arg(ToQStr(desc.xChromaOffset))
.arg(ToQStr(desc.yChromaOffset));
if(desc.forceExplicitReconstruction)
data += tr(" Explicit");
}
else
{
data = lit("N/A");
}
return data;
}
}
return QVariant();
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
{
if(role != Qt::DisplayRole)
return QVariant();
if(index.isValid())
{
int row = index.row();
int col = index.column();
quintptr id = index.internalId();
if(m_View.m_D3D12RootSig.resourceId != ResourceId())
{
// the fixed nodes are under the root
if(id == FixedNode)
{
if(col == 0)
{
if(row == ParametersRootNode)
return lit("Parameters");
if(row == StaticSamplersRootNode)
return lit("Static Samplers");
QVariant ret =
QFormatStr("Heap %1").arg(ToQStr(m_View.m_D3D12Heaps[row - FirstHeapRootNode]));
RichResourceTextInitialise(ret, &m_Ctx, false);
return ret;
}
if(col == 1 && row >= FirstHeapRootNode)
return QVariant::fromValue(ButtonTag(m_View.m_D3D12Heaps[row - FirstHeapRootNode]));
return QVariant();
}
if(id == ParameterFlag)
{
if(col == 0)
return QFormatStr("Parameter %1").arg(row);
return QVariant();
}
if(id == StaticSamplerData)
{
if(col == 0)
return QFormatStr("Static Sampler %1").arg(row);
return QVariant();
}
if((id & ParameterData) == 0)
{
// this is the index + 1
uint32_t sampIndex = id & ~StaticSamplerData;
if(sampIndex == 0)
return QVariant();
sampIndex--;
const D3D12Pipe::StaticSampler &samp = m_View.m_D3D12RootSig.staticSamplers[sampIndex];
if(row >= StaticSamplerFixedRowCount)
return data(samp.descriptor, row - StaticSamplerFixedRowCount, col);
if(row == 0)
return col == 0 ? lit("Visibility") : ToQStr(samp.visibility);
if(row == 1)
return col == 0 ? lit("Register Space") : Formatter::Format(samp.space);
if(row == 2)
return col == 0 ? lit("Register") : Formatter::Format(samp.reg);
return QVariant();
}
RootIdx decodedIndex = Decode(id);
const D3D12Pipe::RootParam &param =
m_View.m_D3D12RootSig.parameters[decodedIndex.parameter - 1];
if(!param.constants.empty())
{
if(row == 0)
return col == 0 ? lit("Visibility") : ToQStr(param.visibility);
rdcarray<uint32_t> words;
words.resize(param.constants.byteSize() >> 2);
memcpy(words.data(), param.constants.data(), words.byteSize());
if(col == 0)
{
if(row == 1)
return lit("Data (Decimal)");
if(row == 2)
return lit("Data (Hexadecimal)");
if(row == 3)
return lit("Data (Float)");
}
QString data;
if(row == 3)
{
float *f = (float *)words.data();
for(size_t i = 0; i < words.size(); i++)
data += Formatter::Format(f[i]) + lit(" ");
}
else
{
for(uint32_t w : words)
data += Formatter::Format(w, row == 2) + lit(" ");
}
return data;
}
if(param.descriptor.type != DescriptorType::Unknown)
{
if(row >= DescParameterFixedRowCount)
return data(param.descriptor, row - DescParameterFixedRowCount, col);
if(row == 0)
return col == 0 ? lit("Visibility") : ToQStr(param.visibility);
if(row == 1)
return col == 0 ? lit("Register Space") : Formatter::Format(param.space);
if(row == 2)
return col == 0 ? lit("Register") : Formatter::Format(param.reg);
return QVariant();
}
if(decodedIndex.range == 0)
{
if(row >= TableParameterFixedRowCount)
{
if(col == 0)
return QFormatStr("Range %1").arg(row - TableParameterFixedRowCount);
return QVariant();
}
if(row == 0)
return col == 0 ? lit("Visibility") : ToQStr(param.visibility);
if(row == 1)
return col == 0 ? lit("Heap") : QVariant::fromValue(param.heap);
if(row == 2)
return col == 0 ? lit("Table Offset") : ToQStr(param.heapByteOffset);
return QVariant();
}
const D3D12Pipe::RootTableRange &range = param.tableRanges[decodedIndex.range - 1];
if(decodedIndex.descriptor == 0)
{
if(row >= RangeFixedRowCount)
{
if(col == 0)
{
// with no descriptors, we put the space/register here
if(m_View.m_Descriptors.empty())
{
if(row == RangeFixedRowCount)
return col == 0 ? lit("Register Space") : ToQStr(range.space);
return col == 0 ? lit("Base Register") : ToQStr(range.baseRegister);
}
// otherwise we name all the descriptors
QLatin1Char regChar = QLatin1Char('?');
if(range.category == DescriptorCategory::Sampler)
regChar = QLatin1Char('s');
if(range.category == DescriptorCategory::ConstantBlock)
regChar = QLatin1Char('b');
if(range.category == DescriptorCategory::ReadOnlyResource)
regChar = QLatin1Char('t');
if(range.category == DescriptorCategory::ReadWriteResource)
regChar = QLatin1Char('u');
return QFormatStr("%1%2, space %3")
.arg(regChar)
.arg(range.baseRegister + row - RangeFixedRowCount)
.arg(range.space);
}
return QVariant();
}
if(row == 0)
{
if(col == 0)
return lit("Range Type");
if(range.category == DescriptorCategory::Sampler)
return lit("Sampler");
if(range.category == DescriptorCategory::ConstantBlock)
return lit("Constant Buffer");
if(range.category == DescriptorCategory::ReadOnlyResource)
return lit("SRV");
if(range.category == DescriptorCategory::ReadWriteResource)
return lit("UAV");
return QVariant();
}
if(row == 1)
{
if(col == 0)
return lit("Table offset");
if(range.appended)
return QFormatStr("D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND (%1)").arg(range.tableByteOffset);
return Formatter::HumanFormat(range.tableByteOffset, Formatter::NoFlags);
}
if(row == 2)
return col == 0 ? lit("Descriptor Count")
: Formatter::HumanFormat(range.count, Formatter::NoFlags);
return QVariant();
}
// descriptor node data itself
const Descriptor &descriptor =
m_View.m_Descriptors[param.heapByteOffset + range.tableByteOffset +
decodedIndex.descriptor - 1];
if(row == 0)
return col == 0 ? lit("Index in heap")
: Formatter::HumanFormat(param.heapByteOffset + range.tableByteOffset +
decodedIndex.descriptor - 1,
Formatter::NoFlags);
return data(descriptor, row - RootSigDescriptorFixedRows, col);
}
else
{
// the descriptors are parented directly under the root
if(index.internalId() & DescriptorFlag)
{
if(col != 0)
return QVariant();
if(row < m_View.m_Locations.count())
return m_View.m_Locations[row].logicalBindName;
else
return QFormatStr("Descriptor %1").arg(row);
}
if(index.internalId() & DescriptorDataFlag)
{
uint32_t descIndex = index.internalId() & ~DescriptorDataFlag;
if(descIndex < m_View.m_Locations.size())
{
if(row == 0)
return col == 0 ? lit("Shader Mask") : ToQStr(m_View.m_Locations[descIndex].stageMask);
row--;
}
const Descriptor &desc = m_View.m_Descriptors[descIndex];
uint32_t sampIndex = descIndex;
if(!m_View.m_DescriptorToSamplerLookup.empty())
sampIndex = m_View.m_DescriptorToSamplerLookup[descIndex];
SamplerDescriptor dummy;
const SamplerDescriptor &samp = sampIndex < m_View.m_SamplerDescriptors.size()
? m_View.m_SamplerDescriptors[sampIndex]
: dummy;
if(desc.type == DescriptorType::Sampler)
{
return data(samp, row, col);
}
else if(desc.type == DescriptorType::ImageSampler)
{
int pureDescriptorRowCount = rowCount(desc, false);
if(row >= pureDescriptorRowCount)
return data(samp, row - pureDescriptorRowCount, col, true);
}
return data(desc, row, col);
}
}
}
return QVariant();
}
private:
ICaptureContext &m_Ctx;
DescriptorViewer &m_View;
GraphicsAPI m_API;
#define BITS (QT_POINTER_SIZE * 8)
static const int ParametersRootNode = 0;
static const int StaticSamplersRootNode = 1;
static const int FirstHeapRootNode = 2;
// the number of rows in a table parameter before the ranges: visibility, heap, heap offset
static const int TableParameterFixedRowCount = 3;
// the number of rows in a range before the descriptors: category, table offset, count
static const int RangeFixedRowCount = 3;
// visibility only
static const int DescParameterFixedRowCount = 3;
// visibility, and 3 forms of interpretation of constants (float, decimal, hex)
static const int ConstParameterFixedRowCount = 4;
// 3 for space/reg/visibility, plus sampler properties
static const int StaticSamplerFixedRowCount = 3;
// one extra row for root-signature based descriptors to give the absolute heap offset
static const int RootSigDescriptorFixedRows = 1;
// extra row for the descripor visibility from location
static const int DescriptorLocationFixedRowCount = 1;
// top bit indicates parameter data or static sampler data
static const quintptr ParameterData = 1ULL << (BITS - 1);
static const quintptr StaticSamplerData = 0;
#if BITS == 32
// 32-bit packing:
// | 1 bit ParameterData flag |
// | 6 bits Parameter Index |
// | 5 bits Table Index |
// | 20 bits Descriptor Index |
static const quintptr ParameterFlag = ~0U;
static const quintptr FixedNode = ~0U - 1;
static const quintptr ParameterIndexMask = 0x3f;
static const quintptr ParameterIndexShift = 25;
static const quintptr TableIndexMask = 0x1f;
static const quintptr TableIndexShift = 20;
static const quintptr DescriptorIndexMask = 0xfffff;
#else
// 64-bit packing:
// | 1 bit ParameterData flag |
// | 1 bit Parameter Node flag |
// | 1 bit Fixed Node flag |
// | 5 bits padding |
// | 8-bit Parameter Index |
// | 16 bit Table Index |
// | 32 bits Descriptor Index |
static const quintptr ParameterFlag = 3ULL << (BITS - 2);
static const quintptr FixedNode = 1ULL << (BITS - 3);
static const quintptr ParameterIndexMask = 0xff;
static const quintptr ParameterIndexShift = 48;
static const quintptr TableIndexMask = 0xffff;
static const quintptr TableIndexShift = 32;
static const quintptr DescriptorIndexMask = 0xffffffff;
#endif
static_assert(ParameterFlag & ParameterData, "ParameterFlag should have ParameterData bit set");
static_assert((DescriptorIndexMask & (ParameterIndexMask << ParameterIndexShift)) == 0,
"Mask overlaps");
static_assert((DescriptorIndexMask & (TableIndexMask << TableIndexShift)) == 0, "Mask overlaps");
static_assert(((ParameterIndexMask << ParameterIndexShift) &
(TableIndexMask << TableIndexShift)) == 0,
"Mask overlaps");
static_assert(quintptr(quintptr(ParameterIndexMask << ParameterIndexShift) >>
ParameterIndexShift) == ParameterIndexMask,
"Mask is clipped");
static_assert(quintptr(quintptr(TableIndexMask << TableIndexShift) >> TableIndexShift) ==
TableIndexMask,
"Mask is clipped");
struct RootIdx
{
uint8_t parameter;
uint16_t range;
uint32_t descriptor;
};
static_assert(ParameterIndexMask <= UINT8_MAX, "Parameter mask is too large for decoded storage");
static_assert(TableIndexMask <= UINT16_MAX, "Table mask is too large for decoded storage");
static_assert(DescriptorIndexMask <= UINT32_MAX,
"Descriptor mask is too large for decoded storage");
RootIdx Decode(quintptr id) const
{
return {uint8_t((id >> ParameterIndexShift) & ParameterIndexMask),
uint16_t((id >> TableIndexShift) & TableIndexMask), uint32_t(id & DescriptorIndexMask)};
}
quintptr Encode(RootIdx idx) const
{
return ParameterData | (quintptr(idx.parameter & ParameterIndexMask) << ParameterIndexShift) |
(quintptr(idx.range & TableIndexMask) << TableIndexShift) |
quintptr(idx.descriptor & DescriptorIndexMask);
}
// simple flags for plain descriptor index
static const quintptr DescriptorDataFlag = 1ULL << (BITS - 1);
static const quintptr DescriptorFlag = 1ULL << (BITS - 2);
#undef BITS
};
DescriptorViewer::DescriptorViewer(ICaptureContext &ctx, QWidget *parent)
: QFrame(parent), ui(new Ui::DescriptorViewer), m_Ctx(ctx)
{
ui->setupUi(this);
{
static bool registered = false;
if(!registered)
{
registered = true;
QMetaType::registerComparators<ButtonTag>();
}
}
m_Model = new DescriptorItemModel(ctx, *this, this);
ui->descriptors->setModel(m_Model);
ui->descriptors->setFont(Formatter::PreferredFont());
ui->descriptors->header()->setStretchLastSection(true);
ui->descriptors->header()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
ui->descriptors->header()->setSectionResizeMode(0, QHeaderView::Interactive);
ui->descriptors->header()->setMinimumSectionSize(40);
ui->descriptors->header()->resizeSection(0, 150);
ui->descriptors->header()->setSectionsMovable(false);
ui->descriptors->header()->setCascadingSectionResizes(false);
ButtonDelegate *viewDelegate = new ButtonDelegate(Icons::action_hover(), QString(), this);
viewDelegate->setVisibleTrigger(Qt::DisplayRole,
QVariant::fromValue(ButtonTag(false, Descriptor())));
viewDelegate->setCentred(false);
ui->descriptors->setItemDelegate(viewDelegate);
QObject::connect(viewDelegate, &ButtonDelegate::messageClicked, [this](const QModelIndex &idx) {
ButtonTag tag = idx.data(Qt::DisplayRole).value<ButtonTag>();
if(tag.heap != ResourceId())
{
IDescriptorViewer *viewer = m_Ctx.ViewDescriptorStore(tag.heap);
m_Ctx.AddDockWindow(viewer->Widget(), DockReference::AddTo, this);
return;
}
if(tag.descriptor.type == DescriptorType::Unknown)
return;
if(tag.buffer && tag.descriptor.resource != ResourceId())
{
QString format;
if(tag.descriptor.type == DescriptorType::TypedBuffer ||
tag.descriptor.type == DescriptorType::ReadWriteTypedBuffer)
format = BufferFormatter::GetBufferFormatString(Packing::C, ResourceId(), ShaderResource(),
tag.descriptor.format);
IBufferViewer *viewer = m_Ctx.ViewBuffer(tag.descriptor.byteOffset, tag.descriptor.byteSize,
tag.descriptor.resource, format);
m_Ctx.AddDockWindow(viewer->Widget(), DockReference::AddTo, this);
}
else
{
TextureDescription *tex = NULL;
CompType typeCast = CompType::Typeless;
tex = m_Ctx.GetTexture(tag.descriptor.resource);
typeCast = tag.descriptor.format.compType;
if(tex)
{
if(tex->type == TextureType::Buffer)
{
IBufferViewer *viewer = m_Ctx.ViewTextureAsBuffer(
tex->resourceId, Subresource(), BufferFormatter::GetTextureFormatString(*tex));
m_Ctx.AddDockWindow(viewer->Widget(), DockReference::AddTo, this);
}
else
{
if(!m_Ctx.HasTextureViewer())
m_Ctx.ShowTextureViewer();
ITextureViewer *viewer = m_Ctx.GetTextureViewer();
viewer->ViewTexture(tex->resourceId, typeCast, true);
}
}
}
});
m_Ctx.AddCaptureViewer(this);
}
DescriptorViewer::~DescriptorViewer()
{
m_Ctx.RemoveCaptureViewer(this);
delete ui;
}
void DescriptorViewer::ViewDescriptorStore(ResourceId id)
{
DescriptorStoreDescription *desc = m_Ctx.GetDescriptorStore(id);
if(!desc)
{
qCritical() << "Invalid ID passed to ViewDescriptorStore";
return;
}
m_DescriptorStore = *desc;
setWindowTitle(tr("%1 contents").arg(m_Ctx.GetResourceName(m_DescriptorStore.resourceId)));
ui->pipeLabel->setText(
tr("The pipeline state viewer shows the current bindings in an easier format."));
// refresh contents for the descriptor store
OnEventChanged(m_Ctx.CurEvent());
}
void DescriptorViewer::ViewDescriptors(const rdcarray<Descriptor> &descriptors,
const rdcarray<SamplerDescriptor> &samplerDescriptors)
{
m_Descriptors = descriptors;
m_SamplerDescriptors = samplerDescriptors;
m_Descriptors.resize(qMin(descriptors.size(), samplerDescriptors.size()));
m_SamplerDescriptors.resize(qMin(descriptors.size(), samplerDescriptors.size()));
setWindowTitle(tr("Descriptors"));
ui->pipeLabel->setText(QString());
m_Model->refresh();
}
void DescriptorViewer::ViewD3D12State()
{
m_D3D12Heaps = m_Ctx.CurD3D12PipelineState()->descriptorHeaps;
m_D3D12RootSig = m_Ctx.CurD3D12PipelineState()->rootSignature;
setWindowTitle(
tr("%1 at EID %2").arg(m_Ctx.GetResourceName(m_D3D12RootSig.resourceId)).arg(m_Ctx.CurEvent()));
ui->pipeLabel->setText(
tr("The pipeline state viewer shows the current bindings in an easier format.\n"
"This is a snapshot of the root signature & bound parameters at EID %1.")
.arg(m_Ctx.CurEvent()));
// fetch the descriptor contents for both heaps
m_Ctx.Replay().AsyncInvoke([this](IReplayController *r) {
ResourceId resourceHeap, samplerHeap;
for(const D3D12Pipe::RootParam &param : m_D3D12RootSig.parameters)
{
for(const D3D12Pipe::RootTableRange &range : param.tableRanges)
{
if(range.category == DescriptorCategory::Sampler)
{
if(param.heap != ResourceId())
samplerHeap = param.heap;
}
else
{
if(param.heap != ResourceId())
resourceHeap = param.heap;
}
if(resourceHeap != ResourceId() && samplerHeap != ResourceId())
break;
}
if(resourceHeap != ResourceId() && samplerHeap != ResourceId())
break;
}
rdcarray<DescriptorRange> ranges;
ranges.resize(1);
rdcarray<Descriptor> descriptors;
rdcarray<SamplerDescriptor> samplerDescriptors;
if(resourceHeap != ResourceId())
{
DescriptorStoreDescription *resourceDesc = m_Ctx.GetDescriptorStore(resourceHeap);
if(resourceDesc)
{
ranges[0].count = resourceDesc->descriptorCount;
ranges[0].descriptorSize = resourceDesc->descriptorByteSize;
ranges[0].offset = resourceDesc->firstDescriptorOffset;
// we are interpreting typeless descriptors, assume D3D12 can interpret it
ranges[0].type = DescriptorType::Unknown;
descriptors = r->GetDescriptors(resourceDesc->resourceId, ranges);
}
}
else if(samplerHeap != ResourceId())
{
DescriptorStoreDescription *samplerDesc = m_Ctx.GetDescriptorStore(samplerHeap);
if(samplerDesc)
{
ranges[0].count = samplerDesc->descriptorCount;
ranges[0].descriptorSize = samplerDesc->descriptorByteSize;
ranges[0].offset = samplerDesc->firstDescriptorOffset;
// we are interpreting typeless descriptors, assume D3D12 can interpret it
ranges[0].type = DescriptorType::Unknown;
samplerDescriptors = r->GetSamplerDescriptors(samplerDesc->resourceId, ranges);
}
}
GUIInvoke::call(this, [this, descriptors = std::move(descriptors),
samplerDescriptors = std::move(samplerDescriptors)] {
m_Descriptors = std::move(descriptors);
m_SamplerDescriptors = std::move(samplerDescriptors);
m_DescriptorToSamplerLookup.clear();
m_Locations.clear();
m_Model->refresh();
});
});
}
void DescriptorViewer::OnCaptureClosed()
{
ToolWindowManager::closeToolWindow(this);
}
void DescriptorViewer::OnCaptureLoaded()
{
}
void DescriptorViewer::OnEventChanged(uint32_t eventId)
{
// each time, re-fetch the descriptors to get up to date contents
if(m_DescriptorStore.resourceId != ResourceId() && m_DescriptorStore.descriptorByteSize != 0)
{
m_Ctx.Replay().AsyncInvoke([this](IReplayController *r) {
uint32_t descSize = m_DescriptorStore.descriptorByteSize;
rdcarray<DescriptorRange> ranges;
ranges.resize(1);
ranges[0].count = m_DescriptorStore.descriptorCount;
ranges[0].descriptorSize = descSize;
ranges[0].offset = m_DescriptorStore.firstDescriptorOffset;
// assume this descriptor store knows its type information and can interpret typeless descriptors
ranges[0].type = DescriptorType::Unknown;
rdcarray<Descriptor> descriptors = r->GetDescriptors(m_DescriptorStore.resourceId, ranges);
rdcarray<DescriptorLogicalLocation> locations =
r->GetDescriptorLocations(m_DescriptorStore.resourceId, ranges);
// fetch only sampler descriptors that we need
rdcarray<uint32_t> descriptorToSamplerLookup;
descriptorToSamplerLookup.fill(descriptors.size(), ~0u);
ranges.clear();
uint32_t idx = 0;
for(size_t i = 0; i < descriptors.size(); i++)
{
if(descriptors[i].type == DescriptorType::Sampler ||
descriptors[i].type == DescriptorType::ImageSampler)
{
descriptorToSamplerLookup[i] = idx;
idx++;
// combine contiguous ranges
if(!ranges.empty() &&
ranges.back().offset + ranges.back().count * descSize == i * descSize &&
ranges.back().type == descriptors[i].type)
{
ranges.back().count++;
}
else
{
DescriptorRange range;
range.offset = m_DescriptorStore.firstDescriptorOffset + uint32_t(i * descSize);
range.descriptorSize = descSize;
range.count = 1;
range.type = descriptors[i].type;
ranges.push_back(range);
}
}
}
rdcarray<SamplerDescriptor> samplerDescriptors =
r->GetSamplerDescriptors(m_DescriptorStore.resourceId, ranges);
GUIInvoke::call(this,
[this, descriptors = std::move(descriptors), locations = std::move(locations),
descriptorToSamplerLookup = std::move(descriptorToSamplerLookup),
samplerDescriptors = std::move(samplerDescriptors)] {
m_Descriptors = std::move(descriptors);
m_Locations = std::move(locations);
m_SamplerDescriptors = std::move(samplerDescriptors);
m_DescriptorToSamplerLookup = std::move(descriptorToSamplerLookup);
RDTreeViewExpansionState state;
ui->descriptors->saveExpansion(state, 0);
m_Model->refresh();
ui->descriptors->applyExpansion(state, 0);
});
});
}
}
void DescriptorViewer::on_pipeButton_clicked()
{
m_Ctx.ShowPipelineViewer();
}