Add lazy-populating item model for SDObjects

* We also add intermediate paging nodes for large arrays to ease expansions
This commit is contained in:
baldurk
2020-10-27 15:15:20 +00:00
parent 6fbd8511bb
commit 1ceeb159b7
5 changed files with 437 additions and 79 deletions
+356 -61
View File
@@ -1332,6 +1332,60 @@ void CombineUsageEvents(ICaptureContext &ctx, const rdcarray<EventUsage> &usage,
callback(start, end, us);
}
QVariant SDObject2Variant(const SDObject *obj)
{
QVariant param;
// we don't identify via the type name as many types could be serialised as a ResourceId -
// e.g. ID3D11Resource* or ID3D11Buffer* which would be the actual typename. We want to preserve
// that for the best raw structured data representation instead of flattening those out to just
// "ResourceId", and we also don't want to store two types ('fake' and 'real'), so instead we
// check the custom string.
if(obj->type.basetype == SDBasic::Resource)
{
param = QVariant::fromValue(obj->data.basic.id);
}
else if(obj->type.flags & SDTypeFlags::NullString)
{
param = lit("NULL");
}
else if(obj->type.flags & SDTypeFlags::HasCustomString)
{
param = obj->data.str;
}
else
{
switch(obj->type.basetype)
{
case SDBasic::Chunk: param = QVariant(); break;
case SDBasic::Struct: param = QFormatStr("%1()").arg(obj->type.name); break;
case SDBasic::Array: param = QFormatStr("%1[]").arg(obj->type.name); break;
case SDBasic::Null: param = lit("NULL"); break;
case SDBasic::Buffer: param = lit("(%1 bytes)").arg(obj->type.byteSize); break;
case SDBasic::String:
{
QStringList lines = QString(obj->data.str).split(QLatin1Char('\n'));
QString trimmedStr;
for(int i = 0; i < 3 && i < lines.count(); i++)
trimmedStr += lines[i] + QLatin1Char('\n');
if(lines.count() > 3)
trimmedStr += lit("...");
param = trimmedStr.trimmed();
break;
}
case SDBasic::Resource:
case SDBasic::Enum:
case SDBasic::UnsignedInteger: param = Formatter::HumanFormat(obj->data.basic.u); break;
case SDBasic::SignedInteger: param = Formatter::Format(obj->data.basic.i); break;
case SDBasic::Float: param = Formatter::Format(obj->data.basic.d); break;
case SDBasic::Boolean: param = (obj->data.basic.b ? lit("True") : lit("False")); break;
case SDBasic::Character: param = QString(QLatin1Char(obj->data.basic.c)); break;
}
}
return param;
}
void addStructuredChildren(RDTreeWidgetItem *parent, const SDObject &parentObj)
{
for(const SDObject *obj : parentObj)
@@ -1339,73 +1393,20 @@ void addStructuredChildren(RDTreeWidgetItem *parent, const SDObject &parentObj)
if(obj->type.flags & SDTypeFlags::Hidden)
continue;
QVariant param;
QVariant name;
if(parentObj.type.basetype == SDBasic::Array)
param = QFormatStr("[%1]").arg(parent->childCount());
name = QFormatStr("[%1]").arg(parent->childCount());
else
param = obj->name;
name = obj->name;
RDTreeWidgetItem *item = new RDTreeWidgetItem({param, QString()});
RDTreeWidgetItem *item = new RDTreeWidgetItem({name, QString()});
// we don't identify via the type name as many types could be serialised as a ResourceId -
// e.g. ID3D11Resource* or ID3D11Buffer* which would be the actual typename. We want to preserve
// that for the best raw structured data representation instead of flattening those out to just
// "ResourceId", and we also don't want to store two types ('fake' and 'real'), so instead we
// check the custom string.
if(obj->type.basetype == SDBasic::Resource)
{
ResourceId id;
static_assert(sizeof(id) == sizeof(obj->data.basic.u), "ResourceId is no longer uint64_t!");
memcpy(&id, &obj->data.basic.u, sizeof(id));
item->setText(1, SDObject2Variant(obj));
param = id;
}
else if(obj->type.flags & SDTypeFlags::NullString)
{
param = lit("NULL");
}
else if(obj->type.flags & SDTypeFlags::HasCustomString)
{
param = obj->data.str;
}
else
{
switch(obj->type.basetype)
{
case SDBasic::Chunk:
case SDBasic::Struct:
param = QFormatStr("%1()").arg(obj->type.name);
addStructuredChildren(item, *obj);
break;
case SDBasic::Array:
param = QFormatStr("%1[]").arg(obj->type.name);
addStructuredChildren(item, *obj);
break;
case SDBasic::Null: param = lit("NULL"); break;
case SDBasic::Buffer: param = lit("(%1 bytes)").arg(obj->type.byteSize); break;
case SDBasic::String:
{
QStringList lines = QString(obj->data.str).split(QLatin1Char('\n'));
QString trimmedStr;
for(int i = 0; i < 3 && i < lines.count(); i++)
trimmedStr += lines[i] + QLatin1Char('\n');
if(lines.count() > 3)
trimmedStr += lit("...");
param = trimmedStr.trimmed();
break;
}
case SDBasic::Resource:
case SDBasic::Enum:
case SDBasic::UnsignedInteger: param = Formatter::HumanFormat(obj->data.basic.u); break;
case SDBasic::SignedInteger: param = Formatter::Format(obj->data.basic.i); break;
case SDBasic::Float: param = Formatter::Format(obj->data.basic.d); break;
case SDBasic::Boolean: param = (obj->data.basic.b ? lit("True") : lit("False")); break;
case SDBasic::Character: param = QString(QLatin1Char(obj->data.basic.c)); break;
}
}
item->setText(1, param);
if(obj->type.basetype == SDBasic::Chunk || obj->type.basetype == SDBasic::Struct ||
obj->type.basetype == SDBasic::Array)
addStructuredChildren(item, *obj);
parent->addChild(item);
}
@@ -2742,3 +2743,297 @@ void UpdateVisibleColumns(rdcstr windowTitle, int columnCount, QHeaderView *head
header->moveSection(header->visualIndex(logicalIdx), i);
}
}
void StructuredDataItemModel::setObjects(const rdcarray<SDObject *> &objs)
{
emit beginResetModel();
m_Objects = objs;
emit endResetModel();
}
// on 64-bit we've got plenty of bits so we can pack the indices into top/bottom 32-bits.
// on 32-bit we assume there won't be any huge arrays or we'll run out of memory elsewhere probably,
// so we pack 9/21 bits allowing for up to 512 arrays of ~2 million entries each.
// bear in mind 2 bits are reserved for the index tag itself
struct IndexMasks
{
static constexpr quintptr IndexBits() { return sizeof(void *) == 8 ? 32U : 21U; }
static quintptr GetArrayID(quintptr packed) { return packed >> IndexBits(); }
static quintptr GetIndexInArray(quintptr packed)
{
return packed & ((quintptr(1U) << IndexBits()) - 1);
}
static quintptr Pack(quintptr arrayId, quintptr idxInArray)
{
return (arrayId << IndexBits()) | idxInArray;
}
};
StructuredDataItemModel::Index StructuredDataItemModel::decodeIndex(QModelIndex idx) const
{
// if it's a direct pointer, the low bits will be 0 for alignment.
Index ret;
ret.tag = IndexTag(idx.internalId() & 0x3);
if(ret.tag == Direct)
{
ret.obj = (SDObject *)idx.internalPointer();
}
else
{
quintptr packed = idx.internalId() >> 2;
ret.obj = (SDObject *)m_Arrays[IndexMasks::GetArrayID(packed)];
ret.indexInArray = IndexMasks::GetIndexInArray(packed);
}
return ret;
}
quintptr StructuredDataItemModel::encodeIndex(Index idx) const
{
if(idx.tag == Direct)
return (quintptr)idx.obj;
int arrayId = m_Arrays.indexOf(idx.obj);
if(arrayId == -1)
{
m_Arrays.push_back(idx.obj);
arrayId = m_Arrays.count() - 1;
}
quintptr packed = IndexMasks::Pack(arrayId, idx.indexInArray);
return (packed << 2U) | quintptr(idx.tag);
}
// for large arrays (more than this size) paginate it with pages of this size.
// This is primarily beneficial for lazy arrays to avoid needing to lazily evaluate a huge array.
const int ArrayPageSize = 250;
bool StructuredDataItemModel::isLargeArray(SDObject *obj) const
{
return (int)obj->NumChildren() > ArrayPageSize;
}
QModelIndex StructuredDataItemModel::index(int row, int column, const QModelIndex &parent) const
{
if(row < 0 || column < 0 || row >= rowCount(parent) || column >= columnCount(parent))
return QModelIndex();
SDObject *par = NULL;
if(parent.isValid())
{
Index decodedParent = decodeIndex(parent);
// the children of page nodes are real nodes. We cache the array member's index here too for
// parent() lookups
if(decodedParent.tag == PageNode)
{
int idx = decodedParent.indexInArray + row;
m_ArrayMembers[decodedParent.obj->GetChild(idx)] = idx;
return createIndex(row, column, encodeIndex({ArrayMember, decodedParent.obj, idx}));
}
else if(decodedParent.tag == ArrayMember)
{
par = decodedParent.obj->GetChild(decodedParent.indexInArray);
}
else
{
par = decodedParent.obj;
}
// if this parent node is a large array, the children are page nodes, otherwise the child is a
// direct node
if(isLargeArray(par))
{
return createIndex(row, column, encodeIndex({PageNode, par, row * ArrayPageSize}));
}
else
{
return createIndex(row, column, encodeIndex({Direct, par->GetChild(row), 0}));
}
}
else
{
return createIndex(row, column, encodeIndex({Direct, m_Objects[row], 0}));
}
}
QModelIndex StructuredDataItemModel::parent(const QModelIndex &index) const
{
if(index.internalPointer() == NULL)
return QModelIndex();
Index decodedIndex = decodeIndex(index);
SDObject *obj = NULL;
// array members have parents that are page nodes
if(decodedIndex.tag == ArrayMember)
{
int pageRow = decodedIndex.indexInArray / ArrayPageSize;
return createIndex(decodedIndex.indexInArray / ArrayPageSize, 0,
encodeIndex({PageNode, decodedIndex.obj, pageRow * ArrayPageSize}));
}
else if(decodedIndex.tag == PageNode)
{
obj = decodedIndex.obj;
}
else
{
obj = decodedIndex.obj->GetParent();
}
// need to figure out the index for obj, it could be an array member itself in theory, or it might
// be direct, or it could be a root object
if(obj)
{
SDObject *parent = obj->GetParent();
if(parent == NULL)
{
int row = m_Objects.indexOf(obj);
if(row >= 0)
return createIndex(row, 0, obj);
qCritical() << "Encountered object with no parent that is not a root";
return QModelIndex();
}
// if the parent is a large array
if(isLargeArray(parent))
{
// we expect to have set up our member index before this lookup was ever needed
auto it = m_ArrayMembers.find(obj);
if(it == m_ArrayMembers.end())
{
qCritical() << "Expected member index to be set up, but it is not";
return QModelIndex();
}
// return the index for this item, with the child index we looked up
return createIndex(it.value(), 0, encodeIndex({ArrayMember, parent, it.value()}));
}
// search our parent to find out our child index
for(size_t i = 0; i < parent->NumChildren(); i++)
if(parent->GetChild(i) == obj)
return createIndex((int)i, 0, obj);
return QModelIndex();
}
return QModelIndex();
}
int StructuredDataItemModel::rowCount(const QModelIndex &parent) const
{
if(!parent.isValid())
return m_Objects.count();
Index decodedIdx = decodeIndex(parent);
SDObject *obj = NULL;
// if this is a page node, it either has PageSize children if it's not the last one, or the
// remainder
if(decodedIdx.tag == PageNode)
{
size_t pageBase = decodedIdx.indexInArray;
return qMin(ArrayPageSize, int(decodedIdx.obj->NumChildren() - pageBase));
}
else if(decodedIdx.tag == ArrayMember)
{
obj = decodedIdx.obj->GetChild(decodedIdx.indexInArray);
}
else
{
obj = decodedIdx.obj;
}
if(obj)
{
if(isLargeArray(obj))
return (int(obj->NumChildren()) + ArrayPageSize - 1) / ArrayPageSize;
else
return (int)obj->NumChildren();
}
return 0;
}
int StructuredDataItemModel::columnCount(const QModelIndex &parent) const
{
return m_ColumnNames.count();
}
QVariant StructuredDataItemModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if(orientation == Qt::Horizontal && role == Qt::DisplayRole)
{
if(section < m_ColumnNames.count())
return m_ColumnNames[section];
}
return QVariant();
}
Qt::ItemFlags StructuredDataItemModel::flags(const QModelIndex &index) const
{
if(!index.isValid())
return 0;
return QAbstractItemModel::flags(index);
}
QVariant StructuredDataItemModel::data(const QModelIndex &index, int role) const
{
if(role != Qt::DisplayRole || index.column() >= columnCount())
return QVariant();
Index decodedIdx = decodeIndex(index);
SDObject *obj = NULL;
if(decodedIdx.tag == PageNode)
{
if(m_ColumnValues[index.column()] == Name)
{
size_t pageBase = decodedIdx.indexInArray;
size_t pageCount = qMin(ArrayPageSize, int(decodedIdx.obj->NumChildren() - pageBase));
return QFormatStr("[%1..%2]").arg(pageBase).arg(pageBase + pageCount - 1);
}
return QVariant();
}
else if(decodedIdx.tag == ArrayMember)
{
obj = decodedIdx.obj->GetChild(decodedIdx.indexInArray);
}
else
{
obj = decodedIdx.obj;
}
if(obj)
{
switch(m_ColumnValues[index.column()])
{
case Name:
if(decodedIdx.tag == ArrayMember)
return QFormatStr("[%1]").arg(decodedIdx.indexInArray);
else if(obj->GetParent() && obj->GetParent()->type.basetype == SDBasic::Array)
return QFormatStr("[%1]").arg(index.row());
else
return obj->name;
case Value: return SDObject2Variant(obj);
case Type: return obj->type.name;
}
}
return QVariant();
}
+61
View File
@@ -577,6 +577,67 @@ private:
QAbstractItemView *m_widget;
};
class StructuredDataItemModel : public QAbstractItemModel
{
public:
enum SDColumn
{
Name,
Value,
Type,
};
void setColumns(QStringList names, rdcarray<SDColumn> values)
{
m_ColumnNames = names;
m_ColumnValues = values;
}
void setObjects(const rdcarray<SDObject *> &objs);
StructuredDataItemModel(QWidget *parent) : QAbstractItemModel(parent) {}
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &index) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
private:
enum IndexTag
{
Direct,
PageNode,
ArrayMember,
};
struct Index
{
IndexTag tag;
SDObject *obj;
int indexInArray;
};
Index decodeIndex(QModelIndex idx) const;
quintptr encodeIndex(Index idx) const;
bool isLargeArray(SDObject *obj) const;
rdcarray<SDObject *> m_Objects;
QStringList m_ColumnNames;
rdcarray<SDColumn> m_ColumnValues;
// these below members have to be mutable since we update these caches in const functions
// set of large arrays, so we can refer to them by compressed index instead of needing a full
// pointer.
mutable rdcarray<SDObject *> m_Arrays;
// objects that are in large arrays. This map gives us the index they are in their parent.
// This is only used for parent() when the parent is an array member, so we only add into this
// list when creating a child index of such an array member - which only happens when the array
// member is expanded. That keeps to a minimum the number of entries.
mutable QMap<SDObject *, int> m_ArrayMembers;
};
// helper functions for using a double spinbox for 64-bit integers. We do this because it's
// infeasible in Qt to actually derive and create a real 64-bit integer spinbox because critical
// functionality depends on deriving QAbstractSpinBoxPrivate which is unavailable. So instead we use
+10 -17
View File
@@ -128,6 +128,9 @@ ResourceInspector::ResourceInspector(ICaptureContext &ctx, QWidget *parent)
m_ChunksModel->setColumns({tr("Parameter"), tr("Value")},
{StructuredDataItemModel::Name, StructuredDataItemModel::Value});
m_delegate = new RichTextViewDelegate(ui->initChunks);
ui->initChunks->setItemDelegate(m_delegate);
ui->initChunks->header()->resizeSection(0, 200);
ui->initChunks->setFont(Formatter::PreferredFont());
@@ -308,27 +311,17 @@ void ResourceInspector::Inspect(ResourceId id)
}
ui->relatedResources->endUpdate();
rdcarray<SDObject *> objs;
for(uint32_t chunk : desc->initialisationChunks)
{
RDTreeWidgetItem *root = new RDTreeWidgetItem({QString(), QString()});
if(chunk < file.chunks.size())
{
SDChunk *chunkObj = file.chunks[chunk];
root->setText(0, chunkObj->name);
addStructuredChildren(root, *chunkObj);
}
objs.push_back(file.chunks[chunk]);
else
{
root->setText(1, tr("Invalid chunk index %1").arg(chunk));
}
ui->initChunks->addTopLevelItem(root);
ui->initChunks->setSelectedItem(root);
qCritical() << "Invalid chunk index" << chunk;
}
m_ChunksModel->setObjects(objs);
}
else
{
@@ -376,7 +369,7 @@ void ResourceInspector::OnCaptureClosed()
m_ResourceModel->reset();
ui->initChunks->clear();
m_ChunksModel->setObjects({});
ui->initChunks->clearInternalExpansions();
ui->relatedResources->clear();
ui->resourceUsage->clear();
+4
View File
@@ -36,6 +36,8 @@ class QCollatorSortFilterProxyModel;
class RDTreeWidgetItem;
class ResourceListItemModel;
class StructuredDataItemModel;
class RichTextViewDelegate;
class ResourceInspector : public QFrame, public IResourceInspector, public ICaptureViewer
{
@@ -87,4 +89,6 @@ private:
ResourceListItemModel *m_ResourceModel;
int m_ResourceCacheID = -1;
QCollatorSortFilterProxyModel *m_FilterModel;
StructuredDataItemModel *m_ChunksModel;
RichTextViewDelegate *m_delegate;
};
+6 -1
View File
@@ -232,7 +232,7 @@
</item>
</layout>
</widget>
<widget class="RDTreeWidget" name="initChunks">
<widget class="RDTreeView" name="initChunks">
<property name="geometry">
<rect>
<x>60</x>
@@ -320,6 +320,11 @@
<extends>QLineEdit</extends>
<header>Widgets/Extended/RDLineEdit.h</header>
</customwidget>
<customwidget>
<class>RDTreeView</class>
<extends>QTreeView</extends>
<header>Widgets/Extended/RDTreeView.h</header>
</customwidget>
<customwidget>
<class>RDTreeWidget</class>
<extends>QTreeView</extends>