/****************************************************************************** * The MIT License (MIT) * * Copyright (c) 2016-2026 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 "BufferViewer.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Code/QRDUtils.h" #include "Code/Resources.h" #include "Widgets/CollapseGroupBox.h" #include "Widgets/ComputeDebugSelector.h" #include "Widgets/Extended/RDLabel.h" #include "Widgets/Extended/RDSplitter.h" #include "Windows/Dialogs/AxisMappingDialog.h" #include "Windows/Dialogs/CameraControlsDialog.h" #include "ui_BufferViewer.h" struct FixedVarTag { FixedVarTag() = default; FixedVarTag(uint32_t size) : valid(true), padding(true), byteSize(size) {} FixedVarTag(rdcstr varName, uint32_t offset) : valid(true), padding(false), name(varName), byteOffset(offset) { } bool valid = false; bool padding = false; bool matrix = false; bool rowmajor = false; rdcstr name; union { uint32_t byteOffset; uint32_t byteSize; }; }; Q_DECLARE_METATYPE(FixedVarTag); static const uint32_t MaxVisibleRows = 10000; class CameraWrapper { public: CameraWrapper(ICaptureContext &ctx) : m_Ctx(ctx) {} virtual ~CameraWrapper() {} virtual bool Update(QRect winSize) = 0; virtual ICamera *camera() = 0; virtual void MouseMove(QMouseEvent *e) { if(e->buttons() & Qt::LeftButton) { m_DragStartPos = e->pos(); } else { m_DragStartPos = QPoint(-1, -1); } } KeyPressDirection GetDirection(QKeyEvent *e) { const rdcarray &keys = m_Ctx.Config().MeshViewer_KeySettings; for(int i = 0; i < (int)KeyPressDirection::Count; i++) { KeyPressDirection dir = KeyPressDirection(i); Qt::Key primary, secondary; int p = keySettingIdx(dir, true); int s = keySettingIdx(dir, false); if(p < keys.count() && keys[p] != 0) primary = getKeySetting(keys[p]); else primary = getDefaultKey(dir, true); if(s < keys.count() && keys[s] != 0) secondary = getKeySetting(keys[s]); else secondary = getDefaultKey(dir, false); if(e->key() == primary || e->key() == secondary) return dir; } return KeyPressDirection::None; } KeyPressDirection GetDirection(QMouseEvent *e) { if(m_Ctx.Config().MeshViewer_KeySettings.size() >= (size_t)KeyPressDirection::NumSettings) { for(int i = 0; i < (int)KeyPressDirection::Count; i++) { KeyPressDirection dir = KeyPressDirection(i); Qt::MouseButton primary = getMouseButtonSetting(m_Ctx.Config().MeshViewer_KeySettings[keySettingIdx(dir, true)]); Qt::MouseButton secondary = getMouseButtonSetting(m_Ctx.Config().MeshViewer_KeySettings[keySettingIdx(dir, false)]); if(e->button() == primary || e->button() == secondary) return dir; } } return KeyPressDirection::None; } KeyPressDirection GetDirection(QWheelEvent *e) { if(m_Ctx.Config().MeshViewer_KeySettings.size() >= (size_t)KeyPressDirection::NumSettings) { QPoint angleDelta = e->angleDelta(); angleDelta.setX(qMin(1, qMax(-1, angleDelta.x()))); angleDelta.setY(qMin(1, qMax(-1, angleDelta.y()))); for(int i = 0; i < (int)KeyPressDirection::Count; i++) { KeyPressDirection dir = KeyPressDirection(i); QPoint primary = getMouseWheelSetting(m_Ctx.Config().MeshViewer_KeySettings[keySettingIdx(dir, true)]); QPoint secondary = getMouseWheelSetting(m_Ctx.Config().MeshViewer_KeySettings[keySettingIdx(dir, false)]); if(angleDelta == primary || angleDelta == secondary) return dir; } } return KeyPressDirection::None; } virtual void KeyUp(QKeyEvent *e) { KeyPressDirection dir = GetDirection(e); if(dir == KeyPressDirection::Left || dir == KeyPressDirection::Right) setMove(Direction::Horiz, 0); else if(dir == KeyPressDirection::Forward || dir == KeyPressDirection::Back) setMove(Direction::Fwd, 0); else if(dir == KeyPressDirection::Up || dir == KeyPressDirection::Down) setMove(Direction::Vert, 0); Qt::KeyboardModifier speedMod = Qt::ShiftModifier; if(m_Ctx.Config().MeshViewer_SpeedModifier > 0) speedMod = Qt::KeyboardModifier(m_Ctx.Config().MeshViewer_SpeedModifier); if(speedMod != Qt::NoModifier && (e->modifiers() & speedMod)) m_CurrentSpeed = 3.0f; else m_CurrentSpeed = 1.0f; } virtual void KeyDown(QKeyEvent *e) { KeyPressDirection dir = GetDirection(e); switch(dir) { default: break; case KeyPressDirection::Left: setMove(Direction::Horiz, -1); break; case KeyPressDirection::Right: setMove(Direction::Horiz, 1); break; case KeyPressDirection::Forward: setMove(Direction::Fwd, 1); break; case KeyPressDirection::Back: setMove(Direction::Fwd, -1); break; case KeyPressDirection::Up: setMove(Direction::Vert, 1); break; case KeyPressDirection::Down: setMove(Direction::Vert, -1); break; } Qt::KeyboardModifier speedMod = Qt::ShiftModifier; if(m_Ctx.Config().MeshViewer_SpeedModifier > 0) speedMod = Qt::KeyboardModifier(m_Ctx.Config().MeshViewer_SpeedModifier); if(speedMod != Qt::NoModifier && (e->modifiers() & speedMod)) m_CurrentSpeed = 3.0f; else m_CurrentSpeed = 1.0f; } virtual void MouseClick(QMouseEvent *e) { m_DragStartPos = e->pos(); KeyPressDirection dir = GetDirection(e); switch(dir) { default: break; case KeyPressDirection::Left: setMove(Direction::Horiz, -1); break; case KeyPressDirection::Right: setMove(Direction::Horiz, 1); break; case KeyPressDirection::Forward: setMove(Direction::Fwd, 1); break; case KeyPressDirection::Back: setMove(Direction::Fwd, -1); break; case KeyPressDirection::Up: setMove(Direction::Vert, 1); break; case KeyPressDirection::Down: setMove(Direction::Vert, -1); break; } } virtual void MouseUnclick(QMouseEvent *e) { KeyPressDirection dir = GetDirection(e); if(dir == KeyPressDirection::Left || dir == KeyPressDirection::Right) setMove(Direction::Horiz, 0); else if(dir == KeyPressDirection::Forward || dir == KeyPressDirection::Back) setMove(Direction::Fwd, 0); else if(dir == KeyPressDirection::Up || dir == KeyPressDirection::Down) setMove(Direction::Vert, 0); } virtual void MouseWheel(QWheelEvent *e) {} float SpeedMultiplier = 0.05f; protected: enum class Direction { Fwd, Horiz, Vert, Num }; int move(Direction dir) { return m_CurrentMove[(int)dir]; } float currentSpeed() { return m_CurrentSpeed * SpeedMultiplier; } QPoint dragStartPos() { return m_DragStartPos; } ICaptureContext &m_Ctx; private: float m_CurrentSpeed = 1.0f; int m_CurrentMove[(int)Direction::Num] = {0, 0, 0}; void setMove(Direction dir, int val) { m_CurrentMove[(int)dir] = val; } QPoint m_DragStartPos = QPoint(-1, -1); }; class ArcballWrapper : public CameraWrapper { public: ArcballWrapper(ICaptureContext &ctx) : CameraWrapper(ctx) { m_Cam = RENDERDOC_InitCamera(CameraType::Arcball); } virtual ~ArcballWrapper() { m_Cam->Shutdown(); } ICamera *camera() override { return m_Cam; } void Reset(FloatVector pos, float dist) { m_Cam->ResetArcball(); setLookAtPos(pos); SetDistance(dist); } void SetDistance(float dist) { m_Distance = qAbs(dist); m_Cam->SetArcballDistance(m_Distance); } bool Update(QRect size) override { m_WinSize = size; return false; } void MouseWheel(QWheelEvent *e) override { CameraWrapper::MouseWheel(e); float mod = (1.0f - e->delta() / 2500.0f); SetDistance(qMax(1e-6f, m_Distance * mod)); } void MouseMove(QMouseEvent *e) override { if(dragStartPos().x() > 0) { if(e->buttons() == Qt::MiddleButton || (e->buttons() == Qt::LeftButton && e->modifiers() & Qt::AltModifier)) { float xdelta = (float)(e->pos().x() - dragStartPos().x()) / 300.0f; float ydelta = (float)(e->pos().y() - dragStartPos().y()) / 300.0f; xdelta *= qMax(1.0f, m_Distance); ydelta *= qMax(1.0f, m_Distance); FloatVector right = m_Cam->GetRight(); FloatVector up = m_Cam->GetUp(); m_LookAt.x -= right.x * xdelta; m_LookAt.y -= right.y * xdelta; m_LookAt.z -= right.z * xdelta; m_LookAt.x += up.x * ydelta; m_LookAt.y += up.y * ydelta; m_LookAt.z += up.z * ydelta; m_Cam->SetPosition(m_LookAt.x, m_LookAt.y, m_LookAt.z); } else if(e->buttons() == Qt::LeftButton) { RotateArcball(dragStartPos(), e->pos()); } } CameraWrapper::MouseMove(e); } FloatVector lookAtPos() { return m_LookAt; } void setLookAtPos(const FloatVector &v) { m_LookAt = v; m_Cam->SetPosition(v.x, v.y, v.z); } private: ICamera *m_Cam; QRect m_WinSize; float m_Distance = 10.0f; FloatVector m_LookAt; void RotateArcball(QPoint from, QPoint to) { // this isn't a 'true arcball' but it handles extreme aspect ratios // better. We basically 'centre' around the from point always being // 0,0 (straight out of the screen) as if you're always dragging // the arcball from the middle, and just use the relative movement int minDimension = qMin(m_WinSize.width(), m_WinSize.height()); float ax = 0.0f, ay = 0.0f; float bx = ((float)(to.x() - from.x()) / (float)minDimension) * 2.0f; float by = ((float)(to.y() - from.y()) / (float)minDimension) * 2.0f; ay = -ay; by = -by; m_Cam->RotateArcball(ax, ay, bx, by); } }; class FlycamWrapper : public CameraWrapper { public: FlycamWrapper(ICaptureContext &ctx) : CameraWrapper(ctx) { m_Cam = RENDERDOC_InitCamera(CameraType::FPSLook); } virtual ~FlycamWrapper() { m_Cam->Shutdown(); } ICamera *camera() override { return m_Cam; } void Reset(FloatVector pos) { m_Position = pos; m_Rotation = FloatVector(); m_Cam->SetPosition(m_Position.x, m_Position.y, m_Position.z); m_Cam->SetFPSRotation(m_Rotation.x, m_Rotation.y, m_Rotation.z); } bool Update(QRect size) override { FloatVector fwd = m_Cam->GetForward(); FloatVector right = m_Cam->GetRight(); float speed = currentSpeed(); int horizMove = move(CameraWrapper::Direction::Horiz); if(horizMove) { m_Position.x += right.x * speed * (float)horizMove; m_Position.y += right.y * speed * (float)horizMove; m_Position.z += right.z * speed * (float)horizMove; } int vertMove = move(CameraWrapper::Direction::Vert); if(vertMove) { // this makes less intuitive sense, instead go 'absolute' up // m_Position.x += up.x * speed * (float)vertMove; // m_Position.y += up.y * speed * (float)vertMove; // m_Position.z += up.z * speed * (float)vertMove; m_Position.y += speed * (float)vertMove; } int fwdMove = move(CameraWrapper::Direction::Fwd); if(fwdMove) { m_Position.x += fwd.x * speed * (float)fwdMove; m_Position.y += fwd.y * speed * (float)fwdMove; m_Position.z += fwd.z * speed * (float)fwdMove; } if(horizMove || vertMove || fwdMove) { m_Cam->SetPosition(m_Position.x, m_Position.y, m_Position.z); return true; } return false; } virtual void MouseWheel(QWheelEvent *e) override { CameraWrapper::MouseWheel(e); KeyPressDirection dir = GetDirection(e); FloatVector fwd = m_Cam->GetForward(); FloatVector right = m_Cam->GetRight(); float speed = currentSpeed(); if(dir == KeyPressDirection::Left || dir == KeyPressDirection::Right) { int horizMove = dir == KeyPressDirection::Left ? -1 : 1; m_Position.x += right.x * speed * (float)horizMove; m_Position.y += right.y * speed * (float)horizMove; m_Position.z += right.z * speed * (float)horizMove; } else if(dir == KeyPressDirection::Up || dir == KeyPressDirection::Down) { // this makes less intuitive sense, instead go 'absolute' up // m_Position.x += up.x * speed * (float)vertMove; // m_Position.y += up.y * speed * (float)vertMove; // m_Position.z += up.z * speed * (float)vertMove; int vertMove = dir == KeyPressDirection::Up ? -1 : 1; m_Position.y += speed * (float)vertMove; } else if(dir == KeyPressDirection::Forward || dir == KeyPressDirection::Back) { int fwdMove = dir == KeyPressDirection::Back ? -1 : 1; m_Position.x += fwd.x * speed * (float)fwdMove; m_Position.y += fwd.y * speed * (float)fwdMove; m_Position.z += fwd.z * speed * (float)fwdMove; } else { return; } m_Cam->SetPosition(m_Position.x, m_Position.y, m_Position.z); } void MouseMove(QMouseEvent *e) override { if(dragStartPos().x() > 0 && e->buttons() == Qt::LeftButton) { m_Rotation.y -= (float)(e->pos().x() - dragStartPos().x()) / 300.0f; m_Rotation.x -= (float)(e->pos().y() - dragStartPos().y()) / 300.0f; m_Cam->SetFPSRotation(m_Rotation.x, m_Rotation.y, m_Rotation.z); } CameraWrapper::MouseMove(e); } private: ICamera *m_Cam; FloatVector m_Position, m_Rotation; }; struct BufferData { BufferData() { refcount.store(1); stride = 0; } void ref() { refcount.ref(); } void deref() { bool alive = refcount.deref(); if(!alive) delete this; } size_t stride; bytebuf storage; QAtomicInteger refcount; const byte *data() const { return storage.begin(); }; const byte *end() const { return storage.end(); } bool hasData() const { return !storage.empty(); } size_t size() const { return storage.size(); } }; struct BufferElementProperties { ResourceFormat format; int buffer = 0; ShaderBuiltin systemValue = ShaderBuiltin::Undefined; bool perinstance = false; bool perprimitive = false; bool floatCastWrong = false; int instancerate = 1; }; struct BufferConfiguration { uint32_t curInstance = 0, curView = 0; uint32_t numRows = 0, unclampedNumRows = 0; uint32_t pagingOffset = 0; Packing::Rules packing; ShaderConstant fixedVars; rdcarray evalVars; uint32_t repeatStride = 1; uint32_t repeatOffset = 0; QString statusString; bool noVertices = false; bool noInstances = false; // we can have two index buffers for VSOut data: // the original index buffer is used for the displayed value (in displayIndices), and the actual // potentially remapped or permuated index buffer used for fetching data (in indices). BufferData *displayIndices = NULL; int32_t displayBaseVertex = 0; BufferData *indices = NULL; int32_t baseVertex = 0; rdcfixedarray dispatchSize; rdcarray taskSizes; rdcarray meshletVertexPrefixCounts; uint32_t taskOrMeshletOffset = 0; uint64_t perPrimitiveOffset = 0; uint32_t perPrimitiveStride = 0; Topology topology = Topology::TriangleList; rdcarray columns; rdcarray props; QVector generics; QVector genericsEnabled; QList buffers; uint32_t primRestart = 0; BufferConfiguration() = default; BufferConfiguration(const BufferConfiguration &o) = delete; ~BufferConfiguration() { reset(); } BufferConfiguration &operator=(const BufferConfiguration &o) { reset(); curInstance = o.curInstance; numRows = o.numRows; unclampedNumRows = o.unclampedNumRows; pagingOffset = o.pagingOffset; packing = o.packing; fixedVars = o.fixedVars; evalVars = o.evalVars; repeatStride = o.repeatStride; repeatOffset = o.repeatOffset; statusString = o.statusString; noVertices = o.noVertices; noInstances = o.noInstances; displayIndices = o.displayIndices; if(displayIndices) displayIndices->ref(); displayBaseVertex = o.displayBaseVertex; indices = o.indices; if(indices) indices->ref(); baseVertex = o.baseVertex; meshletVertexPrefixCounts = o.meshletVertexPrefixCounts; dispatchSize = o.dispatchSize; taskSizes = o.taskSizes; taskOrMeshletOffset = o.taskOrMeshletOffset; perPrimitiveOffset = o.perPrimitiveOffset; perPrimitiveStride = o.perPrimitiveStride; topology = o.topology; columns = o.columns; props = o.props; generics = o.generics; genericsEnabled = o.genericsEnabled; primRestart = o.primRestart; buffers = o.buffers; for(BufferData *b : buffers) b->ref(); return *this; } void reset() { if(indices) indices->deref(); indices = NULL; if(displayIndices) displayIndices->deref(); displayIndices = NULL; for(BufferData *b : buffers) b->deref(); meshletVertexPrefixCounts.clear(); dispatchSize = {}; taskSizes.clear(); buffers.clear(); columns.clear(); props.clear(); generics.clear(); genericsEnabled.clear(); numRows = 0; unclampedNumRows = 0; statusString.clear(); noVertices = false; noInstances = false; } QString columnName(int col) const { if(col >= 0 && col < columns.count()) return columns[col].name; return QString(); } int guessPositionColumn() const { int posEl = -1; if(!columns.empty()) { // prioritise system value over general "POSITION" string matching for(int i = 0; i < columns.count(); i++) { const BufferElementProperties &prop = props[i]; if(prop.systemValue == ShaderBuiltin::Position) { posEl = i; break; } } // look for an exact match for(int i = 0; posEl == -1 && i < columns.count(); i++) { const ShaderConstant &el = columns[i]; if(QString(el.name).compare(lit("POSITION"), Qt::CaseInsensitive) == 0 || QString(el.name).compare(lit("POSITION0"), Qt::CaseInsensitive) == 0 || QString(el.name).compare(lit("POS"), Qt::CaseInsensitive) == 0 || QString(el.name).compare(lit("POS0"), Qt::CaseInsensitive) == 0) { posEl = i; break; } } // try anything containing position for(int i = 0; posEl == -1 && i < columns.count(); i++) { const ShaderConstant &el = columns[i]; if(QString(el.name).contains(lit("POSITION"), Qt::CaseInsensitive)) { posEl = i; break; } } // OK last resort, just look for 'pos' for(int i = 0; posEl == -1 && i < columns.count(); i++) { const ShaderConstant &el = columns[i]; if(QString(el.name).contains(lit("POS"), Qt::CaseInsensitive)) { posEl = i; break; } } // if we still have absolutely nothing, just use the first available element if(posEl == -1) { posEl = 0; } } return posEl; } int guessSecondaryColumn() const { int secondEl = -1; if(!columns.empty()) { // prioritise TEXCOORD over general COLOR for(int i = 0; i < columns.count(); i++) { const ShaderConstant &el = columns[i]; if(QString(el.name).compare(lit("TEXCOORD"), Qt::CaseInsensitive) == 0 || QString(el.name).compare(lit("TEXCOORD0"), Qt::CaseInsensitive) == 0 || QString(el.name).compare(lit("TEX"), Qt::CaseInsensitive) == 0 || QString(el.name).compare(lit("TEX0"), Qt::CaseInsensitive) == 0 || QString(el.name).compare(lit("UV"), Qt::CaseInsensitive) == 0 || QString(el.name).compare(lit("UV0"), Qt::CaseInsensitive) == 0) { secondEl = i; break; } } for(int i = 0; secondEl == -1 && i < columns.count(); i++) { const ShaderConstant &el = columns[i]; if(QString(el.name).compare(lit("COLOR"), Qt::CaseInsensitive) == 0 || QString(el.name).compare(lit("COLOR0"), Qt::CaseInsensitive) == 0 || QString(el.name).compare(lit("COL"), Qt::CaseInsensitive) == 0 || QString(el.name).compare(lit("COL0"), Qt::CaseInsensitive) == 0) { secondEl = i; break; } } } return secondEl; } }; uint32_t CalcIndex(BufferData *data, uint32_t vertID, int32_t baseVertex, uint32_t primRestart) { const byte *idxData = data->data() + vertID * sizeof(uint32_t); if(idxData + sizeof(uint32_t) > data->end()) return ~0U; uint32_t idx = *(const uint32_t *)idxData; // check for primitive restart *before* adding base vertex if(primRestart && idx == primRestart) return idx; // apply base vertex but clamp to 0 if subtracting if(baseVertex < 0) { uint32_t subtract = (uint32_t)(-baseVertex); if(idx < subtract) idx = 0; else idx -= subtract; } else if(baseVertex > 0) { idx += (uint32_t)baseVertex; } return idx; } static int columnGroupRole = Qt::UserRole + 10000; static QString interpretVariant(const QVariant &v, const ShaderConstant &el, const BufferElementProperties &prop) { QString ret; QMetaType::Type vt = GetVariantMetatype(v); if(vt == QMetaType::Double) { double d = v.toDouble(); // pad with space on left if sign is missing, to better align if(d < 0.0) ret = Formatter::Format(d); else if(d > 0.0) ret = lit(" ") + Formatter::Format(d); else if(qIsNaN(d)) ret = lit(" NaN"); else // force negative and positive 0 together ret = lit(" ") + Formatter::Format(0.0); } else if(vt == QMetaType::Float) { float f = v.toFloat(); // pad with space on left if sign is missing, to better align if(f < 0.0) ret = Formatter::Format(f); else if(f > 0.0) ret = lit(" ") + Formatter::Format(f); else if(qIsNaN(f)) ret = lit(" NaN"); else // force negative and positive 0 together ret = lit(" ") + Formatter::Format(0.0); } else if(vt == QMetaType::UInt || vt == QMetaType::UShort || vt == QMetaType::UChar) { uint32_t u = v.toUInt(); if(prop.floatCastWrong) { float f = (float)u; memcpy(&u, &f, sizeof(f)); } const bool hexDisplay = bool(el.type.flags & ShaderVariableFlags::HexDisplay); const bool binDisplay = bool(el.type.flags & ShaderVariableFlags::BinaryDisplay); if(hexDisplay && prop.format.type == ResourceFormatType::Regular) ret = Formatter::HexFormat(u, prop.format.compByteWidth); else if(binDisplay && prop.format.type == ResourceFormatType::Regular) ret = Formatter::BinFormat(u, prop.format.compByteWidth); else ret = Formatter::Format(u, hexDisplay); } else if(vt == QMetaType::Int || vt == QMetaType::Short || vt == QMetaType::SChar) { int32_t i = v.toInt(); if(prop.floatCastWrong) { float f = (float)i; memcpy(&i, &f, sizeof(f)); } if(i >= 0) ret = lit(" ") + Formatter::Format(i); else ret = Formatter::Format(i); } else if(vt == QMetaType::ULongLong) { const bool hexDisplay = bool(el.type.flags & ShaderVariableFlags::HexDisplay); const bool binDisplay = bool(el.type.flags & ShaderVariableFlags::BinaryDisplay); if(binDisplay) ret = Formatter::BinFormat((uint64_t)v.toULongLong(), 8); else ret = Formatter::Format((uint64_t)v.toULongLong(), hexDisplay); } else if(vt == QMetaType::LongLong) { int64_t i = v.toLongLong(); if(i >= 0) ret = lit(" ") + Formatter::Format(i); else ret = Formatter::Format(i); } else { ret = v.toString(); } return ret; } class BufferItemModel : public QAbstractItemModel { public: BufferItemModel(RDTableView *v, bool vertexInput, bool mesh, QObject *parent) : QAbstractItemModel(parent) { vertexInputData = vertexInput; meshView = mesh; view = v; view->setModel(this); } void beginReset() { emit beginResetModel(); config.reset(); } void endReset(const BufferConfiguration &conf) { config = conf; cacheColumns(); totalColumnCount = columnLookup.count() + reservedColumnCount(); emit endResetModel(); } QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override { if(row < 0 || row >= rowCount()) return QModelIndex(); return createIndex(row, column); } QModelIndex parent(const QModelIndex &index) const override { return QModelIndex(); } int rowCount(const QModelIndex &parent = QModelIndex()) const override { int ret = config.numRows; if(config.pagingOffset > 0) ret++; if(ret == 0) { if(!config.statusString.isEmpty()) ret += config.statusString.count(QLatin1Char('\n')) + 1; if(config.noVertices) ret++; if(config.noInstances) ret++; } return ret; } int columnCount(const QModelIndex &parent = QModelIndex()) const override { return totalColumnCount; } 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(section < totalColumnCount && orientation == Qt::Horizontal) { if(role == Qt::DisplayRole || role == columnGroupRole) { if(section == 0) { return meshView ? lit("VTX") : lit("Element"); } else if(section == 1 && meshView) { return lit("IDX"); } else { const ShaderConstant &el = elementForColumn(section); if(el.type.columns == 1 || role == columnGroupRole) return el.name; QChar comps[] = {QLatin1Char('x'), QLatin1Char('y'), QLatin1Char('z'), QLatin1Char('w')}; return QFormatStr("%1.%2").arg(el.name).arg(comps[componentForIndex(section)]); } } } return QVariant(); } QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override { if(index.isValid()) { if(role == Qt::SizeHintRole) { QStyleOptionViewItem opt = view->viewOptions(); opt.features |= QStyleOptionViewItem::HasDisplay; // pad these columns to allow for sufficiently wide data if(index.column() < reservedColumnCount()) opt.text = lit("4294967295"); else opt.text = data(index).toString(); opt.text.replace(QLatin1Char('\n'), QChar::LineSeparator); opt.styleObject = NULL; QStyle *style = opt.widget ? opt.widget->style() : QApplication::style(); return style->sizeFromContents(QStyle::CT_ItemViewItem, &opt, QSize(), opt.widget); } uint32_t row = index.row(); int col = index.column(); if(config.pagingOffset > 0) { if(row == 0) { if(role == Qt::DisplayRole) return lit("..."); return QVariant(); } row--; } if(role == columnGroupRole) { if(col < reservedColumnCount()) return -1 - col; else return columnLookup[col - reservedColumnCount()]; } if((role == Qt::BackgroundRole || role == Qt::ForegroundRole) && col >= reservedColumnCount()) { if(meshView) { int elIdx = columnLookup[col - reservedColumnCount()]; int compIdx = componentForIndex(col); float lightnessOn = qBound(0.25, view->palette().color(QPalette::Base).lightnessF(), 0.75); float lightnessOff = lightnessOn > 0.5f ? lightnessOn + 0.2f : lightnessOn - 0.2f; static float a = 0.55f; static float b = 0.8f; if(elIdx == positionEl) { QColor backCol; if(compIdx != 3 || !vertexInputData) { backCol = QColor::fromHslF(0.55f, 0.75f, lightnessOn); } else { backCol = QColor::fromHslF(0.55f, 0.75f, lightnessOff); } if(role == Qt::ForegroundRole) return QBrush(contrastingColor(backCol, view->palette().color(QPalette::Text))); return backCol; } else if(secondaryEnabled && elIdx == secondaryEl) { QColor backCol; if((secondaryElAlpha && compIdx == 3) || (!secondaryElAlpha && compIdx != 3)) { backCol = QColor::fromHslF(0.33f, 0.75f, lightnessOn); } else { backCol = QColor::fromHslF(0.33f, 0.75f, lightnessOff); } if(role == Qt::ForegroundRole) return QBrush(contrastingColor(backCol, view->palette().color(QPalette::Text))); return backCol; } } else { const ShaderConstant &el = elementForColumn(col); const BufferElementProperties &prop = propForColumn(col); if((el.type.flags & ShaderVariableFlags::RGBDisplay) && prop.buffer < config.buffers.size()) { const byte *data = config.buffers[prop.buffer]->data(); const byte *end = config.buffers[prop.buffer]->end(); data += config.buffers[prop.buffer]->stride * row; data += el.byteOffset; // only slightly wasteful, we need to fetch all variants together // since some formats are packed and can't be read individually QVariantList list = GetVariants(prop.format, el, data, end); if(!list.isEmpty()) { QMetaType::Type vt = GetVariantMetatype(list[0]); QColor rgb; if(vt == QMetaType::Double) { double r = qBound(0.0, list[0].toDouble(), 1.0); double g = list.size() > 1 ? qBound(0.0, list[1].toDouble(), 1.0) : 0.0; double b = list.size() > 2 ? qBound(0.0, list[2].toDouble(), 1.0) : 0.0; rgb = QColor::fromRgbF(ConvertLinearToSRGB(float(r)), ConvertLinearToSRGB(float(g)), ConvertLinearToSRGB(float(b))); } else if(vt == QMetaType::Float) { float r = qBound(0.0f, list[0].toFloat(), 1.0f); float g = list.size() > 1 ? qBound(0.0f, list[1].toFloat(), 1.0f) : 0.0; float b = list.size() > 2 ? qBound(0.0f, list[2].toFloat(), 1.0f) : 0.0; rgb = QColor::fromRgbF(ConvertLinearToSRGB(float(r)), ConvertLinearToSRGB(float(g)), ConvertLinearToSRGB(float(b))); } else if(vt == QMetaType::UInt || vt == QMetaType::UShort || vt == QMetaType::UChar) { uint r = qBound(0U, list[0].toUInt(), 255U); uint g = list.size() > 1 ? qBound(0U, list[1].toUInt(), 255U) : 0.0; uint b = list.size() > 2 ? qBound(0U, list[2].toUInt(), 255U) : 0.0; // we leave this as assuming it's in sRGB space since most commonly this will be an // 8-bit texture being viewed as a buffer rgb = QColor::fromRgb(r, g, b); } else if(vt == QMetaType::Int || vt == QMetaType::Short || vt == QMetaType::SChar) { int r = qBound(0, list[0].toInt(), 255); int g = list.size() > 1 ? qBound(0, list[1].toInt(), 255) : 0.0; int b = list.size() > 2 ? qBound(0, list[2].toInt(), 255) : 0.0; rgb = QColor::fromRgb(r, g, b); } else { return QVariant(); } if(role == Qt::BackgroundRole) return QBrush(rgb); else if(role == Qt::ForegroundRole) return QBrush(contrastingColor(rgb, QColor::fromRgb(0, 0, 0))); } } } } if(role == Qt::BackgroundRole && meshView && !config.meshletVertexPrefixCounts.empty()) { auto it = std::upper_bound(config.meshletVertexPrefixCounts.begin(), config.meshletVertexPrefixCounts.end(), row); if(it != config.meshletVertexPrefixCounts.begin()) it--; size_t meshletIdx = it - config.meshletVertexPrefixCounts.begin(); return meshletIdx % 2 ? view->palette().color(QPalette::AlternateBase) : view->palette().color(QPalette::Base); } if(role == Qt::DisplayRole) { if(config.numRows == 0 && (config.noInstances || config.noVertices || !config.statusString.isEmpty())) { if(col < 2) return lit("---"); if(col != 2) return QVariant(); if(!config.statusString.isEmpty()) { return config.statusString.split(QLatin1Char('\n'))[row]; } else if(config.noVertices && config.noInstances) { if(row == 0) return lit("No Vertices"); else return lit("No Instances"); } else if(config.noVertices) { return lit("No Vertices"); } else if(config.noInstances) { return lit("No Instances"); } } if(config.unclampedNumRows > config.pagingOffset + config.numRows && row >= config.numRows - 2) { if(meshView) { if(col < 2 && row == config.numRows - 1) return QString::number(config.unclampedNumRows - 1); } else { if(col == 0 && row == config.numRows - 1) return QString::number(config.unclampedNumRows - 1); } return lit("..."); } if(col >= 0 && col < totalColumnCount && row < config.numRows) { if(col == 0) { if(meshView && !config.meshletVertexPrefixCounts.empty()) { auto it = std::upper_bound(config.meshletVertexPrefixCounts.begin(), config.meshletVertexPrefixCounts.end(), row); if(it != config.meshletVertexPrefixCounts.begin()) it--; size_t meshletIdx = it - config.meshletVertexPrefixCounts.begin(); return QFormatStr("%1[%2]") .arg(meshletIdx + config.taskOrMeshletOffset) .arg(row + config.pagingOffset - *it); } else { return row + config.pagingOffset; } } uint32_t idx = row; if(config.indices && config.indices->hasData()) { idx = CalcIndex(config.indices, row, config.baseVertex, config.primRestart); if(config.primRestart && idx == config.primRestart) return col == 1 ? lit("--") : lit(" Restart"); if(idx == ~0U) return outOfBounds(); } if(col == 1 && meshView) { // if we have separate displayIndices, fetch that for display instead if(config.displayIndices && config.displayIndices->hasData()) idx = CalcIndex(config.displayIndices, row, config.displayBaseVertex, config.primRestart); if(idx == ~0U) return outOfBounds(); return idx; } const ShaderConstant &el = elementForColumn(col); const BufferElementProperties &prop = propForColumn(col); if(useGenerics(col)) return interpretGeneric(col, el, prop); uint32_t instIdx = 0; if(prop.instancerate > 0) instIdx = config.curInstance / prop.instancerate; if(prop.buffer < config.buffers.size()) { const byte *data = config.buffers[prop.buffer]->data(); const byte *end = config.buffers[prop.buffer]->end(); if(prop.perprimitive) { uint32_t prim = row / RENDERDOC_NumVerticesPerPrimitive(config.topology); data += config.perPrimitiveOffset; data += config.perPrimitiveStride * prim; } else if(!prop.perinstance) { data += config.buffers[prop.buffer]->stride * idx; } else { data += config.buffers[prop.buffer]->stride * instIdx; } data += el.byteOffset; // only slightly wasteful, we need to fetch all variants together // since some formats are packed and can't be read individually QVariantList list = GetVariants(prop.format, el, data, end); int comp = componentForIndex(col); if(comp < list.count()) { uint32_t rowdim = el.type.rows; uint32_t coldim = el.type.columns; if(rowdim == 1) { QVariant v = list[comp]; if(el.type.pointerTypeID != ~0U) { PointerVal ptr; ptr.pointer = v.toULongLong(); ptr.pointerTypeID = el.type.pointerTypeID; v = ToQStr(ptr); } RichResourceTextInitialise(v, getCaptureContext(view)); if(RichResourceTextCheck(v)) return v; return interpretVariant(v, el, prop); } else { QString ret; for(uint32_t r = 0; r < rowdim; r++) { if(r > 0) ret += lit("\n"); ret += interpretVariant(list[r * coldim + comp], el, prop); } return ret; } } } return outOfBounds(); } } } return QVariant(); } void setPosColumn(int pos) { QVector roles = {Qt::BackgroundRole, Qt::ForegroundRole}; if(pos == -1) pos = config.guessPositionColumn(); if(positionEl != pos) { if(positionEl >= 0) emit dataChanged(index(0, firstColumnForElement(positionEl)), index(rowCount() - 1, lastColumnForElement(positionEl)), roles); if(pos >= 0) emit dataChanged(index(0, firstColumnForElement(pos)), index(rowCount() - 1, lastColumnForElement(pos)), roles); } positionEl = pos; } int posColumn() { return positionEl; } QString posName() { return config.columnName(positionEl); } void setSecondaryColumn(int sec, bool secEnabled, bool secAlpha) { QVector roles = {Qt::BackgroundRole, Qt::ForegroundRole}; if(sec == -1) sec = config.guessSecondaryColumn(); if(secondaryEl != sec || secondaryElAlpha != secAlpha || secondaryEnabled != secEnabled) { if(secondaryEl >= 0 && secondaryEl != sec) emit dataChanged(index(0, firstColumnForElement(secondaryEl)), index(rowCount() - 1, lastColumnForElement(secondaryEl)), roles); if(sec >= 0) emit dataChanged(index(0, firstColumnForElement(sec)), index(rowCount() - 1, lastColumnForElement(sec)), roles); } secondaryEl = sec; secondaryElAlpha = secAlpha; secondaryEnabled = secEnabled; } int secondaryColumn() { return secondaryEl; } bool secondaryAlpha() { return secondaryElAlpha; } QString secondaryName() { return config.columnName(secondaryEl); } int elementIndexForColumn(int col) const { if(col < reservedColumnCount()) return -1; return columnLookup[col - reservedColumnCount()]; } const ShaderConstant &elementForColumn(int col) const { if(col >= reservedColumnCount()) col -= reservedColumnCount(); return config.columns[columnLookup[col]]; } const BufferElementProperties &propForColumn(int col) const { if(col >= reservedColumnCount()) col -= reservedColumnCount(); return config.props[columnLookup[col]]; } bool useGenerics(int col) const { if(col >= reservedColumnCount()) col -= reservedColumnCount(); col = columnLookup[col]; return col < config.genericsEnabled.size() && config.genericsEnabled[col]; } const BufferConfiguration &getConfig() { return config; } private: // constant data over the item model's lifetime // The view that this model is for RDTableView *view = NULL; // Is this the vertex input stage bool vertexInputData = false; // are we configured for mesh viewing, or for raw buffer data bool meshView = true; // the mutable configuration of what we're displaying. BufferConfiguration config; // Internal cached data, generated by cacheColumns() from endReset(). // Only accessible to main UI thread // maps from column number (0-based from data, so excluding VTX/IDX columns) // to the column element in the columns list, and lists its component. // // So a float4, float3, int set of columns would be: // { 0, 0, 0, 0, 1, 1, 1, 2 }; // { 0, 1, 2, 3, 0, 1, 2, 0 }; QVector columnLookup; QVector componentLookup; // the total number of columns including any reserved ones like VTX / IDX int totalColumnCount = 0; // which format element is selected as position data int positionEl = -1; // which format element is selected as secondary data int secondaryEl = -1; // is secondary data enabled bool secondaryEnabled = false; // are we using the alpha channel for secondary data bool secondaryElAlpha = false; int reservedColumnCount() const { return (meshView ? 2 : 1); } int componentForIndex(int col) const { if(col >= reservedColumnCount()) col -= reservedColumnCount(); return componentLookup[col]; } int firstColumnForElement(int el) const { for(int i = 0; i < columnLookup.count(); i++) { if(columnLookup[i] == el) return reservedColumnCount() + i; } return 0; } int lastColumnForElement(int el) const { for(int i = columnLookup.count() - 1; i >= 0; i--) { if(columnLookup[i] == el) return reservedColumnCount() + i; } return columnCount() - 1; } void cacheColumns() { columnLookup.clear(); columnLookup.reserve(config.columns.count() * 4); componentLookup.clear(); componentLookup.reserve(config.columns.count() * 4); for(int i = 0; i < config.columns.count(); i++) { uint32_t columnCount = config.columns[i].type.columns; for(uint32_t c = 0; c < columnCount; c++) { columnLookup.push_back(i); componentLookup.push_back((int)c); } } } QString outOfBounds() const { return lit("---"); } QString interpretGeneric(int col, const ShaderConstant &el, const BufferElementProperties &prop) const { int comp = componentForIndex(col); if(col >= reservedColumnCount()) col -= reservedColumnCount(); col = columnLookup[col]; if(col < config.generics.size()) { if(prop.format.compType == CompType::Float) { return interpretVariant(QVariant(config.generics[col].floatValue[comp]), el, prop); } else if(prop.format.compType == CompType::SInt) { return interpretVariant(QVariant(config.generics[col].intValue[comp]), el, prop); } else if(prop.format.compType == CompType::UInt) { return interpretVariant(QVariant(config.generics[col].uintValue[comp]), el, prop); } } return outOfBounds(); } }; struct CachedElData { const ShaderConstant *el = NULL; const BufferElementProperties *prop = NULL; const byte *data = NULL; const byte *end = NULL; size_t stride; int byteSize; uint32_t instIdx = 0; int numColumns = 0; QByteArray nulls; }; struct PopulateBufferData { int sequence; int inHoriz; int out1Horiz; int out2Horiz; int inVert; int out1Vert; int out2Vert; CBufferData cb; // {In, Out1, Out2} x {primary, secondary} QString highlightNames[6]; bool meshDispatch = false; BufferConfiguration inConfig, out1Config, out2Config; MeshFormat postOut1, postOut2; }; struct CalcBoundingBoxData { uint32_t eventId; BufferConfiguration input[3]; BBoxData output; }; void CacheDataForIteration(QVector &cache, const rdcarray &columns, const rdcarray &props, const QList buffers, uint32_t inst) { cache.reserve(columns.count()); for(int col = 0; col < columns.count(); col++) { const ShaderConstant &el = columns[col]; const BufferElementProperties &prop = props[col]; CachedElData d; d.el = ⪙ d.prop = ∝ d.byteSize = el.type.arrayByteStride; d.nulls = QByteArray(d.byteSize, '\0'); d.numColumns = el.type.columns; if(prop.instancerate > 0) d.instIdx = inst / prop.instancerate; if(prop.buffer < buffers.size()) { d.data = buffers[prop.buffer]->data(); d.end = buffers[prop.buffer]->end(); d.stride = buffers[prop.buffer]->stride; d.data += el.byteOffset; if(prop.perinstance) d.data += d.stride * d.instIdx; } if(prop.perprimitive) d.end = d.data; cache.push_back(d); } } static void ConfigureStatusColumn(rdcarray &columns, rdcarray &props) { ShaderConstant f; f.name = "STATUS"; f.type.columns = 1; f.type.rows = 1; BufferElementProperties p; p.format.type = ResourceFormatType::Regular; p.format.compType = CompType::UInt; p.format.compCount = 1; p.format.compByteWidth = 4; columns.push_back(f); props.push_back(p); } static void ConfigureColumnsForShader(ICaptureContext &ctx, int32_t streamSelect, const ShaderReflection *shader, rdcarray &columns, rdcarray &props) { if(!shader) return; columns.reserve(shader->outputSignature.count()); props.reserve(shader->outputSignature.count()); int i = 0, posidx = -1; for(const SigParameter &sig : shader->outputSignature) { if(sig.stream != (uint32_t)streamSelect) continue; if(sig.systemValue == ShaderBuiltin::OutputIndices) continue; ShaderConstant f; BufferElementProperties p; f.name = !sig.varName.isEmpty() ? sig.varName : sig.semanticIdxName; if(sig.perPrimitiveRate) f.name += lit(" (Per-Prim)"); f.type.rows = 1; f.type.columns = sig.compCount; p.buffer = 0; p.perinstance = false; p.perprimitive = sig.perPrimitiveRate; p.instancerate = 1; p.systemValue = sig.systemValue; p.format.type = ResourceFormatType::Regular; p.format.compByteWidth = qMax(sizeof(float), VarTypeByteSize(sig.varType)); p.format.compCount = sig.compCount; p.format.compType = VarTypeCompType(sig.varType); f.type.arrayByteStride = p.format.compByteWidth * p.format.compCount; if(sig.systemValue == ShaderBuiltin::Position) posidx = i; columns.push_back(f); props.push_back(p); i++; } // shift position attribute up to first, keeping order otherwise // the same if(posidx > 0) { columns.insert(0, columns.takeAt(posidx)); props.insert(0, props.takeAt(posidx)); } i = 0; uint32_t perPrimOffset = 0, perVertOffset = 0; for(i = 0; i < columns.count(); i++) { BufferElementProperties &prop = props[i]; ShaderConstant &el = columns[i]; uint numComps = el.type.columns; uint elemSize = prop.format.compByteWidth > 4 ? 8U : 4U; MeshDataStage outStage = MeshDataStage::VSOut; switch(shader->stage) { case ShaderStage::Vertex: outStage = MeshDataStage::VSOut; break; case ShaderStage::Hull: outStage = MeshDataStage::GSOut; break; case ShaderStage::Domain: outStage = MeshDataStage::GSOut; break; case ShaderStage::Geometry: outStage = MeshDataStage::GSOut; break; case ShaderStage::Task: outStage = MeshDataStage::TaskOut; break; case ShaderStage::Mesh: outStage = MeshDataStage::MeshOut; break; default: break; } uint32_t &offset = prop.perprimitive ? perPrimOffset : perVertOffset; if(ctx.CurPipelineState().HasAlignedPostVSData(outStage)) { if(numComps == 2) offset = AlignUp(offset, 2U * elemSize); else if(numComps > 2) offset = AlignUp(offset, 4U * elemSize); } el.byteOffset = offset; offset += numComps * elemSize; } } static void ConfigureColumnsForMeshPipe(ICaptureContext &ctx, PopulateBufferData *bufdata) { bufdata->inConfig.statusString = lit("No input visualisation supported for mesh shaders"); ConfigureStatusColumn(bufdata->inConfig.columns, bufdata->inConfig.props); const ShaderReflection *ts = ctx.CurPipelineState().GetShaderReflection(ShaderStage::Task); if(ts && bufdata->out1Config.statusString.isEmpty()) { bufdata->out1Config.columns = ts->taskPayload.variables; bufdata->out1Config.props.resize(bufdata->out1Config.columns.size()); } else { if(bufdata->out1Config.statusString.isEmpty()) bufdata->out1Config.statusString = lit("No output visualisation supported for task shaders"); ConfigureStatusColumn(bufdata->out1Config.columns, bufdata->out1Config.props); } const ShaderReflection *ms = ctx.CurPipelineState().GetShaderReflection(ShaderStage::Mesh); ConfigureColumnsForShader(ctx, 0, ms, bufdata->out2Config.columns, bufdata->out2Config.props); } static void ConfigureColumnsForVertexPipe(ICaptureContext &ctx, PopulateBufferData *bufdata) { const ActionDescription *action = ctx.CurAction(); bufdata->inConfig.numRows = 0; bufdata->inConfig.unclampedNumRows = 0; bufdata->inConfig.noVertices = false; bufdata->inConfig.noInstances = false; rdcarray vinputs = ctx.CurPipelineState().GetVertexInputs(); bufdata->inConfig.columns.reserve(vinputs.count()); bufdata->inConfig.columns.clear(); bufdata->inConfig.props.reserve(vinputs.count()); bufdata->inConfig.props.clear(); bufdata->inConfig.genericsEnabled.resize(vinputs.count()); bufdata->inConfig.generics.resize(vinputs.count()); for(const VertexInputAttribute &a : vinputs) { if(!a.used) continue; ShaderConstant f; f.name = a.name; f.byteOffset = a.byteOffset; f.type.columns = a.format.compCount; f.type.rows = 1; f.type.arrayByteStride = f.type.matrixByteStride = a.format.ElementSize(); BufferElementProperties p; p.buffer = a.vertexBuffer; p.perinstance = a.perInstance; p.instancerate = a.instanceRate; p.floatCastWrong = a.floatCastWrong; p.format = a.format; bufdata->inConfig.genericsEnabled[bufdata->inConfig.columns.count()] = false; if(a.genericEnabled) { bufdata->inConfig.genericsEnabled[bufdata->inConfig.columns.count()] = true; bufdata->inConfig.generics[bufdata->inConfig.columns.count()] = a.genericValue; } bufdata->inConfig.columns.push_back(f); bufdata->inConfig.props.push_back(p); } bufdata->inConfig.numRows = action->numIndices; bufdata->inConfig.unclampedNumRows = 0; // calculate an upper bound on the valid number of rows just in case it's an invalid value (e.g. // 0xdeadbeef) and we want to clamp. uint32_t numRowsUpperBound = 0; if(action->flags & ActionFlags::Indexed) { // In an indexed draw we clamp to however many indices are available in the index buffer BoundVBuffer ib = ctx.CurPipelineState().GetIBuffer(); uint32_t bytesAvailable = ib.byteSize; if(bytesAvailable == ~0U) { BufferDescription *buf = ctx.GetBuffer(ib.resourceId); if(buf) { uint64_t offset = ib.byteOffset + action->indexOffset * ib.byteStride; if(offset > buf->length) bytesAvailable = 0; else bytesAvailable = buf->length - offset; } else { bytesAvailable = 0; } } // drawing more than this many indices will read off the end of the index buffer - which while // technically not invalid is certainly not intended, so serves as a good 'upper bound' numRowsUpperBound = bytesAvailable / qMax(1U, ib.byteStride); } else { // for a non-indexed draw, we take the largest vertex buffer rdcarray VBs = ctx.CurPipelineState().GetVBuffers(); for(const BoundVBuffer &vb : VBs) { if(vb.byteStride == 0) continue; uint32_t bytesAvailable = vb.byteSize; if(bytesAvailable == ~0U) { BufferDescription *buf = ctx.GetBuffer(vb.resourceId); if(buf) { if(vb.byteOffset > buf->length) bytesAvailable = 0; else bytesAvailable = buf->length - vb.byteOffset; } else { bytesAvailable = 0; } } numRowsUpperBound = qMax(numRowsUpperBound, bytesAvailable / qMax(1U, vb.byteStride)); } // if there are no vertex buffers we can't clamp. if(numRowsUpperBound == 0) numRowsUpperBound = ~0U; } // if we have significantly clamped, then set the unclamped number of rows and clamp. if(numRowsUpperBound != ~0U && numRowsUpperBound + 100 < bufdata->inConfig.numRows) { bufdata->inConfig.unclampedNumRows = bufdata->inConfig.numRows; bufdata->inConfig.numRows = numRowsUpperBound + 100; } if((action->flags & ActionFlags::Drawcall) && action->numIndices == 0) bufdata->inConfig.noVertices = true; if((action->flags & ActionFlags::Instanced) && action->numInstances == 0) { bufdata->inConfig.noInstances = true; bufdata->inConfig.numRows = bufdata->inConfig.unclampedNumRows = 0; } bufdata->out1Config.columns.clear(); bufdata->out1Config.props.clear(); bufdata->out2Config.columns.clear(); bufdata->out2Config.props.clear(); const ShaderReflection *vs = ctx.CurPipelineState().GetShaderReflection(ShaderStage::Vertex); const ShaderReflection *last = ctx.CurPipelineState().GetShaderReflection(ShaderStage::Geometry); if(last == NULL) last = ctx.CurPipelineState().GetShaderReflection(ShaderStage::Domain); ConfigureColumnsForShader(ctx, 0, vs, bufdata->out1Config.columns, bufdata->out1Config.props); ConfigureColumnsForShader(ctx, ctx.CurPipelineState().GetRasterizedStream(), last, bufdata->out2Config.columns, bufdata->out2Config.props); } static void ConfigureColumns(ICaptureContext &ctx, PopulateBufferData *bufdata) { const ActionDescription *action = ctx.CurAction(); if(action && (action->flags & ActionFlags::MeshDispatch)) { ConfigureColumnsForMeshPipe(ctx, bufdata); } else if(action && (action->flags & ActionFlags::Drawcall)) { ConfigureColumnsForVertexPipe(ctx, bufdata); } else { IEventBrowser *eb = ctx.GetEventBrowser(); bufdata->inConfig.statusString = bufdata->out1Config.statusString = bufdata->out2Config.statusString = lit("No current draw action\nSelected EID @%1 - %2\nEffective EID: @%3 - %4") .arg(ctx.CurSelectedEvent()) .arg(QString(eb->GetEventName(ctx.CurSelectedEvent()))) .arg(ctx.CurEvent()) .arg(QString(eb->GetEventName(ctx.CurEvent()))); ConfigureStatusColumn(bufdata->inConfig.columns, bufdata->inConfig.props); ConfigureStatusColumn(bufdata->out1Config.columns, bufdata->out1Config.props); ConfigureStatusColumn(bufdata->out2Config.columns, bufdata->out2Config.props); bufdata->inConfig.genericsEnabled.push_back(false); bufdata->inConfig.generics.push_back(PixelValue()); } } static void RT_FetchMeshPipeData(IReplayController *r, ICaptureContext &ctx, PopulateBufferData *data) { uint32_t numIndices = data->postOut2.numIndices; if(data->inConfig.indices) data->inConfig.indices->deref(); data->inConfig.indices = NULL; data->out1Config.numRows = data->postOut1.numIndices; data->out1Config.unclampedNumRows = 0; if(data->out1Config.indices) data->out1Config.indices->deref(); if(data->out1Config.displayIndices) data->out1Config.displayIndices->deref(); data->out1Config.displayIndices = NULL; data->out1Config.dispatchSize = data->postOut1.dispatchSize; data->out1Config.taskSizes = data->postOut1.taskSizes; if(data->postOut1.vertexResourceId != ResourceId()) { BufferData *postts = new BufferData; postts->storage = r->GetBufferData(data->postOut1.vertexResourceId, data->postOut1.vertexByteOffset, 0); postts->stride = data->postOut1.vertexByteStride; // ref passes to model data->out1Config.buffers.push_back(postts); } data->out1Config.statusString = data->postOut1.status; if(data->out2Config.indices) data->out2Config.indices->deref(); if(data->out2Config.displayIndices) data->out2Config.displayIndices->deref(); data->out2Config.displayIndices = NULL; uint32_t count = 0; for(const MeshletSize &meshletSize : data->postOut2.meshletSizes) { data->out2Config.meshletVertexPrefixCounts.push_back(count); count += meshletSize.numIndices; } data->out2Config.numRows = numIndices; data->out2Config.unclampedNumRows = 0; data->out2Config.topology = data->postOut2.topology; data->out2Config.perPrimitiveOffset = data->postOut2.perPrimitiveOffset; data->out2Config.perPrimitiveStride = data->postOut2.perPrimitiveStride; bytebuf idata = r->GetBufferData(data->postOut2.indexResourceId, data->postOut2.indexByteOffset, numIndices * data->postOut2.indexByteStride); data->out2Config.indices = new BufferData(); data->out2Config.indices->storage.resize(sizeof(uint32_t) * numIndices); uint32_t *indices = (uint32_t *)data->out2Config.indices->data(); memcpy(indices, idata.data(), qMin(idata.size(), numIndices * sizeof(uint32_t))); if(data->postOut2.vertexResourceId != ResourceId()) { BufferData *postms = new BufferData; postms->storage = r->GetBufferData(data->postOut2.vertexResourceId, data->postOut2.vertexByteOffset, 0); postms->stride = data->postOut2.vertexByteStride; // ref passes to model data->out2Config.buffers.push_back(postms); } data->out2Config.perPrimitiveOffset = data->postOut2.perPrimitiveOffset; data->out2Config.perPrimitiveStride = data->postOut2.perPrimitiveStride; data->out2Config.statusString = data->postOut2.status; } static void RT_FetchVertexPipeData(IReplayController *r, ICaptureContext &ctx, PopulateBufferData *data) { const ActionDescription *action = ctx.CurAction(); BoundVBuffer ib = ctx.CurPipelineState().GetIBuffer(); rdcarray vbs = ctx.CurPipelineState().GetVBuffers(); uint32_t numIndices = action ? action->numIndices : 0; bytebuf idata; if(ib.resourceId != ResourceId() && action && (action->flags & ActionFlags::Indexed)) { uint64_t readBytes = numIndices * ib.byteStride; uint32_t offset = action->indexOffset * ib.byteStride; if(ib.byteSize > offset) readBytes = qMin(ib.byteSize - offset, readBytes); else readBytes = 0; if(readBytes > 0) idata = r->GetBufferData(ib.resourceId, ib.byteOffset + offset, readBytes); } if(data->inConfig.indices) data->inConfig.indices->deref(); data->inConfig.indices = new BufferData(); if(action && ib.byteStride != 0 && !idata.isEmpty()) data->inConfig.indices->storage.resize( sizeof(uint32_t) * qMin(numIndices, (((uint32_t)idata.size() + ib.byteStride - 1) / ib.byteStride))); else if(action && (action->flags & ActionFlags::Indexed)) data->inConfig.indices->storage.resize(sizeof(uint32_t)); uint32_t *indices = (uint32_t *)data->inConfig.indices->data(); uint32_t maxIndex = 0; if(action) maxIndex = qMax(1U, numIndices) - 1; if(action && !idata.isEmpty()) { maxIndex = 0; if(ib.byteStride == 1) { uint8_t primRestart = data->inConfig.primRestart & 0xff; for(size_t i = 0; i < idata.size() && (uint32_t)i < numIndices; i++) { indices[i] = (uint32_t)idata[i]; if(primRestart && indices[i] == primRestart) continue; maxIndex = qMax(maxIndex, indices[i]); } } else if(ib.byteStride == 2) { uint16_t primRestart = data->inConfig.primRestart & 0xffff; uint16_t *src = (uint16_t *)idata.data(); for(size_t i = 0; i < idata.size() / sizeof(uint16_t) && (uint32_t)i < numIndices; i++) { indices[i] = (uint32_t)src[i]; if(primRestart && indices[i] == primRestart) continue; maxIndex = qMax(maxIndex, indices[i]); } } else if(ib.byteStride == 4) { uint32_t primRestart = data->inConfig.primRestart; memcpy(indices, idata.data(), qMin(idata.size(), numIndices * sizeof(uint32_t))); for(uint32_t i = 0; i < idata.size() / sizeof(uint32_t) && i < numIndices; i++) { if(primRestart && indices[i] == primRestart) continue; maxIndex = qMax(maxIndex, indices[i]); } } } int vbIdx = 0; for(BoundVBuffer vb : vbs) { bool used = false; bool pi = false; bool pv = false; uint32_t maxAttrOffset = 0; for(int c = 0; c < data->inConfig.columns.count(); c++) { const ShaderConstant &col = data->inConfig.columns[c]; const BufferElementProperties &prop = data->inConfig.props[c]; if(prop.buffer == vbIdx) { used = true; maxAttrOffset = qMax(maxAttrOffset, col.byteOffset); if(prop.perinstance) pi = true; else pv = true; } } vbIdx++; uint32_t maxIdx = 0; uint32_t offset = 0; if(used && action) { if(pi) { maxIdx = qMax(1U, action->numInstances) - 1; offset = action->instanceOffset; } if(pv) { maxIdx = qMax(maxIndex, maxIdx); offset = action->vertexOffset; if(action->baseVertex > 0) maxIdx = qMax(maxIdx, maxIdx + (uint32_t)action->baseVertex); } if(pi && pv) qCritical() << "Buffer used for both instance and vertex rendering!"; } BufferData *buf = new BufferData; if(used) { uint64_t readBytes = qMax(maxIdx, maxIdx + 1) * vb.byteStride + maxAttrOffset; // if the stride is 0, allow reading at most one float4. This will still get clamped by the // declared vertex buffer size below if(vb.byteStride == 0) readBytes += 16; offset *= vb.byteStride; if(vb.byteSize > offset) readBytes = qMin(vb.byteSize - offset, readBytes); else readBytes = 0; if(readBytes > 0) buf->storage = r->GetBufferData(vb.resourceId, vb.byteOffset + offset, readBytes); buf->stride = vb.byteStride; } // ref passes to model data->inConfig.buffers.push_back(buf); } if(data->postOut1.numIndices <= data->inConfig.numRows) { data->out1Config.numRows = data->postOut1.numIndices; data->out1Config.unclampedNumRows = 0; } else { // the vertex shader can't run any expansion, so apply the same clamping to it as we applied to // the inputs. This protects against draws with an invalid number of vertices. data->out1Config.numRows = data->inConfig.numRows; data->out1Config.unclampedNumRows = data->inConfig.unclampedNumRows; } data->out1Config.statusString = data->postOut1.status; data->out1Config.baseVertex = data->postOut1.baseVertex; data->out1Config.displayBaseVertex = data->inConfig.baseVertex; if(action && data->postOut1.indexResourceId != ResourceId() && (action->flags & ActionFlags::Indexed)) idata = r->GetBufferData(data->postOut1.indexResourceId, data->postOut1.indexByteOffset, numIndices * data->postOut1.indexByteStride); indices = NULL; if(data->out1Config.indices) data->out1Config.indices->deref(); if(data->out1Config.displayIndices) data->out1Config.displayIndices->deref(); { // display the same index values data->out1Config.displayIndices = data->inConfig.indices; data->out1Config.displayIndices->ref(); data->out1Config.indices = new BufferData(); if(action && ib.byteStride != 0 && !idata.isEmpty()) { data->out1Config.indices->storage.resize(sizeof(uint32_t) * numIndices); indices = (uint32_t *)data->out1Config.indices->data(); if(ib.byteStride == 1) { for(size_t i = 0; i < idata.size() && (uint32_t)i < numIndices; i++) indices[i] = (uint32_t)idata[i]; } else if(ib.byteStride == 2) { uint16_t *src = (uint16_t *)idata.data(); for(size_t i = 0; i < idata.size() / sizeof(uint16_t) && (uint32_t)i < numIndices; i++) indices[i] = (uint32_t)src[i]; } else if(ib.byteStride == 4) { memcpy(indices, idata.data(), qMin(idata.size(), numIndices * sizeof(uint32_t))); } } } if(data->postOut1.vertexResourceId != ResourceId()) { BufferData *postvs = new BufferData; postvs->storage = r->GetBufferData(data->postOut1.vertexResourceId, data->postOut1.vertexByteOffset, 0); postvs->stride = data->postOut1.vertexByteStride; // ref passes to model data->out1Config.buffers.push_back(postvs); } data->out2Config.statusString = data->postOut2.status; data->out2Config.numRows = data->postOut2.numIndices; data->out2Config.unclampedNumRows = 0; data->out2Config.baseVertex = data->postOut2.baseVertex; data->out2Config.displayBaseVertex = data->inConfig.baseVertex; indices = NULL; data->out2Config.indices = NULL; if(data->postOut2.vertexResourceId != ResourceId()) { BufferData *postgs = new BufferData; postgs->storage = r->GetBufferData(data->postOut2.vertexResourceId, data->postOut2.vertexByteOffset, 0); postgs->stride = data->postOut2.vertexByteStride; // ref passes to model data->out2Config.buffers.push_back(postgs); } } static int MaxNumRows(const ShaderConstant &c) { int ret = c.type.rows; if(c.type.baseType != VarType::Enum) { for(const ShaderConstant &child : c.type.members) ret = qMax(ret, MaxNumRows(child)); } return ret; } static void UnrollConstant(rdcstr prefix, uint32_t baseOffset, const ShaderConstant &constant, rdcarray &columns, rdcarray &props) { bool isArray = constant.type.elements > 1; rdcstr baseName = constant.name; if(!prefix.isEmpty()) baseName = prefix + "." + baseName; if(constant.type.baseType == VarType::Enum || constant.type.members.isEmpty()) { BufferElementProperties prop; prop.format = GetInterpretedResourceFormat(constant); ShaderConstant c = constant; c.byteOffset += baseOffset; if(isArray) { for(uint32_t a = 0; a < constant.type.elements; a++) { c.name = QFormatStr("%1[%2]").arg(baseName).arg(a); columns.push_back(c); props.push_back(prop); c.byteOffset += constant.type.arrayByteStride; } } else { c.name = baseName; columns.push_back(c); props.push_back(prop); } return; } // struct, expand by members uint32_t arraySize = qMax(1U, constant.type.elements); if(arraySize == ~0U) arraySize = 1U; for(uint32_t a = 0; a < arraySize; a++) { for(const ShaderConstant &child : constant.type.members) { UnrollConstant(isArray ? QFormatStr("%1[%2]").arg(baseName).arg(a) : QString(baseName), baseOffset + constant.byteOffset + a * constant.type.arrayByteStride, child, columns, props); } } } static void UnrollConstant(const ShaderConstant &constant, rdcarray &columns, rdcarray &props) { UnrollConstant("", 0, constant, columns, props); } QList BufferViewer::m_CBufferViews; BufferViewer::BufferViewer(ICaptureContext &ctx, bool meshview, QWidget *parent) : QFrame(parent), ui(new Ui::BufferViewer), m_Ctx(ctx) { ui->setupUi(this); ui->render->SetContext(m_Ctx); byteRangeStart = (RDSpinBox64 *)ui->byteRangeStart; byteRangeLength = (RDSpinBox64 *)ui->byteRangeLength; byteRangeStart->configure(); byteRangeLength->configure(); byteRangeStart->setMinimum(0ULL); byteRangeLength->setMinimum(0ULL); m_ModelIn = new BufferItemModel(ui->inTable, true, meshview, this); m_ModelOut1 = new BufferItemModel(ui->out1Table, false, meshview, this); m_ModelOut2 = new BufferItemModel(ui->out2Table, false, meshview, this); if(meshview) { ui->inTable->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); ui->inTable->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); ui->out1Table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); ui->out1Table->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); ui->out2Table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); ui->out2Table->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); } m_MeshDebugSelector = new ComputeDebugSelector(this); // we keep the old UI names for serialised layouts compatibility QString containerNames[] = { lit("vsinData"), lit("vsoutData"), lit("gsoutData"), }; for(size_t i = 0; i < 3; i++) { m_Containers[i] = new QWidget(this); // for layout compatibility m_Containers[i]->setObjectName(containerNames[i]); QVBoxLayout *layout = new QVBoxLayout(m_Containers[i]); layout->setSpacing(0); layout->setContentsMargins(0, 0, 0, 0); } if(meshview) { m_Containers[0]->layout()->addWidget(ui->inTable); m_Containers[0]->layout()->addWidget(ui->fixedVars); m_Containers[1]->layout()->addWidget(ui->out1Table); m_Containers[2]->layout()->addWidget(ui->out2Table); ui->fixedVars->setVisible(false); } m_MeshView = meshview; ui->formatSpecifier->setContext(&m_Ctx); m_Flycam = new FlycamWrapper(m_Ctx); m_Arcball = new ArcballWrapper(m_Ctx); m_CurrentCamera = m_Arcball; m_Output = NULL; m_Config = MeshDisplay(); m_Config.type = MeshDataStage::VSIn; m_Config.wireframeDraw = true; m_Config.exploderScale = 1.0f; ui->outputTabs->setCurrentIndex(0); m_CurStage = MeshDataStage::VSIn; ui->inTable->setFont(Formatter::FixedFont()); ui->out1Table->setFont(Formatter::FixedFont()); ui->out2Table->setFont(Formatter::FixedFont()); ui->minBoundsLabel->setFont(Formatter::FixedFont()); ui->maxBoundsLabel->setFont(Formatter::FixedFont()); ui->rowOffset->setFont(Formatter::PreferredFont()); ui->instance->setFont(Formatter::PreferredFont()); ui->viewIndex->setFont(Formatter::PreferredFont()); ui->camSpeed->setFont(Formatter::PreferredFont()); if(meshview) { SetupMeshView(); if(isMeshDraw()) { m_CurStage = MeshDataStage::TaskOut; m_Config.type = MeshDataStage::TaskOut; } } else SetupRawView(); m_ExportMenu = new QMenu(this); m_ExportCSV = new QAction(this); m_ExportCSV->setIcon(Icons::save()); m_ExportBytes = new QAction(this); m_ExportBytes->setIcon(Icons::save()); m_ExportMenu->addAction(m_ExportCSV); m_ExportMenu->addAction(m_ExportBytes); m_DebugVert = new QAction(tr("&Debug this Vertex"), this); m_DebugVert->setIcon(Icons::wrench()); m_DebugMeshThread = new QAction(tr("&Debug Mesh Thread"), this); m_DebugMeshThread->setIcon(Icons::wrench()); m_FilterMesh = new QAction(tr("&Filter to this Meshlet"), this); m_FilterMesh->setIcon(Icons::filter()); m_RemoveFilter = new QAction(tr("&Remove Filter"), this); m_RemoveFilter->setIcon(Icons::arrow_undo()); m_GotoTask = new QAction(tr("&Go to task"), this); m_GotoTask->setIcon(Icons::arrow_join()); ui->exportDrop->setMenu(m_ExportMenu); QObject::connect(m_ExportMenu, &QMenu::aboutToShow, this, &BufferViewer::updateExportActionNames); QObject::connect(m_ExportCSV, &QAction::triggered, [this] { exportData(BufferExport(BufferExport::CSV)); }); QObject::connect(m_ExportBytes, &QAction::triggered, [this] { exportData(BufferExport(BufferExport::RawBytes)); }); QObject::connect(m_DebugVert, &QAction::triggered, this, &BufferViewer::debugVertex); QObject::connect(m_DebugMeshThread, &QAction::triggered, this, &BufferViewer::debugMeshThread); QObject::connect(m_RemoveFilter, &QAction::triggered, [this]() { SetMeshFilter(MeshFilter::None); }); QObject::connect(m_FilterMesh, &QAction::triggered, [this]() { QModelIndex idx = m_CurView->selectionModel()->currentIndex(); if(!idx.isValid()) return; uint32_t taskIndex = 0, meshletIndex = 0; GetIndicesForMeshRow((uint32_t)idx.row(), taskIndex, meshletIndex); SetMeshFilter(MeshFilter::Mesh, taskIndex, meshletIndex); }); QObject::connect(m_GotoTask, &QAction::triggered, [this]() { // if there's a filter then by definition only one task is visible, just scroll to it if(m_CurMeshFilter != MeshFilter::None) { ShowMeshData(MeshDataStage::TaskOut); ScrollToRow(0, MeshDataStage::TaskOut); return; } QModelIndex idx = m_CurView->selectionModel()->currentIndex(); if(!idx.isValid()) return; uint32_t taskIndex = 0, meshletIndex = 0; GetIndicesForMeshRow((uint32_t)idx.row(), taskIndex, meshletIndex); ShowMeshData(MeshDataStage::TaskOut); ScrollToRow((int)taskIndex, MeshDataStage::TaskOut); }); QObject::connect(ui->exportDrop, &QToolButton::clicked, [this] { exportData(BufferExport(BufferExport::CSV)); }); ui->inTable->setContextMenuPolicy(Qt::CustomContextMenu); ui->out1Table->setContextMenuPolicy(Qt::CustomContextMenu); ui->out2Table->setContextMenuPolicy(Qt::CustomContextMenu); ui->fixedVars->setContextMenuPolicy(Qt::CustomContextMenu); ui->fixedVars->setFrameShape(QFrame::NoFrame); QMenu *menu = new QMenu(this); ui->inTable->setCustomHeaderSizing(true); ui->out1Table->setCustomHeaderSizing(true); ui->out2Table->setCustomHeaderSizing(true); ui->inTable->setAllowKeyboardSearches(false); ui->out1Table->setAllowKeyboardSearches(false); ui->out2Table->setAllowKeyboardSearches(false); QObject::connect(ui->fixedVars, &RDTreeWidget::customContextMenuRequested, this, &BufferViewer::fixedVars_contextMenu); QObject::connect(ui->inTable, &RDTableView::customContextMenuRequested, [this, menu](const QPoint &pos) { stageRowMenu(MeshDataStage::VSIn, menu, pos); }); menu = new QMenu(this); QObject::connect( ui->out1Table, &RDTableView::customContextMenuRequested, [this, menu](const QPoint &pos) { stageRowMenu(MeshDataStage::VSOut, menu, pos); }); menu = new QMenu(this); QObject::connect( ui->out2Table, &RDTableView::customContextMenuRequested, [this, menu](const QPoint &pos) { stageRowMenu(MeshDataStage::GSOut, menu, pos); }); ui->dockarea->setAllowFloatingWindow(false); ui->controlType->addItems({tr("Arcball"), tr("Flycam")}); ui->controlType->adjustSize(); configureDrawRange(); ui->visualisation->clear(); ui->visualisation->addItems( {tr("None"), tr("Solid Colour"), tr("Flat Shaded"), tr("Secondary"), tr("Exploded")}); ui->visualisation->adjustSize(); ui->visualisation->setCurrentIndex(0); ui->axisMappingCombo->addItems({tr("Y-up, left handed"), tr("Y-up, right handed"), tr("Z-up, left handed"), tr("Z-up, right handed"), tr("Custom...")}); ui->axisMappingCombo->setCurrentIndex(0); // wireframe only available on solid shaded options ui->wireframeRender->setEnabled(false); ui->setFormat->setVisible(false); ui->controlType->setCurrentIndex(0); on_controlType_currentIndexChanged(0); QObject::connect(ui->inTable->selectionModel(), &QItemSelectionModel::selectionChanged, this, &BufferViewer::data_selected); QObject::connect(ui->out1Table->selectionModel(), &QItemSelectionModel::selectionChanged, this, &BufferViewer::data_selected); QObject::connect(ui->out2Table->selectionModel(), &QItemSelectionModel::selectionChanged, this, &BufferViewer::data_selected); m_CurView = ui->inTable; m_CurFixed = false; QObject::connect(ui->inTable, &RDTableView::clicked, [this]() { m_CurView = ui->inTable; m_CurFixed = false; }); QObject::connect(ui->out1Table, &RDTableView::clicked, [this]() { m_CurView = ui->out1Table; }); QObject::connect(ui->out2Table, &RDTableView::clicked, [this]() { m_CurView = ui->out2Table; }); QObject::connect(ui->fixedVars, &RDTreeWidget::clicked, [this]() { m_CurView = NULL; m_CurFixed = true; }); QObject::connect(ui->inTable->verticalScrollBar(), &QScrollBar::valueChanged, this, &BufferViewer::data_scrolled); QObject::connect(ui->out1Table->verticalScrollBar(), &QScrollBar::valueChanged, this, &BufferViewer::data_scrolled); QObject::connect(ui->out2Table->verticalScrollBar(), &QScrollBar::valueChanged, this, &BufferViewer::data_scrolled); { QMenu *extensionsMenu = new QMenu(this); ui->extensions->setMenu(extensionsMenu); ui->extensions->setPopupMode(QToolButton::InstantPopup); QObject::connect(extensionsMenu, &QMenu::aboutToShow, [this, extensionsMenu]() { extensionsMenu->clear(); m_Ctx.Extensions().MenuDisplaying(m_MeshView ? PanelMenu::MeshPreview : PanelMenu::BufferViewer, extensionsMenu, ui->extensions, {}); }); } QObject::connect(ui->render, &CustomPaintWidget::mouseMove, this, &BufferViewer::render_mouseMove); QObject::connect(ui->render, &CustomPaintWidget::clicked, this, &BufferViewer::render_clicked); QObject::connect(ui->render, &CustomPaintWidget::unclicked, this, &BufferViewer::render_unclicked); QObject::connect(ui->render, &CustomPaintWidget::keyPress, this, &BufferViewer::render_keyPress); QObject::connect(ui->render, &CustomPaintWidget::keyRelease, this, &BufferViewer::render_keyRelease); QObject::connect(ui->render, &CustomPaintWidget::mouseWheel, this, &BufferViewer::render_mouseWheel); // event filter to pick up tooltip events ui->fixedVars->setTooltipElidedItems(false); ui->fixedVars->installEventFilter(this); QObject::connect(m_MeshDebugSelector, &ComputeDebugSelector::beginDebug, this, &BufferViewer::meshDebugSelector_beginDebug); Reset(); m_Ctx.AddCaptureViewer(this); } void BufferViewer::GetIndicesForMeshRow(uint32_t row, uint32_t &taskIndex, uint32_t &meshletIdx) { const BufferConfiguration &config2 = m_ModelOut2->getConfig(); auto it = std::upper_bound(config2.meshletVertexPrefixCounts.begin(), config2.meshletVertexPrefixCounts.end(), row); if(it != config2.meshletVertexPrefixCounts.begin()) it--; meshletIdx = uint32_t(it - config2.meshletVertexPrefixCounts.begin()); const BufferConfiguration &config1 = m_ModelOut1->getConfig(); taskIndex = 0; uint32_t meshletCounter = 0; for(taskIndex = 0; taskIndex < meshletIdx && taskIndex < config1.taskSizes.size(); taskIndex++) { meshletCounter += config1.taskSizes[taskIndex].x * config1.taskSizes[taskIndex].y * config1.taskSizes[taskIndex].z; if(meshletIdx < meshletCounter) break; } taskIndex += config1.taskOrMeshletOffset; meshletIdx += config2.taskOrMeshletOffset; } void BufferViewer::SetupRawView() { ui->formatSpecifier->setVisible(true); ui->outputTabs->setVisible(false); ui->out1Table->setVisible(false); ui->out2Table->setVisible(false); m_Containers[0]->setVisible(false); m_Containers[1]->setVisible(false); m_Containers[2]->setVisible(false); // hide buttons we don't want in the toolbar ui->syncViews->setVisible(false); ui->instanceLabel->setVisible(false); ui->instance->setVisible(false); ui->viewLabel->setVisible(false); ui->viewIndex->setVisible(false); ui->dockarea->setVisible(false); ui->meshFilterLabel->setVisible(false); ui->resetMeshFilterButton->setVisible(false); ui->inTable->setFrameShape(QFrame::NoFrame); ui->inTable->setPinnedColumns(1); ui->inTable->setColumnGroupRole(columnGroupRole); m_delegate = new RichTextViewDelegate(ui->inTable); ui->inTable->setItemDelegate(m_delegate); ui->inTable->viewport()->installEventFilter(this); ui->inTable->setMouseTracking(true); ui->formatSpecifier->setWindowTitle(tr("Buffer Format")); QObject::connect(ui->formatSpecifier, &BufferFormatSpecifier::processFormat, [this](const QString &format) { m_PagingByteOffset = 0; processFormat(format); }); ui->fixedVars->setColumns({tr("Name"), tr("Value"), tr("Byte Offset"), tr("Type")}); { ui->fixedVars->header()->setSectionResizeMode(0, QHeaderView::Interactive); ui->fixedVars->header()->setSectionResizeMode(1, QHeaderView::Interactive); ui->fixedVars->header()->setSectionResizeMode(2, QHeaderView::Interactive); } ui->fixedVars->setFont(Formatter::FixedFont()); m_FixedGroup = new CollapseGroupBox(this); m_RepeatedGroup = new CollapseGroupBox(this); m_RepeatedControlBar = new QFrame(this); m_RepeatedControlBar->setFrameShape(QFrame::Panel); m_RepeatedControlBar->setFrameShadow(QFrame::Raised); QHBoxLayout *controlLayout = new QHBoxLayout(m_RepeatedControlBar); controlLayout->setSpacing(2); controlLayout->setContentsMargins(6, 2, 6, 2); m_RepeatedOffset = new RDLabel(this); QFrame *line = new QFrame(this); line->setFrameShape(QFrame::VLine); line->setFrameShadow(QFrame::Sunken); controlLayout->addWidget(line); controlLayout->addWidget(m_RepeatedOffset); controlLayout->addItem(new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum)); QVBoxLayout *fixedLayout = new QVBoxLayout(m_FixedGroup); fixedLayout->setSpacing(0); fixedLayout->setContentsMargins(0, 0, 0, 0); QVBoxLayout *repeatedLayout = new QVBoxLayout(m_RepeatedGroup); repeatedLayout->setSpacing(3); repeatedLayout->setContentsMargins(2, 0, 0, 0); repeatedLayout->addWidget(m_RepeatedControlBar); m_FixedGroup->setTitle(tr("Fixed SoA data")); m_RepeatedGroup->setTitle(tr("Repeated AoS values")); m_VLayout = new QVBoxLayout(this); m_VLayout->setSpacing(3); m_VLayout->setContentsMargins(3, 3, 3, 3); m_OuterSplitter = new RDSplitter(Qt::Vertical, this); m_OuterSplitter->setHandleWidth(12); m_OuterSplitter->setChildrenCollapsible(false); m_InnerSplitter = new RDSplitter(Qt::Vertical, this); m_InnerSplitter->setHandleWidth(12); m_InnerSplitter->setChildrenCollapsible(false); m_InnerSplitter->setVisible(false); // inner splitter is only used when we have these groups, so we can add these unconditionally m_InnerSplitter->addWidget(m_FixedGroup); m_InnerSplitter->addWidget(m_RepeatedGroup); m_VLayout->addWidget(ui->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); m_VLayout->addWidget(m_OuterSplitter); } void BufferViewer::SetupMeshView() { // hide buttons we don't want in the toolbar ui->byteRangeLine->setVisible(false); ui->byteRangeStartLabel->setVisible(false); byteRangeStart->setVisible(false); ui->byteRangeLengthLabel->setVisible(false); byteRangeLength->setVisible(false); ui->meshFilterLabel->setVisible(false); ui->resetMeshFilterButton->setVisible(false); ui->fixedVars->setVisible(false); ui->showPadding->setVisible(false); ui->fixedVars->setColumns({tr("Name"), tr("Value"), tr("Type")}); { ui->fixedVars->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); ui->fixedVars->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); } ui->fixedVars->setFont(Formatter::FixedFont()); ui->resourceDetails->setVisible(false); ui->formatSpecifier->setVisible(false); ui->configurationGroup->setVisible(false); ui->minBoundsLabel->setText(lit("---")); ui->maxBoundsLabel->setText(lit("---")); ui->outputTabs->setWindowTitle(tr("Preview")); ui->dockarea->addToolWindow(ui->outputTabs, ToolWindowManager::EmptySpace); ui->dockarea->setToolWindowProperties(ui->outputTabs, ToolWindowManager::HideCloseButton); ui->inTable->setFrameShape(QFrame::NoFrame); ui->dockarea->addToolWindow( m_Containers[0], ToolWindowManager::AreaReference( ToolWindowManager::TopOf, ui->dockarea->areaOf(ui->outputTabs), 0.5f)); ui->dockarea->setToolWindowProperties(m_Containers[0], ToolWindowManager::HideCloseButton); ui->out1Table->setFrameShape(QFrame::NoFrame); ui->dockarea->addToolWindow( m_Containers[1], ToolWindowManager::AreaReference( ToolWindowManager::RightOf, ui->dockarea->areaOf(m_Containers[0]), 0.5f)); ui->dockarea->setToolWindowProperties(m_Containers[1], ToolWindowManager::HideCloseButton); ui->out2Table->setFrameShape(QFrame::NoFrame); ui->dockarea->addToolWindow( m_Containers[2], ToolWindowManager::AreaReference( ToolWindowManager::AddTo, ui->dockarea->areaOf(m_Containers[1]), 0.5f)); ui->dockarea->setToolWindowProperties(m_Containers[2], ToolWindowManager::HideCloseButton); ToolWindowManager::raiseToolWindow(m_Containers[1]); updateLabelsAndLayout(); m_HeaderMenu = new QMenu(this); m_ResetColumnSel = new QAction(tr("Reset Selected Columns"), this); m_SelectPosColumn = new QAction(tr("Select as Position"), this); m_SelectSecondColumn = new QAction(tr("Select as Secondary"), this); m_SelectSecondAlphaColumn = new QAction(tr("Select Alpha as Secondary"), this); m_HeaderMenu->addAction(m_ResetColumnSel); m_HeaderMenu->addSeparator(); m_HeaderMenu->addAction(m_SelectPosColumn); m_HeaderMenu->addAction(m_SelectSecondColumn); m_HeaderMenu->addAction(m_SelectSecondAlphaColumn); QObject::connect(m_ResetColumnSel, &QAction::triggered, [this]() { BufferItemModel *model = (BufferItemModel *)m_CurView->model(); model->setPosColumn(-1); model->setSecondaryColumn(-1, m_Config.visualisationMode == Visualisation::Secondary, false); UI_ConfigureFormats(); on_resetCamera_clicked(); UpdateCurrentMeshConfig(); INVOKE_MEMFN(RT_UpdateAndDisplay); }); QObject::connect(m_SelectPosColumn, &QAction::triggered, [this]() { BufferItemModel *model = (BufferItemModel *)m_CurView->model(); model->setPosColumn(m_ContextColumn); UI_ConfigureFormats(); on_resetCamera_clicked(); UpdateCurrentMeshConfig(); INVOKE_MEMFN(RT_UpdateAndDisplay); }); QObject::connect(m_SelectSecondColumn, &QAction::triggered, [this]() { BufferItemModel *model = (BufferItemModel *)m_CurView->model(); model->setSecondaryColumn(m_ContextColumn, m_Config.visualisationMode == Visualisation::Secondary, false); UI_ConfigureFormats(); UpdateCurrentMeshConfig(); INVOKE_MEMFN(RT_UpdateAndDisplay); }); QObject::connect(m_SelectSecondAlphaColumn, &QAction::triggered, [this]() { BufferItemModel *model = (BufferItemModel *)m_CurView->model(); model->setSecondaryColumn(m_ContextColumn, m_Config.visualisationMode == Visualisation::Secondary, true); UI_ConfigureFormats(); UpdateCurrentMeshConfig(); INVOKE_MEMFN(RT_UpdateAndDisplay); }); ui->inTable->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); ui->out1Table->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); ui->out2Table->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); ui->inTable->setPinnedColumns(2); ui->out1Table->setPinnedColumns(2); ui->out2Table->setPinnedColumns(2); ui->inTable->setColumnGroupRole(columnGroupRole); ui->out1Table->setColumnGroupRole(columnGroupRole); ui->out2Table->setColumnGroupRole(columnGroupRole); QObject::connect(ui->inTable->horizontalHeader(), &QHeaderView::customContextMenuRequested, [this](const QPoint &pos) { meshHeaderMenu(MeshDataStage::VSIn, pos); }); QObject::connect(ui->out1Table->horizontalHeader(), &QHeaderView::customContextMenuRequested, [this](const QPoint &pos) { meshHeaderMenu(MeshDataStage::VSOut, pos); }); QObject::connect(ui->out2Table->horizontalHeader(), &QHeaderView::customContextMenuRequested, [this](const QPoint &pos) { meshHeaderMenu(MeshDataStage::GSOut, pos); }); QVBoxLayout *vertical = new QVBoxLayout(this); vertical->setSpacing(3); vertical->setContentsMargins(3, 3, 3, 3); vertical->addWidget(ui->meshToolbar); vertical->addWidget(ui->dockarea); QTimer *renderTimer = new QTimer(this); QObject::connect(renderTimer, &QTimer::timeout, this, &BufferViewer::render_timer); renderTimer->setSingleShot(false); renderTimer->setInterval(10); renderTimer->start(); } void BufferViewer::meshHeaderMenu(MeshDataStage stage, const QPoint &pos) { int col = tableForStage(stage)->horizontalHeader()->logicalIndexAt(pos); if(col < 2) return; m_CurView = tableForStage(stage); m_CurFixed = false; m_ContextColumn = modelForStage(stage)->elementIndexForColumn(col); bool perPrim = modelForStage(stage)->propForColumn(col).perprimitive; if(perPrim) { m_SelectPosColumn->setEnabled(false); m_SelectSecondColumn->setEnabled(false); m_SelectSecondAlphaColumn->setEnabled(false); } else { m_SelectPosColumn->setEnabled(true); m_SelectSecondColumn->setEnabled(true); m_SelectSecondAlphaColumn->setEnabled(modelForStage(stage)->elementForColumn(col).type.columns == 4); } m_HeaderMenu->popup(tableForStage(stage)->horizontalHeader()->mapToGlobal(pos)); } void BufferViewer::fixedVars_contextMenu(const QPoint &pos) { RDTreeWidgetItem *item = ui->fixedVars->itemAt(pos); m_CurView = NULL; m_CurFixed = true; updateExportActionNames(); QMenu contextMenu(this); QAction expandAll(tr("&Expand All"), this); QAction collapseAll(tr("C&ollapse All"), this); QAction copy(tr("&Copy"), this); QAction showPadding(tr("&Show Padding"), this); QAction removeFilter(tr("&Remove Filter"), this); QAction filterTask(tr("&Filter to this Task"), this); QAction gotoMesh(tr("&Go to meshes"), this); expandAll.setIcon(Icons::arrow_out()); collapseAll.setIcon(Icons::arrow_in()); copy.setIcon(Icons::copy()); removeFilter.setIcon(Icons::arrow_undo()); filterTask.setIcon(Icons::filter()); gotoMesh.setIcon(Icons::arrow_join()); showPadding.setCheckable(true); showPadding.setChecked(ui->showPadding->isChecked()); expandAll.setEnabled(item && item->childCount() > 0); removeFilter.setEnabled(item && m_CurMeshFilter != MeshFilter::None); filterTask.setEnabled(item); gotoMesh.setEnabled(item); collapseAll.setEnabled(expandAll.isEnabled()); contextMenu.addAction(&expandAll); contextMenu.addAction(&collapseAll); contextMenu.addAction(©); contextMenu.addSeparator(); int idx = ui->fixedVars->indexOfTopLevelItem(item); if(m_MeshView) { contextMenu.addAction(&removeFilter); contextMenu.addAction(&filterTask); contextMenu.addAction(&gotoMesh); const BufferConfiguration &config1 = m_ModelOut1->getConfig(); // if we're already filtering to a task, don't offer to filter any more. However if we're // filtered to a mesh allow 'broadening' the filter back to the task // also don't allow filtering at all if there is no task shader bound filterTask.setEnabled(!config1.taskSizes.empty() && m_CurMeshFilter != MeshFilter::TaskGroup); if(config1.taskSizes.empty() || config1.taskSizes[idx].x * config1.taskSizes[idx].y * config1.taskSizes[idx].z == 0) gotoMesh.setEnabled(false); // if there's a filter don't enable goto mesh as normally we just scroll to the first mesh - it // would be redundant and potentially annoying to be able to and doesn't do anything useful if(m_CurMeshFilter != MeshFilter::None) gotoMesh.setEnabled(false); } else { contextMenu.addAction(&showPadding); } contextMenu.addSeparator(); contextMenu.addAction(m_ExportCSV); contextMenu.addAction(m_ExportBytes); QObject::connect(&removeFilter, &QAction::triggered, [this]() { SetMeshFilter(MeshFilter::None); }); QObject::connect(&filterTask, &QAction::triggered, [this, idx]() { // if there's no filter, select this task. If we were mesh filtering, filter back to all meshes // under the current task (don't use idx there, since it will just be 0) if(m_CurMeshFilter == MeshFilter::None) SetMeshFilter(MeshFilter::TaskGroup, idx); else SetMeshFilter(MeshFilter::TaskGroup, m_FilteredTaskGroup); }); QObject::connect(&gotoMesh, &QAction::triggered, [this, idx]() { const BufferConfiguration &config1 = m_ModelOut1->getConfig(); uint32_t meshletIndex = 0; for(int i = 0; i < idx && i < config1.taskSizes.count(); i++) { meshletIndex += config1.taskSizes[i].x * config1.taskSizes[i].y * config1.taskSizes[i].z; } const BufferConfiguration &config2 = m_ModelOut2->getConfig(); uint32_t vertexOffset = config2.meshletVertexPrefixCounts[meshletIndex]; ShowMeshData(MeshDataStage::MeshOut); ScrollToRow((int)vertexOffset, MeshDataStage::MeshOut); }); QObject::connect(&expandAll, &QAction::triggered, [this, item]() { ui->fixedVars->expandAllItems(item); }); QObject::connect(&collapseAll, &QAction::triggered, [this, item]() { ui->fixedVars->collapseAllItems(item); }); QObject::connect(©, &QAction::triggered, [this, item, pos]() { ui->fixedVars->copyItem(pos, item); }); QObject::connect(&showPadding, &QAction::triggered, [this]() { ui->showPadding->setChecked(!ui->showPadding->isChecked()); }); RDDialog::show(&contextMenu, ui->fixedVars->viewport()->mapToGlobal(pos)); } void BufferViewer::stageRowMenu(MeshDataStage stage, QMenu *menu, const QPoint &pos) { m_CurView = tableForStage(stage); m_CurFixed = false; updateExportActionNames(); menu->clear(); menu->setToolTipsVisible(true); QModelIndex idx = m_CurView->selectionModel()->currentIndex(); const ActionDescription *action = m_Ctx.CurAction(); if(action && (action->flags & ActionFlags::MeshDispatch)) { if(stage == MeshDataStage::GSOut) { const BufferConfiguration &config = m_ModelOut2->getConfig(); auto it = std::upper_bound(config.meshletVertexPrefixCounts.begin(), config.meshletVertexPrefixCounts.end(), (uint32_t)idx.row()); if(it != config.meshletVertexPrefixCounts.begin()) it--; size_t meshletIdx = it - config.meshletVertexPrefixCounts.begin(); m_RemoveFilter->setEnabled(m_CurMeshFilter != MeshFilter::None); menu->addAction(m_RemoveFilter); menu->addAction(m_FilterMesh); menu->addAction(m_GotoTask); const ShaderReflection *shaderDetails = m_Ctx.CurPipelineState().GetShaderReflection(ShaderStage::Mesh); m_DebugMeshThread->setEnabled(false); if(!m_Ctx.APIProps().shaderDebugging) { m_DebugMeshThread->setToolTip(tr("This API does not support shader debugging")); } else if(!m_Ctx.CurAction() || !(m_Ctx.CurAction()->flags & (ActionFlags::Drawcall | ActionFlags::MeshDispatch))) { m_DebugMeshThread->setToolTip(tr("No draw call selected")); } else if(!shaderDetails) { m_DebugMeshThread->setToolTip(tr("No mesh shader bound")); } else if(!shaderDetails->debugInfo.debuggable) { m_DebugMeshThread->setToolTip( tr("This shader doesn't support debugging: %1").arg(shaderDetails->debugInfo.debugStatus)); } else { m_DebugMeshThread->setEnabled(true); m_DebugMeshThread->setToolTip(QString()); } menu->addAction(m_DebugMeshThread); menu->addSeparator(); m_GotoTask->setEnabled(m_Ctx.CurPipelineState().GetShaderReflection(ShaderStage::Task)); } } if(m_MeshView && stage != MeshDataStage::GSOut) { const ShaderReflection *shaderDetails = m_Ctx.CurPipelineState().GetShaderReflection(ShaderStage::Vertex); m_DebugVert->setEnabled(false); if(!m_Ctx.APIProps().shaderDebugging) { m_DebugVert->setToolTip(tr("This API does not support shader debugging")); } else if(!m_Ctx.CurAction() || !(m_Ctx.CurAction()->flags & (ActionFlags::Drawcall | ActionFlags::MeshDispatch))) { m_DebugVert->setToolTip(tr("No draw call selected")); } else if(!shaderDetails) { m_DebugVert->setToolTip(tr("No vertex shader bound")); } else if(!shaderDetails->debugInfo.debuggable) { m_DebugVert->setToolTip( tr("This shader doesn't support debugging: %1").arg(shaderDetails->debugInfo.debugStatus)); } else { m_DebugVert->setEnabled(true); m_DebugVert->setToolTip(QString()); } menu->addAction(m_DebugVert); menu->addSeparator(); } menu->addAction(m_ExportCSV); menu->addAction(m_ExportBytes); menu->popup(m_CurView->viewport()->mapToGlobal(pos)); ContextMenu contextMenu = ContextMenu::MeshPreview_VSInVertex; if(stage == MeshDataStage::VSOut) contextMenu = ContextMenu::MeshPreview_VSOutVertex; else if(stage == MeshDataStage::GSOut) contextMenu = ContextMenu::MeshPreview_GSOutVertex; else if(stage == MeshDataStage::TaskOut) contextMenu = ContextMenu::MeshPreview_TaskOutVertex; else if(stage == MeshDataStage::MeshOut) contextMenu = ContextMenu::MeshPreview_MeshOutVertex; ExtensionCallbackData callbackdata = {make_pyarg("stage", (uint32_t)stage)}; if(idx.isValid()) { uint32_t vertid = m_CurView->model()->data(m_CurView->model()->index(idx.row(), 0), Qt::DisplayRole).toUInt(); uint32_t index = m_CurView->model()->data(m_CurView->model()->index(idx.row(), 1), Qt::DisplayRole).toUInt(); callbackdata.push_back(make_pyarg("vertex", vertid)); callbackdata.push_back(make_pyarg("index", index)); } m_Ctx.Extensions().MenuDisplaying(contextMenu, menu, callbackdata); } BufferViewer::~BufferViewer() { if(m_Output) { m_Ctx.Replay().BlockInvoke([this](IReplayController *r) { m_Output->Shutdown(); }); } delete m_Arcball; delete m_Flycam; if(m_MeshView) m_Ctx.BuiltinWindowClosed(this); m_Ctx.RemoveCaptureViewer(this); delete ui; m_CBufferViews.removeOne(this); } void BufferViewer::OnCaptureLoaded() { Reset(); if(!m_MeshView) return; WindowingData winData = ui->render->GetWidgetWindowingData(); m_Ctx.Replay().BlockInvoke([winData, this](IReplayController *r) { m_Output = r->CreateOutput(winData, ReplayOutputType::Mesh); ui->render->SetOutput(m_Output); RT_UpdateAndDisplay(r); }); } void BufferViewer::OnCaptureClosed() { Reset(); if(!m_MeshView) ToolWindowManager::closeToolWindow(this); } void BufferViewer::FillScrolls(PopulateBufferData *bufdata) { bufdata->inHoriz = ui->inTable->horizontalScrollBar()->value(); bufdata->out1Horiz = ui->out1Table->horizontalScrollBar()->value(); bufdata->out2Horiz = ui->out2Table->horizontalScrollBar()->value(); bufdata->inVert = ui->inTable->indexAt(QPoint(0, 0)).row(); bufdata->out1Vert = ui->out1Table->indexAt(QPoint(0, 0)).row(); bufdata->out2Vert = ui->out2Table->indexAt(QPoint(0, 0)).row(); if(bufdata->meshDispatch) { bufdata->out1Horiz = ui->fixedVars->horizontalScrollBar()->value(); bufdata->out1Vert = ui->fixedVars->indexOfTopLevelItem(ui->fixedVars->itemAt(QPoint(0, 0))); } } void BufferViewer::OnEventChanged(uint32_t eventId) { PopulateBufferData *bufdata = new PopulateBufferData; m_Sequence++; bufdata->sequence = m_Sequence; if(m_Scrolls) { bufdata->inHoriz = m_Scrolls->inHoriz; bufdata->out1Horiz = m_Scrolls->out1Horiz; bufdata->out2Horiz = m_Scrolls->out2Horiz; bufdata->inVert = m_Scrolls->inVert; bufdata->out1Vert = m_Scrolls->out1Vert; bufdata->out2Vert = m_Scrolls->out2Vert; delete m_Scrolls; m_Scrolls = NULL; } else { FillScrolls(bufdata); } // remove any pending scrolls, which have been applied. If nothing changes over the data // population the above scroll preserving will work. // however if m_Scroll is set while data is populationg, we'll apply it when it comes to the end m_Scroll[(int)MeshDataStage::VSIn] = QPoint(-1, -1); m_Scroll[(int)MeshDataStage::VSOut] = QPoint(-1, -1); m_Scroll[(int)MeshDataStage::GSOut] = QPoint(-1, -1); bufdata->highlightNames[0] = m_ModelIn->posName(); bufdata->highlightNames[1] = m_ModelIn->secondaryName(); bufdata->highlightNames[2] = m_ModelOut1->posName(); bufdata->highlightNames[3] = m_ModelOut1->secondaryName(); bufdata->highlightNames[4] = m_ModelOut2->posName(); bufdata->highlightNames[5] = m_ModelOut2->secondaryName(); const ActionDescription *action = m_Ctx.CurAction(); bufdata->meshDispatch = action && (action->flags & ActionFlags::MeshDispatch); configureDrawRange(); if(m_MeshView) { ClearModels(); CalcColumnWidth(); ClearModels(); const PipeState &pipe = m_Ctx.CurPipelineState(); if(pipe.IsRestartEnabled() && action && (action->flags & ActionFlags::Indexed)) { bufdata->inConfig.primRestart = pipe.GetRestartIndex(); if(pipe.GetIBuffer().byteStride == 1) bufdata->inConfig.primRestart &= 0xff; else if(pipe.GetIBuffer().byteStride == 2) bufdata->inConfig.primRestart &= 0xffff; bufdata->out1Config.primRestart = bufdata->inConfig.primRestart; // GS Out doesn't use primitive restart because it is post-expansion } ConfigureColumns(m_Ctx, bufdata); Viewport vp = m_Ctx.CurPipelineState().GetViewport(0); float vpWidth = qAbs(vp.width); float vpHeight = qAbs(vp.height); m_Config.fov = m_ProjGuess.fov; m_Config.aspect = (vpWidth > 0.0f && vpHeight > 0.0f) ? (vpWidth / vpHeight) : 1.0f; m_Config.highlightVert = 0; if(m_ProjGuess.aspect > 0.0) m_Config.aspect = m_ProjGuess.aspect; } else { // update with the current cbuffer for the current slot if(IsCBufferView()) { UsedDescriptor cb = m_Ctx.CurPipelineState().GetConstantBlock( m_CBufferSlot.stage, m_CBufferSlot.slot, m_CBufferSlot.arrayIdx); m_BufferID = cb.descriptor.resource; m_ByteOffset = cb.descriptor.byteOffset; m_ByteSize = cb.descriptor.byteSize; const ShaderReflection *reflection = m_Ctx.CurPipelineState().GetShaderReflection(m_CBufferSlot.stage); bufdata->cb.valid = (reflection != NULL && m_CBufferSlot.slot < reflection->constantBlocks.size()); if(bufdata->cb.valid) { bufdata->cb.bytesBacked = reflection->constantBlocks[m_CBufferSlot.slot].bufferBacked || reflection->constantBlocks[m_CBufferSlot.slot].inlineDataBytes; bufdata->cb.compileConstants = reflection->constantBlocks[m_CBufferSlot.slot].compileConstants; } ui->setFormat->setEnabled(bufdata->cb.bytesBacked); if(ui->setFormat->isEnabled()) ui->setFormat->setToolTip(tr("Specify a custom format for this constant buffer")); else ui->setFormat->setToolTip(tr("Cannot specify custom format without backing memory")); bufdata->cb.pipe = m_CBufferSlot.stage == ShaderStage::Compute ? m_Ctx.CurPipelineState().GetComputePipelineObject() : m_Ctx.CurPipelineState().GetGraphicsPipelineObject(); bufdata->cb.shader = m_Ctx.CurPipelineState().GetShader(m_CBufferSlot.stage); bufdata->cb.entryPoint = m_Ctx.CurPipelineState().GetShaderEntryPoint(m_CBufferSlot.stage); if(m_Format.isEmpty()) { // stage, slot, and array index are all invariant when viewing a constant buffer // ee only need to use the actual bound shader as a key. RDTreeViewExpansionState &prevShaderExpansionState = ui->fixedVars->getInternalExpansion(qHash(ToQStr(m_CurCBuffer.shader))); ui->fixedVars->saveExpansion(prevShaderExpansionState, 0); } } ParsedFormat parsed = BufferFormatter::ParseFormatString(m_Format, m_ByteSize, IsCBufferView()); bufdata->inConfig.fixedVars = parsed.fixed; bufdata->inConfig.packing = parsed.packing; if(parsed.repeating.type.baseType != VarType::Unknown) { bufdata->inConfig.repeatStride = parsed.repeating.type.arrayByteStride; bufdata->inConfig.repeatOffset = parsed.repeating.byteOffset; UnrollConstant(parsed.repeating, bufdata->inConfig.columns, bufdata->inConfig.props); } else { bufdata->inConfig.repeatStride = 1U; bufdata->inConfig.repeatOffset = parsed.fixed.type.arrayByteStride; } if((m_Format.isEmpty() || !bufdata->cb.bytesBacked) && IsCBufferView()) { if(bufdata->cb.valid) { const ShaderReflection *reflection = m_Ctx.CurPipelineState().GetShaderReflection(m_CBufferSlot.stage); bufdata->inConfig.fixedVars.type.members = reflection->constantBlocks[m_CBufferSlot.slot].variables; if(IsD3D(m_Ctx.APIProps().pipelineType)) bufdata->inConfig.packing = Packing::D3DCB; else bufdata->inConfig.packing = BufferFormatter::EstimatePackingRules( reflection->resourceId, bufdata->inConfig.fixedVars.type.members); } } ClearModels(); } updateLabelsAndLayout(); bufdata->inConfig.curInstance = bufdata->out1Config.curInstance = bufdata->out2Config.curInstance = m_Config.curInstance; bufdata->inConfig.curView = bufdata->out1Config.curView = bufdata->out2Config.curView = m_Config.curView; m_ModelIn->beginReset(); m_ModelOut1->beginReset(); m_ModelOut2->beginReset(); bufdata->inConfig.baseVertex = action ? action->baseVertex : 0; ui->formatSpecifier->setEnabled(!IsCBufferView() || bufdata->cb.bytesBacked); ui->instance->setEnabled(action && (action->flags & ActionFlags::Instanced)); if(!ui->instance->isEnabled()) ui->instance->setValue(0); if(action) ui->instance->setMaximum(qMax(0, int(action->numInstances) - 1)); uint32_t numViews = m_Ctx.CurPipelineState().MultiviewBroadcastCount(); if(action && numViews > 1) { ui->viewIndex->setEnabled(true); ui->viewIndex->setMaximum(qMax(0, int(numViews) - 1)); } else { ui->viewIndex->setEnabled(false); ui->viewIndex->setValue(0); } QPointer me(this); m_Ctx.Replay().AsyncInvoke([this, me, bufdata](IReplayController *r) { if(!me) { delete bufdata; return; } BufferData *buf = NULL; if(m_MeshView) { if(bufdata->meshDispatch) { bufdata->postOut1 = r->GetPostVSData(0, bufdata->inConfig.curView, MeshDataStage::TaskOut); bufdata->postOut2 = r->GetPostVSData(0, bufdata->inConfig.curView, MeshDataStage::MeshOut); const uint32_t vertsPerPrim = RENDERDOC_NumVerticesPerPrimitive(bufdata->postOut2.topology); // apply mesh/task filtering to mesh data here, which will also propagate to preview if(m_FilteredMeshGroup != ~0U) { bufdata->out1Config.taskOrMeshletOffset = m_FilteredTaskGroup; // find this meshlet's offset in the index buffer and filter to only it uint32_t indexCount = 0, vertexCount = 0; for(uint32_t i = 0; i <= m_FilteredMeshGroup && i < bufdata->postOut2.meshletSizes.size(); i++) { MeshletSize meshletSize = bufdata->postOut2.meshletSizes[i]; uint32_t numIndices = meshletSize.numIndices; if(i == m_FilteredMeshGroup) { bufdata->postOut2.meshletIndexOffset = vertexCount; bufdata->postOut2.meshletOffset = m_FilteredMeshGroup; bufdata->out2Config.taskOrMeshletOffset = m_FilteredMeshGroup; bufdata->postOut2.numIndices = numIndices; bufdata->postOut2.meshletSizes = {meshletSize}; bufdata->postOut2.indexByteOffset += indexCount * bufdata->postOut2.indexByteStride; bufdata->postOut2.perPrimitiveOffset += (indexCount / vertsPerPrim) * bufdata->postOut2.perPrimitiveStride; } indexCount += numIndices; vertexCount += meshletSize.numVertices; } } else if(m_FilteredTaskGroup != ~0U) { bufdata->out1Config.taskOrMeshletOffset = m_FilteredTaskGroup; // find the relevant task and which mesh indices it corresponds to uint32_t meshletCounter = 0; for(uint32_t taskIndex = 0; taskIndex <= m_FilteredTaskGroup && taskIndex < bufdata->postOut1.taskSizes.size(); taskIndex++) { uint32_t numMeshesInTask = bufdata->postOut1.taskSizes[taskIndex].x * bufdata->postOut1.taskSizes[taskIndex].y * bufdata->postOut1.taskSizes[taskIndex].z; // once we've found the desired task, filter our view to only its meshes if(taskIndex == m_FilteredTaskGroup) { bufdata->postOut2.numIndices = 0; rdcarray meshletSizes; meshletSizes.reserve(numMeshesInTask); uint32_t indexCount = 0, vertexCount = 0; for(uint32_t i = 0; i < meshletCounter + numMeshesInTask && i < bufdata->postOut2.meshletSizes.size(); i++) { uint32_t indicesInMeshlet = bufdata->postOut2.meshletSizes[i].numIndices; if(i >= meshletCounter) { bufdata->postOut2.numIndices += indicesInMeshlet; meshletSizes.push_back(bufdata->postOut2.meshletSizes[i]); } if(i == meshletCounter) { bufdata->postOut2.meshletIndexOffset = vertexCount; bufdata->postOut2.meshletOffset = meshletCounter; bufdata->out2Config.taskOrMeshletOffset = meshletCounter; bufdata->postOut2.indexByteOffset += indexCount * bufdata->postOut2.indexByteStride; bufdata->postOut2.perPrimitiveOffset += (indexCount / vertsPerPrim) * bufdata->postOut2.perPrimitiveStride; } indexCount += indicesInMeshlet; vertexCount += bufdata->postOut2.meshletSizes[i].numVertices; } bufdata->postOut2.meshletSizes = meshletSizes; break; } meshletCounter += numMeshesInTask; } } RT_FetchMeshPipeData(r, m_Ctx, bufdata); } else { bufdata->postOut1 = r->GetPostVSData(bufdata->inConfig.curInstance, bufdata->inConfig.curView, MeshDataStage::VSOut); bufdata->postOut2 = r->GetPostVSData(bufdata->inConfig.curInstance, bufdata->inConfig.curView, MeshDataStage::GSOut); RT_FetchVertexPipeData(r, m_Ctx, bufdata); } if(!me) { delete bufdata; return; } } else { buf = new BufferData; // calculate tight stride buf->stride = std::max(1U, bufdata->inConfig.repeatStride); // we want to fetch the data for fixed and repeated sections (either of which might be 0) // but calculate the number of rows etc for the repeated sections based on just the data // available for it const uint64_t fixedLength = bufdata->inConfig.repeatOffset; // the "permanent" repeated range starts after the fixed data and goes for m_ByteSize uint64_t repeatedRangeStart = m_ByteOffset + fixedLength; uint64_t repeatedRangeEnd = m_ByteOffset + m_ByteSize; // if the byte size is unbounded, the end is unbounded - fix the potential overflow from // adding the offset if(m_ByteSize == UINT64_MAX) repeatedRangeEnd = UINT64_MAX; // get the underlying buffer length uint64_t bufferLength = 0; if(m_IsBuffer && m_BufferID != ResourceId()) { const BufferDescription *desc = m_Ctx.GetBuffer(m_BufferID); if(desc) bufferLength = desc->length; } // clamp the range to the buffer length, which may end up with it being empty repeatedRangeEnd = qMin(repeatedRangeEnd, bufferLength); repeatedRangeStart = qMin(repeatedRangeStart, bufferLength); // store the number of rows unclamped without the paging window bufdata->inConfig.unclampedNumRows = uint32_t((repeatedRangeEnd - repeatedRangeStart + buf->stride - 1) / buf->stride); // advance the range by the paging offset repeatedRangeStart = qMin(repeatedRangeEnd, repeatedRangeStart + m_PagingByteOffset); // calculate the length clamped to the MaxVisibleRows const uint64_t clampedRepeatedLength = qMin(repeatedRangeEnd - repeatedRangeStart, uint64_t(buf->stride * (MaxVisibleRows + 2))); if(m_IsBuffer) { if(m_BufferID == ResourceId()) { buf->storage.clear(); } else if(repeatedRangeStart > fixedLength) { // if the repeated range subsection we're fetching is paged further in, we still need to // fetch the fixed data from the 'start' if(fixedLength > 0) buf->storage = r->GetBufferData(m_BufferID, m_ByteOffset, fixedLength); // then append the data from where we're paged to buf->storage.append(r->GetBufferData(m_BufferID, repeatedRangeStart, clampedRepeatedLength)); } else { // otherwise we can fetch it all at once buf->storage = r->GetBufferData(m_BufferID, m_ByteOffset, fixedLength + clampedRepeatedLength); } } else { buf->storage = r->GetTextureData(m_BufferID, m_TexSub); // recalculate total size for this subresource based on the data returned if(!buf->storage.empty()) m_ObjectByteSize = buf->storage.size(); } uint32_t repeatedDataAvailable = uint32_t(buf->size()); if(repeatedDataAvailable > fixedLength) repeatedDataAvailable -= fixedLength; bufdata->inConfig.pagingOffset = uint32_t(m_PagingByteOffset / buf->stride); bufdata->inConfig.numRows = uint32_t((repeatedDataAvailable + buf->stride - 1) / buf->stride); // ownership passes to model bufdata->inConfig.buffers.push_back(buf); if(!me) { delete buf; delete bufdata; return; } } // for cbuffers, if the format is empty or if we're not buffer-backed and don't have inline // data, we evaluate variables here and don't use the format override with a fetched buffer if((m_Format.isEmpty() || !bufdata->cb.bytesBacked) && IsCBufferView()) { // only fetch the cbuffer constants if this binding is currently valid if(bufdata->cb.valid) bufdata->inConfig.evalVars = r->GetCBufferVariableContents( bufdata->cb.pipe, bufdata->cb.shader, m_CBufferSlot.stage, bufdata->cb.entryPoint, m_CBufferSlot.slot, m_BufferID, m_ByteOffset, m_ByteSize); } GUIInvoke::call(this, [this, bufdata]() { if(bufdata->sequence != m_Sequence) { delete bufdata; return; } if(!bufdata->out1Config.statusString.isEmpty()) { bufdata->out1Config.columns.clear(); bufdata->out1Config.props.clear(); ConfigureStatusColumn(bufdata->out1Config.columns, bufdata->out1Config.props); } if(!bufdata->out2Config.statusString.isEmpty()) { bufdata->out2Config.columns.clear(); bufdata->out2Config.props.clear(); ConfigureStatusColumn(bufdata->out2Config.columns, bufdata->out2Config.props); } m_ModelIn->endReset(bufdata->inConfig); m_ModelOut1->endReset(bufdata->out1Config); m_ModelOut2->endReset(bufdata->out2Config); m_Out1Data = bufdata->postOut1; m_Out2Data = bufdata->postOut2; m_CurCBuffer = bufdata->cb; // if we didn't have a position column selected before, or the name has changed, re-guess if(m_ModelIn->posColumn() == -1 || bufdata->highlightNames[0] != bufdata->inConfig.columnName(m_ModelIn->posColumn())) m_ModelIn->setPosColumn(-1); // similarly for secondary columns if(m_ModelIn->secondaryColumn() == -1 || bufdata->highlightNames[1] != bufdata->inConfig.columnName(m_ModelIn->secondaryColumn())) m_ModelIn->setSecondaryColumn(-1, m_Config.visualisationMode == Visualisation::Secondary, false); // and as above for VS Out / GS Out if(m_ModelOut1->posColumn() == -1 || bufdata->highlightNames[2] != bufdata->out1Config.columnName(m_ModelOut1->posColumn())) m_ModelOut1->setPosColumn(-1); if(m_ModelOut1->secondaryColumn() == -1 || bufdata->highlightNames[3] != bufdata->out1Config.columnName(m_ModelOut1->secondaryColumn())) m_ModelOut1->setSecondaryColumn(-1, m_Config.visualisationMode == Visualisation::Secondary, false); if(m_ModelOut2->posColumn() == -1 || bufdata->highlightNames[4] != bufdata->out2Config.columnName(m_ModelOut2->posColumn())) m_ModelOut2->setPosColumn(-1); if(m_ModelOut2->secondaryColumn() == -1 || bufdata->highlightNames[5] != bufdata->out2Config.columnName(m_ModelOut2->secondaryColumn())) m_ModelOut2->setSecondaryColumn(-1, m_Config.visualisationMode == Visualisation::Secondary, false); UpdateStageDataControls(); populateBBox(bufdata); UI_ConfigureFormats(); UpdateCurrentMeshConfig(); ApplyRowAndColumnDims( m_ModelIn->columnCount(), ui->inTable, bufdata->inConfig.statusString.isEmpty() ? m_DataColWidth : m_ErrorColWidth); ApplyRowAndColumnDims( m_ModelOut1->columnCount(), ui->out1Table, bufdata->out1Config.statusString.isEmpty() ? m_DataColWidth : m_ErrorColWidth); ApplyRowAndColumnDims( m_ModelOut2->columnCount(), ui->out2Table, bufdata->out2Config.statusString.isEmpty() ? m_DataColWidth : m_ErrorColWidth); uint32_t numRows = qMax(qMax(bufdata->inConfig.numRows, bufdata->out1Config.numRows), bufdata->out2Config.numRows); if(!m_MeshView) numRows = qMax(numRows, bufdata->inConfig.unclampedNumRows); ui->rowOffset->setMaximum((int)qMax(1U, numRows) - 1); ScrollToRow(ui->inTable, qMin(int(bufdata->inConfig.numRows) - 1, bufdata->inVert)); ScrollToRow(ui->out1Table, qMin(int(bufdata->out1Config.numRows) - 1, bufdata->out1Vert)); ScrollToRow(ui->out2Table, qMin(int(bufdata->out2Config.numRows) - 1, bufdata->out2Vert)); ui->inTable->horizontalScrollBar()->setValue(bufdata->inHoriz); ui->out1Table->horizontalScrollBar()->setValue(bufdata->out1Horiz); ui->out2Table->horizontalScrollBar()->setValue(bufdata->out2Horiz); for(MeshDataStage stage : {MeshDataStage::VSIn, MeshDataStage::VSOut, MeshDataStage::GSOut}) { int i = (int)stage; if(m_Scroll[i].y() >= 0) ScrollToRow(tableForStage(stage), m_Scroll[i].y()); if(m_Scroll[i].x() >= 0) ScrollToColumn(tableForStage(stage), m_Scroll[i].x()); m_Scroll[i] = QPoint(-1, -1); } if(m_MeshView) { RDTreeViewExpansionState state; ui->fixedVars->saveExpansion(state, 0); ui->fixedVars->beginUpdate(); ui->fixedVars->clear(); if(bufdata->meshDispatch && !bufdata->out1Config.statusString.isEmpty()) { RDTreeWidgetItem *n = new RDTreeWidgetItem({lit("-, -, -"), bufdata->out1Config.statusString, QString()}); ui->fixedVars->addTopLevelItem(n); } else if(bufdata->meshDispatch && !bufdata->out1Config.taskSizes.empty()) { const ActionDescription *action = m_Ctx.CurAction(); uint32_t i = 0; for(uint32_t x = 0; x < bufdata->out1Config.dispatchSize[0]; x++) { for(uint32_t y = 0; y < bufdata->out1Config.dispatchSize[1]; y++) { for(uint32_t z = 0; z < bufdata->out1Config.dispatchSize[2]; z++) { TaskGroupSize size = bufdata->out1Config.taskSizes[i]; RDTreeWidgetItem *n = NULL; if(m_CurMeshFilter == MeshFilter::None || m_FilteredTaskGroup == i) { n = new RDTreeWidgetItem( {QFormatStr("%1, %2, %3").arg(x).arg(y).arg(z), QFormatStr("Dispatched [%1, %2, %3]").arg(size.x).arg(size.y).arg(size.z), lit("Task Group")}); ui->fixedVars->addTopLevelItem(n); } if(n && !bufdata->out1Config.columns.empty()) { UI_AddTaskPayloads(n, i * bufdata->out1Config.buffers[0]->stride, bufdata->out1Config.columns, bufdata->out1Config.buffers[0]); } i++; } } } } ui->fixedVars->endUpdate(); ResourceId shader = m_Ctx.CurPipelineState().GetShader(ShaderStage::Task); // if we have saved expansion state for the new shader, apply it, otherwise apply the // previous one to get any overlap (e.g. two different shaders with very similar or // identical constants) if(ui->fixedVars->hasInternalExpansion(qHash(ToQStr(shader)))) ui->fixedVars->applyExpansion(ui->fixedVars->getInternalExpansion(qHash(ToQStr(shader))), 0); else ui->fixedVars->applyExpansion(state, 0); if(bufdata->out1Vert >= 0 && bufdata->out1Vert < ui->fixedVars->topLevelItemCount()) { ScrollToRow(bufdata->out1Vert, MeshDataStage::TaskOut); ui->fixedVars->horizontalScrollBar()->setValue(bufdata->out1Horiz); } } if(!m_MeshView) { m_RepeatedOffset->setText( tr("Starting at: %1 bytes") .arg(Formatter::HumanFormat(m_ByteOffset + bufdata->inConfig.repeatOffset, Formatter::OffsetSize))); { rdcarray vars; if(m_BufferID == ResourceId() || m_Format.isEmpty()) { vars = bufdata->inConfig.evalVars; } else { ShaderVariable var = InterpretShaderVar(bufdata->inConfig.fixedVars, bufdata->inConfig.buffers[0]->storage.begin(), bufdata->inConfig.buffers[0]->storage.end()); vars.swap(var.members); } bool wasEmpty = ui->fixedVars->topLevelItemCount() == 0; RDTreeViewExpansionState state; ui->fixedVars->saveExpansion(state, 0); ui->fixedVars->beginUpdate(); ui->fixedVars->clear(); if(!vars.isEmpty()) { UI_AddFixedVariables(ui->fixedVars->invisibleRootItem(), 0, bufdata->inConfig.fixedVars.type.members, vars); if(IsCBufferView() && !bufdata->cb.bytesBacked) UI_RemoveOffsets(ui->fixedVars->invisibleRootItem()); } ui->fixedVars->endUpdate(); if(wasEmpty) { // Expand before resizing so that collapsed data will already be visible when expanded ui->fixedVars->expandAll(); for(int i = 0; i < ui->fixedVars->header()->count(); i++) ui->fixedVars->resizeColumnToContents(i); ui->fixedVars->collapseAll(); } // if we have saved expansion state for the new shader, apply it, otherwise apply the // previous one to get any overlap (e.g. two different shaders with very similar or // identical constants) if(ui->fixedVars->hasInternalExpansion(qHash(ToQStr(m_CurCBuffer.shader)))) ui->fixedVars->applyExpansion( ui->fixedVars->getInternalExpansion(qHash(ToQStr(m_CurCBuffer.shader))), 0); else ui->fixedVars->applyExpansion(state, 0); } on_rowOffset_valueChanged(ui->rowOffset->value()); const bool prev = (bufdata->inConfig.pagingOffset > 0); const bool next = (bufdata->inConfig.numRows >= MaxVisibleRows); if(prev && next) { ui->inTable->setIndexWidget(m_ModelIn->index(0, 0), MakePreviousPageButton()); ui->inTable->setIndexWidget(m_ModelIn->index(0, 1), MakeNextPageButton()); ui->inTable->setIndexWidget(m_ModelIn->index(MaxVisibleRows + 1, 0), MakePreviousPageButton()); ui->inTable->setIndexWidget(m_ModelIn->index(MaxVisibleRows + 1, 1), MakeNextPageButton()); } else if(prev) { ui->inTable->setIndexWidget(m_ModelIn->index(0, 0), MakePreviousPageButton()); } else if(next) { ui->inTable->setIndexWidget(m_ModelIn->index(MaxVisibleRows, 1), MakeNextPageButton()); } } // we're done with it, the buffer configurations are individually copied/refcounted delete bufdata; INVOKE_MEMFN(RT_UpdateAndDisplay); }); }); } void BufferViewer::populateBBox(PopulateBufferData *bufdata) { const ActionDescription *action = m_Ctx.CurAction(); if(action && m_MeshView) { uint32_t eventId = action->eventId; bool calcNeeded = false; { QMutexLocker autolock(&m_BBoxLock); calcNeeded = !m_BBoxes.contains(eventId); } if(!calcNeeded) { UI_ResetArcball(); return; } { QMutexLocker autolock(&m_BBoxLock); m_BBoxes.insert(eventId, BBoxData()); } CalcBoundingBoxData *bbox = new CalcBoundingBoxData; bbox->eventId = eventId; bbox->input[0] = bufdata->inConfig; bbox->input[1] = bufdata->out1Config; bbox->input[2] = bufdata->out2Config; QPointer me(this); // fire up a thread to calculate the bounding box LambdaThread *thread = new LambdaThread([this, me, bbox] { if(!me) { delete bbox; return; } calcBoundingData(*bbox); if(!me) { delete bbox; return; } GUIInvoke::call(this, [this, bbox]() { UI_UpdateBoundingBox(*bbox); }); }); thread->setName(lit("BBox calc")); thread->selfDelete(true); thread->start(); // give the thread a few ms to finish, so we don't get a tiny flicker on small/fast meshes thread->wait(10); } } QVariant BufferViewer::persistData() { QVariantMap state; state = ui->dockarea->saveState(); state[lit("axisMappingIndex")] = ui->axisMappingCombo->currentIndex(); QVariantList xAxisMapping = {QVariant(m_Config.axisMapping.xAxis.x), QVariant(m_Config.axisMapping.xAxis.y), QVariant(m_Config.axisMapping.xAxis.z)}; state[lit("xAxisMapping")] = xAxisMapping; QVariantList yAxisMapping = {QVariant(m_Config.axisMapping.yAxis.x), QVariant(m_Config.axisMapping.yAxis.y), QVariant(m_Config.axisMapping.yAxis.z)}; state[lit("yAxisMapping")] = yAxisMapping; QVariantList zAxisMapping = {QVariant(m_Config.axisMapping.zAxis.x), QVariant(m_Config.axisMapping.zAxis.y), QVariant(m_Config.axisMapping.zAxis.z)}; state[lit("zAxisMapping")] = zAxisMapping; return state; } void BufferViewer::setPersistData(const QVariant &persistData) { QVariantMap state = persistData.toMap(); ui->dockarea->restoreState(state); previousAxisMappingIndex = state[lit("axisMappingIndex")].toInt(); ui->axisMappingCombo->setCurrentIndex(previousAxisMappingIndex); if(!state[lit("xAxisMapping")].toList().isEmpty()) { m_Config.axisMapping.xAxis.x = state[lit("xAxisMapping")].toList()[0].toInt(); m_Config.axisMapping.xAxis.y = state[lit("xAxisMapping")].toList()[1].toInt(); m_Config.axisMapping.xAxis.z = state[lit("xAxisMapping")].toList()[2].toInt(); m_Config.axisMapping.yAxis.x = state[lit("yAxisMapping")].toList()[0].toInt(); m_Config.axisMapping.yAxis.y = state[lit("yAxisMapping")].toList()[1].toInt(); m_Config.axisMapping.yAxis.z = state[lit("yAxisMapping")].toList()[2].toInt(); m_Config.axisMapping.zAxis.x = state[lit("zAxisMapping")].toList()[0].toInt(); m_Config.axisMapping.zAxis.y = state[lit("zAxisMapping")].toList()[1].toInt(); m_Config.axisMapping.zAxis.z = state[lit("zAxisMapping")].toList()[2].toInt(); } } void BufferViewer::UI_FixedAddMatrixRows(RDTreeWidgetItem *n, const ShaderConstant &c, const ShaderVariable &v) { const bool showPadding = ui->showPadding->isChecked() && m_CurCBuffer.bytesBacked; if(v.rows > 1) { uint32_t vecSize = VarTypeByteSize(v.type) * v.columns; FixedVarTag tag = n->tag().value(); tag.matrix = true; tag.rowmajor = v.RowMajor(); n->setTag(QVariant::fromValue(tag)); if(v.ColMajor()) vecSize = VarTypeByteSize(v.type) * v.rows; for(uint32_t r = 0; r < v.rows; r++) { n->addChild(new RDTreeWidgetItem({QFormatStr("%1.row%2").arg(v.name).arg(r), RowString(v, r), QString(), RowTypeString(v)})); if(showPadding && v.RowMajor() && c.type.matrixByteStride > vecSize) { uint32_t size = c.type.matrixByteStride - vecSize; RDTreeWidgetItem *pad = new RDTreeWidgetItem({ tr(""), QFormatStr("%1 bytes").arg(Formatter::HumanFormat(size, Formatter::OffsetSize)), QString(), tr("Padding"), }); pad->setItalic(true); pad->setTag(QVariant::fromValue(FixedVarTag(size))); n->addChild(pad); } } if(showPadding && v.ColMajor() && c.type.matrixByteStride > vecSize) { uint32_t size = c.type.matrixByteStride - vecSize; RDTreeWidgetItem *pad = new RDTreeWidgetItem({ tr(""), QFormatStr("%1 bytes each column").arg(Formatter::HumanFormat(size, Formatter::OffsetSize)), QString(), tr("Padding"), }); pad->setItalic(true); pad->setTag(QVariant::fromValue(FixedVarTag(size))); n->addChild(pad); } } } static void TaskAddMatrixRows(RDTreeWidgetItem *n, const ShaderConstant &c, const ShaderVariable &v) { if(v.rows > 1) { uint32_t vecSize = VarTypeByteSize(v.type) * v.columns; if(v.ColMajor()) vecSize = VarTypeByteSize(v.type) * v.rows; for(uint32_t r = 0; r < v.rows; r++) { n->addChild(new RDTreeWidgetItem( {QFormatStr("%1.row%2").arg(v.name).arg(r), RowString(v, r), RowTypeString(v)})); } } } void BufferViewer::UI_AddTaskPayloads(RDTreeWidgetItem *root, size_t baseOffset, const rdcarray &consts, BufferData *buffer) { uint32_t offset = 0; for(size_t idx = 0; idx < consts.size(); idx++) { const ShaderConstant &c = consts[idx]; ShaderVariable v = InterpretShaderVar(c, buffer->data() + baseOffset + offset, buffer->end()); RDTreeWidgetItem *n = new RDTreeWidgetItem({v.name, VarString(v, c), TypeString(v, c)}); root->addChild(n); TaskAddMatrixRows(n, c, v); // 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) { ShaderConstant noarray = c; noarray.type.elements = 1; // calculate the tight scalar-packed advance, so we can detect padding uint32_t elSize = BufferFormatter::GetVarAdvance(Packing::Scalar, noarray); for(uint32_t e = 0; e < v.members.size(); e++) { const uint32_t elOffset = (uint32_t)baseOffset + c.byteOffset + c.type.arrayByteStride * e; RDTreeWidgetItem *el = new RDTreeWidgetItem( {v.members[e].name, VarString(v.members[e], c), TypeString(v.members[e], c)}); // if it's an array of structs we can recurse, just need to do the outer iteration here // because v.members[...].members will be the actual struct members because of the expansion if(c.type.baseType == VarType::Struct) { UI_AddTaskPayloads(el, elOffset, c.type.members, buffer); } else { // otherwise just expand by hand since there will be no more members in c.type.members for // us to recurse with TaskAddMatrixRows(el, c, v.members[e]); } n->addChild(el); // don't count the padding in the last struct in an array of structs, it will be handled as // padding after the array if(c.type.baseType == VarType::Struct && e + 1 == v.members.size()) break; } } // for single structs, recurse else if(v.type == VarType::Struct) { UI_AddTaskPayloads(n, c.byteOffset, c.type.members, buffer); } // advance by the tight scalar-packed advance, so we can detect padding offset += BufferFormatter::GetVarAdvance(Packing::Scalar, c); } } void BufferViewer::UI_AddFixedVariables(RDTreeWidgetItem *root, uint32_t baseOffset, const rdcarray &consts, const rdcarray &vars) { const bool showPadding = ui->showPadding->isChecked() && m_CurCBuffer.bytesBacked; if(consts.size() != vars.size()) qCritical() << "Shader variable mismatch"; uint32_t offset = 0; for(size_t idx = 0; idx < consts.size() && idx < vars.size(); idx++) { const ShaderConstant &c = consts[idx]; const ShaderVariable &v = vars[idx]; if(showPadding && c.byteOffset > offset) { uint32_t size = c.byteOffset - offset; RDTreeWidgetItem *pad = new RDTreeWidgetItem({ QString(), QFormatStr("%1 bytes").arg(Formatter::HumanFormat(size, Formatter::OffsetSize)), QString(), tr("Padding"), }); pad->setItalic(true); pad->setTag(QVariant::fromValue(FixedVarTag(size))); root->addChild(pad); offset = c.byteOffset; } QVariant offsetStr = Formatter::HumanFormat(baseOffset + c.byteOffset, Formatter::OffsetSize); if(c.bitFieldSize != 0) { offsetStr = offsetStr.toString() + QFormatStr(" (bits %1:%2)").arg(c.bitFieldOffset).arg(c.bitFieldOffset + c.bitFieldSize); } if(m_CurCBuffer.compileConstants) offsetStr = lit("-"); RDTreeWidgetItem *n = new RDTreeWidgetItem({v.name, VarString(v, c), offsetStr, TypeString(v, c)}); // display colour swatch for floats with RGB display if((v.flags & ShaderVariableFlags::RGBDisplay) && VarTypeCompType(v.type) == CompType::Float && v.rows == 1 && v.columns >= 1 && v.members.empty()) { QColor swatchColor(0, 0, 0, 255); float rgb[3] = {0.0f, 0.0f, 0.0f}; for(uint8_t col = 0; col < v.columns && col < 4; col++) { float fval = 0.0f; if(v.type == VarType::Float) fval = v.value.f32v[col]; else if(v.type == VarType::Double) fval = float(v.value.f64v[col]); else if(v.type == VarType::Half) fval = float(v.value.f16v[col]); rgb[col] = ConvertLinearToSRGB(fval); } swatchColor.setRgbF(rgb[0], rgb[1], rgb[2], 1.0f); n->setIcon(1, MakeSwatchIcon(ui->fixedVars, swatchColor)); } n->setTag(QVariant::fromValue(FixedVarTag(v.name, baseOffset + c.byteOffset))); root->addChild(n); UI_FixedAddMatrixRows(n, c, v); // 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) { ShaderConstant noarray = c; noarray.type.elements = 1; // calculate the tight scalar-packed advance, so we can detect padding uint32_t elSize = BufferFormatter::GetVarAdvance(Packing::Scalar, noarray); for(uint32_t e = 0; e < v.members.size(); e++) { const uint32_t elOffset = baseOffset + c.byteOffset + c.type.arrayByteStride * e; RDTreeWidgetItem *el = new RDTreeWidgetItem({ v.members[e].name, VarString(v.members[e], c), m_CurCBuffer.compileConstants ? lit("-") : Formatter::HumanFormat(elOffset, Formatter::OffsetSize), TypeString(v.members[e], c), }); el->setTag(QVariant::fromValue(FixedVarTag(v.members[e].name, elOffset))); // if it's an array of structs we can recurse, just need to do the outer iteration here // 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); } else { // otherwise just expand by hand since there will be no more members in c.type.members for // us to recurse with UI_FixedAddMatrixRows(el, c, v.members[e]); } n->addChild(el); // don't count the padding in the last struct in an array of structs, it will be handled as // padding after the array if(c.type.baseType == VarType::Struct && e + 1 == v.members.size()) break; if(showPadding && c.type.arrayByteStride > elSize) { uint32_t size = c.type.arrayByteStride - elSize; RDTreeWidgetItem *pad = new RDTreeWidgetItem({ QString(), QFormatStr("%1 bytes").arg(Formatter::HumanFormat(size, Formatter::OffsetSize)), QString(), tr("Padding"), }); pad->setItalic(true); pad->setTag(QVariant::fromValue(FixedVarTag(size))); n->addChild(pad); } } } // for single structs, recurse else if(v.type == VarType::Struct) { UI_AddFixedVariables(n, c.byteOffset, c.type.members, v.members); } // advance by the tight scalar-packed advance, so we can detect padding offset += BufferFormatter::GetVarAdvance(Packing::Scalar, c); } } void BufferViewer::UI_RemoveOffsets(RDTreeWidgetItem *root) { for(int i = 0; i < root->childCount(); i++) { RDTreeWidgetItem *item = root->child(i); item->setText(2, QVariant()); UI_RemoveOffsets(item); } } void BufferViewer::calcBoundingData(CalcBoundingBoxData &bbox) { for(size_t stage = 0; stage < ARRAY_COUNT(bbox.input); stage++) { const BufferConfiguration &s = bbox.input[stage]; QList &minOutputList = bbox.output.bounds[stage].Min; QList &maxOutputList = bbox.output.bounds[stage].Max; minOutputList.reserve(s.columns.count()); maxOutputList.reserve(s.columns.count()); for(int i = 0; i < s.columns.count(); i++) { FloatVector maxvec(FLT_MAX, FLT_MAX, FLT_MAX, FLT_MAX); if(s.columns[i].type.columns == 1) maxvec.y = maxvec.z = maxvec.w = 0.0; else if(s.columns[i].type.columns == 2) maxvec.z = maxvec.w = 0.0; else if(s.columns[i].type.columns == 3) maxvec.w = 0.0; minOutputList.push_back(maxvec); maxOutputList.push_back(FloatVector(-maxvec.x, -maxvec.y, -maxvec.z, -maxvec.w)); } QVector cache; CacheDataForIteration(cache, s.columns, s.props, s.buffers, bbox.input[0].curInstance); // possible optimisation here if this shows up as a hot spot - sort and unique the indices and // iterate in ascending order, to be more cache friendly for(uint32_t row = 0; row < s.numRows; row++) { uint32_t idx = row; if(s.indices && s.indices->hasData()) { idx = CalcIndex(s.indices, row, s.baseVertex, s.primRestart); if(idx == ~0U || (s.primRestart && idx == s.primRestart)) continue; } for(int col = 0; col < s.columns.count(); col++) { const CachedElData &d = cache[col]; const ShaderConstant *el = d.el; const BufferElementProperties *prop = d.prop; float *minOut = (float *)&minOutputList[col]; float *maxOut = (float *)&maxOutputList[col]; if(d.data) { const byte *bytes = d.data; if(!prop->perinstance) bytes += d.stride * idx; QVariantList list = GetVariants(prop->format, *el, bytes, d.end); for(int comp = 0; comp < 4 && comp < list.count(); comp++) { const QVariant &v = list[comp]; QMetaType::Type vt = GetVariantMetatype(v); float fval = 0.0f; if(vt == QMetaType::Double) fval = (float)v.toDouble(); else if(vt == QMetaType::Float) fval = v.toFloat(); else if(vt == QMetaType::UInt || vt == QMetaType::UShort || vt == QMetaType::UChar) fval = (float)v.toUInt(); else if(vt == QMetaType::Int || vt == QMetaType::Short || vt == QMetaType::SChar) fval = (float)v.toInt(); else continue; if(qIsFinite(fval)) { minOut[comp] = qMin(minOut[comp], fval); maxOut[comp] = qMax(maxOut[comp], fval); } } } } } } } void BufferViewer::UI_UpdateBoundingBox(const CalcBoundingBoxData &bbox) { { QMutexLocker autolock(&m_BBoxLock); m_BBoxes[bbox.eventId] = bbox.output; } if(m_Ctx.CurEvent() == bbox.eventId) UpdateCurrentMeshConfig(); UI_ResetArcball(); delete &bbox; } void BufferViewer::UI_UpdateBoundingBoxLabels(int compCount) { if(compCount == 0) { BufferItemModel *model = currentBufferModel(); if(model) { int posEl = model->posColumn(); if(posEl >= 0 && posEl < model->getConfig().columns.count()) { compCount = model->getConfig().columns[posEl].type.columns; } } } QString min, max; float *minData = &m_Config.minBounds.x; float *maxData = &m_Config.maxBounds.x; const QString comps = lit("xyzw"); for(int i = 0; i < compCount && i < 4; i++) { if(i != 0) { min += lit("\n"); max += lit("\n"); } min += tr("Min %1: %2").arg(comps[i]).arg(Formatter::Format(minData[i])); max += tr("Max %1: %2").arg(comps[i]).arg(Formatter::Format(maxData[i])); } if(min.isEmpty()) ui->minBoundsLabel->setText(lit("---")); else ui->minBoundsLabel->setText(min); if(max.isEmpty()) ui->maxBoundsLabel->setText(lit("---")); else ui->maxBoundsLabel->setText(max); } void BufferViewer::UI_ResetArcball() { BBoxData bbox; { QMutexLocker autolock(&m_BBoxLock); if(m_BBoxes.contains(m_Ctx.CurEvent())) bbox = m_BBoxes[m_Ctx.CurEvent()]; } BufferItemModel *model = currentBufferModel(); int stage = currentStageIndex(); if(model) { int posEl = model->posColumn(); if(posEl >= 0 && posEl < model->getConfig().columns.count() && posEl < bbox.bounds[stage].Min.count()) { FloatVector diag; diag.x = bbox.bounds[stage].Max[posEl].x - bbox.bounds[stage].Min[posEl].x; diag.y = bbox.bounds[stage].Max[posEl].y - bbox.bounds[stage].Min[posEl].y; diag.z = bbox.bounds[stage].Max[posEl].z - bbox.bounds[stage].Min[posEl].z; float len = qSqrt(diag.x * diag.x + diag.y * diag.y + diag.z * diag.z); if(diag.x >= 0.0f && diag.y >= 0.0f && diag.z >= 0.0f && len >= 1.0e-6f && len <= 1.0e+10f) { FloatVector mid; mid.x = bbox.bounds[stage].Min[posEl].x + diag.x * 0.5f; mid.y = bbox.bounds[stage].Min[posEl].y + diag.y * 0.5f; mid.z = bbox.bounds[stage].Min[posEl].z + diag.z * 0.5f; if(!isCurrentRasterOut()) { // apply axis mapping to midpoint FloatVector transformedMid; transformedMid.x = m_Config.axisMapping.xAxis.x * mid.x + m_Config.axisMapping.yAxis.x * mid.y + m_Config.axisMapping.zAxis.x * mid.z; transformedMid.y = m_Config.axisMapping.xAxis.y * mid.x + m_Config.axisMapping.yAxis.y * mid.y + m_Config.axisMapping.zAxis.y * mid.z; transformedMid.z = m_Config.axisMapping.xAxis.z * mid.x + m_Config.axisMapping.yAxis.z * mid.y + m_Config.axisMapping.zAxis.z * mid.z; mid = transformedMid; } m_Arcball->Reset(mid, len * 0.7f); GUIInvoke::call(this, [this, len]() { ui->camSpeed->setValue(len / 200.0f); }); } } } INVOKE_MEMFN(RT_UpdateAndDisplay); } void BufferViewer::UI_ConfigureFormats() { if(!m_MeshView) return; const ActionDescription *action = m_Ctx.CurAction(); if(action && (action->flags & ActionFlags::MeshDispatch)) UI_ConfigureMeshPipeFormats(); else UI_ConfigureVertexPipeFormats(); } void BufferViewer::UI_ConfigureVertexPipeFormats() { const PipeState &pipe = m_Ctx.CurPipelineState(); rdcarray vbs = pipe.GetVBuffers(); const ActionDescription *action = m_Ctx.CurAction(); if(action) { m_InPosition = MeshFormat(); m_InSecondary = MeshFormat(); m_InPosition.allowRestart = pipe.IsRestartEnabled() && (action->flags & ActionFlags::Indexed); m_InPosition.restartIndex = pipe.GetRestartIndex(); const BufferConfiguration &vsinConfig = m_ModelIn->getConfig(); if(!vsinConfig.columns.empty()) { int elIdx = m_ModelIn->posColumn(); if(elIdx < 0 || elIdx >= vsinConfig.columns.count()) elIdx = 0; if(vsinConfig.unclampedNumRows > 0) m_InPosition.numIndices = vsinConfig.numRows; else m_InPosition.numIndices = action->numIndices; if((action->flags & ActionFlags::Instanced) && action->numInstances == 0) m_InPosition.numIndices = 0; BoundVBuffer ib = pipe.GetIBuffer(); m_InPosition.topology = pipe.GetPrimitiveTopology(); m_InPosition.indexByteStride = ib.byteStride; m_InPosition.baseVertex = action->baseVertex; m_InPosition.indexResourceId = ib.resourceId; uint32_t drawIdxByteOffs = action->indexOffset * ib.byteStride; m_InPosition.indexByteOffset = ib.byteOffset + drawIdxByteOffs; if(ib.byteSize >= ~0U) m_InPosition.indexByteSize = ib.byteSize; else if(drawIdxByteOffs > ib.byteSize) m_InPosition.indexByteSize = 0; else m_InPosition.indexByteSize = ib.byteSize - drawIdxByteOffs; if((action->flags & ActionFlags::Indexed) && m_InPosition.indexByteStride == 0) m_InPosition.indexByteStride = 4U; { const ShaderConstant &el = vsinConfig.columns[elIdx]; const BufferElementProperties &prop = vsinConfig.props[elIdx]; m_InPosition.instanced = prop.perinstance; m_InPosition.instStepRate = prop.instancerate; if(prop.buffer < vbs.count() && !vsinConfig.genericsEnabled[elIdx]) { m_InPosition.vertexResourceId = vbs[prop.buffer].resourceId; m_InPosition.vertexByteStride = vbs[prop.buffer].byteStride; m_InPosition.vertexByteOffset = vbs[prop.buffer].byteOffset + el.byteOffset + action->vertexOffset * m_InPosition.vertexByteStride; m_InPosition.vertexByteSize = vbs[prop.buffer].byteSize; } else { m_InPosition.vertexResourceId = ResourceId(); m_InPosition.vertexByteStride = 0; m_InPosition.vertexByteOffset = 0; } m_InPosition.format = prop.format; } elIdx = m_ModelIn->secondaryColumn(); if(elIdx >= 0 && elIdx < vsinConfig.columns.count()) { const ShaderConstant &el = vsinConfig.columns[elIdx]; const BufferElementProperties &prop = vsinConfig.props[elIdx]; m_InSecondary.instanced = prop.perinstance; m_InSecondary.instStepRate = prop.instancerate; if(prop.buffer < vbs.count() && !vsinConfig.genericsEnabled[elIdx]) { m_InSecondary.vertexResourceId = vbs[prop.buffer].resourceId; m_InSecondary.vertexByteStride = vbs[prop.buffer].byteStride; m_InSecondary.vertexByteOffset = vbs[prop.buffer].byteOffset + el.byteOffset + action->vertexOffset * m_InSecondary.vertexByteStride; m_InSecondary.vertexByteSize = vbs[prop.buffer].byteSize; } else { m_InSecondary.vertexResourceId = ResourceId(); m_InSecondary.vertexByteStride = 0; m_InSecondary.vertexByteOffset = 0; } m_InSecondary.format = prop.format; m_InSecondary.showAlpha = m_ModelIn->secondaryAlpha(); } } const BufferConfiguration &out1Config = m_ModelOut1->getConfig(); m_Out1Position = MeshFormat(); m_Out1Secondary = MeshFormat(); if(!out1Config.columns.empty()) { int elIdx = m_ModelOut1->posColumn(); if(elIdx < 0 || elIdx >= out1Config.columns.count()) elIdx = 0; const ShaderConstant &el = out1Config.columns[elIdx]; const BufferElementProperties &prop = out1Config.props[elIdx]; m_Out1Position = m_Out1Data; m_Out1Position.vertexByteOffset += el.byteOffset; m_Out1Position.unproject = prop.systemValue == ShaderBuiltin::Position; m_Out1Position.format.compCount = el.type.columns; // if geometry/tessellation is enabled, don't unproject VS output data if(m_Ctx.CurPipelineState().GetShader(ShaderStage::Tess_Eval) != ResourceId() || m_Ctx.CurPipelineState().GetShader(ShaderStage::Geometry) != ResourceId()) m_Out1Position.unproject = false; elIdx = m_ModelOut1->secondaryColumn(); if(elIdx >= 0 && elIdx < out1Config.columns.count()) { m_Out1Secondary = m_Out1Data; m_Out1Secondary.vertexByteOffset += out1Config.columns[elIdx].byteOffset; m_Out1Secondary.format = prop.format; m_Out1Secondary.showAlpha = m_ModelOut1->secondaryAlpha(); } } m_Out1Position.allowRestart = m_InPosition.allowRestart; m_Out1Position.restartIndex = m_InPosition.restartIndex; const BufferConfiguration &out2Config = m_ModelOut2->getConfig(); m_Out2Position = MeshFormat(); m_Out2Secondary = MeshFormat(); if(!out2Config.columns.empty()) { int elIdx = m_ModelOut2->posColumn(); if(elIdx < 0 || elIdx >= out2Config.columns.count()) elIdx = 0; const ShaderConstant &el = out2Config.columns[elIdx]; const BufferElementProperties &prop = out2Config.props[elIdx]; m_Out2Position = m_Out2Data; m_Out2Position.vertexByteOffset += el.byteOffset; m_Out2Position.unproject = prop.systemValue == ShaderBuiltin::Position; elIdx = m_ModelOut2->secondaryColumn(); if(elIdx >= 0 && elIdx < out2Config.columns.count()) { m_Out2Secondary = m_Out2Data; m_Out2Secondary.vertexByteOffset += out2Config.columns[elIdx].byteOffset; m_Out2Secondary.showAlpha = m_ModelOut2->secondaryAlpha(); } } m_Out2Position.allowRestart = false; m_Out2Position.indexByteStride = 0; if(!(action->flags & ActionFlags::Indexed)) m_Out1Position.indexByteStride = m_InPosition.indexByteStride = 0; } else { m_InPosition = MeshFormat(); m_InSecondary = MeshFormat(); m_Out1Position = MeshFormat(); m_Out1Secondary = MeshFormat(); m_Out2Position = MeshFormat(); m_Out2Secondary = MeshFormat(); } } void BufferViewer::UI_ConfigureMeshPipeFormats() { const PipeState &pipe = m_Ctx.CurPipelineState(); const ActionDescription *action = m_Ctx.CurAction(); m_InPosition = MeshFormat(); m_InSecondary = MeshFormat(); // out1 is task shaders, which do not have displayable data m_Out1Position = MeshFormat(); m_Out1Secondary = MeshFormat(); const BufferConfiguration &out2Config = m_ModelOut2->getConfig(); m_Out2Position = MeshFormat(); m_Out2Secondary = MeshFormat(); m_Out2Position.allowRestart = false; if(!out2Config.columns.empty()) { int elIdx = m_ModelOut2->posColumn(); if(elIdx < 0 || elIdx >= out2Config.columns.count()) elIdx = 0; const ShaderConstant &el = out2Config.columns[elIdx]; const BufferElementProperties &prop = out2Config.props[elIdx]; m_Out2Position = m_Out2Data; m_Out2Position.vertexByteOffset += el.byteOffset; m_Out2Position.unproject = prop.systemValue == ShaderBuiltin::Position; elIdx = m_ModelOut2->secondaryColumn(); if(elIdx >= 0 && elIdx < out2Config.columns.count()) { m_Out2Secondary = m_Out2Data; m_Out2Secondary.vertexByteOffset += out2Config.columns[elIdx].byteOffset; m_Out2Secondary.showAlpha = m_ModelOut2->secondaryAlpha(); } } } void BufferViewer::configureDrawRange() { const ActionDescription *action = m_Ctx.CurAction(); int curIndex = ui->drawRange->currentIndex(); bool instanced = true; // don't check the flags, check if there are actually multiple instances if(m_Ctx.IsCaptureLoaded()) instanced = action && action->numInstances > 1; ui->drawRange->blockSignals(true); ui->drawRange->clear(); if(instanced) ui->drawRange->addItems( {tr("This instance"), tr("Previous instances"), tr("All instances"), tr("Whole pass")}); else ui->drawRange->addItems({tr("This draw"), tr("Previous instances (N/A)"), tr("All instances (N/A)"), tr("Whole pass")}); // preserve the previously selected index ui->drawRange->setCurrentIndex(qMax(0, curIndex)); ui->drawRange->blockSignals(false); ui->drawRange->adjustSize(); ui->drawRange->setEnabled(m_CurStage != MeshDataStage::VSIn); curIndex = ui->drawRange->currentIndex(); m_Config.showPrevInstances = (curIndex >= 1); m_Config.showAllInstances = (curIndex >= 2); m_Config.showWholePass = (curIndex >= 3); } void BufferViewer::ApplyRowAndColumnDims(int numColumns, RDTableView *view, int dataColWidth) { int start = 0; QList widths; // vertex/element widths << m_IdxColWidth; // mesh view only - index if(m_MeshView) widths << m_IdxColWidth; for(int i = start; i < numColumns; i++) widths << dataColWidth; view->verticalHeader()->setDefaultSectionSize(m_DataRowHeight); view->setColumnWidths(widths); } void BufferViewer::UpdateCurrentMeshConfig() { BBoxData bbox; uint32_t eventId = m_Ctx.CurEvent(); { QMutexLocker autolocker(&m_BBoxLock); if(m_BBoxes.contains(eventId)) bbox = m_BBoxes[eventId]; } m_Config.type = m_CurStage; switch(m_CurStage) { case MeshDataStage::VSIn: m_Config.position = m_InPosition; m_Config.second = m_InSecondary; break; case MeshDataStage::VSOut: m_Config.position = m_Out1Position; m_Config.second = m_Out1Secondary; break; case MeshDataStage::GSOut: case MeshDataStage::MeshOut: m_Config.position = m_Out2Position; m_Config.second = m_Out2Secondary; break; case MeshDataStage::TaskOut: default: break; } UI_UpdateGuessParameters(); m_Config.showBBox = false; if(m_CurStage == MeshDataStage::TaskOut) return; BufferItemModel *model = currentBufferModel(); int stage = currentStageIndex(); if(model) { int posEl = model->posColumn(); if(posEl >= 0 && posEl < model->getConfig().columns.count() && posEl < bbox.bounds[stage].Min.count()) { m_Config.minBounds = bbox.bounds[stage].Min[posEl]; m_Config.maxBounds = bbox.bounds[stage].Max[posEl]; m_Config.showBBox = !isCurrentRasterOut(); int compCount = model->getConfig().columns[posEl].type.columns; UI_UpdateBoundingBoxLabels(compCount); } } } void BufferViewer::render_mouseMove(QMouseEvent *e) { if(!m_Ctx.IsCaptureLoaded()) return; if(m_CurrentCamera) m_CurrentCamera->MouseMove(e); if(e->buttons() & Qt::RightButton) render_clicked(e); // display if any mouse buttons are held while moving. if(e->buttons() != Qt::NoButton) { INVOKE_MEMFN(RT_UpdateAndDisplay); } } void BufferViewer::render_clicked(QMouseEvent *e) { if(!m_Ctx.IsCaptureLoaded()) return; QPoint curpos = e->pos(); curpos *= ui->render->devicePixelRatioF(); if((e->buttons() & Qt::RightButton) && m_Output) { QPointer me(this); m_Ctx.Replay().AsyncInvoke(lit("PickVertex"), [this, me, curpos](IReplayController *r) { if(!me) return; uint32_t instanceSelected = 0; uint32_t vertSelected = 0; rdctie(vertSelected, instanceSelected) = m_Output->PickVertex((uint32_t)curpos.x(), (uint32_t)curpos.y()); if(vertSelected != ~0U) { if(!me) return; GUIInvoke::call(this, [this, vertSelected, instanceSelected] { int row = (int)vertSelected; if(instanceSelected != m_Config.curInstance) ui->instance->setValue(instanceSelected); BufferItemModel *model = currentBufferModel(); if(model && row >= 0 && row < model->rowCount()) ScrollToRow(currentTable(), row); SyncViews(currentTable(), true, true); }); } }); } if(m_CurrentCamera) m_CurrentCamera->MouseClick(e); ui->render->setFocus(); INVOKE_MEMFN(RT_UpdateAndDisplay); } void BufferViewer::render_unclicked(QMouseEvent *e) { if(!m_Ctx.IsCaptureLoaded()) return; if(m_CurrentCamera) m_CurrentCamera->MouseUnclick(e); INVOKE_MEMFN(RT_UpdateAndDisplay); } void BufferViewer::ScrollToRow(RDTableView *view, int row) { int hs = view->horizontalScrollBar()->value(); view->scrollTo(view->model()->index(row, 0), QAbstractItemView::PositionAtTop); view->clearSelection(); view->selectRow(row); view->horizontalScrollBar()->setValue(hs); } void BufferViewer::ScrollToColumn(RDTableView *view, int column) { int vs = view->verticalScrollBar()->value(); view->scrollTo(view->model()->index(0, column), QAbstractItemView::PositionAtTop); view->verticalScrollBar()->setValue(vs); } void BufferViewer::ShowMeshData(MeshDataStage stage) { const ActionDescription *action = m_Ctx.CurAction(); if(action && (action->flags & ActionFlags::MeshDispatch) && stage == MeshDataStage::VSIn) { ToolWindowManager::raiseToolWindow(m_Containers[2]); return; } if(stage == MeshDataStage::VSIn) ToolWindowManager::raiseToolWindow(m_Containers[0]); else if(stage == MeshDataStage::VSOut) ToolWindowManager::raiseToolWindow(m_Containers[1]); else if(stage == MeshDataStage::GSOut) ToolWindowManager::raiseToolWindow(m_Containers[2]); else if(stage == MeshDataStage::TaskOut) ToolWindowManager::raiseToolWindow(m_Containers[0]); else if(stage == MeshDataStage::MeshOut) ToolWindowManager::raiseToolWindow(m_Containers[1]); } void BufferViewer::SetCurrentInstance(int32_t instance) { if(ui->instance->isVisible() && ui->instance->isEnabled()) ui->instance->setValue(instance); } void BufferViewer::SetCurrentView(int32_t view) { if(ui->viewIndex->isVisible() && ui->viewIndex->isEnabled()) ui->viewIndex->setValue(view); } void BufferViewer::SetPreviewStage(MeshDataStage stage) { if(m_MeshView) { if(stage == MeshDataStage::VSIn) ui->outputTabs->setCurrentIndex(0); else if(stage == MeshDataStage::VSOut) ui->outputTabs->setCurrentIndex(1); else if(stage == MeshDataStage::GSOut) ui->outputTabs->setCurrentIndex(2); else if(stage == MeshDataStage::TaskOut) ui->outputTabs->setCurrentIndex(1); else if(stage == MeshDataStage::MeshOut) ui->outputTabs->setCurrentIndex(2); } } void BufferViewer::ViewBuffer(uint64_t byteOffset, uint64_t byteSize, ResourceId id, const rdcstr &format) { if(!m_Ctx.IsCaptureLoaded()) return; m_IsBuffer = true; m_ByteOffset = byteOffset; m_ByteSize = byteSize; m_BufferID = id; m_TexSub = {0, 0, 0}; updateLabelsAndLayout(); BufferDescription *buf = m_Ctx.GetBuffer(id); if(buf) m_ObjectByteSize = buf->length; m_PagingByteOffset = 0; ui->formatSpecifier->setAutoFormat(format); } BufferViewer *BufferViewer::HasCBufferView(ShaderStage stage, uint32_t slot, uint32_t idx) { CBufferSlot cbuffer = {stage, slot, idx}; for(BufferViewer *c : m_CBufferViews) { if(c->m_CBufferSlot == cbuffer) return c; } return NULL; } BufferViewer *BufferViewer::GetFirstCBufferView(BufferViewer *exclude) { for(BufferViewer *b : m_CBufferViews) { if(b != exclude) return b; } return NULL; } void BufferViewer::ViewCBuffer(const ShaderStage stage, uint32_t slot, uint32_t idx) { if(!m_Ctx.IsCaptureLoaded()) return; m_IsBuffer = true; m_ByteOffset = 0; m_ByteSize = UINT64_MAX; m_BufferID = ResourceId(); m_CBufferSlot = {stage, slot, idx}; m_TexSub = {0, 0, 0}; updateLabelsAndLayout(); m_ObjectByteSize = 0; m_PagingByteOffset = 0; // enable the button to toggle on formatting, so we can pre-fill with a sensible format when it's // enabled ui->setFormat->setVisible(true); ui->formatSpecifier->setFormat(QString()); ui->formatSpecifier->setVisible(false); ui->formatSpecifier->setAutoFormat(QString()); m_CBufferViews.push_back(this); } void BufferViewer::ViewTexture(ResourceId id, const Subresource &sub, const rdcstr &format) { if(!m_Ctx.IsCaptureLoaded()) return; m_IsBuffer = false; m_ByteOffset = 0; m_ByteSize = UINT64_MAX; m_BufferID = id; m_TexSub = sub; updateLabelsAndLayout(); TextureDescription *tex = m_Ctx.GetTexture(id); if(tex) { m_ObjectByteSize = tex->byteSize; if(m_TexSub.sample == ~0U) m_TexSub.sample = tex->msSamp - 1; } m_PagingByteOffset = 0; ui->formatSpecifier->setAutoFormat(format); } void BufferViewer::ScrollToRow(int32_t row, MeshDataStage stage) { if(m_MeshView && stage == MeshDataStage::TaskOut) { ui->fixedVars->scrollToItem(ui->fixedVars->topLevelItem(row)); ui->fixedVars->setSelectedItem(ui->fixedVars->topLevelItem(row)); return; } ScrollToRow(tableForStage(stage), row); if(m_MeshView) m_Scroll[(int)stage].setY(row); else // the row scroll is visible and handles paging in the non-mesh view, so use it ui->rowOffset->setValue(row); } void BufferViewer::ScrollToColumn(int32_t column, MeshDataStage stage) { ScrollToColumn(tableForStage(stage), column); m_Scroll[(int)stage].setX(column); } bool BufferViewer::eventFilter(QObject *watched, QEvent *event) { if(event->type() == QEvent::ToolTip) { RDTreeWidget *tree = qobject_cast(watched); if(tree) { RDTreeWidgetItem *item = tree->itemAt(tree->viewport()->mapFromGlobal(QCursor::pos())); if(item) { FixedVarTag tag = item->tag().value(); QString tooltip; Packing::Rules pack = m_ModelIn->getConfig().packing; if(tag.valid && tag.padding) { tooltip = tr("%1 bytes of padding. Packing rules in effect:\n\n") .arg(Formatter::HumanFormat(tag.byteSize, Formatter::OffsetSize)); if(pack == Packing::D3DCB) tooltip += tr("Standard D3D constant buffer packing.\n\n"); else if(pack == Packing::std140) tooltip += tr("Standard std140 buffer packing.\n\n"); else if(pack == Packing::std430) tooltip += tr("Standard std430 buffer packing.\n\n"); else if(pack == Packing::C) tooltip += tr("Standard C / D3D UAV packing.\n\n"); else if(pack == Packing::Scalar) tooltip += tr("Scalar packing.\n\n"); if(pack.vector_align_component) tooltip += tr("- Vectors are only aligned to their component (float4 to 4-byte boundary)\n"); else tooltip += tr("- 3- and 4-wide vectors must be aligned to a 4-wide boundary\n" " (vec3 and vec4 to 16-byte boundary)\n"); if(pack.tight_arrays) tooltip += tr("- Arrays are tightly packed to each element\n"); else tooltip += tr("- Arrays have a stride of a 16 bytes\n"); if(pack.trailing_overlap) tooltip += tr("- Variables can overlap the trailing padding in arrays or structs.\n"); else tooltip += tr("- Variables must not overlap the trailing padding in arrays or structs.\n"); if(pack.vector_straddle_16b) tooltip += tr("- Vectors can straddle 16-byte boundaries.\n"); else tooltip += tr("- Vectors must not straddle 16-byte boundaries.\n"); } else if(tag.valid && !tag.padding) { tooltip = tr("Variable %1 is at byte offset %2") .arg(tag.name) .arg(Formatter::HumanFormat(tag.byteOffset, Formatter::OffsetSize)); if(!IsCBufferView()) tooltip += tr(", not including overall base byte offset %1 in buffer") .arg(Formatter::HumanFormat(m_ByteOffset, Formatter::OffsetSize)); tooltip += lit("."); if(tag.matrix) { tooltip += tr("\n\nMatrix stored "); if(tag.rowmajor) tooltip += tr("row-major."); else tooltip += tr("column-major."); } } if(!tooltip.isEmpty()) { QPoint pos = QCursor::pos(); pos.setX(pos.x() + 10); pos.setY(pos.y() + 10); QToolTip::showText(pos, tooltip.trimmed()); return true; } } } else if(!m_MeshView && watched == ui->inTable->viewport()) { QModelIndex index = ui->inTable->indexAt(ui->inTable->viewport()->mapFromGlobal(QCursor::pos())); if(index.isValid()) { const ShaderConstant &c = m_ModelIn->elementForColumn(index.column()); QModelIndex rowidx = m_ModelIn->index(index.row(), 0, index.parent()); int row = m_ModelIn->data(rowidx).toInt(); size_t stride = m_ModelIn->getConfig().buffers[0]->stride; QString tooltip; tooltip = tr("%1 at overall byte offset %2") .arg(c.name) .arg(Formatter::HumanFormat(stride * row + c.byteOffset, Formatter::OffsetSize)); tooltip += tr(", not including overall base byte offset %1 in buffer") .arg(Formatter::HumanFormat(m_ByteOffset, Formatter::OffsetSize)); tooltip += lit(".\n\n"); tooltip += tr("Row %1 begins at offset %2 (stride of %3 bytes)\n%4 is at offset %5 in each row.") .arg(row) .arg(Formatter::HumanFormat(stride * row, Formatter::OffsetSize)) .arg(Formatter::HumanFormat(stride, Formatter::OffsetSize)) .arg(c.name) .arg(Formatter::HumanFormat(c.byteOffset, Formatter::OffsetSize)); QPoint pos = QCursor::pos(); pos.setX(pos.x() + 10); pos.setY(pos.y() + 10); QToolTip::showText(pos, tooltip.trimmed()); return true; } } } else if(!m_MeshView && watched == ui->inTable->viewport()) { if(event->type() == QEvent::MouseMove) { bool ret = QObject::eventFilter(watched, event); QMouseEvent *mouseEvent = (QMouseEvent *)event; if(m_delegate->linkHover(mouseEvent, font(), ui->inTable->indexAt(mouseEvent->localPos().toPoint()))) ui->inTable->setCursor(QCursor(Qt::PointingHandCursor)); else ui->inTable->unsetCursor(); return ret; } } return QObject::eventFilter(watched, event); } void BufferViewer::updateLabelsAndLayout() { if(m_MeshView) { setWindowTitle(tr("Mesh Viewer")); if(m_Ctx.IsCaptureLoaded()) { GraphicsAPI pipeType = m_Ctx.APIProps().pipelineType; if(isMeshDraw()) { m_Containers[0]->layout()->addWidget(ui->out1Table); m_Containers[0]->layout()->addWidget(ui->fixedVars); m_Containers[1]->layout()->addWidget(ui->out2Table); m_Containers[2]->layout()->addWidget(ui->inTable); ui->instanceLabel->setVisible(false); ui->instance->setVisible(false); ui->meshFilterLabel->setVisible(true); ui->resetMeshFilterButton->setVisible(true); ui->fixedVars->setVisible(true); ui->out1Table->setVisible(false); m_Containers[2]->setWindowTitle(tr("Mesh Input")); m_Containers[0]->setWindowTitle(IsD3D(pipeType) ? tr("Amp. Out") : tr("Task Out")); m_Containers[1]->setWindowTitle(tr("Mesh Output")); if(ui->outputTabs->indexOf(ui->out1Tab) == 1) ui->outputTabs->removeTab(1); ui->outputTabs->setTabText(0, tr("Mesh Input")); ui->outputTabs->setTabText(1, tr("Mesh Out")); if(ui->visualisation->itemText(ui->visualisation->count() - 1) != tr("Meshlet")) ui->visualisation->addItem(tr("Meshlet")); ui->visualisation->adjustSize(); } else { m_Containers[0]->layout()->addWidget(ui->inTable); m_Containers[0]->layout()->addWidget(ui->fixedVars); m_Containers[1]->layout()->addWidget(ui->out1Table); m_Containers[2]->layout()->addWidget(ui->out2Table); ui->instanceLabel->setVisible(true); ui->instance->setVisible(true); ui->meshFilterLabel->setVisible(false); ui->resetMeshFilterButton->setVisible(false); ui->fixedVars->setVisible(false); ui->out1Table->setVisible(true); m_Containers[0]->setWindowTitle(tr("VS Input")); m_Containers[1]->setWindowTitle(tr("VS Output")); m_Containers[2]->setWindowTitle(tr("GS/DS Output")); ui->outputTabs->setTabText(0, tr("VS In")); if(ui->outputTabs->indexOf(ui->out1Tab) < 0) ui->outputTabs->insertTab(1, ui->out1Tab, tr("VS Out")); ui->outputTabs->setTabText(1, tr("VS Out")); ui->outputTabs->setTabText(2, tr("GS/DS Out")); if(ui->visualisation->itemText(ui->visualisation->count() - 1) == tr("Meshlet")) ui->visualisation->removeItem(ui->visualisation->count() - 1); ui->visualisation->adjustSize(); } } else { m_Containers[0]->layout()->addWidget(ui->inTable); m_Containers[0]->layout()->addWidget(ui->fixedVars); m_Containers[1]->layout()->addWidget(ui->out1Table); m_Containers[2]->layout()->addWidget(ui->out2Table); ui->instanceLabel->setVisible(true); ui->instance->setVisible(true); ui->meshFilterLabel->setVisible(false); ui->resetMeshFilterButton->setVisible(false); ui->fixedVars->setVisible(false); ui->out1Table->setVisible(true); m_Containers[0]->setWindowTitle(tr("VS Input")); m_Containers[1]->setWindowTitle(tr("VS Output")); m_Containers[2]->setWindowTitle(tr("GS/DS Output")); ui->outputTabs->setTabText(0, tr("VS In")); if(ui->outputTabs->indexOf(ui->out1Tab) < 0) ui->outputTabs->insertTab(1, ui->out1Tab, tr("VS Out")); ui->outputTabs->setTabText(1, tr("VS Out")); ui->outputTabs->setTabText(2, tr("GS/DS Out")); if(ui->visualisation->itemText(ui->visualisation->count() - 1) == tr("Meshlet")) ui->visualisation->removeItem(ui->visualisation->count() - 1); ui->visualisation->adjustSize(); } } else { if(IsCBufferView()) { QString bufName; const ShaderReflection *reflection = m_Ctx.CurPipelineState().GetShaderReflection(m_CBufferSlot.stage); uint32_t arraySize = ~0U; if(reflection != NULL) { if(m_CBufferSlot.slot < reflection->constantBlocks.size() && !reflection->constantBlocks[m_CBufferSlot.slot].name.isEmpty()) { bufName = QFormatStr("<%1>").arg(reflection->constantBlocks[m_CBufferSlot.slot].name); arraySize = reflection->constantBlocks[m_CBufferSlot.slot].bindArraySize; } } if(bufName.isEmpty()) { if(m_BufferID != ResourceId()) bufName = m_Ctx.GetResourceName(m_BufferID); else bufName = tr("Unbound"); } GraphicsAPI pipeType = m_Ctx.APIProps().pipelineType; QString title = QFormatStr("%1 %2 %3") .arg(ToQStr(m_CBufferSlot.stage, pipeType)) .arg(IsD3D(pipeType) ? lit("CB") : lit("UBO")) .arg(m_CBufferSlot.slot); if(m_Ctx.CurPipelineState().SupportsResourceArrays() && arraySize > 1) title += QFormatStr("[%1]").arg(m_CBufferSlot.arrayIdx); title += QFormatStr(" - %1").arg(bufName); setWindowTitle(title); } else { setWindowTitle(m_Ctx.GetResourceName(m_BufferID) + lit(" - Contents")); } } } void BufferViewer::on_resourceDetails_clicked() { if(m_BufferID == ResourceId()) return; if(!m_Ctx.HasResourceInspector()) m_Ctx.ShowResourceInspector(); m_Ctx.GetResourceInspector()->Inspect(m_BufferID); ToolWindowManager::raiseToolWindow(m_Ctx.GetResourceInspector()->Widget()); } void BufferViewer::render_mouseWheel(QWheelEvent *e) { if(m_CurrentCamera) m_CurrentCamera->MouseWheel(e); INVOKE_MEMFN(RT_UpdateAndDisplay); } void BufferViewer::render_keyPress(QKeyEvent *e) { m_CurrentCamera->KeyDown(e); } void BufferViewer::render_keyRelease(QKeyEvent *e) { m_CurrentCamera->KeyUp(e); } void BufferViewer::render_timer() { if(m_CurrentCamera && m_CurrentCamera->Update(ui->render->rect())) INVOKE_MEMFN(RT_UpdateAndDisplay); } void BufferViewer::RT_UpdateAndDisplay(IReplayController *) { if(m_Output) { m_Config.cam = m_CurrentCamera->camera(); m_Output->SetMeshDisplay(m_Config); } GUIInvoke::call(this, [this]() { ui->render->update(); }); } QPushButton *BufferViewer::MakePreviousPageButton() { QPushButton *b = new QPushButton(tr("Prev Page"), this); QObject::connect(b, &QPushButton::clicked, [this] { int page = ui->rowOffset->value() / MaxVisibleRows; if(page > 0) ui->rowOffset->setValue((page - 1) * MaxVisibleRows); }); return b; } QPushButton *BufferViewer::MakeNextPageButton() { QPushButton *b = new QPushButton(tr("Next Page"), this); QObject::connect(b, &QPushButton::clicked, [this] { int page = ui->rowOffset->value() / MaxVisibleRows; ui->rowOffset->setValue((page + 1) * MaxVisibleRows); }); return b; } RDTableView *BufferViewer::tableForStage(MeshDataStage stage) { if(stage == MeshDataStage::VSIn) return ui->inTable; else if(stage == MeshDataStage::VSOut) return ui->out1Table; else if(stage == MeshDataStage::GSOut) return ui->out2Table; else if(stage == MeshDataStage::TaskOut) return ui->out1Table; else if(stage == MeshDataStage::MeshOut) return ui->out2Table; return NULL; } BufferItemModel *BufferViewer::modelForStage(MeshDataStage stage) { if(stage == MeshDataStage::VSIn) return m_ModelIn; else if(stage == MeshDataStage::VSOut) return m_ModelOut1; else if(stage == MeshDataStage::GSOut) return m_ModelOut2; else if(stage == MeshDataStage::TaskOut) return m_ModelOut1; else if(stage == MeshDataStage::MeshOut) return m_ModelOut2; return NULL; } bool BufferViewer::isCurrentRasterOut() { BufferItemModel *model = currentBufferModel(); // if geometry/tessellation is enabled, only the GS out stage is rasterized output if((m_Ctx.CurPipelineState().GetShader(ShaderStage::Tess_Eval) != ResourceId() || m_Ctx.CurPipelineState().GetShader(ShaderStage::Geometry) != ResourceId()) && m_CurStage != MeshDataStage::GSOut) return false; // task shader outputs are not rasterized by definition if(m_CurStage == MeshDataStage::TaskOut) return false; if(model) { int posEl = model->posColumn(); if(posEl >= 0 && posEl < model->getConfig().columns.count()) { return model->getConfig().props[posEl].systemValue == ShaderBuiltin::Position; } // if the model isn't prepared yet then return a sensible default answer - if no tess/geom, // vertex is the output. Otherwise geom is the output. For task/mesh then mesh is the output if(model->getConfig().columns.empty()) { if(m_Ctx.CurPipelineState().GetShader(ShaderStage::Tess_Eval) != ResourceId() || m_Ctx.CurPipelineState().GetShader(ShaderStage::Geometry) != ResourceId()) return m_CurStage == MeshDataStage::GSOut; else if(m_CurStage == MeshDataStage::MeshOut) return true; else if(m_Ctx.CurPipelineState().GetShader(ShaderStage::Tess_Eval) == ResourceId() && m_Ctx.CurPipelineState().GetShader(ShaderStage::Geometry) == ResourceId() && m_CurStage == MeshDataStage::VSOut) return true; } } return false; } int BufferViewer::currentStageIndex() { if(m_CurStage == MeshDataStage::VSIn) return 0; else if(m_CurStage == MeshDataStage::VSOut) return 1; else if(m_CurStage == MeshDataStage::GSOut) return 2; else if(m_CurStage == MeshDataStage::TaskOut) return 1; else if(m_CurStage == MeshDataStage::MeshOut) return 2; return 0; } bool BufferViewer::isMeshDraw() { const ActionDescription *action = m_Ctx.CurAction(); return action && action->flags & ActionFlags::MeshDispatch; } void BufferViewer::Reset() { m_Output = NULL; configureDrawRange(); ClearModels(); updateLabelsAndLayout(); SetMeshFilter(MeshFilter::None); ui->fixedVars->clear(); ui->inTable->setColumnWidths({40, 40}); ui->out1Table->setColumnWidths({40, 40}); ui->out2Table->setColumnWidths({40, 40}); m_BBoxes.clear(); } void BufferViewer::ClearModels() { for(BufferItemModel *m : {m_ModelIn, m_ModelOut1, m_ModelOut2}) { if(!m) continue; m->beginReset(); m->endReset(BufferConfiguration()); } } void BufferViewer::CalcColumnWidth(int maxNumRows) { // while the calculated column widths aren't actually isn't quite based on maxNumRows, it can only // be affected by a style change so that is good enough for us to cache it and save time // recalculating this repeatedly. if(m_ColumnWidthRowCount == maxNumRows) return; m_ColumnWidthRowCount = maxNumRows; ResourceFormat floatFmt; floatFmt.compByteWidth = 4; floatFmt.compType = CompType::Float; floatFmt.compCount = 1; ResourceFormat intFmt; intFmt.compByteWidth = 4; intFmt.compType = CompType::UInt; intFmt.compCount = 1; QString headerText = lit("ColumnSizeTest"); BufferConfiguration bufconfig; BufferElementProperties floatProp, intProp; floatProp.format = floatFmt; intProp.format = intFmt; ShaderConstant elem; elem.name = headerText; elem.byteOffset = 0; elem.type.rows = maxNumRows; elem.type.columns = 1; bufconfig.columns.clear(); bufconfig.columns.push_back(elem); bufconfig.props.push_back(floatProp); elem.type.rows = 1; elem.byteOffset = 4; bufconfig.columns.push_back(elem); bufconfig.props.push_back(floatProp); elem.byteOffset = 8; bufconfig.columns.push_back(elem); bufconfig.props.push_back(floatProp); elem.byteOffset = 12; bufconfig.columns.push_back(elem); bufconfig.props.push_back(intProp); elem.byteOffset = 16; bufconfig.columns.push_back(elem); bufconfig.props.push_back(intProp); bufconfig.numRows = 2; bufconfig.unclampedNumRows = 0; bufconfig.baseVertex = 0; if(bufconfig.indices) bufconfig.indices->deref(); bufconfig.indices = new BufferData; bufconfig.indices->stride = sizeof(uint32_t); bufconfig.indices->storage.resize(sizeof(uint32_t) * 2); uint32_t *indices = (uint32_t *)bufconfig.indices->data(); indices[0] = 0; indices[1] = 1000000; bufconfig.buffers.clear(); struct TestData { float f[4]; uint32_t ui[3]; }; BufferData *bufdata = new BufferData; bufdata->stride = sizeof(TestData); bufdata->storage.resize(sizeof(TestData)); bufconfig.buffers.push_back(bufdata); TestData *test = (TestData *)bufdata->data(); test->f[0] = 1.0f; test->f[1] = 1.2345e-20f; test->f[2] = 123456.7890123456789f; test->f[3] = -1.0f; test->ui[1] = 0x12345678; test->ui[2] = 0xffffffff; m_ModelIn->beginReset(); m_ModelIn->endReset(bufconfig); // measure this data so we can use this as column widths ui->inTable->resizeColumnsToContents(); // index/element column m_IdxColWidth = ui->inTable->columnWidth(0); int col = 1; if(m_MeshView) col = 2; m_DataColWidth = 10; for(int c = 0; c < 5; c++) { int colWidth = ui->inTable->columnWidth(col + c); m_DataColWidth = qMax(m_DataColWidth, colWidth); } ui->inTable->resizeRowsToContents(); m_DataRowHeight = ui->inTable->rowHeight(0); } void BufferViewer::data_selected(const QItemSelection &selected, const QItemSelection &deselected) { QObject *sender = QObject::sender(); RDTableView *view = qobject_cast(sender); if(view == NULL) view = qobject_cast(sender->parent()); if(view == NULL) return; m_CurView = view; m_CurFixed = false; if(selected.count() > 0) { UpdateHighlightVerts(); SyncViews(view, true, false); INVOKE_MEMFN(RT_UpdateAndDisplay); } } void BufferViewer::data_scrolled(int scrollvalue) { QObject *sender = QObject::sender(); RDTableView *view = qobject_cast(sender); while(sender != NULL && view == NULL) { sender = sender->parent(); view = qobject_cast(sender); } if(view == NULL) return; SyncViews(view, false, true); } void BufferViewer::UI_UpdateGuessParameters() { m_Arcball->camera()->SetNearFar(m_Ctx.Config().MeshViewer_CameraNear, m_Ctx.Config().MeshViewer_CameraFar); m_Flycam->camera()->SetNearFar(m_Ctx.Config().MeshViewer_CameraNear, m_Ctx.Config().MeshViewer_CameraFar); m_Config.ortho = m_ProjGuess.orthographic; m_Config.fov = m_ProjGuess.fov; m_Config.aspect = 1.0f; // take a guess for the aspect ratio, for if the user hasn't overridden it Viewport vp = m_Ctx.CurPipelineState().GetViewport(0); float vpWidth = qAbs(vp.width); float vpHeight = qAbs(vp.height); m_Config.aspect = (vpWidth > 0.0f && vpHeight > 0.0f) ? (vpWidth / vpHeight) : 1.0f; if(m_ProjGuess.aspect > 0.0) m_Config.aspect = m_ProjGuess.aspect; // use estimates from post vs data (calculated from vertex position data) if the user // hasn't overridden the values m_Config.position.nearPlane = 0.1f; m_Config.position.flipY = false; if(m_CurStage == MeshDataStage::VSOut) { m_Config.position.nearPlane = m_Out1Data.nearPlane; m_Config.position.flipY = m_Out1Data.flipY; } else if(m_CurStage == MeshDataStage::GSOut) { m_Config.position.nearPlane = m_Out2Data.nearPlane; m_Config.position.flipY = m_Out2Data.flipY; } else if(m_CurStage == MeshDataStage::TaskOut) { m_Config.position.nearPlane = m_Out1Data.nearPlane; m_Config.position.flipY = m_Out1Data.flipY; } else if(m_CurStage == MeshDataStage::MeshOut) { m_Config.position.nearPlane = m_Out2Data.nearPlane; m_Config.position.flipY = m_Out2Data.flipY; } if(m_ProjGuess.nearPlane > 0.0) m_Config.position.nearPlane = m_ProjGuess.nearPlane; m_Config.position.farPlane = 100.0f; if(m_CurStage == MeshDataStage::VSOut) m_Config.position.farPlane = m_Out1Data.farPlane; else if(m_CurStage == MeshDataStage::GSOut) m_Config.position.farPlane = m_Out2Data.farPlane; else if(m_CurStage == MeshDataStage::TaskOut) m_Config.position.farPlane = m_Out1Data.farPlane; else if(m_CurStage == MeshDataStage::MeshOut) m_Config.position.farPlane = m_Out2Data.farPlane; if(m_ProjGuess.farPlane > 0.0) m_Config.position.farPlane = m_ProjGuess.farPlane; UpdateStageDataControls(); INVOKE_MEMFN(RT_UpdateAndDisplay); } void BufferViewer::on_axisMappingCombo_currentIndexChanged(int index) { if(index != 4) { switch(index) { case 0: // Y-up, Left Handed m_Config.axisMapping.xAxis = FloatVector(1.0f, 0.0f, 0.0f, 0.0f); m_Config.axisMapping.yAxis = FloatVector(0.0f, 1.0f, 0.0f, 0.0f); m_Config.axisMapping.zAxis = FloatVector(0.0f, 0.0f, 1.0f, 0.0f); break; case 1: // Y-up, Right Handed m_Config.axisMapping.xAxis = FloatVector(1.0f, 0.0f, 0.0f, 0.0f); m_Config.axisMapping.yAxis = FloatVector(0.0f, 1.0f, 0.0f, 0.0f); m_Config.axisMapping.zAxis = FloatVector(0.0f, 0.0f, -1.0f, 0.0f); break; case 2: // Z-up, Left Handed m_Config.axisMapping.xAxis = FloatVector(1.0f, 0.0f, 0.0f, 0.0f); m_Config.axisMapping.yAxis = FloatVector(0.0f, 0.0f, -1.0f, 0.0f); m_Config.axisMapping.zAxis = FloatVector(0.0f, 1.0f, 0.0f, 0.0f); break; case 3: // Z-up, Right Handed m_Config.axisMapping.xAxis = FloatVector(1.0f, 0.0f, 0.0f, 0.0f); m_Config.axisMapping.yAxis = FloatVector(0.0f, 0.0f, 1.0f, 0.0f); m_Config.axisMapping.zAxis = FloatVector(0.0f, 1.0f, 0.0f, 0.0f); break; default: break; } ui->axisMappingButton->setEnabled(false); previousAxisMappingIndex = index; on_resetCamera_clicked(); INVOKE_MEMFN(RT_UpdateAndDisplay); } else { ui->axisMappingButton->setEnabled(true); if(previousAxisMappingIndex != 4) { bool validConfig = showAxisMappingDialog(); if(!validConfig) { ui->axisMappingCombo->setCurrentIndex(previousAxisMappingIndex); ui->axisMappingButton->setEnabled(false); } } } } bool BufferViewer::showAxisMappingDialog() { AxisMappingDialog dialog(m_Ctx, m_Config, this); RDDialog::show(&dialog); if(dialog.result() == QDialog::Accepted) { m_Config.axisMapping = dialog.getAxisMapping(); on_resetCamera_clicked(); INVOKE_MEMFN(RT_UpdateAndDisplay); return true; } return false; } void BufferViewer::on_axisMappingButton_clicked() { showAxisMappingDialog(); } void BufferViewer::on_camParameters_clicked() { CameraControlsDialog dialog(m_Ctx, this); RDDialog::show(&dialog); if(dialog.result() == QDialog::Accepted) UI_UpdateGuessParameters(); } void BufferViewer::on_guessButton_clicked() { ProjectionGuessDialog dialog(m_Ctx, m_ProjGuess, this); RDDialog::show(&dialog); if(dialog.result() == QDialog::Accepted) { m_ProjGuess = dialog.getParameters(); UI_UpdateGuessParameters(); } } void BufferViewer::on_setFormat_toggled(bool checked) { if(!checked) { ui->formatSpecifier->setVisible(false); processFormat(QString()); return; } ui->formatSpecifier->setVisible(true); const ShaderReflection *reflection = m_Ctx.CurPipelineState().GetShaderReflection(m_CBufferSlot.stage); if(m_CBufferSlot.slot >= reflection->constantBlocks.size()) { ui->formatSpecifier->setVisible(false); processFormat(QString()); return; } if(IsD3D(m_Ctx.APIProps().pipelineType)) ui->formatSpecifier->setAutoFormat(BufferFormatter::DeclareStruct( Packing::D3DCB, reflection->resourceId, reflection->constantBlocks[m_CBufferSlot.slot].name, reflection->constantBlocks[m_CBufferSlot.slot].variables, 0)); else ui->formatSpecifier->setAutoFormat(BufferFormatter::DeclareStruct( BufferFormatter::EstimatePackingRules( reflection->resourceId, reflection->constantBlocks[m_CBufferSlot.slot].variables), reflection->resourceId, reflection->constantBlocks[m_CBufferSlot.slot].name, reflection->constantBlocks[m_CBufferSlot.slot].variables, 0)); } void BufferViewer::on_resetMeshFilterButton_clicked() { SetMeshFilter(MeshFilter::None); } void BufferViewer::processFormat(const QString &format) { // save scroll values now before we reset all the models m_Scrolls = new PopulateBufferData; FillScrolls(m_Scrolls); Reset(); BufferConfiguration bufconfig; ParsedFormat parsed; if(IsCBufferView() && format.isEmpty()) { // insert a dummy member so we get identified as plain fixed vars - we will automatically // evaluate ignoring the format parsed.fixed.type.members.push_back(ShaderConstant()); } else { parsed = BufferFormatter::ParseFormatString(format, m_ByteSize, IsCBufferView()); } const bool repeatedVars = parsed.repeating.type.baseType != VarType::Unknown; const bool fixedVars = !parsed.fixed.type.members.empty(); if(fixedVars && repeatedVars) { if(m_OuterSplitter->widget(0) != m_InnerSplitter) m_OuterSplitter->replaceWidget(0, m_InnerSplitter); m_FixedGroup->layout()->addWidget(ui->fixedVars); m_RepeatedGroup->layout()->addWidget(ui->inTable); // row offset should be shown in the repeated control bar, but no separator line is needed ui->offsetLine->setVisible(false); ui->rowOffsetLabel->setVisible(true); ui->rowOffset->setVisible(true); if(ui->rowOffset->parentWidget() != m_RepeatedControlBar) { QHBoxLayout *hbox = qobject_cast(m_RepeatedControlBar->layout()); hbox->insertWidget(0, ui->rowOffsetLabel); hbox->insertWidget(1, ui->rowOffset); } ui->fixedVars->setVisible(true); ui->inTable->setVisible(true); ui->showPadding->setVisible(true); m_InnerSplitter->setVisible(true); if(m_CurView == NULL && !m_CurFixed) m_CurView = ui->inTable; } else if(fixedVars) { if(m_OuterSplitter->widget(0) != ui->fixedVars) m_OuterSplitter->replaceWidget(0, ui->fixedVars); // row offset should not be shown ui->offsetLine->setVisible(false); ui->rowOffsetLabel->setVisible(false); ui->rowOffset->setVisible(false); ui->fixedVars->setVisible(true); ui->inTable->setVisible(false); ui->showPadding->setVisible(true); m_InnerSplitter->setVisible(false); m_CurView = NULL; m_CurFixed = true; } else if(repeatedVars) { if(m_OuterSplitter->widget(0) != ui->inTable) m_OuterSplitter->replaceWidget(0, ui->inTable); // row offset should be shown with the other controls ui->offsetLine->setVisible(true); ui->rowOffsetLabel->setVisible(true); ui->rowOffset->setVisible(true); // insert after the offsetLine if(ui->rowOffset->parentWidget() != ui->meshToolbar) { QHBoxLayout *hbox = qobject_cast(ui->meshToolbar->layout()); int i = 0; for(; i < hbox->count(); i++) { if(hbox->itemAt(i)->widget() == ui->offsetLine) break; } i++; if(i < hbox->count()) { hbox->insertWidget(i, ui->rowOffset); hbox->insertWidget(i, ui->rowOffsetLabel); } } ui->fixedVars->setVisible(false); ui->inTable->setVisible(true); ui->showPadding->setVisible(false); m_InnerSplitter->setVisible(false); m_CurView = ui->inTable; m_CurFixed = false; } CalcColumnWidth(MaxNumRows(parsed.repeating)); ClearModels(); m_Format = format; if(IsCBufferView()) { ui->byteRangeLine->setVisible(false); ui->byteRangeStartLabel->setVisible(false); byteRangeStart->setVisible(false); ui->byteRangeLengthLabel->setVisible(false); byteRangeLength->setVisible(false); GraphicsAPI pipeType = m_Ctx.APIProps().pipelineType; if(IsD3D(pipeType)) ui->formatSpecifier->setTitle(tr("Constant Buffer Custom Format")); else ui->formatSpecifier->setTitle(tr("Uniform Buffer Custom Format")); } else { qulonglong stride = qMax(1U, parsed.repeating.type.arrayByteStride); byteRangeStart->setSingleStep(stride); byteRangeLength->setSingleStep(stride); byteRangeStart->setMaximum((qulonglong)m_ObjectByteSize); byteRangeLength->setMaximum((qulonglong)m_ObjectByteSize); byteRangeStart->setValue(m_ByteOffset); byteRangeLength->setValue(m_ByteSize); if(!m_IsBuffer) { byteRangeStart->setVisible(false); TextureDescription *tex = m_Ctx.GetTexture(m_BufferID); if(tex) { if(tex->arraysize == 1 && tex->mips == 1 && tex->msSamp == 1 && tex->depth == 1) { ui->byteRangeStartLabel->setVisible(false); } else { QString text; if(tex->arraysize > 1 || tex->depth > 1) text = tr("Slice %1").arg(m_TexSub.slice); if(tex->mips > 1) { if(!text.isEmpty()) text += lit(", "); text += tr("Mip %1").arg(m_TexSub.mip); } if(tex->msSamp > 1) { if(!text.isEmpty()) text += lit(", "); text += tr("Sample %1").arg(m_TexSub.sample); } text += lit(". "); ui->byteRangeStartLabel->setText(text); } } byteRangeLength->setEnabled(false); } } ui->formatSpecifier->setErrors(parsed.errors); OnEventChanged(m_Ctx.CurEvent()); } void BufferViewer::on_byteRangeStart_valueChanged(double value) { m_ByteOffset = RDSpinBox64::getUValue(value); m_PagingByteOffset = 0; processFormat(m_Format); } void BufferViewer::on_byteRangeLength_valueChanged(double value) { m_ByteSize = RDSpinBox64::getUValue(value); m_PagingByteOffset = 0; processFormat(m_Format); } void BufferViewer::updateExportActionNames() { QString csv = tr("Export%1 to &CSV"); QString bytes = tr("Export%1 to &Bytes"); bool valid = m_Ctx.IsCaptureLoaded() && m_Ctx.CurAction(); if(m_MeshView) { valid = valid && m_CurView != NULL; } else { valid = valid && (m_CurView != NULL || m_CurFixed); } if(!valid) { m_ExportCSV->setText(csv.arg(QString())); m_ExportBytes->setText(bytes.arg(QString())); m_ExportCSV->setEnabled(false); m_ExportBytes->setEnabled(false); return; } m_ExportCSV->setEnabled(true); m_ExportBytes->setEnabled(m_BufferID != ResourceId()); if(m_MeshView) { m_ExportCSV->setText(csv.arg(lit(" ") + m_CurView->windowTitle())); m_ExportBytes->setText(bytes.arg(lit(" ") + m_CurView->windowTitle())); m_ExportBytes->setEnabled(true); } else { // if only one type of data is visible, the export is unambiguous if(!ui->inTable->isVisible() || !ui->fixedVars->isVisible()) { m_ExportCSV->setText(csv.arg(QString())); m_ExportBytes->setText(bytes.arg(QString())); } // otherwise go by which is selected else if(m_CurFixed) { m_ExportCSV->setText(csv.arg(lit(" ") + m_FixedGroup->title())); m_ExportBytes->setText(bytes.arg(lit(" ") + m_FixedGroup->title())); } else { m_ExportCSV->setText(csv.arg(lit(" ") + m_RepeatedGroup->title())); m_ExportBytes->setText(bytes.arg(lit(" ") + m_RepeatedGroup->title())); } } } void BufferViewer::exportCSV(QTextStream &ts, const QString &prefix, RDTreeWidgetItem *item) { if(item->childCount() == 0) { ts << QFormatStr("%1,\"%2\",%3,%4\n") .arg(item->text(0)) .arg(item->text(1)) .arg(item->text(2)) .arg(item->text(3)); } else { ts << QFormatStr("%1,,%2,%3\n").arg(item->text(0)).arg(item->text(2)).arg(item->text(3)); for(int i = 0; i < item->childCount(); i++) exportCSV(ts, item->text(0) + lit("."), item->child(i)); } } void BufferViewer::exportData(const BufferExport ¶ms) { if(!m_Ctx.IsCaptureLoaded()) return; if(!m_Ctx.CurAction()) return; if(!m_CurView && !m_CurFixed) return; QString filter; QString title; if(params.format == BufferExport::CSV) { filter = tr("CSV Files (*.csv)"); title = tr("Export buffer to CSV"); } else if(params.format == BufferExport::RawBytes) { filter = tr("Binary Files (*.bin)"); title = tr("Export buffer to bytes"); } QString filename = RDDialog::getSaveFileName(this, title, QString(), tr("%1;;All files (*)").arg(filter)); if(filename.isEmpty()) return; QFile *f = new QFile(filename); QIODevice::OpenMode flags = QIODevice::WriteOnly | QFile::Truncate; if(params.format == BufferExport::CSV) flags |= QIODevice::Text; if(!f->open(flags)) { delete f; RDDialog::critical(this, tr("Error exporting file"), tr("Couldn't open file '%1' for writing").arg(filename)); return; } if(m_MeshView) { ANALYTIC_SET(Export.MeshOutput, true); } else { ANALYTIC_SET(Export.RawBuffer, true); } if(m_CurView) { BufferItemModel *model = (BufferItemModel *)m_CurView->model(); LambdaThread *exportThread = new LambdaThread([this, params, model, f]() { if(params.format == BufferExport::RawBytes) { const BufferConfiguration &config = model->getConfig(); if(!m_MeshView) { // this is the simplest possible case, we just dump the contents of the first buffer. if(!m_IsBuffer || config.buffers[0]->size() >= m_ByteSize) { f->write((const char *)config.buffers[0]->data(), int(config.buffers[0]->size())); } else { // For buffers we have to handle reading in pages though as we might not have everything // in memory. ResourceId buff = m_BufferID; static const uint64_t maxChunkSize = 4 * 1024 * 1024; for(uint64_t byteOffset = m_ByteOffset; byteOffset < m_ByteSize + m_ByteOffset; byteOffset += maxChunkSize) { uint64_t chunkSize = qMin(m_ByteOffset + m_ByteSize - byteOffset, maxChunkSize); // it's fine to block invoke, because this is on the export thread m_Ctx.Replay().BlockInvoke([buff, f, byteOffset, chunkSize](IReplayController *r) { bytebuf chunk = r->GetBufferData(buff, byteOffset, chunkSize); f->write((const char *)chunk.data(), (qint64)chunk.size()); }); } } } else { // cache column data for the inner loop QVector cache; CacheDataForIteration(cache, config.columns, config.props, config.buffers, config.curInstance); // go row by row, finding the start of the row and dumping out the elements using their // offset and sizes for(int i = 0; i < model->rowCount(); i++) { // manually calculate the index so that we get the real offset (not the displayed // offset) // in the case of vertex output. uint32_t idx = i; if(config.indices && config.indices->hasData()) { idx = CalcIndex(config.indices, i, config.baseVertex, config.primRestart); // completely omit primitive restart indices if(config.primRestart && idx == config.primRestart) continue; } for(int col = 0; col < cache.count(); col++) { const CachedElData &d = cache[col]; const ShaderConstant *el = d.el; const BufferElementProperties *prop = d.prop; if(d.data) { const char *bytes = (const char *)d.data; if(!prop->perinstance) bytes += d.stride * idx; if(bytes + d.byteSize <= (const char *)d.end) { f->write(bytes, d.byteSize); continue; } } // if we didn't continue above, something was wrong, so write nulls f->write(d.nulls); } } } } else if(params.format == BufferExport::CSV) { // otherwise we need to iterate over all the data ourselves const BufferConfiguration &config = model->getConfig(); QTextStream s(f); for(int i = 0; i < model->columnCount(); i++) { s << model->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString(); if(i + 1 < model->columnCount()) s << ", "; } s << "\n"; if(m_MeshView || !m_IsBuffer || config.buffers[0]->size() >= m_ByteSize) { // if there's no pagination to worry about, dump using the model's data() for(int row = 0; row < model->rowCount(); row++) { for(int col = 0; col < model->columnCount(); col++) { QList lines = model->data(model->index(row, col), Qt::DisplayRole).toString().split(lit("\n")); bool quote = (lines.count() > 1); if(quote) s << "\""; for(int l = 0; l < lines.count(); l++) { s << lines[l].trimmed(); if(l + 1 < lines.size()) s << "\n"; } if(quote) s << "\""; if(col + 1 < model->columnCount()) s << ", "; } s << "\n"; } } else { // write 64k rows at a time ResourceId buff = m_BufferID; const uint64_t maxChunkSize = 64 * 1024 * config.buffers[0]->stride; for(uint64_t byteOffset = m_ByteOffset; byteOffset < m_ByteSize; byteOffset += maxChunkSize) { uint64_t chunkSize = qMin(m_ByteSize - byteOffset, maxChunkSize); // it's fine to block invoke, because this is on the export thread m_Ctx.Replay().BlockInvoke( [buff, &s, &config, byteOffset, chunkSize](IReplayController *controller) { // cache column data for the inner loop QVector cache; BufferData bufferData; bufferData.storage = controller->GetBufferData(buff, byteOffset, chunkSize); bufferData.stride = config.buffers[0]->stride; size_t numRows = (bufferData.storage.size() + bufferData.stride - 1) / bufferData.stride; size_t rowOffset = byteOffset / bufferData.stride; CacheDataForIteration(cache, config.columns, config.props, {&bufferData}, 0); // go row by row, finding the start of the row and dumping out the elements using // their // offset and sizes for(size_t idx = 0; idx < numRows; idx++) { s << (rowOffset + idx) << ", "; for(int col = 0; col < cache.count(); col++) { const CachedElData &d = cache[col]; const ShaderConstant *el = d.el; const BufferElementProperties *prop = d.prop; if(d.data) { const byte *data = d.data; const byte *end = d.end; data += d.stride * idx; // only slightly wasteful, we need to fetch all variants together // since some formats are packed and can't be read individually QVariantList list = GetVariants(prop->format, *el, data, end); if(el->type.rows > 1) { for(int c = 0; c < el->type.columns; c++) { s << "\""; for(int r = 0; r < el->type.rows; r++) { if(list.empty()) { s << "---"; } else { int el_idx = r * el->type.columns + c; s << interpretVariant(list[el_idx], *el, *prop).trimmed(); } if(r + 1 < el->type.rows) s << "\n"; } s << "\", "; } } else if(list.empty()) { for(int v = 0; v < d.numColumns; v++) { s << "---"; if(v + 1 < d.numColumns) s << ", "; } } else { for(int v = 0; v < list.count(); v++) { s << interpretVariant(list[v], *el, *prop); if(v + 1 < list.count()) s << ", "; } } if(col + 1 < cache.count()) s << ", "; } } s << "\n"; } }); } } } f->close(); delete f; }); exportThread->start(); ShowProgressDialog(this, tr("Exporting data"), [exportThread]() { return !exportThread->isRunning(); }); exportThread->deleteLater(); } else if(m_CurFixed) { if(params.format == BufferExport::RawBytes) { BufferItemModel *model = (BufferItemModel *)ui->inTable->model(); const BufferConfiguration &config = model->getConfig(); size_t byteSize = 0; if(!config.fixedVars.type.members.empty()) byteSize = BufferFormatter::GetVarAdvance(config.packing, config.fixedVars); const bytebuf &bufdata = config.buffers[0]->storage; f->write((const char *)bufdata.data(), qMin(bufdata.size(), byteSize)); // if the buffer wasn't large enough for the variables, fill with 0s if(byteSize > bufdata.size()) { QByteArray nulls; nulls.resize(int(byteSize - config.buffers[0]->storage.size())); f->write(nulls); } } else if(params.format == BufferExport::CSV) { QTextStream ts(f); ts << tr("Name,Value,Byte Offset,Type\n"); for(int i = 0; i < ui->fixedVars->topLevelItemCount(); i++) exportCSV(ts, QString(), ui->fixedVars->topLevelItem(i)); } f->close(); delete f; } } void BufferViewer::debugVertex() { if(!m_Ctx.IsCaptureLoaded()) return; if(!m_Ctx.CurAction()) return; if(!m_CurView) return; QModelIndex idx = m_CurView->selectionModel()->currentIndex(); if(!idx.isValid()) { GUIInvoke::call(this, [this]() { RDDialog::critical(this, tr("Error debugging"), tr("Error debugging vertex - make sure a valid vertex is selected")); }); return; } uint32_t vertid = m_CurView->model()->data(m_CurView->model()->index(idx.row(), 0), Qt::DisplayRole).toUInt(); uint32_t index = m_CurView->model()->data(m_CurView->model()->index(idx.row(), 1), Qt::DisplayRole).toUInt(); uint32_t view = m_Config.curView; bool done = false; ShaderDebugTrace *trace = NULL; m_Ctx.Replay().AsyncInvoke([this, &done, &trace, vertid, index, view](IReplayController *r) { trace = r->DebugVertex(vertid, m_Config.curInstance, index, view); if(trace->debugger == NULL) { r->FreeTrace(trace); trace = NULL; } done = true; }); QString debugContext = tr("Vertex %1").arg(vertid); if(m_Ctx.CurAction()->numInstances > 1) debugContext += tr(", Instance %1").arg(m_Config.curInstance); // wait a short while before displaying the progress dialog (which won't show if we're already // done by the time we reach it) for(int i = 0; !done && i < 100; i++) QThread::msleep(5); ShowProgressDialog(this, tr("Debugging %1").arg(debugContext), [&done]() { return done; }); if(!trace) { RDDialog::critical(this, tr("Error debugging"), tr("Error debugging vertex - make sure a valid vertex is selected")); return; } const ShaderReflection *shaderDetails = m_Ctx.CurPipelineState().GetShaderReflection(ShaderStage::Vertex); ResourceId pipeline = m_Ctx.CurPipelineState().GetGraphicsPipelineObject(); // viewer takes ownership of the trace IShaderViewer *s = m_Ctx.DebugShader(shaderDetails, pipeline, trace, debugContext); m_Ctx.AddDockWindow(s->Widget(), DockReference::AddTo, this); } void BufferViewer::debugMeshThread() { if(!m_Ctx.IsCaptureLoaded()) return; const ActionDescription *action = m_Ctx.CurAction(); if(!action) return; if(!m_CurView) return; QModelIndex idx = m_CurView->selectionModel()->currentIndex(); if(!idx.isValid()) { GUIInvoke::call(this, [this]() { RDDialog::critical(this, tr("Error debugging"), tr("Error debugging meshlet - make sure a valid meshlet is selected")); }); return; } uint32_t taskIndex = 0, meshletIndex = 0; GetIndicesForMeshRow((uint32_t)idx.row(), taskIndex, meshletIndex); const ShaderReflection *shaderDetails = m_Ctx.CurPipelineState().GetShaderReflection(ShaderStage::Mesh); if(!shaderDetails) return; rdcfixedarray threadGroupSize = action->dispatchThreadsDimension[0] == 0 ? shaderDetails->dispatchThreadsDimension : action->dispatchThreadsDimension; m_MeshDebugSelector->SetThreadBounds(action->dispatchDimension, threadGroupSize); // Calculate 3d group id from 1d meshlet index and dispatch dimensions // Imagine 8x2x4 with idx 60 // 8x2 = 16 // 8x2x4 = 64 // 60 % x = 4 // 60 % (x * y) = 12 / x = 1 // 60 / (x * y) = 3 // index 60 is id (4,1,3) // 4 + (8 * 1) + (16 * 3) = 60 uint32_t xDim = action->dispatchDimension[0]; uint32_t yDim = action->dispatchDimension[1]; uint32_t zDim = action->dispatchDimension[2]; rdcfixedarray meshletGroup = { meshletIndex % xDim, (meshletIndex % (xDim * yDim)) / xDim, meshletIndex / (xDim * yDim), }; m_MeshDebugSelector->SetDefaultDispatch(meshletGroup, {0, 0, 0}); RDDialog::show(m_MeshDebugSelector); } void BufferViewer::meshDebugSelector_beginDebug(const rdcfixedarray &group, const rdcfixedarray &thread) { const ActionDescription *action = m_Ctx.CurAction(); if(!action) return; const ShaderReflection *shaderDetails = m_Ctx.CurPipelineState().GetShaderReflection(ShaderStage::Mesh); if(!shaderDetails) return; struct threadSelect { rdcfixedarray g; rdcfixedarray t; } debugThread = { // g[] {group[0], group[1], group[2]}, // t[] {thread[0], thread[1], thread[2]}, }; bool done = false; ShaderDebugTrace *trace = NULL; m_Ctx.Replay().AsyncInvoke([&trace, &done, debugThread](IReplayController *r) { trace = r->DebugMeshThread(debugThread.g, debugThread.t); if(trace->debugger == NULL) { r->FreeTrace(trace); trace = NULL; } done = true; }); QString debugContext = lit("Mesh Group [%1,%2,%3] Thread [%4,%5,%6]") .arg(group[0]) .arg(group[1]) .arg(group[2]) .arg(thread[0]) .arg(thread[1]) .arg(thread[2]); // wait a short while before displaying the progress dialog (which won't show if we're already // done by the time we reach it) for(int i = 0; !done && i < 100; i++) QThread::msleep(5); ShowProgressDialog(this, tr("Debugging %1").arg(debugContext), [&done]() { return done; }); if(!trace) { RDDialog::critical( this, tr("Error debugging"), tr("Error debugging thread - make sure a valid group and thread is selected")); return; } // viewer takes ownership of the trace IShaderViewer *s = m_Ctx.DebugShader( shaderDetails, m_Ctx.CurPipelineState().GetComputePipelineObject(), trace, debugContext); m_Ctx.AddDockWindow(s->Widget(), DockReference::AddTo, this); } void BufferViewer::SyncViews(RDTableView *primary, bool selection, bool scroll) { if(!ui->syncViews->isChecked()) return; RDTableView *views[] = {ui->inTable, ui->out1Table, ui->out2Table}; int horizScrolls[ARRAY_COUNT(views)] = {0}; for(size_t i = 0; i < ARRAY_COUNT(views); i++) horizScrolls[i] = views[i]->horizontalScrollBar()->value(); if(primary == NULL) { for(RDTableView *table : views) { if(table->hasFocus()) { primary = table; break; } } } if(primary == NULL) primary = views[0]; for(RDTableView *table : views) { if(table == primary) continue; if(selection) { QModelIndexList selected = primary->selectionModel()->selectedRows(); if(!selected.empty()) table->selectRow(selected[0].row()); } if(scroll) table->verticalScrollBar()->setValue(primary->verticalScrollBar()->value()); } for(size_t i = 0; i < ARRAY_COUNT(views); i++) views[i]->horizontalScrollBar()->setValue(horizScrolls[i]); } void BufferViewer::UpdateHighlightVerts() { m_Config.highlightVert = ~0U; if(ui->highlightVerts->isHidden() || !ui->highlightVerts->isChecked()) return; RDTableView *table = currentTable(); if(!table) return; QModelIndexList selected = table->selectionModel()->selectedRows(); if(selected.empty()) return; m_Config.highlightVert = selected[0].row(); } void BufferViewer::UpdateStageDataControls() { if(isCurrentRasterOut()) { ui->guessLabel->setVisible(true); ui->guessDetails1->setVisible(true); ui->guessDetails2->setVisible(true); ui->guessButton->setVisible(true); QString aspectStr = tr("Auto"); if(m_ProjGuess.aspect > 0) aspectStr = Formatter::Format(m_ProjGuess.aspect); if(m_ProjGuess.orthographic) ui->guessDetails1->setText(tr("Orthographic Projection")); else ui->guessDetails1->setText( tr("Perspective Projection, FOV %1").arg(Formatter::Format(m_ProjGuess.fov))); if(m_ProjGuess.farPlane == FLT_MAX) { if(m_ProjGuess.nearPlane > 0) ui->guessDetails2->setText(tr("Aspect Ratio %1, Reverse Z Near %2") .arg(aspectStr) .arg(Formatter::Format(m_ProjGuess.nearPlane))); else ui->guessDetails2->setText(tr("Aspect Ratio %1, Reverse Z Near Automatic").arg(aspectStr)); } else { if(m_ProjGuess.nearPlane > 0 && m_ProjGuess.farPlane > 0) ui->guessDetails2->setText(tr("Aspect Ratio %1, Near-Far %2 - %3") .arg(aspectStr) .arg(Formatter::Format(m_ProjGuess.nearPlane)) .arg(Formatter::Format(m_ProjGuess.farPlane))); else if(m_ProjGuess.nearPlane > 0) ui->guessDetails2->setText(tr("Aspect Ratio %1, Near %2 Far Auto") .arg(aspectStr) .arg(Formatter::Format(m_ProjGuess.nearPlane))); else if(m_ProjGuess.farPlane > 0) ui->guessDetails2->setText(tr("Aspect Ratio %1, Near Auto Far %2") .arg(aspectStr) .arg(Formatter::Format(m_ProjGuess.farPlane))); else ui->guessDetails2->setText(tr("Aspect Ratio %1, Near-Far Automatic").arg(aspectStr)); } ui->axisMappingLabel->setVisible(false); ui->axisMappingCombo->setVisible(false); ui->axisMappingButton->setVisible(false); } else { ui->guessLabel->setVisible(false); ui->guessDetails1->setVisible(false); ui->guessDetails2->setVisible(false); ui->guessButton->setVisible(false); ui->axisMappingLabel->setVisible(true); ui->axisMappingCombo->setVisible(true); ui->axisMappingButton->setVisible(true); ui->axisMappingButton->setEnabled(ui->axisMappingCombo->currentIndex() == 4); } } void BufferViewer::on_outputTabs_currentChanged(int index) { ui->renderContainer->parentWidget()->layout()->removeWidget(ui->renderContainer); ui->outputTabs->widget(index)->layout()->addWidget(ui->renderContainer); if(index == 0) m_CurStage = isMeshDraw() ? MeshDataStage::TaskOut : MeshDataStage::VSIn; else if(index == 1) m_CurStage = isMeshDraw() ? MeshDataStage::MeshOut : MeshDataStage::VSOut; else if(index == 2) m_CurStage = MeshDataStage::GSOut; configureDrawRange(); on_resetCamera_clicked(); ui->autofitCamera->setEnabled(!isCurrentRasterOut()); UpdateStageDataControls(); UpdateCurrentMeshConfig(); INVOKE_MEMFN(RT_UpdateAndDisplay); } void BufferViewer::on_toggleControls_toggled(bool checked) { ui->configurationGroup->setVisible(checked); // temporarily set minimum bounds to the longest float we could format, to ensure the minimum size // we calculate below is as big as needs to be (sigh...). This is necessary because Qt doesn't // properly propagate the minimum size up through the scroll area and instead sizes it down much // smaller. FloatVector prev = m_Config.minBounds; m_Config.minBounds.x = 1.0f; m_Config.minBounds.y = 1.2345e-20f; m_Config.minBounds.z = 123456.7890123456789f; m_Config.minBounds.w = 1.2345e+20f; UI_UpdateBoundingBoxLabels(4); m_Config.minBounds = prev; ui->cameraControlsWidget->setMinimumSize(ui->cameraControlsWidget->minimumSizeHint()); ui->cameraControlsScroll->setMinimumWidth(ui->cameraControlsWidget->minimumSizeHint().width() + ui->cameraControlsScroll->verticalScrollBar()->width()); UI_UpdateBoundingBoxLabels(); UpdateStageDataControls(); } void BufferViewer::on_syncViews_toggled(bool checked) { SyncViews(NULL, true, true); } void BufferViewer::on_showPadding_toggled(bool checked) { OnEventChanged(m_Ctx.CurEvent()); } void BufferViewer::on_highlightVerts_toggled(bool checked) { UpdateHighlightVerts(); INVOKE_MEMFN(RT_UpdateAndDisplay); } void BufferViewer::on_vtxExploderSlider_valueChanged(int value) { m_Config.vtxExploderSliderSNorm = (float)value / 100.0f; INVOKE_MEMFN(RT_UpdateAndDisplay); } void BufferViewer::on_exploderReset_clicked() { ui->vtxExploderSlider->setSliderPosition(0); } void BufferViewer::on_exploderScale_valueChanged(double value) { m_Config.exploderScale = (float)value; INVOKE_MEMFN(RT_UpdateAndDisplay); } void BufferViewer::on_wireframeRender_toggled(bool checked) { m_Config.wireframeDraw = checked; INVOKE_MEMFN(RT_UpdateAndDisplay); } void BufferViewer::on_visualisation_currentIndexChanged(int index) { ui->wireframeRender->setEnabled(index > 0); if(!ui->wireframeRender->isEnabled()) { ui->wireframeRender->setChecked(true); m_Config.wireframeDraw = true; } bool explodeHidden = (index != (int)Visualisation::Explode); ui->vtxExploderLabel->setHidden(explodeHidden); ui->vtxExploderSlider->setHidden(explodeHidden); ui->exploderReset->setHidden(explodeHidden); ui->exploderScaleLabel->setHidden(explodeHidden); ui->exploderScale->setHidden(explodeHidden); // Because the vertex/prim highlights draw from a new, temporary vertex buffer, // those vertex IDs (which determine the explode displacement) won't necessarily // match the original mesh's IDs and exploded vertices. Because of this, it seems // cleanest to just avoid drawing the highlighted vert/prim with the explode // visualisation (while also getting back a little room on the toolbar used by // the extra exploder controls). ui->highlightVerts->setHidden(!explodeHidden); UpdateHighlightVerts(); m_Config.visualisationMode = (Visualisation)qMax(0, index); m_ModelIn->setSecondaryColumn(m_ModelIn->secondaryColumn(), m_Config.visualisationMode == Visualisation::Secondary, m_ModelIn->secondaryAlpha()); m_ModelOut1->setSecondaryColumn(m_ModelOut1->secondaryColumn(), m_Config.visualisationMode == Visualisation::Secondary, m_ModelOut1->secondaryAlpha()); m_ModelOut2->setSecondaryColumn(m_ModelOut2->secondaryColumn(), m_Config.visualisationMode == Visualisation::Secondary, m_ModelOut2->secondaryAlpha()); INVOKE_MEMFN(RT_UpdateAndDisplay); } void BufferViewer::on_drawRange_currentIndexChanged(int index) { configureDrawRange(); INVOKE_MEMFN(RT_UpdateAndDisplay); } void BufferViewer::on_controlType_currentIndexChanged(int index) { m_Arcball->Reset(FloatVector(), 10.0f); m_Flycam->Reset(FloatVector()); if(index == 0) { m_CurrentCamera = m_Arcball; UI_ResetArcball(); } else { m_CurrentCamera = m_Flycam; if(isCurrentRasterOut()) m_Flycam->Reset(FloatVector(0.0f, 0.0f, 0.0f, 0.0f)); else m_Flycam->Reset(FloatVector(0.0f, 0.0f, -10.0f, 0.0f)); on_autofitCamera_clicked(); } INVOKE_MEMFN(RT_UpdateAndDisplay); } void BufferViewer::on_resetCamera_clicked() { if(isCurrentRasterOut()) ui->controlType->setCurrentIndex(1); else ui->controlType->setCurrentIndex(0); // make sure callback is called even if we're re-selecting same // camera type on_controlType_currentIndexChanged(ui->controlType->currentIndex()); } void BufferViewer::on_camSpeed_valueChanged(double value) { m_Arcball->SpeedMultiplier = m_Flycam->SpeedMultiplier = value; } void BufferViewer::on_instance_valueChanged(int value) { m_Config.curInstance = value; OnEventChanged(m_Ctx.CurEvent()); } void BufferViewer::on_viewIndex_valueChanged(int value) { m_Config.curView = value; OnEventChanged(m_Ctx.CurEvent()); } void BufferViewer::SetMeshFilter(MeshFilter filter, uint32_t taskGroup, uint32_t meshGroup) { // calculate new scrolls manually to keep the same logical item selected m_Scrolls = new PopulateBufferData; FillScrolls(m_Scrolls); { const BufferConfiguration &config1 = m_ModelOut1->getConfig(); const BufferConfiguration &config2 = m_ModelOut2->getConfig(); // baseTaskRow is the first row in the mesh view for the start of the task with no mesh filter, // and baseMeshRow is the offset to the filtered mesh (if relevant). They could be identical const uint32_t prevBaseTaskRow = m_TaskFilterRowOffset; const uint32_t prevBaseMeshRow = m_MeshFilterRowOffset; // if we're filtering directly to a task from none, we also have the prefix count we just have // to determine the base mesh uint32_t taskBaseMesh = 0; for(uint32_t i = 0; i < taskGroup && i < config1.taskSizes.size(); i++) taskBaseMesh += config1.taskSizes[i].x * config1.taskSizes[i].y * config1.taskSizes[i].z; uint32_t newBaseTaskRow = 0, newBaseMeshRow = 0; if(filter == MeshFilter::None || config2.meshletVertexPrefixCounts.empty()) { // if the new filter is none, then our new base row for both is 0 newBaseTaskRow = newBaseMeshRow = 0; } else if(m_CurMeshFilter == MeshFilter::None && filter == MeshFilter::Mesh) { newBaseTaskRow = config2.meshletVertexPrefixCounts[taskBaseMesh]; newBaseMeshRow = config2.meshletVertexPrefixCounts[meshGroup]; } else if(m_CurMeshFilter == MeshFilter::None && filter == MeshFilter::TaskGroup) { newBaseTaskRow = newBaseMeshRow = config2.meshletVertexPrefixCounts[taskBaseMesh]; } else if(m_CurMeshFilter == MeshFilter::TaskGroup && filter == MeshFilter::Mesh) { // the first complex case - if we're already filtered to a task and now we're filtering to a // mesh, we only have prefix counts relatively so look it up newBaseTaskRow = prevBaseTaskRow; newBaseMeshRow = prevBaseTaskRow + config2.meshletVertexPrefixCounts[meshGroup - taskBaseMesh]; } else if(m_CurMeshFilter == MeshFilter::Mesh && filter == MeshFilter::TaskGroup) { // the second complex case - if we're already filtered to a *mesh* and now we're filtering // back to the task, we undo the previous per-mesh filter newBaseTaskRow = newBaseMeshRow = prevBaseTaskRow; // only support filtering within the same group, not arbitrarily from one mesh in one task // group to a different task group Q_ASSERT(m_FilteredTaskGroup == taskGroup); } const uint32_t prevBaseRow = prevBaseMeshRow; const uint32_t newBaseRow = newBaseMeshRow; // when going to/from no filter, we just rebase by the base row and set the task row that we know directly if(m_CurMeshFilter == MeshFilter::None) { m_Scrolls->out1Vert = 0; m_Scrolls->out2Vert -= newBaseRow; } else if(filter == MeshFilter::None) { m_Scrolls->out1Vert = config1.taskOrMeshletOffset; m_Scrolls->out2Vert += prevBaseRow; } // otherwise changing between task and mesh filter, we rebase based on the difference between // the number of meshes shown. The task filter doesn't have to change else if(m_CurMeshFilter == MeshFilter::TaskGroup && filter == MeshFilter::Mesh) { m_Scrolls->out1Vert = 0; m_Scrolls->out2Vert -= (newBaseRow - prevBaseRow); } else if(m_CurMeshFilter == MeshFilter::Mesh && filter == MeshFilter::TaskGroup) { m_Scrolls->out1Vert = 0; m_Scrolls->out2Vert += (prevBaseRow - newBaseRow); } m_TaskFilterRowOffset = newBaseTaskRow; m_MeshFilterRowOffset = newBaseMeshRow; } m_CurMeshFilter = filter; m_FilteredTaskGroup = taskGroup; m_FilteredMeshGroup = meshGroup; switch(m_CurMeshFilter) { case MeshFilter::None: ui->meshFilterLabel->setText(tr("Current Range filter: None")); ui->resetMeshFilterButton->setEnabled(false); break; case MeshFilter::TaskGroup: if(IsD3D(m_Ctx.APIProps().pipelineType)) ui->meshFilterLabel->setText(tr("Current Range filter: Single Amplification Threadgroup")); else ui->meshFilterLabel->setText(tr("Current Range filter: Single Task")); ui->resetMeshFilterButton->setEnabled(true); break; case MeshFilter::Mesh: ui->meshFilterLabel->setText(tr("Current Range filter: Single Meshlet")); ui->resetMeshFilterButton->setEnabled(true); break; } if(m_Ctx.IsCaptureLoaded()) OnEventChanged(m_Ctx.CurEvent()); } void BufferViewer::on_rowOffset_valueChanged(int value) { if(!m_MeshView && m_ModelIn->getConfig().unclampedNumRows > 0) { int page = value / MaxVisibleRows; value %= MaxVisibleRows; uint64_t pageOffset = page * MaxVisibleRows * m_ModelIn->getConfig().buffers[0]->stride; // account for the extra row at the top with previous/next buttons if(pageOffset > 0) value++; if(pageOffset != m_PagingByteOffset) { m_PagingByteOffset = pageOffset; processFormat(m_Format); return; } } ScrollToRow(ui->inTable, value); ScrollToRow(ui->out1Table, value); ScrollToRow(ui->out2Table, value); // when we're paging and we select the first row, actually scroll up to include the previous/next // buttons. if(!m_MeshView && value == 1 && m_PagingByteOffset > 0) ui->inTable->verticalScrollBar()->setValue(0); } void BufferViewer::on_autofitCamera_clicked() { if(m_CurStage != MeshDataStage::VSIn) return; ui->controlType->setCurrentIndex(1); BBoxData bbox; { QMutexLocker autolock(&m_BBoxLock); if(m_BBoxes.contains(m_Ctx.CurEvent())) bbox = m_BBoxes[m_Ctx.CurEvent()]; } BufferItemModel *model = m_ModelIn; int stage = 0; if(bbox.bounds[stage].Min.isEmpty()) return; if(!model) return; int posEl = model->posColumn(); if(posEl < 0 || posEl >= bbox.bounds[stage].Min.count()) return; FloatVector diag; diag.x = bbox.bounds[stage].Max[posEl].x - bbox.bounds[stage].Min[posEl].x; diag.y = bbox.bounds[stage].Max[posEl].y - bbox.bounds[stage].Min[posEl].y; diag.z = bbox.bounds[stage].Max[posEl].z - bbox.bounds[stage].Min[posEl].z; float len = qSqrt(diag.x * diag.x + diag.y * diag.y + diag.z * diag.z); if(diag.x >= 0.0f && diag.y >= 0.0f && diag.z >= 0.0f && len >= 1.0e-6f && len <= 1.0e+10f) { FloatVector mid; mid.x = bbox.bounds[stage].Min[posEl].x + diag.x * 0.5f; mid.y = bbox.bounds[stage].Min[posEl].y + diag.y * 0.5f; mid.z = bbox.bounds[stage].Min[posEl].z + diag.z * 0.5f; if(!isCurrentRasterOut()) { // apply axis mapping to midpoint FloatVector transformedMid; transformedMid.x = m_Config.axisMapping.xAxis.x * mid.x + m_Config.axisMapping.yAxis.x * mid.y + m_Config.axisMapping.zAxis.x * mid.z; transformedMid.y = m_Config.axisMapping.xAxis.y * mid.x + m_Config.axisMapping.yAxis.y * mid.y + m_Config.axisMapping.zAxis.y * mid.z; transformedMid.z = m_Config.axisMapping.xAxis.z * mid.x + m_Config.axisMapping.yAxis.z * mid.y + m_Config.axisMapping.zAxis.z * mid.z; mid = transformedMid; } mid.z -= len * 0.7f; m_Flycam->Reset(mid); } INVOKE_MEMFN(RT_UpdateAndDisplay); }