Files
renderdoc/qrenderdoc/Windows/BufferViewer.cpp
T
Shahbaz Youssefi ed4c3756d0 Enable primitive restart for list topologies
This is supported by OpenGL, and on Vulkan with
VK_EXT_primitive_topology_list_restart.  On Vulkan, all drivers are
known to support this even without
VK_EXT_primitive_topology_list_restart.  On D3D, primitive restart is
only supported for strip topologies.

Previously, RenderDoc specifically disabled primitive restart for
non-strip topologies.  In this change, that is no longer done.  If the
app enables primitive restart, so will RenderDoc behave accordingly.  It
would be the responsibility of the app to avoid primitive restart if the
API doesn't allow it.
2022-08-29 14:59:14 +01:00

5972 lines
175 KiB
C++

/******************************************************************************
* The MIT License (MIT)
*
* Copyright (c) 2019-2022 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 <float.h>
#include <QDoubleSpinBox>
#include <QFontDatabase>
#include <QItemSelection>
#include <QMenu>
#include <QMouseEvent>
#include <QMutexLocker>
#include <QPushButton>
#include <QScrollBar>
#include <QSplitter>
#include <QTimer>
#include <QToolTip>
#include <QtMath>
#include "Code/QRDUtils.h"
#include "Code/Resources.h"
#include "Widgets/CollapseGroupBox.h"
#include "Widgets/Extended/RDLabel.h"
#include "Widgets/Extended/RDSplitter.h"
#include "Windows/Dialogs/AxisMappingDialog.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;
namespace NativeScanCode
{
enum
{
#if defined(Q_OS_WIN32)
Key_A = 30,
Key_S = 31,
Key_D = 32,
Key_F = 33,
Key_W = 17,
Key_R = 19,
#elif defined(Q_OS_LINUX)
Key_A = 30 + 8,
Key_S = 31 + 8,
Key_D = 32 + 8,
Key_F = 33 + 8,
Key_W = 17 + 8,
Key_R = 19 + 8,
#elif defined(Q_OS_MACOS)
// scan codes not supported on OS X
Key_A = 0xDEADBEF1,
Key_S = 0xDEADBEF2,
Key_D = 0xDEADBEF3,
Key_F = 0xDEADBEF4,
Key_W = 0xDEADBEF5,
Key_R = 0xDEADBEF6,
#else
#error "Unknown platform! Define NativeScanCode"
#endif
};
}; // namespace NativeScanCode
namespace NativeVirtualKey
{
enum
{
#if defined(Q_OS_WIN32)
Key_A = quint32('A'),
Key_S = quint32('S'),
Key_D = quint32('D'),
Key_F = quint32('F'),
Key_W = quint32('W'),
Key_R = quint32('R'),
#elif defined(Q_OS_LINUX)
Key_A = quint32('a'),
Key_S = quint32('s'),
Key_D = quint32('d'),
Key_F = quint32('f'),
Key_W = quint32('w'),
Key_R = quint32('r'),
#elif defined(Q_OS_MACOS)
Key_A = 0x00,
Key_S = 0x01,
Key_D = 0x02,
Key_F = 0x03,
Key_W = 0x0D,
Key_R = 0x0F,
#else
#error "Unknown platform! Define NativeVirtualKey"
#endif
};
}; // namespace NativeVirtualKey
class CameraWrapper
{
public:
virtual ~CameraWrapper() {}
virtual bool Update(QRect winSize) = 0;
virtual ICamera *camera() = 0;
virtual void MouseWheel(QWheelEvent *e) = 0;
virtual void MouseClick(QMouseEvent *e) { m_DragStartPos = e->pos(); }
virtual void MouseMove(QMouseEvent *e)
{
if(e->buttons() & Qt::LeftButton)
{
m_DragStartPos = e->pos();
}
else
{
m_DragStartPos = QPoint(-1, -1);
}
}
enum class KeyPressDirection
{
None,
Left,
Right,
Forward,
Back,
Up,
Down,
};
KeyPressDirection GetDirection(QKeyEvent *e)
{
// if we have a native scancode, we expect to be able to match it. If we don't then don't get
// any false positives by checking the virtual key
if(e->nativeScanCode() > 1)
{
switch(e->nativeScanCode())
{
case NativeScanCode::Key_A: return KeyPressDirection::Left;
case NativeScanCode::Key_D: return KeyPressDirection::Right;
case NativeScanCode::Key_W: return KeyPressDirection::Forward;
case NativeScanCode::Key_S: return KeyPressDirection::Back;
case NativeScanCode::Key_R: return KeyPressDirection::Up;
case NativeScanCode::Key_F: return KeyPressDirection::Down;
default: break;
}
}
else
{
switch(e->nativeVirtualKey())
{
case NativeVirtualKey::Key_A: return KeyPressDirection::Left;
case NativeVirtualKey::Key_D: return KeyPressDirection::Right;
case NativeVirtualKey::Key_W: return KeyPressDirection::Forward;
case NativeVirtualKey::Key_S: return KeyPressDirection::Back;
case NativeVirtualKey::Key_R: return KeyPressDirection::Up;
case NativeVirtualKey::Key_F: return KeyPressDirection::Down;
default: break;
}
}
// handle arrow keys, we can do this safely with Qt::Key
switch(e->key())
{
case Qt::Key_Left: return KeyPressDirection::Left;
case Qt::Key_Right: return KeyPressDirection::Right;
case Qt::Key_Up: return KeyPressDirection::Forward;
case Qt::Key_Down: return KeyPressDirection::Back;
case Qt::Key_PageUp: return KeyPressDirection::Up;
case Qt::Key_PageDown: return KeyPressDirection::Down;
default: break;
}
return KeyPressDirection::None;
}
virtual void KeyUp(QKeyEvent *e)
{
KeyPressDirection dir = GetDirection(e);
if(dir == KeyPressDirection::Left || dir == KeyPressDirection::Right)
setMove(Direction::Horiz, 0);
if(dir == KeyPressDirection::Forward || dir == KeyPressDirection::Back)
setMove(Direction::Fwd, 0);
if(dir == KeyPressDirection::Up || dir == KeyPressDirection::Down)
setMove(Direction::Vert, 0);
if(e->modifiers() & Qt::ShiftModifier)
m_CurrentSpeed = 3.0f;
else
m_CurrentSpeed = 1.0f;
}
virtual void KeyDown(QKeyEvent *e)
{
KeyPressDirection dir = GetDirection(e);
switch(dir)
{
case KeyPressDirection::None: 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;
}
if(e->modifiers() & Qt::ShiftModifier)
m_CurrentSpeed = 3.0f;
else
m_CurrentSpeed = 1.0f;
}
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; }
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() { 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
{
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() { 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;
}
void MouseWheel(QWheelEvent *e) override {}
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<uint32_t> 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 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<ShaderVariable> 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;
rdcarray<ShaderConstant> columns;
rdcarray<BufferElementProperties> props;
QVector<PixelValue> generics;
QVector<bool> genericsEnabled;
QList<BufferData *> 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;
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();
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(r, g, 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(r, g, 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;
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::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)
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.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<int> 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<int> 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
{
return config.columns[columnLookup[col - reservedColumnCount()]];
}
const BufferElementProperties &propForColumn(int col) const
{
return config.props[columnLookup[col - reservedColumnCount()]];
}
bool useGenerics(int col) const
{
col = columnLookup[col - reservedColumnCount()];
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<int> columnLookup;
QVector<int> 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 { return componentLookup[col - reservedColumnCount()]; }
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);
col = columnLookup[col - reservedColumnCount()];
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 vsinHoriz;
int vsoutHoriz;
int gsoutHoriz;
int vsinVert;
int vsoutVert;
int gsoutVert;
CBufferData cb;
QString highlightNames[6];
BufferConfiguration vsinConfig, vsoutConfig, gsoutConfig;
MeshFormat postVS, postGS;
};
struct CalcBoundingBoxData
{
uint32_t eventId;
BufferConfiguration input[3];
BBoxData output;
};
void CacheDataForIteration(QVector<CachedElData> &cache, const rdcarray<ShaderConstant> &columns,
const rdcarray<BufferElementProperties> &props,
const QList<BufferData *> 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 = &el;
d.prop = &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;
}
cache.push_back(d);
}
}
static void ConfigureStatusColumn(rdcarray<ShaderConstant> &columns,
rdcarray<BufferElementProperties> &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, const ShaderReflection *shader,
rdcarray<ShaderConstant> &columns,
rdcarray<BufferElementProperties> &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)
{
ShaderConstant f;
BufferElementProperties p;
f.name = !sig.varName.isEmpty() ? sig.varName : sig.semanticIdxName;
f.type.rows = 1;
f.type.columns = sig.compCount;
p.buffer = 0;
p.perinstance = false;
p.instancerate = 1;
p.systemValue = sig.systemValue;
p.format.type = ResourceFormatType::Regular;
p.format.compByteWidth = qMax<uint32_t>(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 offset = 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;
if(ctx.CurPipelineState().HasAlignedPostVSData(
shader->stage == ShaderStage::Vertex ? MeshDataStage::VSOut : MeshDataStage::GSOut))
{
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 ConfigureMeshColumns(ICaptureContext &ctx, PopulateBufferData *bufdata)
{
const ActionDescription *action = ctx.CurAction();
bufdata->vsinConfig.numRows = 0;
bufdata->vsinConfig.unclampedNumRows = 0;
bufdata->vsinConfig.noVertices = false;
bufdata->vsinConfig.noInstances = false;
if(!action || !(action->flags & ActionFlags::Drawcall))
{
IEventBrowser *eb = ctx.GetEventBrowser();
bufdata->vsinConfig.statusString = bufdata->vsoutConfig.statusString =
bufdata->gsoutConfig.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->vsinConfig.columns, bufdata->vsinConfig.props);
ConfigureStatusColumn(bufdata->vsoutConfig.columns, bufdata->vsoutConfig.props);
ConfigureStatusColumn(bufdata->gsoutConfig.columns, bufdata->gsoutConfig.props);
bufdata->vsinConfig.genericsEnabled.push_back(false);
bufdata->vsinConfig.generics.push_back(PixelValue());
return;
}
rdcarray<VertexInputAttribute> vinputs = ctx.CurPipelineState().GetVertexInputs();
bufdata->vsinConfig.columns.reserve(vinputs.count());
bufdata->vsinConfig.columns.clear();
bufdata->vsinConfig.props.reserve(vinputs.count());
bufdata->vsinConfig.props.clear();
bufdata->vsinConfig.genericsEnabled.resize(vinputs.count());
bufdata->vsinConfig.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->vsinConfig.genericsEnabled[bufdata->vsinConfig.columns.count()] = false;
if(a.genericEnabled)
{
bufdata->vsinConfig.genericsEnabled[bufdata->vsinConfig.columns.count()] = true;
bufdata->vsinConfig.generics[bufdata->vsinConfig.columns.count()] = a.genericValue;
}
bufdata->vsinConfig.columns.push_back(f);
bufdata->vsinConfig.props.push_back(p);
}
if(action)
{
bufdata->vsinConfig.numRows = action->numIndices;
bufdata->vsinConfig.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<BoundVBuffer> 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->vsinConfig.numRows)
{
bufdata->vsinConfig.unclampedNumRows = bufdata->vsinConfig.numRows;
bufdata->vsinConfig.numRows = numRowsUpperBound + 100;
}
if((action->flags & ActionFlags::Drawcall) && action->numIndices == 0)
bufdata->vsinConfig.noVertices = true;
if((action->flags & ActionFlags::Instanced) && action->numInstances == 0)
{
bufdata->vsinConfig.noInstances = true;
bufdata->vsinConfig.numRows = bufdata->vsinConfig.unclampedNumRows = 0;
}
}
bufdata->vsoutConfig.columns.clear();
bufdata->vsoutConfig.props.clear();
bufdata->gsoutConfig.columns.clear();
bufdata->gsoutConfig.props.clear();
if(action)
{
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, vs, bufdata->vsoutConfig.columns, bufdata->vsoutConfig.props);
ConfigureColumnsForShader(ctx, last, bufdata->gsoutConfig.columns, bufdata->gsoutConfig.props);
}
}
static void RT_FetchMeshData(IReplayController *r, ICaptureContext &ctx, PopulateBufferData *data)
{
const ActionDescription *action = ctx.CurAction();
BoundVBuffer ib = ctx.CurPipelineState().GetIBuffer();
rdcarray<BoundVBuffer> 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->vsinConfig.indices)
data->vsinConfig.indices->deref();
data->vsinConfig.indices = new BufferData();
if(action && ib.byteStride != 0 && !idata.isEmpty())
data->vsinConfig.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->vsinConfig.indices->storage.resize(sizeof(uint32_t));
uint32_t *indices = (uint32_t *)data->vsinConfig.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->vsinConfig.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->vsinConfig.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->vsinConfig.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->vsinConfig.columns.count(); c++)
{
const ShaderConstant &col = data->vsinConfig.columns[c];
const BufferElementProperties &prop = data->vsinConfig.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->vsinConfig.buffers.push_back(buf);
}
if(data->postVS.numIndices <= data->vsinConfig.numRows)
{
data->vsoutConfig.numRows = data->postVS.numIndices;
data->vsoutConfig.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->vsoutConfig.numRows = data->vsinConfig.numRows;
data->vsoutConfig.unclampedNumRows = data->vsinConfig.unclampedNumRows;
}
data->vsoutConfig.statusString = data->postVS.status;
data->vsoutConfig.baseVertex = data->postVS.baseVertex;
data->vsoutConfig.displayBaseVertex = data->vsinConfig.baseVertex;
if(action && data->postVS.indexResourceId != ResourceId() && (action->flags & ActionFlags::Indexed))
idata = r->GetBufferData(data->postVS.indexResourceId, data->postVS.indexByteOffset,
numIndices * data->postVS.indexByteStride);
indices = NULL;
if(data->vsoutConfig.indices)
data->vsoutConfig.indices->deref();
if(data->vsoutConfig.displayIndices)
data->vsoutConfig.displayIndices->deref();
{
// display the same index values
data->vsoutConfig.displayIndices = data->vsinConfig.indices;
data->vsoutConfig.displayIndices->ref();
data->vsoutConfig.indices = new BufferData();
if(action && ib.byteStride != 0 && !idata.isEmpty())
{
data->vsoutConfig.indices->storage.resize(sizeof(uint32_t) * numIndices);
indices = (uint32_t *)data->vsoutConfig.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->postVS.vertexResourceId != ResourceId())
{
BufferData *postvs = new BufferData;
postvs->storage =
r->GetBufferData(data->postVS.vertexResourceId, data->postVS.vertexByteOffset, 0);
postvs->stride = data->postVS.vertexByteStride;
// ref passes to model
data->vsoutConfig.buffers.push_back(postvs);
}
data->gsoutConfig.statusString = data->postGS.status;
data->gsoutConfig.numRows = data->postGS.numIndices;
data->gsoutConfig.unclampedNumRows = 0;
data->gsoutConfig.baseVertex = data->postGS.baseVertex;
data->gsoutConfig.displayBaseVertex = data->vsinConfig.baseVertex;
indices = NULL;
data->gsoutConfig.indices = NULL;
if(data->postGS.vertexResourceId != ResourceId())
{
BufferData *postgs = new BufferData;
postgs->storage =
r->GetBufferData(data->postGS.vertexResourceId, data->postGS.vertexByteOffset, 0);
postgs->stride = data->postGS.vertexByteStride;
// ref passes to model
data->gsoutConfig.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<ShaderConstant> &columns,
rdcarray<BufferElementProperties> &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<ShaderConstant> &columns,
rdcarray<BufferElementProperties> &props)
{
UnrollConstant("", 0, constant, columns, props);
}
QList<BufferViewer *> 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_ModelVSIn = new BufferItemModel(ui->vsinData, true, meshview, this);
m_ModelVSOut = new BufferItemModel(ui->vsoutData, false, meshview, this);
m_ModelGSOut = new BufferItemModel(ui->gsoutData, false, meshview, this);
m_MeshView = meshview;
ui->formatSpecifier->setContext(&m_Ctx);
m_Flycam = new FlycamWrapper();
m_Arcball = new ArcballWrapper();
m_CurrentCamera = m_Arcball;
m_Output = NULL;
memset(&m_Config, 0, sizeof(m_Config));
m_Config.type = MeshDataStage::VSIn;
m_Config.wireframeDraw = true;
ui->outputTabs->setCurrentIndex(0);
m_CurStage = MeshDataStage::VSIn;
ui->vsinData->setFont(Formatter::FixedFont());
ui->vsoutData->setFont(Formatter::FixedFont());
ui->gsoutData->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());
ui->fovGuess->setFont(Formatter::PreferredFont());
ui->aspectGuess->setFont(Formatter::PreferredFont());
ui->nearGuess->setFont(Formatter::PreferredFont());
ui->farGuess->setFont(Formatter::PreferredFont());
if(meshview)
SetupMeshView();
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());
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(ui->exportDrop, &QToolButton::clicked,
[this] { exportData(BufferExport(BufferExport::CSV)); });
ui->vsinData->setContextMenuPolicy(Qt::CustomContextMenu);
ui->vsoutData->setContextMenuPolicy(Qt::CustomContextMenu);
ui->gsoutData->setContextMenuPolicy(Qt::CustomContextMenu);
ui->fixedVars->setContextMenuPolicy(Qt::CustomContextMenu);
QMenu *menu = new QMenu(this);
ui->vsinData->setCustomHeaderSizing(true);
ui->vsoutData->setCustomHeaderSizing(true);
ui->gsoutData->setCustomHeaderSizing(true);
QObject::connect(ui->fixedVars, &RDTreeWidget::customContextMenuRequested, this,
&BufferViewer::fixedVars_contextMenu);
QObject::connect(ui->vsinData, &RDTableView::customContextMenuRequested,
[this, menu](const QPoint &pos) { stageRowMenu(MeshDataStage::VSIn, menu, pos); });
menu = new QMenu(this);
QObject::connect(
ui->vsoutData, &RDTableView::customContextMenuRequested,
[this, menu](const QPoint &pos) { stageRowMenu(MeshDataStage::VSOut, menu, pos); });
menu = new QMenu(this);
QObject::connect(
ui->gsoutData, &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->solidShading->addItems({tr("None"), tr("Solid Colour"), tr("Flat Shaded"), tr("Secondary")});
ui->solidShading->adjustSize();
ui->solidShading->setCurrentIndex(0);
ui->matrixType->addItems({tr("Perspective"), tr("Orthographic")});
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->fovGuess->setValue(90.0);
on_controlType_currentIndexChanged(0);
QObject::connect(ui->vsinData->selectionModel(), &QItemSelectionModel::selectionChanged, this,
&BufferViewer::data_selected);
QObject::connect(ui->vsoutData->selectionModel(), &QItemSelectionModel::selectionChanged, this,
&BufferViewer::data_selected);
QObject::connect(ui->gsoutData->selectionModel(), &QItemSelectionModel::selectionChanged, this,
&BufferViewer::data_selected);
m_CurView = ui->vsinData;
m_CurFixed = false;
QObject::connect(ui->vsinData, &RDTableView::clicked, [this]() {
m_CurView = ui->vsinData;
m_CurFixed = false;
});
QObject::connect(ui->vsoutData, &RDTableView::clicked, [this]() { m_CurView = ui->vsoutData; });
QObject::connect(ui->gsoutData, &RDTableView::clicked, [this]() { m_CurView = ui->gsoutData; });
QObject::connect(ui->fixedVars, &RDTreeWidget::clicked, [this]() {
m_CurView = NULL;
m_CurFixed = true;
});
QObject::connect(ui->vsinData->verticalScrollBar(), &QScrollBar::valueChanged, this,
&BufferViewer::data_scrolled);
QObject::connect(ui->vsoutData->verticalScrollBar(), &QScrollBar::valueChanged, this,
&BufferViewer::data_scrolled);
QObject::connect(ui->gsoutData->verticalScrollBar(), &QScrollBar::valueChanged, this,
&BufferViewer::data_scrolled);
QObject::connect(ui->fovGuess, OverloadedSlot<double>::of(&QDoubleSpinBox::valueChanged), this,
&BufferViewer::camGuess_changed);
QObject::connect(ui->aspectGuess, OverloadedSlot<double>::of(&QDoubleSpinBox::valueChanged), this,
&BufferViewer::camGuess_changed);
QObject::connect(ui->nearGuess, OverloadedSlot<double>::of(&QDoubleSpinBox::valueChanged), this,
&BufferViewer::camGuess_changed);
QObject::connect(ui->farGuess, OverloadedSlot<double>::of(&QDoubleSpinBox::valueChanged), this,
&BufferViewer::camGuess_changed);
QObject::connect(ui->matrixType, OverloadedSlot<int>::of(&QComboBox::currentIndexChanged),
[this](int) { camGuess_changed(0.0); });
{
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(PanelMenu::MeshPreview, 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::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);
Reset();
m_Ctx.AddCaptureViewer(this);
}
void BufferViewer::SetupRawView()
{
ui->formatSpecifier->setVisible(true);
ui->outputTabs->setVisible(false);
ui->vsoutData->setVisible(false);
ui->gsoutData->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->vsinData->setFrameShape(QFrame::NoFrame);
ui->fixedVars->setFrameShape(QFrame::NoFrame);
ui->vsinData->setPinnedColumns(1);
ui->vsinData->setColumnGroupRole(columnGroupRole);
m_delegate = new RichTextViewDelegate(ui->vsinData);
ui->vsinData->setItemDelegate(m_delegate);
ui->vsinData->viewport()->installEventFilter(this);
ui->vsinData->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->vsinData);
m_OuterSplitter->insertWidget(1, ui->formatSpecifier);
m_VLayout->addWidget(m_OuterSplitter);
}
void BufferViewer::SetupMeshView()
{
setWindowTitle(tr("Mesh Viewer"));
// 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->fixedVars->setVisible(false);
ui->showPadding->setVisible(false);
ui->resourceDetails->setVisible(false);
ui->formatSpecifier->setVisible(false);
ui->cameraControlsGroup->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->vsinData->setWindowTitle(tr("VS Input"));
ui->vsinData->setFrameShape(QFrame::NoFrame);
ui->dockarea->addToolWindow(
ui->vsinData, ToolWindowManager::AreaReference(ToolWindowManager::TopOf,
ui->dockarea->areaOf(ui->outputTabs), 0.5f));
ui->dockarea->setToolWindowProperties(ui->vsinData, ToolWindowManager::HideCloseButton);
ui->vsoutData->setWindowTitle(tr("VS Output"));
ui->vsoutData->setFrameShape(QFrame::NoFrame);
ui->dockarea->addToolWindow(
ui->vsoutData, ToolWindowManager::AreaReference(ToolWindowManager::RightOf,
ui->dockarea->areaOf(ui->vsinData), 0.5f));
ui->dockarea->setToolWindowProperties(ui->vsoutData, ToolWindowManager::HideCloseButton);
ui->gsoutData->setWindowTitle(tr("GS/DS Output"));
ui->gsoutData->setFrameShape(QFrame::NoFrame);
ui->dockarea->addToolWindow(
ui->gsoutData, ToolWindowManager::AreaReference(ToolWindowManager::AddTo,
ui->dockarea->areaOf(ui->vsoutData), 0.5f));
ui->dockarea->setToolWindowProperties(ui->gsoutData, ToolWindowManager::HideCloseButton);
ToolWindowManager::raiseToolWindow(ui->vsoutData);
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.solidShadeMode == SolidShade::Secondary, false);
UI_CalculateMeshFormats();
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_CalculateMeshFormats();
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.solidShadeMode == SolidShade::Secondary,
false);
UI_CalculateMeshFormats();
UpdateCurrentMeshConfig();
INVOKE_MEMFN(RT_UpdateAndDisplay);
});
QObject::connect(m_SelectSecondAlphaColumn, &QAction::triggered, [this]() {
BufferItemModel *model = (BufferItemModel *)m_CurView->model();
model->setSecondaryColumn(m_ContextColumn, m_Config.solidShadeMode == SolidShade::Secondary,
true);
UI_CalculateMeshFormats();
UpdateCurrentMeshConfig();
INVOKE_MEMFN(RT_UpdateAndDisplay);
});
ui->vsinData->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu);
ui->vsoutData->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu);
ui->gsoutData->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu);
ui->vsinData->setPinnedColumns(2);
ui->vsoutData->setPinnedColumns(2);
ui->gsoutData->setPinnedColumns(2);
ui->vsinData->setColumnGroupRole(columnGroupRole);
ui->vsoutData->setColumnGroupRole(columnGroupRole);
ui->gsoutData->setColumnGroupRole(columnGroupRole);
QObject::connect(ui->vsinData->horizontalHeader(), &QHeaderView::customContextMenuRequested,
[this](const QPoint &pos) { meshHeaderMenu(MeshDataStage::VSIn, pos); });
QObject::connect(ui->vsoutData->horizontalHeader(), &QHeaderView::customContextMenuRequested,
[this](const QPoint &pos) { meshHeaderMenu(MeshDataStage::VSOut, pos); });
QObject::connect(ui->gsoutData->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);
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);
expandAll.setIcon(Icons::arrow_out());
collapseAll.setIcon(Icons::arrow_in());
copy.setIcon(Icons::copy());
showPadding.setIcon(Icons::align());
showPadding.setCheckable(true);
showPadding.setChecked(ui->showPadding->isChecked());
expandAll.setEnabled(item && item->childCount() > 0);
collapseAll.setEnabled(expandAll.isEnabled());
contextMenu.addAction(&expandAll);
contextMenu.addAction(&collapseAll);
contextMenu.addAction(&copy);
contextMenu.addSeparator();
contextMenu.addAction(&showPadding);
contextMenu.addSeparator();
contextMenu.addAction(m_ExportCSV);
contextMenu.addAction(m_ExportBytes);
QObject::connect(&expandAll, &QAction::triggered,
[this, item]() { ui->fixedVars->expandAllItems(item); });
QObject::connect(&collapseAll, &QAction::triggered,
[this, item]() { ui->fixedVars->collapseAllItems(item); });
QObject::connect(&copy, &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);
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))
{
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;
QModelIndex idx = m_CurView->selectionModel()->currentIndex();
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->vsinHoriz = ui->vsinData->horizontalScrollBar()->value();
bufdata->vsoutHoriz = ui->vsoutData->horizontalScrollBar()->value();
bufdata->gsoutHoriz = ui->gsoutData->horizontalScrollBar()->value();
bufdata->vsinVert = ui->vsinData->indexAt(QPoint(0, 0)).row();
bufdata->vsoutVert = ui->vsoutData->indexAt(QPoint(0, 0)).row();
bufdata->gsoutVert = ui->gsoutData->indexAt(QPoint(0, 0)).row();
}
void BufferViewer::OnEventChanged(uint32_t eventId)
{
PopulateBufferData *bufdata = new PopulateBufferData;
m_Sequence++;
bufdata->sequence = m_Sequence;
if(m_Scrolls)
{
bufdata->vsinHoriz = m_Scrolls->vsinHoriz;
bufdata->vsoutHoriz = m_Scrolls->vsoutHoriz;
bufdata->gsoutHoriz = m_Scrolls->gsoutHoriz;
bufdata->vsinVert = m_Scrolls->vsinVert;
bufdata->vsoutVert = m_Scrolls->vsoutVert;
bufdata->gsoutVert = m_Scrolls->gsoutVert;
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_ModelVSIn->posName();
bufdata->highlightNames[1] = m_ModelVSIn->secondaryName();
bufdata->highlightNames[2] = m_ModelVSOut->posName();
bufdata->highlightNames[3] = m_ModelVSOut->secondaryName();
bufdata->highlightNames[4] = m_ModelGSOut->posName();
bufdata->highlightNames[5] = m_ModelGSOut->secondaryName();
const ActionDescription *action = m_Ctx.CurAction();
configureDrawRange();
if(m_MeshView)
{
ClearModels();
CalcColumnWidth();
ClearModels();
const PipeState &pipe = m_Ctx.CurPipelineState();
if(pipe.IsRestartEnabled() && action && (action->flags & ActionFlags::Indexed))
{
bufdata->vsinConfig.primRestart = pipe.GetRestartIndex();
if(pipe.GetIBuffer().byteStride == 1)
bufdata->vsinConfig.primRestart &= 0xff;
else if(pipe.GetIBuffer().byteStride == 2)
bufdata->vsinConfig.primRestart &= 0xffff;
bufdata->vsoutConfig.primRestart = bufdata->vsinConfig.primRestart;
// GS Out doesn't use primitive restart because it is post-expansion
}
ConfigureMeshColumns(m_Ctx, bufdata);
Viewport vp = m_Ctx.CurPipelineState().GetViewport(0);
float vpWidth = qAbs(vp.width);
float vpHeight = qAbs(vp.height);
m_Config.fov = ui->fovGuess->value();
m_Config.aspect = (vpWidth > 0.0f && vpHeight > 0.0f) ? (vpWidth / vpHeight) : 1.0f;
m_Config.highlightVert = 0;
if(ui->aspectGuess->value() > 0.0)
m_Config.aspect = ui->aspectGuess->value();
}
else
{
// update with the current cbuffer for the current slot
if(IsCBufferView())
{
BoundCBuffer cb = m_Ctx.CurPipelineState().GetConstantBuffer(
m_CBufferSlot.stage, m_CBufferSlot.slot, m_CBufferSlot.arrayIdx);
m_BufferID = cb.resourceId;
m_ByteOffset = cb.byteOffset;
m_ByteSize = cb.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;
}
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);
bufdata->cb.inlinedata = cb.inlineData;
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->vsinConfig.fixedVars = parsed.fixed;
bufdata->vsinConfig.packing = parsed.packing;
if(parsed.repeating.type.baseType != VarType::Unknown)
{
bufdata->vsinConfig.repeatStride = parsed.repeating.type.arrayByteStride;
bufdata->vsinConfig.repeatOffset = parsed.repeating.byteOffset;
UnrollConstant(parsed.repeating, bufdata->vsinConfig.columns, bufdata->vsinConfig.props);
}
else
{
bufdata->vsinConfig.repeatStride = 1U;
bufdata->vsinConfig.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->vsinConfig.fixedVars.type.members =
reflection->constantBlocks[m_CBufferSlot.slot].variables;
if(IsD3D(m_Ctx.APIProps().pipelineType))
bufdata->vsinConfig.packing = Packing::D3DCB;
else
bufdata->vsinConfig.packing = BufferFormatter::EstimatePackingRules(
reflection->resourceId, bufdata->vsinConfig.fixedVars.type.members);
}
}
ClearModels();
}
updateWindowTitle();
bufdata->vsinConfig.curInstance = bufdata->vsoutConfig.curInstance =
bufdata->gsoutConfig.curInstance = m_Config.curInstance;
bufdata->vsinConfig.curView = bufdata->vsoutConfig.curView = bufdata->gsoutConfig.curView =
m_Config.curView;
m_ModelVSIn->beginReset();
m_ModelVSOut->beginReset();
m_ModelGSOut->beginReset();
bufdata->vsinConfig.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<BufferViewer> me(this);
m_Ctx.Replay().AsyncInvoke([this, me, bufdata](IReplayController *r) {
if(!me)
return;
BufferData *buf = NULL;
if(m_MeshView)
{
bufdata->postVS = r->GetPostVSData(bufdata->vsinConfig.curInstance,
bufdata->vsinConfig.curView, MeshDataStage::VSOut);
bufdata->postGS = r->GetPostVSData(bufdata->vsinConfig.curInstance,
bufdata->vsinConfig.curView, MeshDataStage::GSOut);
RT_FetchMeshData(r, m_Ctx, bufdata);
if(!me)
return;
}
else
{
buf = new BufferData;
// calculate tight stride
buf->stride = std::max(1U, bufdata->vsinConfig.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->vsinConfig.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->vsinConfig.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 = bufdata->cb.inlinedata;
}
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);
}
uint32_t repeatedDataAvailable = uint32_t(buf->size() - fixedLength);
bufdata->vsinConfig.pagingOffset = uint32_t(m_PagingByteOffset / buf->stride);
bufdata->vsinConfig.numRows = uint32_t((repeatedDataAvailable + buf->stride - 1) / buf->stride);
// ownership passes to model
bufdata->vsinConfig.buffers.push_back(buf);
if(!me)
{
delete buf;
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->vsinConfig.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)
return;
if(!bufdata->vsoutConfig.statusString.isEmpty())
{
bufdata->vsoutConfig.columns.clear();
bufdata->vsoutConfig.props.clear();
ConfigureStatusColumn(bufdata->vsoutConfig.columns, bufdata->vsoutConfig.props);
}
if(!bufdata->gsoutConfig.statusString.isEmpty())
{
bufdata->gsoutConfig.columns.clear();
bufdata->gsoutConfig.props.clear();
ConfigureStatusColumn(bufdata->gsoutConfig.columns, bufdata->gsoutConfig.props);
}
m_ModelVSIn->endReset(bufdata->vsinConfig);
m_ModelVSOut->endReset(bufdata->vsoutConfig);
m_ModelGSOut->endReset(bufdata->gsoutConfig);
m_PostVS = bufdata->postVS;
m_PostGS = bufdata->postGS;
m_CurCBuffer = bufdata->cb;
// if we didn't have a position column selected before, or the name has changed, re-guess
if(m_ModelVSIn->posColumn() == -1 ||
bufdata->highlightNames[0] != bufdata->vsinConfig.columnName(m_ModelVSIn->posColumn()))
m_ModelVSIn->setPosColumn(-1);
// similarly for secondary columns
if(m_ModelVSIn->secondaryColumn() == -1 ||
bufdata->highlightNames[1] != bufdata->vsinConfig.columnName(m_ModelVSIn->secondaryColumn()))
m_ModelVSIn->setSecondaryColumn(-1, m_Config.solidShadeMode == SolidShade::Secondary, false);
// and as above for VS Out / GS Out
if(m_ModelVSOut->posColumn() == -1 ||
bufdata->highlightNames[2] != bufdata->vsoutConfig.columnName(m_ModelVSOut->posColumn()))
m_ModelVSOut->setPosColumn(-1);
if(m_ModelVSOut->secondaryColumn() == -1 ||
bufdata->highlightNames[3] !=
bufdata->vsoutConfig.columnName(m_ModelVSOut->secondaryColumn()))
m_ModelVSOut->setSecondaryColumn(-1, m_Config.solidShadeMode == SolidShade::Secondary, false);
if(m_ModelGSOut->posColumn() == -1 ||
bufdata->highlightNames[4] != bufdata->gsoutConfig.columnName(m_ModelGSOut->posColumn()))
m_ModelGSOut->setPosColumn(-1);
if(m_ModelGSOut->secondaryColumn() == -1 ||
bufdata->highlightNames[5] !=
bufdata->gsoutConfig.columnName(m_ModelGSOut->secondaryColumn()))
m_ModelGSOut->setSecondaryColumn(-1, m_Config.solidShadeMode == SolidShade::Secondary, false);
EnableCameraGuessControls();
populateBBox(bufdata);
UI_CalculateMeshFormats();
UpdateCurrentMeshConfig();
ApplyRowAndColumnDims(
m_ModelVSIn->columnCount(), ui->vsinData,
bufdata->vsinConfig.statusString.isEmpty() ? m_DataColWidth : m_ErrorColWidth);
ApplyRowAndColumnDims(
m_ModelVSOut->columnCount(), ui->vsoutData,
bufdata->vsoutConfig.statusString.isEmpty() ? m_DataColWidth : m_ErrorColWidth);
ApplyRowAndColumnDims(
m_ModelGSOut->columnCount(), ui->gsoutData,
bufdata->gsoutConfig.statusString.isEmpty() ? m_DataColWidth : m_ErrorColWidth);
uint32_t numRows = qMax(qMax(bufdata->vsinConfig.numRows, bufdata->vsoutConfig.numRows),
bufdata->gsoutConfig.numRows);
if(!m_MeshView)
numRows = qMax(numRows, bufdata->vsinConfig.unclampedNumRows);
ui->rowOffset->setMaximum((int)qMax(1U, numRows) - 1);
ScrollToRow(ui->vsinData, qMin(int(bufdata->vsinConfig.numRows) - 1, bufdata->vsinVert));
ScrollToRow(ui->vsoutData, qMin(int(bufdata->vsoutConfig.numRows) - 1, bufdata->vsoutVert));
ScrollToRow(ui->gsoutData, qMin(int(bufdata->gsoutConfig.numRows) - 1, bufdata->gsoutVert));
ui->vsinData->horizontalScrollBar()->setValue(bufdata->vsinHoriz);
ui->vsoutData->horizontalScrollBar()->setValue(bufdata->vsoutHoriz);
ui->gsoutData->horizontalScrollBar()->setValue(bufdata->gsoutHoriz);
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)
{
m_RepeatedOffset->setText(
tr("Starting at: %1 bytes").arg(m_ByteOffset + bufdata->vsinConfig.repeatOffset));
{
rdcarray<ShaderVariable> vars;
if((m_BufferID == ResourceId() && m_CurCBuffer.inlinedata.empty()) || m_Format.isEmpty())
{
vars = bufdata->vsinConfig.evalVars;
}
else
{
ShaderVariable var = InterpretShaderVar(bufdata->vsinConfig.fixedVars,
bufdata->vsinConfig.buffers[0]->storage.begin(),
bufdata->vsinConfig.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->vsinConfig.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->vsinConfig.pagingOffset > 0);
const bool next = (bufdata->vsinConfig.numRows >= MaxVisibleRows);
if(prev && next)
{
ui->vsinData->setIndexWidget(m_ModelVSIn->index(0, 0), MakePreviousPageButton());
ui->vsinData->setIndexWidget(m_ModelVSIn->index(0, 1), MakeNextPageButton());
ui->vsinData->setIndexWidget(m_ModelVSIn->index(MaxVisibleRows + 1, 0),
MakePreviousPageButton());
ui->vsinData->setIndexWidget(m_ModelVSIn->index(MaxVisibleRows + 1, 1),
MakeNextPageButton());
}
else if(prev)
{
ui->vsinData->setIndexWidget(m_ModelVSIn->index(0, 0), MakePreviousPageButton());
}
else if(next)
{
ui->vsinData->setIndexWidget(m_ModelVSIn->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->vsinConfig;
bbox->input[1] = bufdata->vsoutConfig;
bbox->input[2] = bufdata->vsoutConfig;
QPointer<BufferViewer> me(this);
// fire up a thread to calculate the bounding box
LambdaThread *thread = new LambdaThread([this, me, bbox] {
if(!me)
return;
calcBoundingData(*bbox);
if(!me)
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<FixedVarTag>();
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(size), 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(size), QString(), tr("Padding")});
pad->setItalic(true);
pad->setTag(QVariant::fromValue(FixedVarTag(size)));
n->addChild(pad);
}
}
}
void BufferViewer::UI_AddFixedVariables(RDTreeWidgetItem *root, uint32_t baseOffset,
const rdcarray<ShaderConstant> &consts,
const rdcarray<ShaderVariable> &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(size), QString(), tr("Padding")});
pad->setItalic(true);
pad->setTag(QVariant::fromValue(FixedVarTag(size)));
root->addChild(pad);
offset = c.byteOffset;
}
QVariant offsetStr;
offsetStr = baseOffset + c.byteOffset;
if(c.bitFieldSize != 0)
{
offsetStr =
offsetStr.toString() +
QFormatStr(" (bits %1:%2)").arg(c.bitFieldOffset).arg(c.bitFieldOffset + c.bitFieldSize);
}
RDTreeWidgetItem *n =
new RDTreeWidgetItem({v.name, VarString(v, c), offsetStr, TypeString(v, c)});
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), elOffset, 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(size), 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<FloatVector> &minOutputList = bbox.output.bounds[stage].Min;
QList<FloatVector> &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<CachedElData> 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_CalculateMeshFormats()
{
if(!m_MeshView)
return;
const PipeState &pipe = m_Ctx.CurPipelineState();
rdcarray<BoundVBuffer> vbs = pipe.GetVBuffers();
const ActionDescription *action = m_Ctx.CurAction();
if(action)
{
m_VSInPosition = MeshFormat();
m_VSInSecondary = MeshFormat();
m_VSInPosition.allowRestart = pipe.IsRestartEnabled() && (action->flags & ActionFlags::Indexed);
m_VSInPosition.restartIndex = pipe.GetRestartIndex();
const BufferConfiguration &vsinConfig = m_ModelVSIn->getConfig();
if(!vsinConfig.columns.empty())
{
int elIdx = m_ModelVSIn->posColumn();
if(elIdx < 0 || elIdx >= vsinConfig.columns.count())
elIdx = 0;
if(vsinConfig.unclampedNumRows > 0)
m_VSInPosition.numIndices = vsinConfig.numRows;
else
m_VSInPosition.numIndices = action->numIndices;
if((action->flags & ActionFlags::Instanced) && action->numInstances == 0)
m_VSInPosition.numIndices = 0;
BoundVBuffer ib = pipe.GetIBuffer();
m_VSInPosition.topology = pipe.GetPrimitiveTopology();
m_VSInPosition.indexByteStride = ib.byteStride;
m_VSInPosition.baseVertex = action->baseVertex;
m_VSInPosition.indexResourceId = ib.resourceId;
uint32_t drawIdxByteOffs = action->indexOffset * ib.byteStride;
m_VSInPosition.indexByteOffset = ib.byteOffset + drawIdxByteOffs;
if(ib.byteSize >= ~0U)
m_VSInPosition.indexByteSize = ib.byteSize;
else if(drawIdxByteOffs > ib.byteSize)
m_VSInPosition.indexByteSize = 0;
else
m_VSInPosition.indexByteSize = ib.byteSize - drawIdxByteOffs;
if((action->flags & ActionFlags::Indexed) && m_VSInPosition.indexByteStride == 0)
m_VSInPosition.indexByteStride = 4U;
{
const ShaderConstant &el = vsinConfig.columns[elIdx];
const BufferElementProperties &prop = vsinConfig.props[elIdx];
m_VSInPosition.instanced = prop.perinstance;
m_VSInPosition.instStepRate = prop.instancerate;
if(prop.buffer < vbs.count() && !vsinConfig.genericsEnabled[elIdx])
{
m_VSInPosition.vertexResourceId = vbs[prop.buffer].resourceId;
m_VSInPosition.vertexByteStride = vbs[prop.buffer].byteStride;
m_VSInPosition.vertexByteOffset = vbs[prop.buffer].byteOffset + el.byteOffset +
action->vertexOffset * m_VSInPosition.vertexByteStride;
m_VSInPosition.vertexByteSize = vbs[prop.buffer].byteSize;
}
else
{
m_VSInPosition.vertexResourceId = ResourceId();
m_VSInPosition.vertexByteStride = 0;
m_VSInPosition.vertexByteOffset = 0;
}
m_VSInPosition.format = prop.format;
}
elIdx = m_ModelVSIn->secondaryColumn();
if(elIdx >= 0 && elIdx < vsinConfig.columns.count())
{
const ShaderConstant &el = vsinConfig.columns[elIdx];
const BufferElementProperties &prop = vsinConfig.props[elIdx];
m_VSInSecondary.instanced = prop.perinstance;
m_VSInSecondary.instStepRate = prop.instancerate;
if(prop.buffer < vbs.count() && !vsinConfig.genericsEnabled[elIdx])
{
m_VSInSecondary.vertexResourceId = vbs[prop.buffer].resourceId;
m_VSInSecondary.vertexByteStride = vbs[prop.buffer].byteStride;
m_VSInSecondary.vertexByteOffset = vbs[prop.buffer].byteOffset + el.byteOffset +
action->vertexOffset * m_VSInSecondary.vertexByteStride;
m_VSInSecondary.vertexByteSize = vbs[prop.buffer].byteSize;
}
else
{
m_VSInSecondary.vertexResourceId = ResourceId();
m_VSInSecondary.vertexByteStride = 0;
m_VSInSecondary.vertexByteOffset = 0;
}
m_VSInSecondary.format = prop.format;
m_VSInSecondary.showAlpha = m_ModelVSIn->secondaryAlpha();
}
}
const BufferConfiguration &vsoutConfig = m_ModelVSOut->getConfig();
m_PostVSPosition = MeshFormat();
m_PostVSSecondary = MeshFormat();
if(!vsoutConfig.columns.empty())
{
int elIdx = m_ModelVSOut->posColumn();
if(elIdx < 0 || elIdx >= vsoutConfig.columns.count())
elIdx = 0;
const ShaderConstant &el = vsoutConfig.columns[elIdx];
const BufferElementProperties &prop = vsoutConfig.props[elIdx];
m_PostVSPosition = m_PostVS;
m_PostVSPosition.vertexByteOffset += el.byteOffset;
m_PostVSPosition.unproject = prop.systemValue == ShaderBuiltin::Position;
m_PostVSPosition.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_PostVSPosition.unproject = false;
elIdx = m_ModelVSOut->secondaryColumn();
if(elIdx >= 0 && elIdx < vsoutConfig.columns.count())
{
m_PostVSSecondary = m_PostVS;
m_PostVSSecondary.vertexByteOffset += vsoutConfig.columns[elIdx].byteOffset;
m_PostVSSecondary.format = prop.format;
m_PostVSSecondary.showAlpha = m_ModelVSOut->secondaryAlpha();
}
}
m_PostVSPosition.allowRestart = m_VSInPosition.allowRestart;
m_PostVSPosition.restartIndex = m_VSInPosition.restartIndex;
const BufferConfiguration &gsoutConfig = m_ModelGSOut->getConfig();
m_PostGSPosition = MeshFormat();
m_PostGSSecondary = MeshFormat();
if(!gsoutConfig.columns.empty())
{
int elIdx = m_ModelGSOut->posColumn();
if(elIdx < 0 || elIdx >= gsoutConfig.columns.count())
elIdx = 0;
const ShaderConstant &el = gsoutConfig.columns[elIdx];
const BufferElementProperties &prop = gsoutConfig.props[elIdx];
m_PostGSPosition = m_PostGS;
m_PostGSPosition.vertexByteOffset += el.byteOffset;
m_PostGSPosition.unproject = prop.systemValue == ShaderBuiltin::Position;
elIdx = m_ModelGSOut->secondaryColumn();
if(elIdx >= 0 && elIdx < gsoutConfig.columns.count())
{
m_PostGSSecondary = m_PostGS;
m_PostGSSecondary.vertexByteOffset += gsoutConfig.columns[elIdx].byteOffset;
m_PostGSSecondary.showAlpha = m_ModelGSOut->secondaryAlpha();
}
}
m_PostGSPosition.allowRestart = false;
m_PostGSPosition.indexByteStride = 0;
if(!(action->flags & ActionFlags::Indexed))
m_PostVSPosition.indexByteStride = m_VSInPosition.indexByteStride = 0;
}
else
{
m_VSInPosition = MeshFormat();
m_VSInSecondary = MeshFormat();
m_PostVSPosition = MeshFormat();
m_PostVSSecondary = MeshFormat();
m_PostGSPosition = MeshFormat();
m_PostGSSecondary = MeshFormat();
}
}
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<int> 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_VSInPosition;
m_Config.second = m_VSInSecondary;
break;
case MeshDataStage::VSOut:
m_Config.position = m_PostVSPosition;
m_Config.second = m_PostVSSecondary;
break;
case MeshDataStage::GSOut:
m_Config.position = m_PostGSPosition;
m_Config.second = m_PostGSSecondary;
break;
default: break;
}
camGuess_changed(0.0);
BufferItemModel *model = currentBufferModel();
int stage = currentStageIndex();
m_Config.showBBox = false;
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<BufferViewer> 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::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)
{
if(stage == MeshDataStage::VSIn)
ToolWindowManager::raiseToolWindow(ui->vsinData);
else if(stage == MeshDataStage::VSOut)
ToolWindowManager::raiseToolWindow(ui->vsoutData);
else if(stage == MeshDataStage::GSOut)
ToolWindowManager::raiseToolWindow(ui->gsoutData);
}
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);
}
}
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};
updateWindowTitle();
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};
updateWindowTitle();
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;
updateWindowTitle();
TextureDescription *tex = m_Ctx.GetTexture(id);
if(tex)
m_ObjectByteSize = tex->byteSize;
m_PagingByteOffset = 0;
ui->formatSpecifier->setAutoFormat(format);
}
void BufferViewer::ScrollToRow(int32_t row, MeshDataStage stage)
{
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<RDTreeWidget *>(watched);
if(tree)
{
RDTreeWidgetItem *item = tree->itemAt(tree->viewport()->mapFromGlobal(QCursor::pos()));
if(item)
{
FixedVarTag tag = item->tag().value<FixedVarTag>();
QString tooltip;
Packing::Rules pack = m_ModelVSIn->getConfig().packing;
if(tag.valid && tag.padding)
{
tooltip = tr("%1 bytes of padding. Packing rules in effect:\n\n").arg(tag.byteSize);
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(tag.byteOffset);
if(!IsCBufferView())
tooltip += tr(", not including overall base byte offset %1 in buffer").arg(m_ByteOffset);
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->vsinData->viewport())
{
QModelIndex index =
ui->vsinData->indexAt(ui->vsinData->viewport()->mapFromGlobal(QCursor::pos()));
if(index.isValid())
{
const ShaderConstant &c = m_ModelVSIn->elementForColumn(index.column());
QModelIndex rowidx = m_ModelVSIn->index(index.row(), 0, index.parent());
int row = m_ModelVSIn->data(rowidx).toInt();
size_t stride = m_ModelVSIn->getConfig().buffers[0]->stride;
QString tooltip;
tooltip = tr("%1 at overall byte offset %2").arg(c.name).arg(stride * row + c.byteOffset);
tooltip += tr(", not including overall base byte offset %1 in buffer").arg(m_ByteOffset);
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(stride * row)
.arg(stride)
.arg(c.name)
.arg(c.byteOffset);
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->vsinData->viewport())
{
if(event->type() == QEvent::MouseMove)
{
bool ret = QObject::eventFilter(watched, event);
QMouseEvent *mouseEvent = (QMouseEvent *)event;
if(m_delegate->linkHover(mouseEvent, font(),
ui->vsinData->indexAt(mouseEvent->localPos().toPoint())))
ui->vsinData->setCursor(QCursor(Qt::PointingHandCursor));
else
ui->vsinData->unsetCursor();
return ret;
}
}
return QObject::eventFilter(watched, event);
}
void BufferViewer::updateWindowTitle()
{
if(!m_MeshView)
{
if(IsCBufferView())
{
QString bufName;
const ShaderReflection *reflection =
m_Ctx.CurPipelineState().GetShaderReflection(m_CBufferSlot.stage);
int32_t bindPoint = -1;
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);
bindPoint = reflection->constantBlocks[m_CBufferSlot.slot].bindPoint;
}
}
if(bufName.isEmpty())
{
if(m_BufferID != ResourceId())
bufName = m_Ctx.GetResourceName(m_BufferID);
else
bufName = tr("Unbound");
}
const ShaderBindpointMapping &mapping =
m_Ctx.CurPipelineState().GetBindpointMapping(m_CBufferSlot.stage);
uint32_t arraySize = ~0U;
if(bindPoint >= 0 && bindPoint < mapping.constantBlocks.count())
arraySize = mapping.constantBlocks[bindPoint].arraySize;
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->vsinData;
else if(stage == MeshDataStage::VSOut)
return ui->vsoutData;
else if(stage == MeshDataStage::GSOut)
return ui->gsoutData;
return NULL;
}
BufferItemModel *BufferViewer::modelForStage(MeshDataStage stage)
{
if(stage == MeshDataStage::VSIn)
return m_ModelVSIn;
else if(stage == MeshDataStage::VSOut)
return m_ModelVSOut;
else if(stage == MeshDataStage::GSOut)
return m_ModelGSOut;
return NULL;
}
bool BufferViewer::isCurrentRasterOut()
{
BufferItemModel *model = currentBufferModel();
int stage = currentStageIndex();
// 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;
if(model)
{
int posEl = model->posColumn();
if(posEl >= 0 && posEl < model->getConfig().columns.count())
{
return model->getConfig().props[posEl].systemValue == ShaderBuiltin::Position;
}
}
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;
return 0;
}
void BufferViewer::Reset()
{
m_Output = NULL;
configureDrawRange();
ClearModels();
ui->vsinData->setColumnWidths({40, 40});
ui->vsoutData->setColumnWidths({40, 40});
ui->gsoutData->setColumnWidths({40, 40});
m_BBoxes.clear();
}
void BufferViewer::ClearModels()
{
for(BufferItemModel *m : {m_ModelVSIn, m_ModelVSOut, m_ModelGSOut})
{
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_ModelVSIn->beginReset();
m_ModelVSIn->endReset(bufconfig);
// measure this data so we can use this as column widths
ui->vsinData->resizeColumnsToContents();
// index/element column
m_IdxColWidth = ui->vsinData->columnWidth(0);
int col = 1;
if(m_MeshView)
col = 2;
m_DataColWidth = 10;
for(int c = 0; c < 5; c++)
{
int colWidth = ui->vsinData->columnWidth(col + c);
m_DataColWidth = qMax(m_DataColWidth, colWidth);
}
ui->vsinData->resizeRowsToContents();
m_DataRowHeight = ui->vsinData->rowHeight(0);
}
void BufferViewer::data_selected(const QItemSelection &selected, const QItemSelection &deselected)
{
QObject *sender = QObject::sender();
RDTableView *view = qobject_cast<RDTableView *>(sender);
if(view == NULL)
view = qobject_cast<RDTableView *>(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<RDTableView *>(sender);
while(sender != NULL && view == NULL)
{
sender = sender->parent();
view = qobject_cast<RDTableView *>(sender);
}
if(view == NULL)
return;
SyncViews(view, false, true);
}
void BufferViewer::camGuess_changed(double value)
{
m_Config.ortho = (ui->matrixType->currentIndex() == 1);
m_Config.fov = ui->fovGuess->value();
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(ui->aspectGuess->value() > 0.0)
m_Config.aspect = ui->aspectGuess->value();
// 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_PostVS.nearPlane;
m_Config.position.flipY = m_PostVS.flipY;
}
else if(m_CurStage == MeshDataStage::GSOut)
{
m_Config.position.nearPlane = m_PostGS.nearPlane;
m_Config.position.flipY = m_PostGS.flipY;
}
if(ui->nearGuess->value() > 0.0)
m_Config.position.nearPlane = ui->nearGuess->value();
m_Config.position.farPlane = 100.0f;
if(m_CurStage == MeshDataStage::VSOut)
m_Config.position.farPlane = m_PostVS.farPlane;
else if(m_CurStage == MeshDataStage::GSOut)
m_Config.position.farPlane = m_PostGS.farPlane;
if(ui->farGuess->value() > 0.0)
m_Config.position.farPlane = ui->farGuess->value();
EnableCameraGuessControls();
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_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::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->vsinData);
// 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<QHBoxLayout *>(m_RepeatedControlBar->layout());
hbox->insertWidget(0, ui->rowOffsetLabel);
hbox->insertWidget(1, ui->rowOffset);
}
ui->fixedVars->setVisible(true);
ui->vsinData->setVisible(true);
ui->showPadding->setVisible(true);
m_InnerSplitter->setVisible(true);
if(m_CurView == NULL && !m_CurFixed)
m_CurView = ui->vsinData;
}
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->vsinData->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->vsinData)
m_OuterSplitter->replaceWidget(0, ui->vsinData);
// 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<QHBoxLayout *>(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->vsinData->setVisible(true);
ui->showPadding->setVisible(false);
m_InnerSplitter->setVisible(false);
m_CurView = ui->vsinData;
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);
}
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->vsinData->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 &params)
{
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_ObjectByteSize)
{
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 chunkSize = 4 * 1024 * 1024;
for(uint64_t byteOffset = m_ByteOffset; byteOffset < m_ObjectByteSize;
byteOffset += chunkSize)
{
// it's fine to block invoke, because this is on the export thread
m_Ctx.Replay().BlockInvoke([buff, f, byteOffset](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<CachedElData> 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_ObjectByteSize)
{
// 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++)
{
s << model->data(model->index(row, col), Qt::DisplayRole).toString();
if(col + 1 < model->columnCount())
s << ", ";
}
s << "\n";
}
}
else
{
// write 64k rows at a time
ResourceId buff = m_BufferID;
const uint64_t chunkSize = 64 * 1024 * config.buffers[0]->stride;
for(uint64_t byteOffset = m_ByteOffset; byteOffset < m_ObjectByteSize;
byteOffset += chunkSize)
{
// it's fine to block invoke, because this is on the export thread
m_Ctx.Replay().BlockInvoke(
[buff, &s, &config, byteOffset, chunkSize](IReplayController *r) {
// cache column data for the inner loop
QVector<CachedElData> cache;
BufferData bufferData;
bufferData.storage = r->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);
for(int v = 0; v < list.count(); v++)
{
s << interpretVariant(list[v], *el, *prop);
if(v + 1 < list.count())
s << ", ";
}
if(list.empty())
{
for(int v = 0; v < d.numColumns; v++)
{
s << "---";
if(v + 1 < d.numColumns)
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->vsinData->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);
const ShaderBindpointMapping &bindMapping =
m_Ctx.CurPipelineState().GetBindpointMapping(ShaderStage::Vertex);
ResourceId pipeline = m_Ctx.CurPipelineState().GetGraphicsPipelineObject();
// viewer takes ownership of the trace
IShaderViewer *s = m_Ctx.DebugShader(&bindMapping, shaderDetails, pipeline, 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->vsinData, ui->vsoutData, ui->gsoutData};
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->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::EnableCameraGuessControls()
{
ui->matrixType->setEnabled(isCurrentRasterOut());
ui->aspectGuess->setEnabled(isCurrentRasterOut());
ui->nearGuess->setEnabled(isCurrentRasterOut());
ui->farGuess->setEnabled(isCurrentRasterOut());
// FOV is only available in perspective mode
ui->fovGuess->setEnabled(isCurrentRasterOut() && ui->matrixType->currentIndex() == 0);
}
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 = MeshDataStage::VSIn;
else if(index == 1)
m_CurStage = MeshDataStage::VSOut;
else if(index == 2)
m_CurStage = MeshDataStage::GSOut;
configureDrawRange();
on_resetCamera_clicked();
ui->autofitCamera->setEnabled(!isCurrentRasterOut());
EnableCameraGuessControls();
ui->axisMappingCombo->setEnabled(!isCurrentRasterOut());
ui->axisMappingButton->setEnabled(!isCurrentRasterOut() &&
ui->axisMappingCombo->currentIndex() == 4);
UpdateCurrentMeshConfig();
INVOKE_MEMFN(RT_UpdateAndDisplay);
}
void BufferViewer::on_toggleControls_toggled(bool checked)
{
ui->cameraControlsGroup->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();
EnableCameraGuessControls();
}
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_wireframeRender_toggled(bool checked)
{
m_Config.wireframeDraw = checked;
INVOKE_MEMFN(RT_UpdateAndDisplay);
}
void BufferViewer::on_solidShading_currentIndexChanged(int index)
{
ui->wireframeRender->setEnabled(index > 0);
if(!ui->wireframeRender->isEnabled())
{
ui->wireframeRender->setChecked(true);
m_Config.wireframeDraw = true;
}
m_Config.solidShadeMode = (SolidShade)qMax(0, index);
m_ModelVSIn->setSecondaryColumn(m_ModelVSIn->secondaryColumn(),
m_Config.solidShadeMode == SolidShade::Secondary,
m_ModelVSIn->secondaryAlpha());
m_ModelVSOut->setSecondaryColumn(m_ModelVSOut->secondaryColumn(),
m_Config.solidShadeMode == SolidShade::Secondary,
m_ModelVSOut->secondaryAlpha());
m_ModelGSOut->setSecondaryColumn(m_ModelGSOut->secondaryColumn(),
m_Config.solidShadeMode == SolidShade::Secondary,
m_ModelGSOut->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::on_rowOffset_valueChanged(int value)
{
if(!m_MeshView && m_ModelVSIn->getConfig().unclampedNumRows > 0)
{
int page = value / MaxVisibleRows;
value %= MaxVisibleRows;
uint64_t pageOffset = page * MaxVisibleRows * m_ModelVSIn->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->vsinData, value);
ScrollToRow(ui->vsoutData, value);
ScrollToRow(ui->gsoutData, 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->vsinData->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 = NULL;
int stage = 0;
switch(m_CurStage)
{
case MeshDataStage::VSIn:
model = m_ModelVSIn;
stage = 0;
break;
case MeshDataStage::VSOut:
model = m_ModelVSOut;
stage = 1;
break;
case MeshDataStage::GSOut:
model = m_ModelGSOut;
stage = 2;
break;
default: break;
}
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;
m_Flycam->Reset(mid);
}
INVOKE_MEMFN(RT_UpdateAndDisplay);
}