diff --git a/docs/imgs/icons/filter.png b/docs/imgs/icons/filter.png new file mode 100644 index 000000000..227e8436c Binary files /dev/null and b/docs/imgs/icons/filter.png differ diff --git a/docs/window/buffer_viewer.rst b/docs/window/buffer_viewer.rst index 259e9e5db..1fc0a32ab 100644 --- a/docs/window/buffer_viewer.rst +++ b/docs/window/buffer_viewer.rst @@ -9,7 +9,7 @@ When opening a buffer for view, it can display both fixed non-repeating "struct- The format is defined initially by whatever reflection data is available. The format can be customised, as detailed in :doc:`../how/how_buffer_format`. -Constant buffers are assumed to be AoS data in all cases. Other views of buffers via non-constant bindings such as UAVs and storage buffers, or views opened directly from a buffer from the resource inspector, will default towards SoA data unless the layout explicitly declares a fixed set of SoA data. +Constant buffers are assumed to be SoA data in all cases. Other views of buffers via non-constant bindings such as UAVs and storage buffers, or views opened directly from a buffer from the resource inspector, will default towards AoS data unless the layout explicitly declares a fixed set of SoA data. .. figure:: ../imgs/Screenshots/AOSBuffer.png @@ -28,6 +28,15 @@ The initial values of the offset and length will be taken from the relevant bind In addition to this, you can specify a row offset which is useful in remaining at the same row while watching the change in a buffer between different events. +Filtering in constant buffer view +--------------------------------- + +.. |filter| image:: ../imgs/icons/filter.png + +When viewing a constant buffer and it is known that the data is only a small set of fixed data, an additional |filter| option is available. + +This will add a filtering toolbar that allows a subset of the variables to be shown. The filter is a set of words, space separated, that by default match case insensitively against variables in the declared structure. Any variable that matches any of the words will be displayed. If the name of a structure is matched, all children of that structure will be displayed. + Exporting and display --------------------- diff --git a/qrenderdoc/Widgets/Extended/RDTreeView.cpp b/qrenderdoc/Widgets/Extended/RDTreeView.cpp index 34dea8def..528dabe76 100644 --- a/qrenderdoc/Widgets/Extended/RDTreeView.cpp +++ b/qrenderdoc/Widgets/Extended/RDTreeView.cpp @@ -454,6 +454,52 @@ void RDTreeView::applyExpansion(const RDTreeViewExpansionState &state, const Exp applyExpansionToRow(state, model()->index(i, 0), 0, keygen); } +RDTreeViewSelectedItemState RDTreeView::saveSelectedItemState() +{ + RDTreeViewSelectedItemState ret; + QModelIndexList sel = selectionModel()->selectedIndexes(); + if(!sel.empty()) + { + QModelIndex i = sel[0].sibling(sel[0].row(), 0); + while(i.isValid()) + { + ret.insert(0, qHash(i.data(Qt::DisplayRole).toString())); + i = i.parent(); + } + } + + return ret; +} + +void RDTreeView::applySelectedItemState(RDTreeViewSelectedItemState state) +{ + QModelIndex cur; + + for(int i = 0; i < state.count(); i++) + { + bool found = false; + + for(int row = 0, count = model()->rowCount(cur); row < count; row++) + { + QModelIndex check = model()->index(row, 0, cur); + if(qHash(model()->data(check).toString()) == state[i]) + { + found = true; + cur = check; + break; + } + } + + if(!found) + return; + } + + QItemSelection sel(cur, cur.sibling(cur.row(), model()->columnCount() - 1)); + + scrollTo(cur); + selectionModel()->select(sel, QItemSelectionModel::ClearAndSelect); +} + void RDTreeView::copySelection() { QModelIndexList sel = selectionModel()->selectedRows(); diff --git a/qrenderdoc/Widgets/Extended/RDTreeView.h b/qrenderdoc/Widgets/Extended/RDTreeView.h index b8cadc8c8..0fa3dac4a 100644 --- a/qrenderdoc/Widgets/Extended/RDTreeView.h +++ b/qrenderdoc/Widgets/Extended/RDTreeView.h @@ -33,6 +33,7 @@ class RDTreeView; typedef QSet RDTreeViewExpansionState; +typedef QList RDTreeViewSelectedItemState; class RDTreeViewDelegate : public RichTextViewDelegate { @@ -129,6 +130,9 @@ public: }); } + RDTreeViewSelectedItemState saveSelectedItemState(); + void applySelectedItemState(RDTreeViewSelectedItemState state); + // Internally tracked expansions RDTreeViewExpansionState &getInternalExpansion(uint expansionID) { diff --git a/qrenderdoc/Windows/BufferViewer.cpp b/qrenderdoc/Windows/BufferViewer.cpp index 86d49c953..bde7a110d 100644 --- a/qrenderdoc/Windows/BufferViewer.cpp +++ b/qrenderdoc/Windows/BufferViewer.cpp @@ -41,6 +41,7 @@ #include "Widgets/CollapseGroupBox.h" #include "Widgets/ComputeDebugSelector.h" #include "Widgets/Extended/RDLabel.h" +#include "Widgets/Extended/RDLineEdit.h" #include "Widgets/Extended/RDSplitter.h" #include "Windows/Dialogs/AxisMappingDialog.h" #include "Windows/Dialogs/CameraControlsDialog.h" @@ -2629,6 +2630,33 @@ BufferViewer::BufferViewer(ICaptureContext &ctx, bool meshview, QWidget *parent) ui->setFormat->setVisible(false); + QObject::connect(ui->fixedVars, &RDTreeWidget::keyPress, [this](QKeyEvent *ev) { + if(!IsCBufferView()) + return; + + if((ev->modifiers() & Qt::ControlModifier) && ev->key() == Qt::Key_F) + { + ui->filterCBuffer->setChecked(true); + } + }); + + ui->filterStrip->setVisible(false); + + QObject::connect(ui->filterExpression, &RDLineEdit::keyPress, [this](QKeyEvent *event) { + if(event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) + OnEventChanged(m_Ctx.CurEvent()); + if(event->key() == Qt::Key_Escape) + ui->filterCBuffer->setChecked(false); + }); + + m_FilterTimeout = new QTimer(this); + m_FilterTimeout->setInterval(600); + m_FilterTimeout->setSingleShot(true); + connect(m_FilterTimeout, &QTimer::timeout, [this]() { OnEventChanged(m_Ctx.CurEvent()); }); + + QObject::connect(ui->filterExpression, &RDLineEdit::textChanged, + [this](const QString &) { m_FilterTimeout->start(); }); + ui->controlType->setCurrentIndex(0); on_controlType_currentIndexChanged(0); @@ -2831,7 +2859,17 @@ void BufferViewer::SetupRawView() m_InnerSplitter->addWidget(m_FixedGroup); m_InnerSplitter->addWidget(m_RepeatedGroup); - m_VLayout->addWidget(ui->meshToolbar); + QFrame *meshToolbar = new QFrame(this); + meshToolbar->setFrameShape(QFrame::Panel); + meshToolbar->setFrameShadow(QFrame::Plain); + + QVBoxLayout *meshToolbarLayout = new QVBoxLayout(meshToolbar); + meshToolbarLayout->setSpacing(2); + meshToolbarLayout->setContentsMargins(0, 0, 0, 0); + meshToolbarLayout->addWidget(ui->mainMeshToolbar); + meshToolbarLayout->addWidget(ui->filterStrip); + + m_VLayout->addWidget(meshToolbar); // 0 will be variable, but set it to something here so QSplitter doesn't barf m_OuterSplitter->insertWidget(0, ui->inTable); m_OuterSplitter->insertWidget(1, ui->formatSpecifier); @@ -2971,12 +3009,22 @@ void BufferViewer::SetupMeshView() QObject::connect(ui->out2Table->horizontalHeader(), &QHeaderView::customContextMenuRequested, [this](const QPoint &pos) { meshHeaderMenu(MeshDataStage::GSOut, pos); }); + QFrame *meshToolbar = new QFrame(this); + meshToolbar->setFrameShape(QFrame::Panel); + meshToolbar->setFrameShadow(QFrame::Plain); + + QVBoxLayout *meshToolbarLayout = new QVBoxLayout(meshToolbar); + meshToolbarLayout->setSpacing(2); + meshToolbarLayout->setContentsMargins(0, 0, 0, 0); + meshToolbarLayout->addWidget(ui->mainMeshToolbar); + meshToolbarLayout->addWidget(ui->filterStrip); + QVBoxLayout *vertical = new QVBoxLayout(this); vertical->setSpacing(3); vertical->setContentsMargins(3, 3, 3, 3); - vertical->addWidget(ui->meshToolbar); + vertical->addWidget(meshToolbar); vertical->addWidget(ui->dockarea); QTimer *renderTimer = new QTimer(this); @@ -3977,6 +4025,8 @@ void BufferViewer::OnEventChanged(uint32_t eventId) RDTreeViewExpansionState state; ui->fixedVars->saveExpansion(state, 0); + RDTreeViewSelectedItemState selItemState = ui->fixedVars->saveSelectedItemState(); + ui->fixedVars->beginUpdate(); ui->fixedVars->clear(); @@ -3984,7 +4034,7 @@ void BufferViewer::OnEventChanged(uint32_t eventId) if(!vars.isEmpty()) { UI_AddFixedVariables(ui->fixedVars->invisibleRootItem(), 0, - bufdata->inConfig.fixedVars.type.members, vars); + bufdata->inConfig.fixedVars.type.members, vars, false); if(IsCBufferView() && !bufdata->cb.bytesBacked) UI_RemoveOffsets(ui->fixedVars->invisibleRootItem()); @@ -4009,6 +4059,8 @@ void BufferViewer::OnEventChanged(uint32_t eventId) ui->fixedVars->getInternalExpansion(qHash(ToQStr(m_CurCBuffer.shader))), 0); else ui->fixedVars->applyExpansion(state, 0); + + ui->fixedVars->applySelectedItemState(selItemState); } on_rowOffset_valueChanged(ui->rowOffset->value()); @@ -4288,11 +4340,19 @@ void BufferViewer::UI_AddTaskPayloads(RDTreeWidgetItem *root, size_t baseOffset, } } -void BufferViewer::UI_AddFixedVariables(RDTreeWidgetItem *root, uint32_t baseOffset, +bool BufferViewer::UI_AddFixedVariables(RDTreeWidgetItem *root, uint32_t baseOffset, const rdcarray &consts, - const rdcarray &vars) + const rdcarray &vars, bool ignoreFilter) { + bool ret = false; + + Qt::CaseSensitivity filterCase = + ui->filterCaseSensitive->isChecked() ? Qt::CaseSensitive : Qt::CaseInsensitive; + const bool showPadding = ui->showPadding->isChecked() && m_CurCBuffer.bytesBacked; + const QString filterExpression = ignoreFilter ? QString() : ui->filterExpression->text(); + const QStringList filterWords = + filterExpression.split(QRegularExpression(lit("\\s+")), Qt::SkipEmptyParts); if(consts.size() != vars.size()) qCritical() << "Shader variable mismatch"; @@ -4304,7 +4364,7 @@ void BufferViewer::UI_AddFixedVariables(RDTreeWidgetItem *root, uint32_t baseOff const ShaderConstant &c = consts[idx]; const ShaderVariable &v = vars[idx]; - if(showPadding && c.byteOffset > offset) + if(showPadding && c.byteOffset > offset && filterWords.isEmpty()) { uint32_t size = c.byteOffset - offset; @@ -4319,6 +4379,7 @@ void BufferViewer::UI_AddFixedVariables(RDTreeWidgetItem *root, uint32_t baseOff pad->setTag(QVariant::fromValue(FixedVarTag(size))); root->addChild(pad); + ret = true; offset = c.byteOffset; } @@ -4361,10 +4422,18 @@ void BufferViewer::UI_AddFixedVariables(RDTreeWidgetItem *root, uint32_t baseOff n->setTag(QVariant::fromValue(FixedVarTag(v.name, baseOffset + c.byteOffset))); - root->addChild(n); - UI_FixedAddMatrixRows(n, c, v); + bool anyPassFilter = filterWords.empty(); + QString filterName = v.name; + for(QString filter : filterWords) + { + if(filterName.contains(filter, filterCase)) + anyPassFilter = true; + } + + const bool rootPassFilter = anyPassFilter; + // if it's an array the value (v) will be expanded with one element in each of v.members, but // the constant (c) will just have the type with a number of elements if(c.type.elements > 1) @@ -4393,7 +4462,9 @@ void BufferViewer::UI_AddFixedVariables(RDTreeWidgetItem *root, uint32_t baseOff // because v.members[...].members will be the actual struct members because of the expansion if(c.type.baseType == VarType::Struct) { - UI_AddFixedVariables(el, elOffset, c.type.members, v.members[e].members); + if(UI_AddFixedVariables(el, elOffset, c.type.members, v.members[e].members, + rootPassFilter || ignoreFilter)) + anyPassFilter = true; } else { @@ -4409,7 +4480,7 @@ void BufferViewer::UI_AddFixedVariables(RDTreeWidgetItem *root, uint32_t baseOff if(c.type.baseType == VarType::Struct && e + 1 == v.members.size()) break; - if(showPadding && c.type.arrayByteStride > elSize) + if(showPadding && c.type.arrayByteStride > elSize && filterWords.isEmpty()) { uint32_t size = c.type.arrayByteStride - elSize; @@ -4430,12 +4501,23 @@ void BufferViewer::UI_AddFixedVariables(RDTreeWidgetItem *root, uint32_t baseOff // for single structs, recurse else if(v.type == VarType::Struct) { - UI_AddFixedVariables(n, c.byteOffset, c.type.members, v.members); + if(UI_AddFixedVariables(n, c.byteOffset, c.type.members, v.members, + rootPassFilter || ignoreFilter)) + anyPassFilter = true; } + ret = ret || anyPassFilter; + + if(anyPassFilter) + root->addChild(n); + else + delete n; + // advance by the tight scalar-packed advance, so we can detect padding offset += BufferFormatter::GetVarAdvance(PackingRules::Scalar(), c); } + + return ret; } void BufferViewer::UI_RemoveOffsets(RDTreeWidgetItem *root) @@ -5467,6 +5549,8 @@ void BufferViewer::updateLabelsAndLayout() { setWindowTitle(tr("Mesh Viewer")); + ui->filterCBuffer->setVisible(false); + if(m_Ctx.IsCaptureLoaded()) { GraphicsAPI pipeType = m_Ctx.APIProps().pipelineType; @@ -5596,10 +5680,14 @@ void BufferViewer::updateLabelsAndLayout() title += QFormatStr(" - %1").arg(bufName); + ui->filterCBuffer->setVisible(true); + setWindowTitle(title); } else { + ui->filterCBuffer->setVisible(false); + setWindowTitle(m_Ctx.GetResourceName(m_BufferID) + lit(" - Contents")); } } @@ -6165,6 +6253,30 @@ void BufferViewer::on_setFormat_toggled(bool checked) reflection->constantBlocks[m_CBufferSlot.slot].variables, 0)); } +void BufferViewer::on_filterCBuffer_toggled(bool checked) +{ + if(!checked) + { + ui->filterExpression->setText(QString()); + ui->filterStrip->setVisible(false); + } + else + { + ui->filterStrip->setVisible(true); + ui->filterExpression->setFocus(Qt::OtherFocusReason); + } + + if(m_FilterTimeout->isActive()) + m_FilterTimeout->stop(); + + OnEventChanged(m_Ctx.CurEvent()); +} + +void BufferViewer::on_filterCaseSensitive_toggled(bool checked) +{ + OnEventChanged(m_Ctx.CurEvent()); +} + void BufferViewer::on_resetMeshFilterButton_clicked() { SetMeshFilter(MeshFilter::None); @@ -6254,9 +6366,9 @@ void BufferViewer::processFormat(const QString &format) ui->rowOffsetLabel->setVisible(true); ui->rowOffset->setVisible(true); // insert after the offsetLine - if(ui->rowOffset->parentWidget() != ui->meshToolbar) + if(ui->rowOffset->parentWidget() != ui->mainMeshToolbar) { - QHBoxLayout *hbox = qobject_cast(ui->meshToolbar->layout()); + QHBoxLayout *hbox = qobject_cast(ui->mainMeshToolbar->layout()); int i = 0; for(; i < hbox->count(); i++) diff --git a/qrenderdoc/Windows/BufferViewer.h b/qrenderdoc/Windows/BufferViewer.h index 1f97e27d3..e7c30560f 100644 --- a/qrenderdoc/Windows/BufferViewer.h +++ b/qrenderdoc/Windows/BufferViewer.h @@ -40,6 +40,7 @@ class RDSpinBox64; class QItemSelection; class QMenu; class QPushButton; +class QTimer; class QVBoxLayout; class RDLabel; class RDTableView; @@ -146,6 +147,8 @@ private slots: void on_axisMappingCombo_currentIndexChanged(int index); void on_axisMappingButton_clicked(); void on_setFormat_toggled(bool checked); + void on_filterCBuffer_toggled(bool checked); + void on_filterCaseSensitive_toggled(bool checked); void on_resetMeshFilterButton_clicked(); void on_camParameters_clicked(); void on_guessButton_clicked(); @@ -213,9 +216,9 @@ private: void UI_AddTaskPayloads(RDTreeWidgetItem *root, size_t baseOffset, const rdcarray &consts, BufferData *buffer); - void UI_AddFixedVariables(RDTreeWidgetItem *root, uint32_t baseOffset, + bool UI_AddFixedVariables(RDTreeWidgetItem *root, uint32_t baseOffset, const rdcarray &consts, - const rdcarray &vars); + const rdcarray &vars, bool ignoreFilter); void UI_RemoveOffsets(RDTreeWidgetItem *root); void UI_FixedAddMatrixRows(RDTreeWidgetItem *n, const ShaderConstant &c, const ShaderVariable &v); @@ -339,6 +342,8 @@ private: QAction *m_RemoveFilter = NULL; QAction *m_GotoTask = NULL; + QTimer *m_FilterTimeout = NULL; + RDSpinBox64 *byteRangeStart = NULL; RDSpinBox64 *byteRangeLength = NULL; diff --git a/qrenderdoc/Windows/BufferViewer.ui b/qrenderdoc/Windows/BufferViewer.ui index 46de460fd..ab3d98a62 100644 --- a/qrenderdoc/Windows/BufferViewer.ui +++ b/qrenderdoc/Windows/BufferViewer.ui @@ -545,7 +545,7 @@ 0 0 - 267 + 257 321 @@ -795,12 +795,87 @@ false - + + + + 700 + 50 + 223 + 24 + + + + + 3 + + + 6 + + + 0 + + + 6 + + + 2 + + + + + + + + :/filter.png + + + + + + + Filter + + + Qt::PlainText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + 0 + + + + + + + + + + Filter with case-sensitivity + + + Aa + + + true + + + Qt::ToolButtonTextOnly + + + true + + + + + + 0 10 - 767 + 1137 28 @@ -816,12 +891,6 @@ 28 - - QFrame::Panel - - - QFrame::Raised - 2 @@ -946,6 +1015,29 @@ + + + + Filter constants by name + + + + + + + :/filter.png:/filter.png + + + true + + + Qt::ToolButtonIconOnly + + + true + + + @@ -1154,6 +1246,11 @@
Widgets/BufferFormatSpecifier.h
1 + + RDLineEdit + QLineEdit +
Widgets/Extended/RDLineEdit.h
+