mirror of
https://github.com/baldurk/renderdoc.git
synced 2026-05-05 01:20:42 +00:00
1055 lines
36 KiB
C++
1055 lines
36 KiB
C++
/******************************************************************************
|
|
* The MIT License (MIT)
|
|
*
|
|
* Copyright (c) 2019-2025 Baldur Karlsson
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
* THE SOFTWARE.
|
|
******************************************************************************/
|
|
|
|
#include "ShaderMessageViewer.h"
|
|
#include <QAction>
|
|
#include <QMenu>
|
|
#include <QMouseEvent>
|
|
#include <QScrollBar>
|
|
#include "Code/QRDUtils.h"
|
|
#include "Code/Resources.h"
|
|
#include "Widgets/Extended/RDHeaderView.h"
|
|
#include "Widgets/Extended/RDLineEdit.h"
|
|
#include "toolwindowmanager/ToolWindowManager.h"
|
|
#include "ui_ShaderMessageViewer.h"
|
|
|
|
static const int debuggableRole = Qt::UserRole + 1;
|
|
static const int gotoableRole = Qt::UserRole + 2;
|
|
|
|
ShaderMessageViewer::ShaderMessageViewer(ICaptureContext &ctx, ShaderStageMask stages, QWidget *parent)
|
|
: QFrame(parent), ui(new Ui::ShaderMessageViewer), m_Ctx(ctx)
|
|
{
|
|
ui->setupUi(this);
|
|
|
|
ui->messages->setFont(Formatter::PreferredFont());
|
|
|
|
ui->messages->setMouseTracking(true);
|
|
|
|
m_API = m_Ctx.APIProps().pipelineType;
|
|
|
|
QObject::connect(ui->task, &QToolButton::toggled, [this](bool) { refreshMessages(); });
|
|
QObject::connect(ui->mesh, &QToolButton::toggled, [this](bool) { refreshMessages(); });
|
|
QObject::connect(ui->vertex, &QToolButton::toggled, [this](bool) { refreshMessages(); });
|
|
QObject::connect(ui->hull, &QToolButton::toggled, [this](bool) { refreshMessages(); });
|
|
QObject::connect(ui->domain, &QToolButton::toggled, [this](bool) { refreshMessages(); });
|
|
QObject::connect(ui->geometry, &QToolButton::toggled, [this](bool) { refreshMessages(); });
|
|
QObject::connect(ui->pixel, &QToolButton::toggled, [this](bool) { refreshMessages(); });
|
|
QObject::connect(ui->filterButton, &QToolButton::clicked, [this]() { refreshMessages(); });
|
|
QObject::connect(ui->filter, &RDLineEdit::returnPressed, [this]() { refreshMessages(); });
|
|
|
|
QMenu *menu = new QMenu(this);
|
|
|
|
QAction *action = new QAction(tr("Export to &Text"));
|
|
action->setIcon(Icons::save());
|
|
QObject::connect(action, &QAction::triggered, this, &ShaderMessageViewer::exportText);
|
|
menu->addAction(action);
|
|
|
|
action = new QAction(tr("Export to &CSV"));
|
|
action->setIcon(Icons::save());
|
|
QObject::connect(action, &QAction::triggered, this, &ShaderMessageViewer::exportCSV);
|
|
menu->addAction(action);
|
|
|
|
ui->exportButton->setMenu(menu);
|
|
QObject::connect(ui->exportButton, &QToolButton::clicked, this, &ShaderMessageViewer::exportText);
|
|
|
|
ui->task->setText(ToQStr(ShaderStage::Task, m_API));
|
|
ui->mesh->setText(ToQStr(ShaderStage::Mesh, m_API));
|
|
ui->vertex->setText(ToQStr(ShaderStage::Vertex, m_API));
|
|
ui->hull->setText(ToQStr(ShaderStage::Hull, m_API));
|
|
ui->domain->setText(ToQStr(ShaderStage::Domain, m_API));
|
|
ui->geometry->setText(ToQStr(ShaderStage::Geometry, m_API));
|
|
ui->pixel->setText(ToQStr(ShaderStage::Pixel, m_API));
|
|
|
|
m_EID = m_Ctx.CurEvent();
|
|
m_Action = m_Ctx.GetAction(m_EID);
|
|
|
|
const PipeState &pipe = m_Ctx.CurPipelineState();
|
|
|
|
// check if we have multiview enabled
|
|
m_Multiview = pipe.MultiviewBroadcastCount() > 1;
|
|
|
|
// only display sample information if one of the targets is multisampled
|
|
m_Multisampled = false;
|
|
rdcarray<Descriptor> outs = pipe.GetOutputTargets();
|
|
outs.push_back(pipe.GetDepthTarget());
|
|
outs.push_back(pipe.GetDepthResolveTarget());
|
|
for(const Descriptor &o : outs)
|
|
{
|
|
if(o.resource == ResourceId())
|
|
continue;
|
|
|
|
const TextureDescription *tex = m_Ctx.GetTexture(o.resource);
|
|
|
|
if(tex->msSamp > 1)
|
|
{
|
|
m_Multisampled = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
RDHeaderView *header = new RDHeaderView(Qt::Horizontal, this);
|
|
ui->messages->setHeader(header);
|
|
header->setStretchLastSection(true);
|
|
header->setMinimumSectionSize(40);
|
|
|
|
int sortColumn = 0;
|
|
|
|
m_debugDelegate = new ButtonDelegate(Icons::wrench(), QString(), this);
|
|
m_debugDelegate->setEnableTrigger(debuggableRole, true);
|
|
|
|
if(m_Action && (m_Action->flags & ActionFlags::Dispatch))
|
|
{
|
|
ui->stageFilters->hide();
|
|
|
|
ui->messages->setColumns({lit("Debug"), tr("Workgroup"), lit("Thread"), lit("Message")});
|
|
sortColumn = 1;
|
|
|
|
ui->messages->setItemDelegateForColumn(0, m_debugDelegate);
|
|
|
|
m_OrigShaders[5] = pipe.GetShader(ShaderStage::Compute);
|
|
m_LayoutStage = ShaderStage::Compute;
|
|
}
|
|
else
|
|
{
|
|
if(pipe.GetShader(ShaderStage::Task) != ResourceId())
|
|
{
|
|
ui->messages->setColumns({lit("Debug"), lit("Go to"), tr("Task group"), tr("Mesh group"),
|
|
lit("Thread"), lit("Message")});
|
|
sortColumn = 4;
|
|
m_LayoutStage = ShaderStage::Task;
|
|
}
|
|
else if(pipe.GetShader(ShaderStage::Mesh) != ResourceId())
|
|
{
|
|
ui->messages->setColumns(
|
|
{lit("Debug"), lit("Go to"), tr("Workgroup"), lit("Thread/Location"), lit("Message")});
|
|
sortColumn = 3;
|
|
m_LayoutStage = ShaderStage::Mesh;
|
|
}
|
|
else
|
|
{
|
|
ui->messages->setColumns({lit("Debug"), lit("Go to"), tr("Location"), lit("Message")});
|
|
sortColumn = 2;
|
|
m_LayoutStage = ShaderStage::Vertex;
|
|
}
|
|
|
|
m_gotoDelegate = new ButtonDelegate(Icons::find(), QString(), this);
|
|
m_gotoDelegate->setEnableTrigger(gotoableRole, true);
|
|
|
|
ui->messages->setItemDelegateForColumn(0, m_debugDelegate);
|
|
ui->messages->setItemDelegateForColumn(1, m_gotoDelegate);
|
|
|
|
QCheckBox *boxes[NumShaderStages] = {
|
|
ui->vertex,
|
|
ui->hull,
|
|
ui->domain,
|
|
ui->geometry,
|
|
ui->pixel,
|
|
// compute
|
|
NULL,
|
|
ui->task,
|
|
ui->mesh,
|
|
// raytracing stages
|
|
NULL,
|
|
NULL,
|
|
NULL,
|
|
NULL,
|
|
NULL,
|
|
NULL,
|
|
};
|
|
|
|
for(ShaderStage s : values<ShaderStage>())
|
|
{
|
|
uint32_t idx = (uint32_t)s;
|
|
|
|
if(!boxes[idx])
|
|
continue;
|
|
|
|
m_OrigShaders[idx] = pipe.GetShader(s);
|
|
|
|
boxes[idx]->setChecked(bool(stages & MaskForStage(s)));
|
|
|
|
// if there's no shader bound, we currently don't support adding stages at runtime so just
|
|
// hide this box as no messages can come from the unbound stage
|
|
if(m_OrigShaders[idx] == ResourceId())
|
|
boxes[idx]->hide();
|
|
}
|
|
}
|
|
|
|
ui->messages->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
QObject::connect(ui->messages, &RDTreeWidget::customContextMenuRequested, [this](const QPoint &pos) {
|
|
QModelIndex idx = ui->messages->indexAt(pos);
|
|
RDTreeWidgetItem *item = ui->messages->itemForIndex(idx);
|
|
|
|
QMenu contextMenu(this);
|
|
|
|
QAction copy(tr("&Copy"), this);
|
|
|
|
contextMenu.addAction(©);
|
|
|
|
copy.setIcon(Icons::copy());
|
|
|
|
QObject::connect(©, &QAction::triggered,
|
|
[this, pos, item]() { ui->messages->copyItem(pos, item); });
|
|
|
|
QAction debugAction(tr("&Debug"), this);
|
|
debugAction.setIcon(Icons::wrench());
|
|
QAction gotoAction(tr("&Go to"), this);
|
|
gotoAction.setIcon(Icons::find());
|
|
|
|
QObject::connect(&debugAction, &QAction::triggered,
|
|
[this, idx]() { m_debugDelegate->messageClicked(idx); });
|
|
|
|
QObject::connect(&gotoAction, &QAction::triggered,
|
|
[this, idx]() { m_gotoDelegate->messageClicked(idx); });
|
|
|
|
contextMenu.addAction(&debugAction);
|
|
if(m_gotoDelegate)
|
|
contextMenu.addAction(&gotoAction);
|
|
|
|
RDDialog::show(&contextMenu, ui->messages->viewport()->mapToGlobal(pos));
|
|
});
|
|
|
|
QObject::connect(m_debugDelegate, &ButtonDelegate::messageClicked, [this](const QModelIndex &idx) {
|
|
RDTreeWidgetItem *item = ui->messages->itemForIndex(idx);
|
|
|
|
int msgIdx = 0;
|
|
if(item)
|
|
msgIdx = item->tag().toInt();
|
|
else
|
|
return;
|
|
|
|
const ShaderMessage &msg = m_Messages[msgIdx];
|
|
|
|
const ShaderReflection *refl = m_Ctx.CurPipelineState().GetShaderReflection(msg.stage);
|
|
|
|
if(refl->debugInfo.debuggable)
|
|
{
|
|
bool done = false;
|
|
ShaderDebugTrace *trace = NULL;
|
|
|
|
m_Ctx.Replay().AsyncInvoke([&trace, &done, msg](IReplayController *r) {
|
|
if(msg.stage == ShaderStage::Compute)
|
|
{
|
|
trace = r->DebugThread(msg.location.compute.workgroup, msg.location.compute.thread);
|
|
}
|
|
else if(msg.stage == ShaderStage::Vertex)
|
|
{
|
|
trace = r->DebugVertex(msg.location.vertex.vertexIndex, msg.location.vertex.instance,
|
|
msg.location.vertex.vertexIndex, msg.location.vertex.view);
|
|
}
|
|
else if(msg.stage == ShaderStage::Pixel)
|
|
{
|
|
DebugPixelInputs inputs;
|
|
inputs.sample = msg.location.pixel.sample;
|
|
inputs.primitive = msg.location.pixel.primitive;
|
|
inputs.view = msg.location.pixel.view;
|
|
trace = r->DebugPixel(msg.location.pixel.x, msg.location.pixel.y, inputs);
|
|
}
|
|
else if(msg.stage == ShaderStage::Mesh)
|
|
{
|
|
trace = r->DebugMeshThread(msg.location.mesh.meshGroup, msg.location.mesh.thread);
|
|
}
|
|
|
|
if(trace && trace->debugger == NULL)
|
|
{
|
|
r->FreeTrace(trace);
|
|
trace = NULL;
|
|
}
|
|
|
|
done = true;
|
|
});
|
|
|
|
QString debugContext;
|
|
|
|
if(msg.stage == ShaderStage::Compute)
|
|
{
|
|
debugContext = lit("Group [%1,%2,%3] Thread [%4,%5,%6]")
|
|
.arg(msg.location.compute.workgroup[0])
|
|
.arg(msg.location.compute.workgroup[1])
|
|
.arg(msg.location.compute.workgroup[2])
|
|
.arg(msg.location.compute.thread[0])
|
|
.arg(msg.location.compute.thread[1])
|
|
.arg(msg.location.compute.thread[2]);
|
|
}
|
|
else if(msg.stage == ShaderStage::Vertex)
|
|
{
|
|
debugContext = tr("Vertex %1").arg(msg.location.vertex.vertexIndex);
|
|
}
|
|
else if(msg.stage == ShaderStage::Pixel)
|
|
{
|
|
debugContext = tr("Pixel %1,%2").arg(msg.location.pixel.x).arg(msg.location.pixel.y);
|
|
}
|
|
else if(msg.stage == ShaderStage::Task)
|
|
{
|
|
QString groupIdx = QFormatStr("%1").arg(msg.location.mesh.taskGroup[0]);
|
|
if(msg.location.mesh.taskGroup[1] != ShaderMeshMessageLocation::NotUsed)
|
|
groupIdx += QFormatStr(",%1").arg(msg.location.mesh.taskGroup[1]);
|
|
if(msg.location.mesh.taskGroup[2] != ShaderMeshMessageLocation::NotUsed)
|
|
groupIdx += QFormatStr(",%1").arg(msg.location.mesh.taskGroup[2]);
|
|
|
|
QString threadIdx = QFormatStr("%1").arg(msg.location.mesh.thread[0]);
|
|
if(msg.location.mesh.thread[1] != ShaderMeshMessageLocation::NotUsed)
|
|
threadIdx += QFormatStr(",%1").arg(msg.location.mesh.thread[1]);
|
|
if(msg.location.mesh.thread[2] != ShaderMeshMessageLocation::NotUsed)
|
|
threadIdx += QFormatStr(",%1").arg(msg.location.mesh.thread[2]);
|
|
|
|
debugContext = tr("Task Group [%1] Thread [%2]").arg(groupIdx).arg(threadIdx);
|
|
}
|
|
else if(msg.stage == ShaderStage::Mesh)
|
|
{
|
|
QString groupIdx = QFormatStr("%1").arg(msg.location.mesh.meshGroup[0]);
|
|
if(msg.location.mesh.meshGroup[1] != ShaderMeshMessageLocation::NotUsed)
|
|
groupIdx += QFormatStr(",%1").arg(msg.location.mesh.meshGroup[1]);
|
|
if(msg.location.mesh.meshGroup[2] != ShaderMeshMessageLocation::NotUsed)
|
|
groupIdx += QFormatStr(",%1").arg(msg.location.mesh.meshGroup[2]);
|
|
|
|
QString threadIdx = QFormatStr("%1").arg(msg.location.mesh.thread[0]);
|
|
if(msg.location.mesh.thread[1] != ShaderMeshMessageLocation::NotUsed)
|
|
threadIdx += QFormatStr(",%1").arg(msg.location.mesh.thread[1]);
|
|
if(msg.location.mesh.thread[2] != ShaderMeshMessageLocation::NotUsed)
|
|
threadIdx += QFormatStr(",%1").arg(msg.location.mesh.thread[2]);
|
|
|
|
debugContext = tr("Mesh Group [%1] Thread [%2]").arg(groupIdx).arg(threadIdx);
|
|
|
|
if(msg.location.mesh.taskGroup[0] != ShaderMeshMessageLocation::NotUsed)
|
|
{
|
|
debugContext += tr(" from Task [%1").arg(msg.location.mesh.taskGroup[0]);
|
|
if(msg.location.mesh.taskGroup[1] != ShaderMeshMessageLocation::NotUsed)
|
|
debugContext += tr(",%1").arg(msg.location.mesh.taskGroup[1]);
|
|
if(msg.location.mesh.taskGroup[2] != ShaderMeshMessageLocation::NotUsed)
|
|
debugContext += tr(",%1").arg(msg.location.mesh.taskGroup[2]);
|
|
debugContext += lit("]");
|
|
}
|
|
}
|
|
|
|
// 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("Debug Error"), tr("Error debugging pixel."));
|
|
return;
|
|
}
|
|
|
|
ResourceId pipeline = msg.stage == ShaderStage::Compute
|
|
? m_Ctx.CurPipelineState().GetComputePipelineObject()
|
|
: m_Ctx.CurPipelineState().GetGraphicsPipelineObject();
|
|
|
|
// viewer takes ownership of the trace
|
|
IShaderViewer *s = m_Ctx.DebugShader(refl, pipeline, trace, debugContext);
|
|
|
|
if(msg.disassemblyLine >= 0)
|
|
{
|
|
s->ToggleBreakpointOnDisassemblyLine(msg.disassemblyLine);
|
|
s->RunForward();
|
|
}
|
|
|
|
m_Ctx.AddDockWindow(s->Widget(), DockReference::AddTo, this);
|
|
}
|
|
else
|
|
{
|
|
RDDialog::critical(
|
|
this, tr("Shader can't be debugged"),
|
|
tr("The shader does not support debugging: %1").arg(refl->debugInfo.debugStatus));
|
|
}
|
|
});
|
|
|
|
if(m_gotoDelegate)
|
|
{
|
|
QObject::connect(m_gotoDelegate, &ButtonDelegate::messageClicked, [this](const QModelIndex &idx) {
|
|
RDTreeWidgetItem *item = ui->messages->itemForIndex(idx);
|
|
|
|
int msgIdx = 0;
|
|
if(item)
|
|
msgIdx = item->tag().toInt();
|
|
else
|
|
return;
|
|
|
|
const ShaderMessage &msg = m_Messages[msgIdx];
|
|
|
|
m_Ctx.SetEventID({}, m_EID, m_EID);
|
|
|
|
if(msg.stage == ShaderStage::Vertex)
|
|
{
|
|
m_Ctx.ShowMeshPreview();
|
|
m_Ctx.GetMeshPreview()->SetCurrentInstance(msg.location.vertex.instance);
|
|
m_Ctx.GetMeshPreview()->SetCurrentView(msg.location.vertex.view);
|
|
m_Ctx.GetMeshPreview()->ShowMeshData(MeshDataStage::VSOut);
|
|
m_Ctx.GetMeshPreview()->ScrollToRow(msg.location.vertex.vertexIndex, MeshDataStage::VSOut);
|
|
m_Ctx.GetMeshPreview()->ShowMeshData(MeshDataStage::VSIn);
|
|
// TODO, not accurate for indices
|
|
m_Ctx.GetMeshPreview()->ScrollToRow(msg.location.vertex.vertexIndex, MeshDataStage::VSIn);
|
|
}
|
|
else if(msg.stage == ShaderStage::Pixel)
|
|
{
|
|
m_Ctx.ShowTextureViewer();
|
|
Subresource sub = m_Ctx.GetTextureViewer()->GetSelectedSubresource();
|
|
sub.sample = msg.location.pixel.sample;
|
|
sub.slice = msg.location.pixel.view;
|
|
m_Ctx.GetTextureViewer()->SetSelectedSubresource(sub);
|
|
|
|
// select an actual output. Prefer the first colour output, but if there's no colour output
|
|
// pick depth.
|
|
rdcarray<Descriptor> cols = m_Ctx.CurPipelineState().GetOutputTargets();
|
|
bool hascol = false;
|
|
for(size_t i = 0; i < cols.size(); i++)
|
|
hascol |= cols[i].resource != ResourceId();
|
|
|
|
if(hascol)
|
|
m_Ctx.GetTextureViewer()->ViewFollowedResource(FollowType::OutputColor,
|
|
ShaderStage::Pixel, 0, 0);
|
|
else
|
|
m_Ctx.GetTextureViewer()->ViewFollowedResource(FollowType::OutputDepth,
|
|
ShaderStage::Pixel, 0, 0);
|
|
m_Ctx.GetTextureViewer()->GotoLocation(msg.location.pixel.x, msg.location.pixel.y);
|
|
}
|
|
else if(msg.stage == ShaderStage::Geometry)
|
|
{
|
|
m_Ctx.ShowMeshPreview();
|
|
m_Ctx.GetMeshPreview()->SetCurrentView(msg.location.geometry.view);
|
|
m_Ctx.GetMeshPreview()->ShowMeshData(MeshDataStage::GSOut);
|
|
// TODO, instances not supported
|
|
m_Ctx.GetMeshPreview()->ScrollToRow(
|
|
RENDERDOC_VertexOffset(m_Ctx.CurPipelineState().GetPrimitiveTopology(),
|
|
msg.location.geometry.primitive),
|
|
MeshDataStage::GSOut);
|
|
}
|
|
else if(msg.stage == ShaderStage::Task || msg.stage == ShaderStage::Mesh)
|
|
{
|
|
// TODO mesh shader jumping
|
|
}
|
|
else
|
|
{
|
|
qCritical() << "Can't go to a compute thread";
|
|
}
|
|
});
|
|
}
|
|
|
|
// deliberately copy m_OrigShaders to m_ReplacedShaders. This is impossible because we should
|
|
// either see a ResourceId() for unedited, or a new resource for edited. This means when we first
|
|
// get OnEventChanged() called we will definitely detect the situation as 'stale' and refresh the
|
|
// messages.
|
|
memcpy(m_ReplacedShaders, m_OrigShaders, sizeof(m_ReplacedShaders));
|
|
|
|
header->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
|
|
|
ui->staleStatus->hide();
|
|
|
|
ui->label->setText(tr("Shader messages from @%1 - %2")
|
|
.arg(m_EID)
|
|
.arg(m_Action ? m_Ctx.GetEventBrowser()->GetEventName(m_Action->eventId)
|
|
: rdcstr("Unknown action")));
|
|
|
|
setWindowTitle(tr("Shader messages at @%1").arg(m_EID));
|
|
|
|
m_Ctx.AddCaptureViewer(this);
|
|
|
|
OnEventChanged(m_Ctx.CurEvent());
|
|
|
|
ui->messages->setSortComparison(
|
|
[this](int col, Qt::SortOrder order, const RDTreeWidgetItem *a, const RDTreeWidgetItem *b) {
|
|
if(order == Qt::DescendingOrder)
|
|
std::swap(a, b);
|
|
|
|
const ShaderMessage &am = m_Messages[a->tag().toInt()];
|
|
const ShaderMessage &bm = m_Messages[b->tag().toInt()];
|
|
|
|
if(col == 5)
|
|
{
|
|
// column 5 is the message when task shaders are used
|
|
return am.message < bm.message;
|
|
}
|
|
else if(col == 4)
|
|
{
|
|
// column 4 is the message, except when task shaders are used - then it's
|
|
// the thread index
|
|
if((am.stage == ShaderStage::Task || am.stage == ShaderStage::Mesh) &&
|
|
am.location.mesh.taskGroup[0] != ShaderMeshMessageLocation::NotUsed)
|
|
{
|
|
return am.location.mesh.thread < bm.location.mesh.thread;
|
|
}
|
|
else
|
|
{
|
|
return am.message < bm.message;
|
|
}
|
|
}
|
|
else if(col == 3)
|
|
{
|
|
// column 3 is the mesh thread when only mesh shaders are used, or the mesh group when
|
|
// task shaders are used. For non mesh/task it is the message
|
|
if((am.stage == ShaderStage::Task || am.stage == ShaderStage::Mesh) &&
|
|
am.location.mesh.taskGroup[0] != ShaderMeshMessageLocation::NotUsed)
|
|
{
|
|
return am.location.mesh.meshGroup < bm.location.mesh.meshGroup;
|
|
}
|
|
else if(am.stage == ShaderStage::Mesh)
|
|
{
|
|
return am.location.mesh.thread < bm.location.mesh.thread;
|
|
}
|
|
else
|
|
{
|
|
return am.message < bm.message;
|
|
}
|
|
}
|
|
else if(col == 2 || m_OrigShaders[5] == ResourceId())
|
|
{
|
|
// sort by location either if it's selected, or if it's not dispatch in which case we
|
|
// default to location sorting (don't try to sort by the button-only columns that have no
|
|
// data)
|
|
|
|
// sort by stage first
|
|
if(am.stage != bm.stage)
|
|
return am.stage < bm.stage;
|
|
|
|
if(am.stage == ShaderStage::Vertex)
|
|
{
|
|
const ShaderVertexMessageLocation &aloc = am.location.vertex;
|
|
const ShaderVertexMessageLocation &bloc = bm.location.vertex;
|
|
|
|
if(aloc.view != bloc.view)
|
|
return aloc.view < bloc.view;
|
|
if(aloc.instance != bloc.instance)
|
|
return aloc.instance < bloc.instance;
|
|
return aloc.vertexIndex < bloc.vertexIndex;
|
|
}
|
|
else if(am.stage == ShaderStage::Pixel)
|
|
{
|
|
const ShaderPixelMessageLocation &aloc = am.location.pixel;
|
|
const ShaderPixelMessageLocation &bloc = bm.location.pixel;
|
|
|
|
if(aloc.x != bloc.x)
|
|
return aloc.x < bloc.x;
|
|
if(aloc.y != bloc.y)
|
|
return aloc.y < bloc.y;
|
|
if(aloc.primitive != bloc.primitive)
|
|
return aloc.primitive < bloc.primitive;
|
|
if(aloc.view != bloc.view)
|
|
return aloc.view < bloc.view;
|
|
return aloc.sample < bloc.sample;
|
|
}
|
|
else if(am.stage == ShaderStage::Compute)
|
|
{
|
|
// column 2 is the thread column for compute
|
|
return am.location.compute.thread < bm.location.compute.thread;
|
|
}
|
|
else if(am.stage == ShaderStage::Task || am.stage == ShaderStage::Mesh)
|
|
{
|
|
// column 2 is the mesh group column, or the task group column, depending on if
|
|
// task shaders were in use
|
|
if(am.location.mesh.taskGroup[0] != ShaderMeshMessageLocation::NotUsed)
|
|
return am.location.mesh.taskGroup < bm.location.mesh.taskGroup;
|
|
else
|
|
return am.location.mesh.meshGroup < bm.location.mesh.meshGroup;
|
|
}
|
|
else if(am.stage == ShaderStage::Geometry)
|
|
{
|
|
const ShaderGeometryMessageLocation &aloc = am.location.geometry;
|
|
const ShaderGeometryMessageLocation &bloc = bm.location.geometry;
|
|
|
|
if(aloc.view != bloc.view)
|
|
return aloc.view < bloc.view;
|
|
return am.location.geometry.primitive < bm.location.geometry.primitive;
|
|
}
|
|
else
|
|
{
|
|
// can't sort these, pretend they're all equal
|
|
return false;
|
|
}
|
|
}
|
|
else if(col == 1)
|
|
{
|
|
return am.location.compute.workgroup < bm.location.compute.workgroup;
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
ui->messages->sortByColumn(sortColumn, Qt::SortOrder::AscendingOrder);
|
|
|
|
for(int i = 0; i < 4; i++)
|
|
{
|
|
header->setSectionResizeMode(i, QHeaderView::Interactive);
|
|
ui->messages->resizeColumnToContents(i);
|
|
}
|
|
}
|
|
|
|
ShaderMessageViewer::~ShaderMessageViewer()
|
|
{
|
|
m_Ctx.RemoveCaptureViewer(this);
|
|
delete ui;
|
|
}
|
|
|
|
bool ShaderMessageViewer::IsOutOfDate()
|
|
{
|
|
return ui->staleStatus->isVisible();
|
|
}
|
|
|
|
void ShaderMessageViewer::OnCaptureLoaded()
|
|
{
|
|
}
|
|
|
|
void ShaderMessageViewer::OnCaptureClosed()
|
|
{
|
|
ToolWindowManager::closeToolWindow(this);
|
|
}
|
|
|
|
void ShaderMessageViewer::OnEventChanged(uint32_t eventId)
|
|
{
|
|
ResourceId shaders[NumShaderStages];
|
|
bool needRefresh = false;
|
|
QString staleReason;
|
|
|
|
for(ShaderStage s : values<ShaderStage>())
|
|
{
|
|
uint32_t idx = (uint32_t)s;
|
|
shaders[idx] = m_Ctx.GetResourceReplacement(m_OrigShaders[idx]);
|
|
|
|
// either an edit has been applied, updated, or removed if these don't match
|
|
if(shaders[idx] != m_ReplacedShaders[idx])
|
|
{
|
|
needRefresh = true;
|
|
if(staleReason.isEmpty())
|
|
staleReason = tr("there are edits to shaders typed ");
|
|
else
|
|
staleReason += lit(", ");
|
|
staleReason += QFormatStr("%1").arg(ToQStr(s, m_API));
|
|
}
|
|
}
|
|
|
|
if(!needRefresh && m_ResourceCacheID != m_Ctx.ResourceNameCacheID())
|
|
{
|
|
staleReason = tr("The replay information is out of date");
|
|
m_ResourceCacheID = m_Ctx.ResourceNameCacheID();
|
|
needRefresh = true;
|
|
}
|
|
|
|
// if the edits haven't changed, just skip
|
|
if(!needRefresh)
|
|
return;
|
|
|
|
// if it's the current event we can update with the latest
|
|
if(m_EID == eventId)
|
|
{
|
|
m_Messages = m_Ctx.CurPipelineState().GetShaderMessages();
|
|
|
|
// not stale anymore
|
|
ui->staleStatus->hide();
|
|
|
|
// update current set of replaced shaders
|
|
memcpy(m_ReplacedShaders, shaders, sizeof(m_ReplacedShaders));
|
|
|
|
refreshMessages();
|
|
}
|
|
else
|
|
{
|
|
// otherwise we can't - just update the stale status
|
|
ui->staleStatus->show();
|
|
ui->staleStatus->setText(tr("Messages are stale because %1 since the messages were fetched.\n"
|
|
"Select the event @%2 to refresh.")
|
|
.arg(staleReason)
|
|
.arg(m_EID));
|
|
|
|
ui->messages->beginUpdate();
|
|
|
|
for(int i = 0; i < ui->messages->topLevelItemCount(); i++)
|
|
ui->messages->topLevelItem(i)->setItalic(true);
|
|
|
|
ui->messages->endUpdate();
|
|
}
|
|
}
|
|
|
|
void ShaderMessageViewer::exportText()
|
|
{
|
|
exportData(false);
|
|
}
|
|
|
|
void ShaderMessageViewer::exportCSV()
|
|
{
|
|
exportData(true);
|
|
}
|
|
|
|
void ShaderMessageViewer::exportData(bool csv)
|
|
{
|
|
QString filter;
|
|
QString title;
|
|
if(csv)
|
|
{
|
|
filter = tr("CSV Files (*.csv)");
|
|
title = tr("Export buffer to CSV");
|
|
}
|
|
else
|
|
{
|
|
filter = tr("Text Files (*.txt)");
|
|
title = tr("Export buffer to text");
|
|
}
|
|
|
|
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 | 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;
|
|
}
|
|
|
|
LambdaThread *exportThread = new LambdaThread([this, csv, f]() {
|
|
QTextStream s(f);
|
|
|
|
bool compute = (m_OrigShaders[5] != ResourceId());
|
|
|
|
if(csv)
|
|
{
|
|
if(compute)
|
|
s << tr("Workgroup,Thread,Message\n");
|
|
else
|
|
s << tr("Location,Message\n");
|
|
}
|
|
|
|
const int start = compute ? 1 : 2;
|
|
const int end = 3;
|
|
|
|
int locationWidth = 0;
|
|
for(int i = 0; i < ui->messages->topLevelItemCount(); i++)
|
|
{
|
|
RDTreeWidgetItem *node = ui->messages->topLevelItem(i);
|
|
|
|
locationWidth = qMax(locationWidth, node->text(start).length());
|
|
if(compute)
|
|
locationWidth = qMax(locationWidth, node->text(start + 1).length());
|
|
}
|
|
|
|
for(int i = 0; i < ui->messages->topLevelItemCount(); i++)
|
|
{
|
|
RDTreeWidgetItem *node = ui->messages->topLevelItem(i);
|
|
|
|
if(csv)
|
|
{
|
|
int col = start;
|
|
for(; col <= end - 1; col++)
|
|
s << "\"" << node->text(col) << "\",";
|
|
s << "\"" << node->text(col).replace(QLatin1Char('"'), lit("\"\"")) << "\"\n";
|
|
}
|
|
else
|
|
{
|
|
int col = start;
|
|
for(; col <= end - 1; col++)
|
|
s << QFormatStr("%1").arg(node->text(col), -locationWidth) << "\t";
|
|
s << node->text(col) << "\n";
|
|
}
|
|
}
|
|
|
|
f->close();
|
|
|
|
delete f;
|
|
});
|
|
exportThread->start();
|
|
|
|
// 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; exportThread->isRunning() && i < 100; i++)
|
|
QThread::msleep(5);
|
|
|
|
ShowProgressDialog(this, tr("Exporting messages"),
|
|
[exportThread]() { return !exportThread->isRunning(); });
|
|
|
|
exportThread->deleteLater();
|
|
}
|
|
|
|
void ShaderMessageViewer::refreshMessages()
|
|
{
|
|
ShaderStageMask mask = ShaderStageMask::Compute;
|
|
|
|
if(!m_Action || !(m_Action->flags & ActionFlags::Dispatch))
|
|
{
|
|
mask = ShaderStageMask::Unknown;
|
|
|
|
if(ui->task->isChecked())
|
|
mask |= ShaderStageMask::Task;
|
|
if(ui->mesh->isChecked())
|
|
mask |= ShaderStageMask::Mesh;
|
|
if(ui->vertex->isChecked())
|
|
mask |= ShaderStageMask::Vertex;
|
|
if(ui->hull->isChecked())
|
|
mask |= ShaderStageMask::Hull;
|
|
if(ui->domain->isChecked())
|
|
mask |= ShaderStageMask::Domain;
|
|
if(ui->geometry->isChecked())
|
|
mask |= ShaderStageMask::Geometry;
|
|
if(ui->pixel->isChecked())
|
|
mask |= ShaderStageMask::Pixel;
|
|
}
|
|
|
|
int vs = ui->messages->verticalScrollBar()->value();
|
|
int curMsg = -1;
|
|
{
|
|
RDTreeWidgetItem *item = ui->messages->currentItem();
|
|
if(item)
|
|
curMsg = item->tag().toInt();
|
|
}
|
|
RDTreeWidgetItem *newCurrentItem = NULL;
|
|
ui->messages->beginUpdate();
|
|
ui->messages->clear();
|
|
|
|
QString filter = ui->filter->text().trimmed();
|
|
|
|
const ShaderReflection *vsrefl = m_Ctx.CurPipelineState().GetShaderReflection(ShaderStage::Vertex);
|
|
const ShaderReflection *psrefl = m_Ctx.CurPipelineState().GetShaderReflection(ShaderStage::Pixel);
|
|
const ShaderReflection *csrefl = m_Ctx.CurPipelineState().GetShaderReflection(ShaderStage::Compute);
|
|
const ShaderReflection *tsrefl = m_Ctx.CurPipelineState().GetShaderReflection(ShaderStage::Task);
|
|
const ShaderReflection *msrefl = m_Ctx.CurPipelineState().GetShaderReflection(ShaderStage::Mesh);
|
|
|
|
for(int i = 0; i < m_Messages.count(); i++)
|
|
{
|
|
const ShaderMessage &msg = m_Messages[i];
|
|
|
|
// filter by stages
|
|
if(!(MaskForStage(msg.stage) & mask))
|
|
continue;
|
|
|
|
QString text(msg.message);
|
|
|
|
const ShaderReflection *refl = NULL;
|
|
|
|
QString location;
|
|
if(msg.stage == ShaderStage::Vertex)
|
|
{
|
|
refl = vsrefl;
|
|
|
|
// only show the view if the draw has multiview enabled
|
|
if(m_Multiview)
|
|
{
|
|
location += lit("View %1, ").arg(msg.location.vertex.view);
|
|
}
|
|
|
|
// only show the instance if the draw is actually instanced
|
|
if(m_Action && (m_Action->flags & ActionFlags::Instanced) && m_Action->numInstances > 1)
|
|
{
|
|
location += lit("Inst %1, ").arg(msg.location.vertex.instance);
|
|
}
|
|
|
|
if(m_Action && (m_Action->flags & ActionFlags::Indexed))
|
|
{
|
|
location += lit("Idx %1").arg(msg.location.vertex.vertexIndex);
|
|
}
|
|
else
|
|
{
|
|
location += lit("Vert %1").arg(msg.location.vertex.vertexIndex);
|
|
}
|
|
}
|
|
else if(msg.stage == ShaderStage::Pixel)
|
|
{
|
|
refl = psrefl;
|
|
|
|
location = QFormatStr("%1 %2,%3")
|
|
.arg(IsD3D(m_API) ? lit("Pixel") : lit("Frag"))
|
|
.arg(msg.location.pixel.x)
|
|
.arg(msg.location.pixel.y);
|
|
|
|
if(msg.location.pixel.primitive == ~0U)
|
|
location += lit(", Prim ?");
|
|
else
|
|
location += lit(", Prim %1").arg(msg.location.pixel.primitive);
|
|
|
|
// only show the view if the draw has multiview enabled
|
|
if(m_Multiview)
|
|
{
|
|
location += lit(", View %1").arg(msg.location.pixel.view);
|
|
}
|
|
|
|
if(m_Multisampled && msg.location.pixel.sample != ~0U)
|
|
{
|
|
location += lit(", Samp %1").arg(msg.location.pixel.sample);
|
|
}
|
|
}
|
|
else if(msg.stage == ShaderStage::Compute)
|
|
{
|
|
refl = csrefl;
|
|
}
|
|
else if(msg.stage == ShaderStage::Geometry)
|
|
{
|
|
location = lit("Geometry Prim %1").arg(msg.location.geometry.primitive);
|
|
|
|
// only show the view if the draw has multiview enabled
|
|
if(m_Multiview)
|
|
{
|
|
location += lit(", View %1").arg(msg.location.geometry.view);
|
|
}
|
|
}
|
|
else if(msg.stage == ShaderStage::Task)
|
|
{
|
|
refl = tsrefl;
|
|
}
|
|
else if(msg.stage == ShaderStage::Mesh)
|
|
{
|
|
refl = msrefl;
|
|
}
|
|
else
|
|
{
|
|
// no location info for other stages
|
|
location = tr("Unknown %1").arg(ToQStr(msg.stage, m_Ctx.APIProps().pipelineType));
|
|
}
|
|
|
|
// filter by text on location and messag
|
|
if(!filter.isEmpty() && !text.contains(filter, Qt::CaseInsensitive) &&
|
|
!location.contains(filter, Qt::CaseInsensitive))
|
|
continue;
|
|
|
|
RDTreeWidgetItem *node = NULL;
|
|
|
|
if(msg.stage == ShaderStage::Compute)
|
|
{
|
|
node = new RDTreeWidgetItem({
|
|
QString(),
|
|
QFormatStr("%1, %2, %3")
|
|
.arg(msg.location.compute.workgroup[0])
|
|
.arg(msg.location.compute.workgroup[1])
|
|
.arg(msg.location.compute.workgroup[2]),
|
|
QFormatStr("%1, %2, %3")
|
|
.arg(msg.location.compute.thread[0])
|
|
.arg(msg.location.compute.thread[1])
|
|
.arg(msg.location.compute.thread[2]),
|
|
text,
|
|
});
|
|
|
|
node->setData(0, debuggableRole, refl && refl->debugInfo.debuggable);
|
|
}
|
|
else if(msg.stage == ShaderStage::Task)
|
|
{
|
|
QString groupIdx = QFormatStr("%1").arg(msg.location.mesh.taskGroup[0]);
|
|
if(msg.location.mesh.taskGroup[1] != ShaderMeshMessageLocation::NotUsed)
|
|
groupIdx += QFormatStr(",%1").arg(msg.location.mesh.taskGroup[1]);
|
|
if(msg.location.mesh.taskGroup[2] != ShaderMeshMessageLocation::NotUsed)
|
|
groupIdx += QFormatStr(",%1").arg(msg.location.mesh.taskGroup[2]);
|
|
|
|
QString threadIdx = QFormatStr("%1").arg(msg.location.mesh.thread[0]);
|
|
if(msg.location.mesh.thread[1] != ShaderMeshMessageLocation::NotUsed)
|
|
threadIdx += QFormatStr(",%1").arg(msg.location.mesh.thread[1]);
|
|
if(msg.location.mesh.thread[2] != ShaderMeshMessageLocation::NotUsed)
|
|
threadIdx += QFormatStr(",%1").arg(msg.location.mesh.thread[2]);
|
|
|
|
node = new RDTreeWidgetItem({
|
|
QString(),
|
|
QString(),
|
|
groupIdx,
|
|
lit("-"),
|
|
threadIdx,
|
|
text,
|
|
});
|
|
|
|
node->setData(0, debuggableRole, refl && refl->debugInfo.debuggable);
|
|
node->setData(1, gotoableRole, true);
|
|
}
|
|
else if(msg.stage == ShaderStage::Mesh)
|
|
{
|
|
QString taskIdx;
|
|
if(msg.location.mesh.taskGroup[0] != ShaderMeshMessageLocation::NotUsed)
|
|
taskIdx = QFormatStr("%1").arg(msg.location.mesh.taskGroup[0]);
|
|
if(msg.location.mesh.taskGroup[1] != ShaderMeshMessageLocation::NotUsed)
|
|
taskIdx += QFormatStr(",%1").arg(msg.location.mesh.taskGroup[1]);
|
|
if(msg.location.mesh.taskGroup[2] != ShaderMeshMessageLocation::NotUsed)
|
|
taskIdx += QFormatStr(",%1").arg(msg.location.mesh.taskGroup[2]);
|
|
|
|
QString groupIdx = QFormatStr("%1").arg(msg.location.mesh.meshGroup[0]);
|
|
if(msg.location.mesh.meshGroup[1] != ShaderMeshMessageLocation::NotUsed)
|
|
groupIdx += QFormatStr(",%1").arg(msg.location.mesh.meshGroup[1]);
|
|
if(msg.location.mesh.meshGroup[2] != ShaderMeshMessageLocation::NotUsed)
|
|
groupIdx += QFormatStr(",%1").arg(msg.location.mesh.meshGroup[2]);
|
|
|
|
QString threadIdx = QFormatStr("%1").arg(msg.location.mesh.thread[0]);
|
|
if(msg.location.mesh.thread[1] != ShaderMeshMessageLocation::NotUsed)
|
|
threadIdx += QFormatStr(",%1").arg(msg.location.mesh.thread[1]);
|
|
if(msg.location.mesh.thread[2] != ShaderMeshMessageLocation::NotUsed)
|
|
threadIdx += QFormatStr(",%1").arg(msg.location.mesh.thread[2]);
|
|
|
|
if(m_LayoutStage == ShaderStage::Task)
|
|
node = new RDTreeWidgetItem({
|
|
QString(),
|
|
QString(),
|
|
taskIdx,
|
|
groupIdx,
|
|
threadIdx,
|
|
text,
|
|
});
|
|
else
|
|
node = new RDTreeWidgetItem({
|
|
QString(),
|
|
QString(),
|
|
groupIdx,
|
|
threadIdx,
|
|
text,
|
|
});
|
|
|
|
node->setData(0, debuggableRole, refl && refl->debugInfo.debuggable);
|
|
node->setData(1, gotoableRole, true);
|
|
}
|
|
else
|
|
{
|
|
if(m_LayoutStage == ShaderStage::Task)
|
|
node = new RDTreeWidgetItem({QString(), QString(), QString(), QString(), location, text});
|
|
else if(m_LayoutStage == ShaderStage::Mesh)
|
|
node = new RDTreeWidgetItem({QString(), QString(), QString(), location, text});
|
|
else
|
|
node = new RDTreeWidgetItem({QString(), QString(), location, text});
|
|
|
|
node->setData(0, debuggableRole, refl && refl->debugInfo.debuggable);
|
|
node->setData(1, gotoableRole,
|
|
msg.stage == ShaderStage::Vertex || msg.stage == ShaderStage::Pixel ||
|
|
msg.stage == ShaderStage::Geometry);
|
|
}
|
|
|
|
if(node)
|
|
{
|
|
if(i == curMsg)
|
|
newCurrentItem = node;
|
|
|
|
node->setItalic(ui->staleStatus->isVisible());
|
|
node->setTag(i);
|
|
ui->messages->addTopLevelItem(node);
|
|
}
|
|
}
|
|
|
|
ui->messages->clearSelection();
|
|
ui->messages->endUpdate();
|
|
ui->messages->verticalScrollBar()->setValue(vs);
|
|
|
|
if(newCurrentItem)
|
|
{
|
|
ui->messages->setCurrentItem(newCurrentItem);
|
|
ui->messages->scrollToItem(newCurrentItem);
|
|
}
|
|
}
|