mirror of
https://github.com/baldurk/renderdoc.git
synced 2026-05-04 09:00:44 +00:00
f56a989e4f
Qt on Ubuntu 23.04 includes https://github.com/qt/qtbase/commit/f9e4402ffe, which during show() under certain scenarios (for example Qt::ToolTip windows) would destroy and recreate the xcb window. Adding Qt:WindowStaysOnTopHint to the window flags for ToolTip windows prevents the xcb window from being destroyed and recreated during show().
1574 lines
46 KiB
C++
1574 lines
46 KiB
C++
/******************************************************************************
|
|
* The MIT License (MIT)
|
|
*
|
|
* Copyright (c) 2019-2023 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 "PipelineStateViewer.h"
|
|
#include <QMenu>
|
|
#include <QMouseEvent>
|
|
#include <QPainter>
|
|
#include <QStylePainter>
|
|
#include <QSvgRenderer>
|
|
#include <QToolButton>
|
|
#include <QXmlStreamWriter>
|
|
#include "Code/QRDUtils.h"
|
|
#include "Code/Resources.h"
|
|
#include "Widgets/Extended/RDLabel.h"
|
|
#include "Widgets/Extended/RDTreeWidget.h"
|
|
#include "toolwindowmanager/ToolWindowManager.h"
|
|
#include "D3D11PipelineStateViewer.h"
|
|
#include "D3D12PipelineStateViewer.h"
|
|
#include "GLPipelineStateViewer.h"
|
|
#include "VulkanPipelineStateViewer.h"
|
|
#include "ui_PipelineStateViewer.h"
|
|
|
|
static uint32_t byteSize(const ResourceFormat &fmt)
|
|
{
|
|
if(fmt.Special())
|
|
{
|
|
switch(fmt.type)
|
|
{
|
|
default:
|
|
case ResourceFormatType::R9G9B9E5:
|
|
case ResourceFormatType::R5G6B5:
|
|
case ResourceFormatType::R5G5B5A1:
|
|
case ResourceFormatType::R4G4B4A4:
|
|
case ResourceFormatType::R4G4:
|
|
case ResourceFormatType::BC1:
|
|
case ResourceFormatType::BC2:
|
|
case ResourceFormatType::BC3:
|
|
case ResourceFormatType::BC4:
|
|
case ResourceFormatType::BC5:
|
|
case ResourceFormatType::BC6:
|
|
case ResourceFormatType::BC7:
|
|
case ResourceFormatType::ETC2:
|
|
case ResourceFormatType::EAC:
|
|
case ResourceFormatType::ASTC:
|
|
case ResourceFormatType::D16S8:
|
|
case ResourceFormatType::D24S8:
|
|
case ResourceFormatType::D32S8:
|
|
case ResourceFormatType::S8:
|
|
case ResourceFormatType::YUV8:
|
|
case ResourceFormatType::YUV10:
|
|
case ResourceFormatType::YUV12:
|
|
case ResourceFormatType::YUV16:
|
|
case ResourceFormatType::PVRTC: return ~0U;
|
|
case ResourceFormatType::A8: return 1;
|
|
case ResourceFormatType::R10G10B10A2:
|
|
case ResourceFormatType::R11G11B10: return 4;
|
|
}
|
|
}
|
|
|
|
return fmt.compByteWidth * fmt.compCount;
|
|
}
|
|
|
|
RDPreviewTooltip::RDPreviewTooltip(PipelineStateViewer *parent, CustomPaintWidget *thumbnail,
|
|
ICaptureContext &ctx)
|
|
: QFrame(parent), m_Ctx(ctx)
|
|
{
|
|
int margin = style()->pixelMetric(QStyle::PM_ToolTipLabelFrameWidth, NULL, this);
|
|
int opacity = style()->styleHint(QStyle::SH_ToolTipLabel_Opacity, NULL, this);
|
|
|
|
pipe = parent;
|
|
|
|
setWindowFlags(Qt::ToolTip | Qt::WindowStaysOnTopHint);
|
|
setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
setForegroundRole(QPalette::ToolTipText);
|
|
setBackgroundRole(QPalette::ToolTipBase);
|
|
setFrameStyle(QFrame::NoFrame);
|
|
setWindowOpacity(opacity / 255.0);
|
|
setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
|
|
|
|
QHBoxLayout *hbox = new QHBoxLayout;
|
|
QVBoxLayout *vbox = new QVBoxLayout;
|
|
hbox->setSpacing(0);
|
|
hbox->setContentsMargins(0, 0, 0, 0);
|
|
vbox->setSpacing(2);
|
|
vbox->setContentsMargins(6, 3, 6, 3);
|
|
|
|
label = new QLabel(this);
|
|
label->setAlignment(Qt::AlignLeft);
|
|
|
|
title = new QLabel(this);
|
|
title->setAlignment(Qt::AlignLeft);
|
|
|
|
setLayout(vbox);
|
|
vbox->addWidget(title);
|
|
vbox->addLayout(hbox);
|
|
|
|
hbox->addWidget(thumbnail);
|
|
hbox->addStretch();
|
|
|
|
vbox->addWidget(label);
|
|
}
|
|
|
|
void RDPreviewTooltip::hideTip()
|
|
{
|
|
hide();
|
|
}
|
|
|
|
QSize RDPreviewTooltip::configureTip(QWidget *widget, QModelIndex idx, QString text)
|
|
{
|
|
ResourceId id = pipe->updateThumbnail(widget, idx);
|
|
if(id != ResourceId())
|
|
{
|
|
title->setText(m_Ctx.GetResourceName(id));
|
|
title->show();
|
|
}
|
|
else
|
|
{
|
|
title->hide();
|
|
}
|
|
label->setText(text);
|
|
label->setVisible(!text.isEmpty());
|
|
layout()->update();
|
|
layout()->activate();
|
|
return minimumSizeHint();
|
|
}
|
|
|
|
void RDPreviewTooltip::showTip(QPoint pos)
|
|
{
|
|
move(pos);
|
|
resize(minimumSize());
|
|
show();
|
|
}
|
|
|
|
bool RDPreviewTooltip::forceTip(QWidget *widget, QModelIndex idx)
|
|
{
|
|
return pipe->hasThumbnail(widget, idx);
|
|
}
|
|
|
|
void RDPreviewTooltip::paintEvent(QPaintEvent *ev)
|
|
{
|
|
QStylePainter p(this);
|
|
QStyleOptionFrame opt;
|
|
opt.init(this);
|
|
p.drawPrimitive(QStyle::PE_PanelTipLabel, opt);
|
|
p.end();
|
|
|
|
QWidget::paintEvent(ev);
|
|
}
|
|
|
|
void RDPreviewTooltip::resizeEvent(QResizeEvent *e)
|
|
{
|
|
QStyleHintReturnMask frameMask;
|
|
QStyleOption option;
|
|
option.init(this);
|
|
if(style()->styleHint(QStyle::SH_ToolTip_Mask, &option, this, &frameMask))
|
|
setMask(frameMask.region);
|
|
|
|
QWidget::resizeEvent(e);
|
|
}
|
|
|
|
PipelineStateViewer::PipelineStateViewer(ICaptureContext &ctx, QWidget *parent)
|
|
: QFrame(parent), ui(new Ui::PipelineStateViewer), m_Ctx(ctx)
|
|
{
|
|
ui->setupUi(this);
|
|
|
|
ui->thumbnail->SetContext(m_Ctx);
|
|
ui->layout->removeWidget(ui->thumbnail);
|
|
|
|
m_Tooltip = new RDPreviewTooltip(this, ui->thumbnail, m_Ctx);
|
|
|
|
QColor c = palette().color(QPalette::ToolTipBase).toRgb();
|
|
|
|
m_TexDisplay.backgroundColor = FloatVector();
|
|
m_TexDisplay.backgroundColor.w = 1.0f;
|
|
|
|
// auto-fit and center scale
|
|
m_TexDisplay.scale = -1.0f;
|
|
|
|
m_D3D11 = NULL;
|
|
m_D3D12 = NULL;
|
|
m_GL = NULL;
|
|
m_Vulkan = NULL;
|
|
|
|
m_Current = NULL;
|
|
|
|
for(size_t i = 0; i < ARRAY_COUNT(editMenus); i++)
|
|
editMenus[i] = new QMenu(this);
|
|
|
|
m_Ctx.AddCaptureViewer(this);
|
|
}
|
|
|
|
PipelineStateViewer::~PipelineStateViewer()
|
|
{
|
|
reset();
|
|
|
|
m_Ctx.BuiltinWindowClosed(this);
|
|
m_Ctx.RemoveCaptureViewer(this);
|
|
|
|
delete m_Tooltip;
|
|
|
|
delete ui;
|
|
}
|
|
|
|
void PipelineStateViewer::OnCaptureLoaded()
|
|
{
|
|
if(m_Ctx.APIProps().pipelineType == GraphicsAPI::D3D11)
|
|
setToD3D11();
|
|
else if(m_Ctx.APIProps().pipelineType == GraphicsAPI::D3D12)
|
|
setToD3D12();
|
|
else if(m_Ctx.APIProps().pipelineType == GraphicsAPI::OpenGL)
|
|
setToGL();
|
|
else if(m_Ctx.APIProps().pipelineType == GraphicsAPI::Vulkan)
|
|
setToVulkan();
|
|
|
|
if(m_Current)
|
|
m_Current->OnCaptureLoaded();
|
|
|
|
if(!m_Ctx.APIProps().remoteReplay)
|
|
{
|
|
WindowingData thumbData = ui->thumbnail->GetWidgetWindowingData();
|
|
|
|
m_Ctx.Replay().BlockInvoke([thumbData, this](IReplayController *r) {
|
|
m_Output = r->CreateOutput(thumbData, ReplayOutputType::Texture);
|
|
|
|
ui->thumbnail->SetOutput(m_Output);
|
|
|
|
RT_UpdateAndDisplay(r);
|
|
});
|
|
}
|
|
else
|
|
{
|
|
m_Output = NULL;
|
|
}
|
|
}
|
|
|
|
void PipelineStateViewer::RT_UpdateAndDisplay(IReplayController *r)
|
|
{
|
|
if(m_Output != NULL)
|
|
{
|
|
m_Output->SetTextureDisplay(m_TexDisplay);
|
|
|
|
GUIInvoke::call(this, [this]() { ui->thumbnail->update(); });
|
|
}
|
|
}
|
|
|
|
void PipelineStateViewer::OnCaptureClosed()
|
|
{
|
|
if(m_Current)
|
|
m_Current->OnCaptureClosed();
|
|
}
|
|
|
|
void PipelineStateViewer::OnEventChanged(uint32_t eventId)
|
|
{
|
|
RENDERDOC_PROFILEFUNCTION();
|
|
|
|
if(m_Ctx.APIProps().pipelineType == GraphicsAPI::D3D11)
|
|
setToD3D11();
|
|
else if(m_Ctx.APIProps().pipelineType == GraphicsAPI::D3D12)
|
|
setToD3D12();
|
|
else if(m_Ctx.APIProps().pipelineType == GraphicsAPI::OpenGL)
|
|
setToGL();
|
|
else if(m_Ctx.APIProps().pipelineType == GraphicsAPI::Vulkan)
|
|
setToVulkan();
|
|
|
|
if(m_Current)
|
|
m_Current->OnEventChanged(eventId);
|
|
}
|
|
|
|
QString PipelineStateViewer::GetCurrentAPI()
|
|
{
|
|
if(m_Current == m_D3D11)
|
|
return lit("D3D11");
|
|
else if(m_Current == m_D3D12)
|
|
return lit("D3D12");
|
|
else if(m_Current == m_GL)
|
|
return lit("OpenGL");
|
|
else if(m_Current == m_Vulkan)
|
|
return lit("Vulkan");
|
|
|
|
return lit("");
|
|
}
|
|
|
|
QVariant PipelineStateViewer::persistData()
|
|
{
|
|
QVariantMap state;
|
|
|
|
state[lit("type")] = GetCurrentAPI();
|
|
|
|
return state;
|
|
}
|
|
|
|
void PipelineStateViewer::setPersistData(const QVariant &persistData)
|
|
{
|
|
QString str = persistData.toMap()[lit("type")].toString();
|
|
|
|
if(str == lit("D3D11"))
|
|
setToD3D11();
|
|
else if(str == lit("D3D12"))
|
|
setToD3D12();
|
|
else if(str == lit("GL"))
|
|
setToGL();
|
|
else if(str == lit("Vulkan"))
|
|
setToVulkan();
|
|
}
|
|
|
|
void PipelineStateViewer::reset()
|
|
{
|
|
delete m_D3D11;
|
|
delete m_D3D12;
|
|
delete m_GL;
|
|
delete m_Vulkan;
|
|
|
|
m_D3D11 = NULL;
|
|
m_D3D12 = NULL;
|
|
m_GL = NULL;
|
|
m_Vulkan = NULL;
|
|
|
|
m_Current = NULL;
|
|
}
|
|
|
|
void PipelineStateViewer::setToD3D11()
|
|
{
|
|
if(m_D3D11)
|
|
return;
|
|
|
|
reset();
|
|
|
|
m_D3D11 = new D3D11PipelineStateViewer(m_Ctx, *this, this);
|
|
ui->layout->addWidget(m_D3D11);
|
|
m_Current = m_D3D11;
|
|
}
|
|
|
|
void PipelineStateViewer::setToD3D12()
|
|
{
|
|
if(m_D3D12)
|
|
return;
|
|
|
|
reset();
|
|
|
|
m_D3D12 = new D3D12PipelineStateViewer(m_Ctx, *this, this);
|
|
ui->layout->addWidget(m_D3D12);
|
|
m_Current = m_D3D12;
|
|
}
|
|
|
|
void PipelineStateViewer::setToGL()
|
|
{
|
|
if(m_GL)
|
|
return;
|
|
|
|
reset();
|
|
|
|
m_GL = new GLPipelineStateViewer(m_Ctx, *this, this);
|
|
ui->layout->addWidget(m_GL);
|
|
m_Current = m_GL;
|
|
}
|
|
|
|
void PipelineStateViewer::setToVulkan()
|
|
{
|
|
if(m_Vulkan)
|
|
return;
|
|
|
|
reset();
|
|
|
|
m_Vulkan = new VulkanPipelineStateViewer(m_Ctx, *this, this);
|
|
ui->layout->addWidget(m_Vulkan);
|
|
m_Current = m_Vulkan;
|
|
}
|
|
|
|
QXmlStreamWriter *PipelineStateViewer::beginHTMLExport()
|
|
{
|
|
if(!m_Ctx.IsCaptureLoaded())
|
|
return NULL;
|
|
|
|
QString filename = RDDialog::getSaveFileName(this, tr("Export pipeline state as HTML"), QString(),
|
|
tr("HTML files (*.html)"));
|
|
|
|
if(!filename.isEmpty())
|
|
{
|
|
ANALYTIC_SET(Export.PipelineState, true);
|
|
|
|
QDir dirinfo = QFileInfo(filename).dir();
|
|
if(dirinfo.exists())
|
|
{
|
|
QFile *f = new QFile(filename, this);
|
|
if(f->open(QIODevice::WriteOnly | QIODevice::Truncate))
|
|
{
|
|
QXmlStreamWriter *xmlptr = new QXmlStreamWriter(f);
|
|
|
|
QXmlStreamWriter &xml = *xmlptr;
|
|
|
|
xml.setAutoFormatting(true);
|
|
xml.setAutoFormattingIndent(4);
|
|
xml.writeStartDocument();
|
|
xml.writeDTD(lit("<!DOCTYPE html>"));
|
|
|
|
xml.writeStartElement(lit("html"));
|
|
xml.writeAttribute(lit("lang"), lit("en"));
|
|
|
|
QString title = tr("%1 EID %2 - %3 Pipeline export")
|
|
.arg(QFileInfo(m_Ctx.GetCaptureFilename()).fileName())
|
|
.arg(m_Ctx.CurEvent())
|
|
.arg(GetCurrentAPI());
|
|
|
|
{
|
|
xml.writeStartElement(lit("head"));
|
|
|
|
xml.writeStartElement(lit("meta"));
|
|
xml.writeAttribute(lit("charset"), lit("utf-8"));
|
|
xml.writeEndElement();
|
|
|
|
xml.writeStartElement(lit("meta"));
|
|
xml.writeAttribute(lit("http-equiv"), lit("X-UA-Compatible"));
|
|
xml.writeAttribute(lit("content"), lit("IE=edge"));
|
|
xml.writeEndElement();
|
|
|
|
xml.writeStartElement(lit("meta"));
|
|
xml.writeAttribute(lit("name"), lit("viewport"));
|
|
xml.writeAttribute(lit("content"), lit("width=device-width, initial-scale=1"));
|
|
xml.writeEndElement();
|
|
|
|
xml.writeStartElement(lit("meta"));
|
|
xml.writeAttribute(lit("name"), lit("description"));
|
|
xml.writeAttribute(lit("content"), lit(""));
|
|
xml.writeEndElement();
|
|
|
|
xml.writeStartElement(lit("meta"));
|
|
xml.writeAttribute(lit("name"), lit("author"));
|
|
xml.writeAttribute(lit("content"), lit(""));
|
|
xml.writeEndElement();
|
|
|
|
xml.writeStartElement(lit("meta"));
|
|
xml.writeAttribute(lit("http-equiv"), lit("Content-Type"));
|
|
xml.writeAttribute(lit("content"), lit("text/html;charset=utf-8"));
|
|
xml.writeEndElement();
|
|
|
|
xml.writeStartElement(lit("title"));
|
|
xml.writeCharacters(title);
|
|
xml.writeEndElement();
|
|
|
|
xml.writeStartElement(lit("style"));
|
|
xml.writeComment(lit(R"(
|
|
|
|
/* If you think this css is ugly/bad, open a pull request! */
|
|
body { margin: 20px; }
|
|
div.stage { border: 1px solid #BBBBBB; border-radius: 5px; padding: 16px; margin-bottom: 32px; }
|
|
div.stage h1 { text-decoration: underline; margin-top: 0px; }
|
|
div.stage table { border: 1px solid #AAAAAA; border-collapse: collapse; }
|
|
div.stage table thead tr { border-bottom: 1px solid #AAAAAA; background-color: #EEEEFF; }
|
|
div.stage table tr th { border-right: 1px solid #AAAAAA; padding: 6px; }
|
|
div.stage table tr td { border-right: 1px solid #AAAAAA; background-color: #EEEEEE; padding: 3px; }
|
|
|
|
)"));
|
|
xml.writeEndElement(); // </style>
|
|
|
|
xml.writeEndElement(); // </head>
|
|
}
|
|
|
|
{
|
|
xml.writeStartElement(lit("body"));
|
|
|
|
xml.writeStartElement(lit("h1"));
|
|
xml.writeCharacters(title);
|
|
xml.writeEndElement();
|
|
|
|
xml.writeStartElement(lit("h3"));
|
|
{
|
|
uint32_t frameNumber = m_Ctx.FrameInfo().frameNumber;
|
|
|
|
QString context = frameNumber == ~0U ? tr("Capture") : tr("Frame %1").arg(frameNumber);
|
|
|
|
const ActionDescription *action = m_Ctx.CurAction();
|
|
|
|
QList<const ActionDescription *> actionstack;
|
|
const ActionDescription *parent = action ? action->parent : NULL;
|
|
while(parent)
|
|
{
|
|
actionstack.push_front(parent);
|
|
parent = parent->parent;
|
|
}
|
|
|
|
for(const ActionDescription *d : actionstack)
|
|
{
|
|
context += QFormatStr(" > %1").arg(d->customName);
|
|
}
|
|
|
|
if(action)
|
|
context +=
|
|
QFormatStr(" => %1").arg(m_Ctx.GetEventBrowser()->GetEventName(action->eventId));
|
|
else
|
|
context += tr(" => Capture Start");
|
|
|
|
xml.writeCharacters(context);
|
|
}
|
|
xml.writeEndElement(); // </h3>
|
|
}
|
|
|
|
// body is open
|
|
|
|
return xmlptr;
|
|
}
|
|
|
|
RDDialog::critical(
|
|
this, tr("Error exporting pipeline state"),
|
|
tr("Couldn't open path %1 for write.\n%2").arg(filename).arg(f->errorString()));
|
|
|
|
delete f;
|
|
|
|
return NULL;
|
|
}
|
|
else
|
|
{
|
|
RDDialog::critical(this, tr("Invalid directory"),
|
|
tr("Cannot find target directory to save to"));
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
void PipelineStateViewer::exportHTMLTable(QXmlStreamWriter &xml, const QStringList &cols,
|
|
const QList<QVariantList> &rows)
|
|
{
|
|
xml.writeStartElement(lit("table"));
|
|
|
|
{
|
|
xml.writeStartElement(lit("thead"));
|
|
xml.writeStartElement(lit("tr"));
|
|
|
|
for(const QString &col : cols)
|
|
{
|
|
xml.writeStartElement(lit("th"));
|
|
xml.writeCharacters(col);
|
|
xml.writeEndElement();
|
|
}
|
|
|
|
xml.writeEndElement();
|
|
xml.writeEndElement();
|
|
}
|
|
|
|
{
|
|
xml.writeStartElement(lit("tbody"));
|
|
|
|
if(rows.isEmpty())
|
|
{
|
|
xml.writeStartElement(lit("tr"));
|
|
|
|
for(int i = 0; i < cols.count(); i++)
|
|
{
|
|
xml.writeStartElement(lit("td"));
|
|
xml.writeCharacters(lit("-"));
|
|
xml.writeEndElement();
|
|
}
|
|
|
|
xml.writeEndElement();
|
|
}
|
|
else
|
|
{
|
|
for(const QVariantList &row : rows)
|
|
{
|
|
xml.writeStartElement(lit("tr"));
|
|
|
|
for(const QVariant &el : row)
|
|
{
|
|
xml.writeStartElement(lit("td"));
|
|
|
|
if(el.type() == QVariant::Bool)
|
|
xml.writeCharacters(el.toBool() ? tr("True") : tr("False"));
|
|
else
|
|
xml.writeCharacters(el.toString());
|
|
|
|
xml.writeEndElement();
|
|
}
|
|
|
|
xml.writeEndElement();
|
|
}
|
|
}
|
|
|
|
xml.writeEndElement();
|
|
}
|
|
|
|
xml.writeEndElement();
|
|
}
|
|
|
|
void PipelineStateViewer::exportHTMLTable(QXmlStreamWriter &xml, const QStringList &cols,
|
|
const QVariantList &row)
|
|
{
|
|
exportHTMLTable(xml, cols, QList<QVariantList>({row}));
|
|
}
|
|
|
|
void PipelineStateViewer::endHTMLExport(QXmlStreamWriter *xml)
|
|
{
|
|
xml->writeEndElement(); // </body>
|
|
|
|
xml->writeEndElement(); // </html>
|
|
|
|
xml->writeEndDocument();
|
|
|
|
// delete the file the writer was writing to
|
|
QFile *f = qobject_cast<QFile *>(xml->device());
|
|
delete f;
|
|
|
|
delete xml;
|
|
}
|
|
|
|
void PipelineStateViewer::setTopologyDiagram(QLabel *diagram, Topology topo)
|
|
{
|
|
int idx = qMin((int)topo, (int)Topology::PatchList);
|
|
|
|
if(m_TopoPixmaps[idx].isNull())
|
|
{
|
|
QSvgRenderer svg;
|
|
switch(topo)
|
|
{
|
|
case Topology::PointList: svg.load(lit(":/topologies/topo_pointlist.svg")); break;
|
|
case Topology::LineList: svg.load(lit(":/topologies/topo_linelist.svg")); break;
|
|
case Topology::LineStrip: svg.load(lit(":/topologies/topo_linestrip.svg")); break;
|
|
case Topology::TriangleList: svg.load(lit(":/topologies/topo_trilist.svg")); break;
|
|
case Topology::TriangleStrip: svg.load(lit(":/topologies/topo_tristrip.svg")); break;
|
|
case Topology::LineList_Adj: svg.load(lit(":/topologies/topo_linelist_adj.svg")); break;
|
|
case Topology::LineStrip_Adj: svg.load(lit(":/topologies/topo_linestrip_adj.svg")); break;
|
|
case Topology::TriangleList_Adj: svg.load(lit(":/topologies/topo_trilist_adj.svg")); break;
|
|
case Topology::TriangleStrip_Adj: svg.load(lit(":/topologies/topo_tristrip_adj.svg")); break;
|
|
default: svg.load(lit(":/topologies/topo_patch.svg")); break;
|
|
}
|
|
|
|
QRect rect = svg.viewBox();
|
|
|
|
QImage im(rect.size() * diagram->devicePixelRatio(), QImage::Format_ARGB32);
|
|
|
|
im.fill(QColor(0, 0, 0, 0));
|
|
|
|
QPainter p(&im);
|
|
|
|
svg.render(&p);
|
|
|
|
// convert the colors - black maps to Text (foreground) and white maps to Base (background)
|
|
QColor white = diagram->palette().color(QPalette::Active, QPalette::Base);
|
|
QColor black = diagram->palette().color(QPalette::Active, QPalette::Text);
|
|
|
|
const float br = black.redF();
|
|
const float bg = black.greenF();
|
|
const float bb = black.blueF();
|
|
|
|
const float wr = white.redF();
|
|
const float wg = white.greenF();
|
|
const float wb = white.blueF();
|
|
|
|
for(int y = 0; y < im.height(); y++)
|
|
{
|
|
QRgb *line = (QRgb *)im.scanLine(y);
|
|
|
|
for(int x = 0; x < im.width(); x++)
|
|
{
|
|
// delta of 0 is black, delta of 255 is white
|
|
const float delta = float(qRed(*line));
|
|
const float bd = 255.0f - delta;
|
|
const float wd = delta;
|
|
|
|
const int r = int(br * bd + wr * wd);
|
|
const int g = int(bg * bd + wg * wd);
|
|
const int b = int(bb * bd + wb * wd);
|
|
|
|
*line = qRgba(r, g, b, qAlpha(*line));
|
|
|
|
line++;
|
|
}
|
|
}
|
|
|
|
m_TopoPixmaps[idx] = QPixmap::fromImage(im);
|
|
m_TopoPixmaps[idx].setDevicePixelRatio(diagram->devicePixelRatioF());
|
|
}
|
|
|
|
diagram->setPixmap(m_TopoPixmaps[idx]);
|
|
}
|
|
|
|
void PipelineStateViewer::setMeshViewPixmap(RDLabel *meshView)
|
|
{
|
|
QImage meshIcon = Pixmaps::wireframe_mesh(meshView->devicePixelRatio()).toImage();
|
|
QImage colSwapped(meshIcon.size(), QImage::Format_ARGB32);
|
|
colSwapped.fill(meshView->palette().color(QPalette::WindowText));
|
|
|
|
for(int y = 0; y < meshIcon.height(); y++)
|
|
{
|
|
const QRgb *in = (const QRgb *)meshIcon.constScanLine(y);
|
|
QRgb *out = (QRgb *)colSwapped.scanLine(y);
|
|
|
|
for(int x = 0; x < meshIcon.width(); x++)
|
|
{
|
|
*out = qRgba(qRed(*out), qGreen(*out), qBlue(*out), qAlpha(*in));
|
|
|
|
in++;
|
|
out++;
|
|
}
|
|
}
|
|
|
|
QPixmap p = QPixmap::fromImage(colSwapped);
|
|
p.setDevicePixelRatio(meshView->devicePixelRatioF());
|
|
|
|
meshView->setPixmap(p);
|
|
meshView->setPreserveAspectRatio(true);
|
|
|
|
QPalette pal = meshView->palette();
|
|
pal.setColor(QPalette::Shadow, pal.color(QPalette::Window).darker(120));
|
|
meshView->setPalette(pal);
|
|
meshView->setBackgroundRole(QPalette::Window);
|
|
meshView->setMouseTracking(true);
|
|
|
|
QObject::connect(meshView, &RDLabel::mouseMoved, [meshView](QMouseEvent *) {
|
|
meshView->setBackgroundRole(QPalette::Shadow);
|
|
meshView->setAutoFillBackground(true);
|
|
});
|
|
QObject::connect(meshView, &RDLabel::leave, [meshView]() {
|
|
meshView->setBackgroundRole(QPalette::Window);
|
|
meshView->setAutoFillBackground(false);
|
|
});
|
|
}
|
|
|
|
void PipelineStateViewer::MakeShaderVariablesHLSL(bool cbufferContents,
|
|
const rdcarray<ShaderConstant> &vars,
|
|
QString &struct_contents, QString &struct_defs)
|
|
{
|
|
for(const ShaderConstant &v : vars)
|
|
{
|
|
if(v.type.baseType == VarType::Struct)
|
|
{
|
|
QString def = lit("struct %1 {\n").arg(v.type.name);
|
|
|
|
if(!struct_defs.contains(def))
|
|
{
|
|
QString contents;
|
|
MakeShaderVariablesHLSL(false, v.type.members, contents, struct_defs);
|
|
|
|
struct_defs += def + contents + lit("};\n\n");
|
|
}
|
|
}
|
|
|
|
if(v.type.elements > 1)
|
|
{
|
|
struct_contents += lit("\t%1 %2[%3]").arg(v.type.name).arg(v.name).arg(v.type.elements);
|
|
}
|
|
else
|
|
{
|
|
struct_contents += lit("\t%1 %2").arg(v.type.name).arg(v.name);
|
|
}
|
|
|
|
if((v.byteOffset % 4) != 0)
|
|
qWarning() << "Variable " << QString(v.name) << " is not DWORD aligned";
|
|
|
|
uint32_t dwordOffset = v.byteOffset / 4;
|
|
|
|
uint32_t vectorIndex = dwordOffset / 4;
|
|
uint32_t vectorComponent = dwordOffset % 4;
|
|
|
|
char comp = 'x';
|
|
if(vectorComponent == 1)
|
|
comp = 'y';
|
|
if(vectorComponent == 2)
|
|
comp = 'z';
|
|
if(vectorComponent == 3)
|
|
comp = 'w';
|
|
|
|
if(cbufferContents)
|
|
struct_contents += lit(" : packoffset(c%1.%2);").arg(vectorIndex).arg(QLatin1Char(comp));
|
|
else
|
|
struct_contents += lit(";");
|
|
|
|
struct_contents += lit("\n");
|
|
}
|
|
}
|
|
|
|
void PipelineStateViewer::showEvent(QShowEvent *event)
|
|
{
|
|
// we didn't set any default pipeline state in case it would be overridden by the persist data.
|
|
// But if we don't have any persist data and we're about to show, default to D3D11.
|
|
if(m_Current == NULL)
|
|
{
|
|
setToD3D11();
|
|
}
|
|
}
|
|
|
|
void PipelineStateViewer::SetStencilLabelValue(QLabel *label, uint8_t value)
|
|
{
|
|
label->setText(Formatter::Format(value, true));
|
|
label->setToolTip(tr("%1 / 0x%2 / 0b%3")
|
|
.arg(value, 3, 10, QLatin1Char(' '))
|
|
.arg(Formatter::Format(value, true))
|
|
.arg(value, 8, 2, QLatin1Char('0')));
|
|
}
|
|
|
|
void PipelineStateViewer::SetStencilTreeItemValue(RDTreeWidgetItem *item, int column, uint8_t value)
|
|
{
|
|
item->setText(column, Formatter::Format(value, true));
|
|
item->setToolTip(column, tr("%1 / 0x%2 / 0b%3")
|
|
.arg(value, 3, 10, QLatin1Char(' '))
|
|
.arg(Formatter::Format(value, true))
|
|
.arg(value, 8, 2, QLatin1Char('0')));
|
|
}
|
|
|
|
QString PipelineStateViewer::GenerateHLSLStub(const ShaderBindpointMapping &bindpointMapping,
|
|
const ShaderReflection *shaderDetails,
|
|
const QString &entryFunc)
|
|
{
|
|
QString hlsl = lit("// HLSL function stub generated\n\n");
|
|
|
|
const QString textureDim[arraydim<TextureType>()] = {
|
|
lit("Unknown"), lit("Buffer"), lit("Texture1D"), lit("Texture1DArray"),
|
|
lit("Texture2D"), lit("TextureRect"), lit("Texture2DArray"), lit("Texture2DMS"),
|
|
lit("Texture2DMSArray"), lit("Texture3D"), lit("TextureCube"), lit("TextureCubeArray"),
|
|
};
|
|
|
|
// use bindpoint mapping
|
|
|
|
for(const ShaderSampler &samp : shaderDetails->samplers)
|
|
{
|
|
uint32_t reg = ~0U;
|
|
|
|
if(samp.bindPoint < bindpointMapping.samplers.count())
|
|
reg = bindpointMapping.samplers[samp.bindPoint].bind;
|
|
else
|
|
hlsl += lit("//");
|
|
|
|
hlsl += lit("SamplerState %1 : register(s%2); // can't disambiguate\n"
|
|
"//SamplerComparisonState %1 : register(s%2); // can't disambiguate\n")
|
|
.arg(samp.name)
|
|
.arg(reg);
|
|
}
|
|
|
|
for(int i = 0; i < 2; i++)
|
|
{
|
|
const rdcarray<Bindpoint> &binds =
|
|
(i == 0 ? bindpointMapping.readOnlyResources : bindpointMapping.readWriteResources);
|
|
const rdcarray<ShaderResource> &resources =
|
|
(i == 0 ? shaderDetails->readOnlyResources : shaderDetails->readWriteResources);
|
|
for(const ShaderResource &res : resources)
|
|
{
|
|
char regChar = 't';
|
|
|
|
uint32_t reg = ~0U;
|
|
|
|
if(res.bindPoint < binds.count())
|
|
reg = binds[res.bindPoint].bind;
|
|
else
|
|
hlsl += lit("//");
|
|
|
|
if(i == 1)
|
|
{
|
|
hlsl += lit("RW");
|
|
regChar = 'u';
|
|
}
|
|
|
|
if(res.isTexture)
|
|
{
|
|
hlsl += lit("%1<%2> %3 : register(%4%5);\n")
|
|
.arg(textureDim[(size_t)res.resType])
|
|
.arg(res.variableType.name)
|
|
.arg(res.name)
|
|
.arg(QLatin1Char(regChar))
|
|
.arg(reg);
|
|
}
|
|
else
|
|
{
|
|
if(res.variableType.rows > 1)
|
|
hlsl += lit("Structured");
|
|
|
|
hlsl += lit("Buffer<%1> %2 : register(%3%4);\n")
|
|
.arg(res.variableType.name)
|
|
.arg(res.name)
|
|
.arg(QLatin1Char(regChar))
|
|
.arg(reg);
|
|
}
|
|
}
|
|
}
|
|
|
|
hlsl += lit("\n\n");
|
|
|
|
QString cbuffers;
|
|
|
|
int cbufIdx = 0;
|
|
for(const ConstantBlock &cbuf : shaderDetails->constantBlocks)
|
|
{
|
|
if(!cbuf.name.isEmpty() && !cbuf.variables.isEmpty())
|
|
{
|
|
uint32_t reg = ~0U;
|
|
|
|
if(cbuf.bindPoint < bindpointMapping.constantBlocks.count())
|
|
reg = bindpointMapping.constantBlocks[cbuf.bindPoint].bind;
|
|
else
|
|
hlsl += lit("/*\n");
|
|
|
|
QString cbufName = cbuf.name;
|
|
if(cbufName == lit("$Globals"))
|
|
cbufName = lit("_Globals");
|
|
cbuffers += lit("cbuffer %1 : register(b%2) {\n").arg(cbufName).arg(reg);
|
|
MakeShaderVariablesHLSL(true, cbuf.variables, cbuffers, hlsl);
|
|
cbuffers += lit("};\n\n");
|
|
|
|
if(reg == ~0U)
|
|
hlsl += lit("*/\n");
|
|
}
|
|
cbufIdx++;
|
|
}
|
|
|
|
hlsl += cbuffers;
|
|
|
|
hlsl += lit("\n\n");
|
|
|
|
hlsl += lit("struct InputStruct {\n");
|
|
for(const SigParameter &sig : shaderDetails->inputSignature)
|
|
{
|
|
QString name = !sig.varName.isEmpty() ? QString(sig.varName) : lit("param%1").arg(sig.regIndex);
|
|
|
|
if(sig.varName.isEmpty() && sig.systemValue != ShaderBuiltin::Undefined)
|
|
name = D3DSemanticString(sig).replace(lit("SV_"), QString());
|
|
|
|
hlsl += lit("\t%1 %2 : %3;\n").arg(TypeString(sig)).arg(name).arg(D3DSemanticString(sig));
|
|
}
|
|
hlsl += lit("};\n\n");
|
|
|
|
hlsl += lit("struct OutputStruct {\n");
|
|
for(const SigParameter &sig : shaderDetails->outputSignature)
|
|
{
|
|
QString name = !sig.varName.isEmpty() ? QString(sig.varName) : lit("param%1").arg(sig.regIndex);
|
|
|
|
if(sig.varName.isEmpty() && sig.systemValue != ShaderBuiltin::Undefined)
|
|
name = D3DSemanticString(sig).replace(lit("SV_"), QString());
|
|
|
|
hlsl += lit("\t%1 %2 : %3;\n").arg(TypeString(sig)).arg(name).arg(D3DSemanticString(sig));
|
|
}
|
|
|
|
hlsl += lit("};\n\n");
|
|
|
|
hlsl += lit("OutputStruct %1(in InputStruct IN)\n"
|
|
"{\n"
|
|
"\tOutputStruct OUT = (OutputStruct)0;\n"
|
|
"\n"
|
|
"\t// ...\n"
|
|
"\n"
|
|
"\treturn OUT;\n"
|
|
"}\n")
|
|
.arg(entryFunc);
|
|
|
|
return hlsl;
|
|
}
|
|
|
|
void PipelineStateViewer::shaderEdit_clicked()
|
|
{
|
|
QToolButton *sender = qobject_cast<QToolButton *>(QObject::sender());
|
|
if(!sender)
|
|
return;
|
|
|
|
// activate the first item in the menu, if there are any items, as the default action.
|
|
QMenu *menu = sender->menu();
|
|
if(menu && !menu->actions().isEmpty())
|
|
menu->actions()[0]->trigger();
|
|
}
|
|
|
|
IShaderViewer *PipelineStateViewer::EditShader(ResourceId id, ShaderStage shaderType,
|
|
const rdcstr &entry, ShaderCompileFlags compileFlags,
|
|
KnownShaderTool knownTool,
|
|
ShaderEncoding shaderEncoding,
|
|
const rdcstrpairs &files)
|
|
{
|
|
IShaderViewer *sv = m_Ctx.EditShader(id, shaderType, entry, files, knownTool, shaderEncoding,
|
|
compileFlags, NULL, NULL);
|
|
|
|
m_Ctx.AddDockWindow(sv->Widget(), DockReference::AddTo, this);
|
|
|
|
return sv;
|
|
}
|
|
|
|
IShaderViewer *PipelineStateViewer::EditOriginalShaderSource(ResourceId id,
|
|
const ShaderReflection *shaderDetails)
|
|
{
|
|
QSet<uint> uniqueFiles;
|
|
rdcstrpairs files;
|
|
|
|
// add the entry point file first, if we have one
|
|
const int entryFile = shaderDetails->debugInfo.editBaseFile;
|
|
|
|
for(int i = -1; i < shaderDetails->debugInfo.files.count(); i++)
|
|
{
|
|
int idx = i;
|
|
if(idx < 0)
|
|
idx = entryFile;
|
|
else if(idx == entryFile)
|
|
continue;
|
|
|
|
if(idx < 0)
|
|
continue;
|
|
|
|
const ShaderSourceFile &s = shaderDetails->debugInfo.files[idx];
|
|
|
|
QString filename = s.filename;
|
|
|
|
uint filenameHash = qHash(filename.toLower());
|
|
|
|
if(uniqueFiles.contains(filenameHash))
|
|
{
|
|
qWarning() << lit("Duplicate full filename") << filename;
|
|
continue;
|
|
}
|
|
uniqueFiles.insert(filenameHash);
|
|
|
|
files.push_back(make_rdcpair(s.filename, s.contents));
|
|
}
|
|
|
|
return EditShader(id, shaderDetails->stage, shaderDetails->entryPoint,
|
|
shaderDetails->debugInfo.compileFlags, shaderDetails->debugInfo.compiler,
|
|
shaderDetails->debugInfo.encoding, files);
|
|
}
|
|
|
|
IShaderViewer *PipelineStateViewer::EditDecompiledSource(const ShaderProcessingTool &tool,
|
|
ResourceId id,
|
|
const ShaderReflection *shaderDetails)
|
|
{
|
|
ShaderToolOutput out = tool.DisassembleShader(this, shaderDetails, "");
|
|
|
|
rdcstr source;
|
|
source.assign((const char *)out.result.data(), out.result.size());
|
|
|
|
rdcstrpairs files;
|
|
files.push_back(rdcpair<rdcstr, rdcstr>("decompiled", source));
|
|
|
|
IShaderViewer *sv = EditShader(id, shaderDetails->stage, shaderDetails->entryPoint, {},
|
|
KnownShaderTool::Unknown, tool.output, files);
|
|
|
|
sv->ShowErrors(out.log);
|
|
|
|
return sv;
|
|
}
|
|
|
|
void PipelineStateViewer::SetupShaderEditButton(QToolButton *button, ResourceId pipelineId,
|
|
ResourceId shaderId,
|
|
const ShaderBindpointMapping &bindpointMapping,
|
|
const ShaderReflection *shaderDetails)
|
|
{
|
|
if(!shaderDetails || !button->isEnabled() || button->popupMode() != QToolButton::MenuButtonPopup)
|
|
return;
|
|
|
|
QMenu *menu = editMenus[(int)shaderDetails->stage];
|
|
|
|
menu->clear();
|
|
|
|
rdcarray<ShaderEncoding> accepted = m_Ctx.TargetShaderEncodings();
|
|
|
|
const ShaderDebugInfo &dbg = shaderDetails->debugInfo;
|
|
|
|
// if we have original source and it's in a known format, display it as the first most preferred
|
|
// option
|
|
if(!dbg.files.empty() && dbg.encoding != ShaderEncoding::Unknown)
|
|
{
|
|
int entryFile = qMax(0, dbg.entryLocation.fileIndex);
|
|
if(dbg.editBaseFile >= 0 && dbg.editBaseFile < dbg.files.count())
|
|
entryFile = dbg.editBaseFile;
|
|
QAction *action = new QAction(tr("Edit Source - %1").arg(dbg.files[entryFile].filename), menu);
|
|
action->setIcon(Icons::page_white_edit());
|
|
|
|
QObject::connect(action, &QAction::triggered, [this, shaderId, shaderDetails]() {
|
|
EditOriginalShaderSource(shaderId, shaderDetails);
|
|
});
|
|
|
|
menu->addAction(action);
|
|
}
|
|
|
|
// next up, try the shader processing tools in order - all the ones that will decompile from our
|
|
// native representation. We don't check here yet if we have a valid compiler to compile from the
|
|
// output to what we want.
|
|
for(const ShaderProcessingTool &tool : m_Ctx.Config().ShaderProcessors)
|
|
{
|
|
// skip tools that can't decode our shader, or doesn't produce a textual output
|
|
if(tool.input != shaderDetails->encoding || !IsTextRepresentation(tool.output))
|
|
continue;
|
|
|
|
QAction *action = new QAction(tr("Decompile with %1").arg(tool.name), menu);
|
|
action->setIcon(Icons::page_white_edit());
|
|
|
|
QObject::connect(action, &QAction::triggered, [this, tool, shaderId, shaderDetails]() {
|
|
EditDecompiledSource(tool, shaderId, shaderDetails);
|
|
});
|
|
|
|
menu->addAction(action);
|
|
}
|
|
|
|
// if all else fails we can generate a stub for editing. Skip this for GLSL as it always has
|
|
// source above which is preferred.
|
|
if(shaderDetails->encoding != ShaderEncoding::GLSL)
|
|
{
|
|
QString label = tr("Edit Generated Stub");
|
|
|
|
if(shaderDetails->encoding == ShaderEncoding::SPIRV ||
|
|
shaderDetails->encoding == ShaderEncoding::OpenGLSPIRV)
|
|
label = tr("Edit Pseudocode");
|
|
|
|
QAction *action = new QAction(label, menu);
|
|
action->setIcon(Icons::page_white_edit());
|
|
|
|
QObject::connect(action, &QAction::triggered, [this, pipelineId, shaderId, bindpointMapping,
|
|
shaderDetails]() {
|
|
QString entry;
|
|
QString src;
|
|
|
|
if(shaderDetails->encoding == ShaderEncoding::SPIRV ||
|
|
shaderDetails->encoding == ShaderEncoding::OpenGLSPIRV)
|
|
{
|
|
m_Ctx.Replay().AsyncInvoke([this, pipelineId, shaderId, shaderDetails](IReplayController *r) {
|
|
rdcstr disasm = r->DisassembleShader(pipelineId, shaderDetails, "");
|
|
|
|
QString editeddisasm =
|
|
tr("#### PSEUDOCODE SPIR-V DISASSEMBLY ###\n") +
|
|
tr("#### Use a SPIR-V decompiler to get compileable source ###\n\n");
|
|
|
|
editeddisasm += disasm;
|
|
|
|
GUIInvoke::call(this, [this, shaderId, shaderDetails, editeddisasm]() {
|
|
rdcstrpairs files;
|
|
files.push_back(rdcpair<rdcstr, rdcstr>("pseudocode", editeddisasm));
|
|
|
|
EditShader(shaderId, shaderDetails->stage, shaderDetails->entryPoint,
|
|
shaderDetails->debugInfo.compileFlags, KnownShaderTool::Unknown,
|
|
ShaderEncoding::Unknown, files);
|
|
});
|
|
});
|
|
}
|
|
else if(shaderDetails->encoding == ShaderEncoding::DXBC ||
|
|
shaderDetails->encoding == ShaderEncoding::DXIL)
|
|
{
|
|
entry = lit("EditedShader%1S").arg(ToQStr(shaderDetails->stage, GraphicsAPI::D3D11)[0]);
|
|
|
|
rdcstrpairs files;
|
|
files.push_back(rdcpair<rdcstr, rdcstr>(
|
|
"decompiled_stub.hlsl", GenerateHLSLStub(bindpointMapping, shaderDetails, entry)));
|
|
|
|
EditShader(shaderId, shaderDetails->stage, entry, shaderDetails->debugInfo.compileFlags,
|
|
KnownShaderTool::Unknown, ShaderEncoding::HLSL, files);
|
|
}
|
|
|
|
});
|
|
|
|
menu->addAction(action);
|
|
}
|
|
|
|
button->setMenu(menu);
|
|
}
|
|
|
|
void PipelineStateViewer::AddResourceUsageEntry(QMenu &menu, uint32_t start, uint32_t end,
|
|
ResourceUsage usage)
|
|
{
|
|
QAction *item = NULL;
|
|
|
|
if(start == end)
|
|
item = new QAction(
|
|
QFormatStr("EID %1: %2").arg(start).arg(ToQStr(usage, m_Ctx.APIProps().pipelineType)), this);
|
|
else
|
|
item = new QAction(
|
|
QFormatStr("EID %1-%2: %3").arg(start).arg(end).arg(ToQStr(usage, m_Ctx.APIProps().pipelineType)),
|
|
this);
|
|
|
|
QObject::connect(item, &QAction::triggered, [this, end]() { m_Ctx.SetEventID({}, end, end); });
|
|
|
|
menu.addAction(item);
|
|
}
|
|
|
|
void PipelineStateViewer::ShowResourceContextMenu(RDTreeWidget *widget, const QPoint &pos,
|
|
ResourceId id, const rdcarray<EventUsage> &usage)
|
|
{
|
|
RDTreeWidgetItem *item = widget->itemAt(pos);
|
|
|
|
QMenu contextMenu(this);
|
|
|
|
QAction copy(tr("&Copy"), this);
|
|
|
|
contextMenu.addAction(©);
|
|
|
|
copy.setIcon(Icons::copy());
|
|
|
|
QObject::connect(©, &QAction::triggered,
|
|
[widget, pos, item]() { widget->copyItem(pos, item); });
|
|
|
|
QAction usageTitle(tr("Used:"), this);
|
|
QAction openResourceInspector(tr("Open in Resource Inspector"), this);
|
|
|
|
openResourceInspector.setIcon(Icons::link());
|
|
|
|
if(id != ResourceId())
|
|
{
|
|
contextMenu.addSeparator();
|
|
contextMenu.addAction(&openResourceInspector);
|
|
contextMenu.addAction(&usageTitle);
|
|
|
|
QObject::connect(&openResourceInspector, &QAction::triggered, [this, id]() {
|
|
m_Ctx.ShowResourceInspector();
|
|
|
|
m_Ctx.GetResourceInspector()->Inspect(id);
|
|
});
|
|
|
|
CombineUsageEvents(m_Ctx, usage,
|
|
[this, &contextMenu](uint32_t start, uint32_t end, ResourceUsage use) {
|
|
AddResourceUsageEntry(contextMenu, start, end, use);
|
|
});
|
|
}
|
|
|
|
RDDialog::show(&contextMenu, widget->viewport()->mapToGlobal(pos));
|
|
}
|
|
|
|
ResourceId PipelineStateViewer::updateThumbnail(QWidget *widget, QModelIndex idx)
|
|
{
|
|
ResourceId id;
|
|
|
|
if(!m_Output)
|
|
return id;
|
|
|
|
RDTreeWidget *treeWidget = qobject_cast<RDTreeWidget *>(widget);
|
|
if(treeWidget)
|
|
{
|
|
RDTreeWidgetItem *item = treeWidget->itemForIndex(idx);
|
|
|
|
if(item)
|
|
{
|
|
if(m_D3D11)
|
|
id = m_D3D11->GetResource(item);
|
|
else if(m_D3D12)
|
|
id = m_D3D12->GetResource(item);
|
|
else if(m_GL)
|
|
id = m_GL->GetResource(item);
|
|
else if(m_Vulkan)
|
|
id = m_Vulkan->GetResource(item);
|
|
}
|
|
|
|
TextureDescription *tex = m_Ctx.GetTexture(id);
|
|
|
|
if(tex)
|
|
{
|
|
m_TexDisplay.resourceId = id;
|
|
INVOKE_MEMFN(RT_UpdateAndDisplay);
|
|
|
|
float aspect = (float)tex->width / (float)qMax(1U, tex->height);
|
|
|
|
// keep height fixed at 100, and make width match the aspect ratio of the texture - up to 21:9
|
|
// ratio
|
|
ui->thumbnail->setFixedSize((int)qBound(100.0f, aspect * 100.0f, (21.0f / 9.0f) * 100.0f), 100);
|
|
ui->thumbnail->show();
|
|
}
|
|
else
|
|
{
|
|
ui->thumbnail->hide();
|
|
}
|
|
}
|
|
|
|
return id;
|
|
}
|
|
|
|
bool PipelineStateViewer::hasThumbnail(QWidget *widget, QModelIndex idx)
|
|
{
|
|
if(!m_Output)
|
|
return false;
|
|
|
|
RDTreeWidget *treeWidget = qobject_cast<RDTreeWidget *>(widget);
|
|
if(treeWidget)
|
|
{
|
|
ResourceId id;
|
|
|
|
RDTreeWidgetItem *item = treeWidget->itemForIndex(idx);
|
|
|
|
if(item)
|
|
{
|
|
if(m_D3D11)
|
|
id = m_D3D11->GetResource(item);
|
|
else if(m_D3D12)
|
|
id = m_D3D12->GetResource(item);
|
|
else if(m_GL)
|
|
id = m_GL->GetResource(item);
|
|
else if(m_Vulkan)
|
|
id = m_Vulkan->GetResource(item);
|
|
}
|
|
|
|
if(id != ResourceId() && m_Ctx.GetTexture(id))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void PipelineStateViewer::SetupResourceView(RDTreeWidget *widget)
|
|
{
|
|
auto handler = [this, widget](const QPoint &pos) {
|
|
RDTreeWidgetItem *item = widget->itemAt(pos);
|
|
|
|
ResourceId id;
|
|
|
|
if(m_D3D11)
|
|
id = m_D3D11->GetResource(item);
|
|
else if(m_D3D12)
|
|
id = m_D3D12->GetResource(item);
|
|
else if(m_GL)
|
|
id = m_GL->GetResource(item);
|
|
else if(m_Vulkan)
|
|
id = m_Vulkan->GetResource(item);
|
|
|
|
if(id != ResourceId())
|
|
{
|
|
m_Ctx.Replay().AsyncInvoke([this, widget, pos, id](IReplayController *r) {
|
|
rdcarray<EventUsage> usage = r->GetUsage(id);
|
|
|
|
GUIInvoke::call(this, [this, widget, pos, id, usage]() {
|
|
ShowResourceContextMenu(widget, pos, id, usage);
|
|
});
|
|
});
|
|
}
|
|
else
|
|
{
|
|
ShowResourceContextMenu(widget, pos, id, {});
|
|
}
|
|
};
|
|
|
|
widget->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
QObject::connect(widget, &RDTreeWidget::customContextMenuRequested, handler);
|
|
|
|
widget->setCustomTooltip(m_Tooltip);
|
|
}
|
|
|
|
QString PipelineStateViewer::GetVBufferFormatString(uint32_t slot)
|
|
{
|
|
rdcarray<BoundVBuffer> vbs = m_Ctx.CurPipelineState().GetVBuffers();
|
|
rdcarray<VertexInputAttribute> attrs = m_Ctx.CurPipelineState().GetVertexInputs();
|
|
|
|
if(slot >= vbs.size())
|
|
return tr("// Unbound vertex buffer slot %1").arg(slot);
|
|
|
|
uint32_t stride = vbs[slot].byteStride;
|
|
|
|
// filter attributes to only the ones enabled and using this slot
|
|
attrs.removeIf([slot](const VertexInputAttribute &attr) {
|
|
return (!attr.used || attr.vertexBuffer != (int)slot);
|
|
});
|
|
|
|
// we now have all attributes in this buffer. Sort by offset
|
|
std::sort(attrs.begin(), attrs.end(),
|
|
[](const VertexInputAttribute &a, const VertexInputAttribute &b) {
|
|
return a.byteOffset < b.byteOffset;
|
|
});
|
|
|
|
// ensure we don't have any overlap between attributes or with the stride
|
|
for(size_t i = 0; i < attrs.size(); i++)
|
|
{
|
|
uint32_t cursz = byteSize(attrs[i].format);
|
|
if(cursz == 0)
|
|
return tr("// Unhandled vertex attribute type '%1'").arg(ToQStr(attrs[i].format.type));
|
|
|
|
// for all but the first attribute, ensure no overlaps with previous. We allow identical
|
|
// elements
|
|
if(i > 0)
|
|
{
|
|
uint32_t prevsz = byteSize(attrs[i - 1].format);
|
|
|
|
if((attrs[i - 1].byteOffset != attrs[i].byteOffset || cursz != prevsz) &&
|
|
attrs[i - 1].byteOffset + prevsz > attrs[i].byteOffset)
|
|
return tr("// vertex attributes overlapping, no automatic format available");
|
|
}
|
|
|
|
if(i + 1 == attrs.size())
|
|
{
|
|
// for the last attribute, ensure the total size doesn't overlap stride
|
|
if(attrs[i].byteOffset + cursz > stride && stride > 0)
|
|
return tr("// vertex stride %1 less than total data fetched %2")
|
|
.arg(stride)
|
|
.arg(attrs[i].byteOffset + cursz);
|
|
}
|
|
}
|
|
|
|
QString format;
|
|
|
|
uint32_t offset = 0;
|
|
|
|
format = lit("struct vbuffer {\n");
|
|
|
|
for(size_t i = 0; i < attrs.size(); i++)
|
|
{
|
|
// we disallowed overlaps above, but we do allow *duplicates*. So if our offset has already
|
|
// passed, silently skip this element.
|
|
if(attrs[i].byteOffset < offset)
|
|
continue;
|
|
|
|
// declare an explicit offset if there's a gap from previous element to this one
|
|
if(attrs[i].byteOffset > offset)
|
|
format += lit(" [[offset(%1)]]\n").arg(attrs[i].byteOffset);
|
|
|
|
format += lit(" ");
|
|
|
|
const ResourceFormat &fmt = attrs[i].format;
|
|
|
|
offset = attrs[i].byteOffset + byteSize(fmt);
|
|
|
|
if(fmt.Special())
|
|
{
|
|
switch(fmt.type)
|
|
{
|
|
case ResourceFormatType::R10G10B10A2:
|
|
if(fmt.compType == CompType::UNorm)
|
|
format += lit("[[packed(r10g10b10a2)]] [[unorm]] uint4");
|
|
else
|
|
format += lit("[[packed(r10g10b10a2)]] uint4");
|
|
break;
|
|
case ResourceFormatType::R11G11B10: format += lit("[[packed(r11g11b10)]] float3"); break;
|
|
default: format += tr("// unknown type "); break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
QChar widthchar[] = {
|
|
QLatin1Char('?'), QLatin1Char('b'), QLatin1Char('h'), QLatin1Char('?'), QLatin1Char('f'),
|
|
};
|
|
|
|
if(fmt.compType == CompType::UNorm || fmt.compType == CompType::UNormSRGB)
|
|
{
|
|
format += lit("[[unorm]]");
|
|
}
|
|
else if(fmt.compType == CompType::SNorm)
|
|
{
|
|
format += lit("[[snorm]]");
|
|
}
|
|
|
|
if(fmt.compType == CompType::UInt || fmt.compType == CompType::UNorm ||
|
|
fmt.compType == CompType::UNormSRGB)
|
|
format += lit("u");
|
|
|
|
if(fmt.compByteWidth == 1)
|
|
{
|
|
format += lit("byte");
|
|
}
|
|
else if(fmt.compByteWidth == 2)
|
|
{
|
|
if(fmt.compType == CompType::Float)
|
|
format += lit("half");
|
|
else
|
|
format += lit("short");
|
|
}
|
|
else if(fmt.compByteWidth == 4)
|
|
{
|
|
if(fmt.compType == CompType::Float)
|
|
format += lit("float");
|
|
else
|
|
format += lit("int");
|
|
}
|
|
else if(fmt.compByteWidth == 8)
|
|
{
|
|
if(fmt.compType == CompType::Float)
|
|
format += lit("double");
|
|
else
|
|
format += lit("long");
|
|
}
|
|
|
|
format += QString::number(fmt.compCount);
|
|
}
|
|
|
|
QString real_name = QString(attrs[i].name);
|
|
QString sanitised_name = real_name;
|
|
|
|
sanitised_name.replace(QLatin1Char('.'), QLatin1Char('_'))
|
|
.replace(QLatin1Char(':'), QLatin1Char('_'))
|
|
.replace(QLatin1Char('['), QLatin1Char('_'))
|
|
.replace(QLatin1Char(']'), QLatin1Char('_'));
|
|
|
|
if(real_name == sanitised_name)
|
|
format += QFormatStr(" %1;\n").arg(sanitised_name);
|
|
else
|
|
format += QFormatStr(" %1; // %2\n").arg(sanitised_name).arg(real_name);
|
|
}
|
|
|
|
format += lit("}\n\nvbuffer vertex[];");
|
|
|
|
if(stride > offset)
|
|
format = lit("[[size(%1)]]\n").arg(stride) + format;
|
|
|
|
format = lit("#pack(scalar) // vertex buffers can be tightly packed\n"
|
|
"\n") +
|
|
format;
|
|
|
|
return format;
|
|
}
|
|
|
|
QColor PipelineStateViewer::GetViewDetailsColor()
|
|
{
|
|
return QColor::fromHslF(0.45f, 1.0f,
|
|
qBound(0.25, palette().color(QPalette::Base).lightnessF(), 0.75));
|
|
}
|
|
|
|
bool PipelineStateViewer::SaveShaderFile(const ShaderReflection *shader)
|
|
{
|
|
if(!shader)
|
|
return false;
|
|
|
|
QString filter;
|
|
|
|
switch(shader->encoding)
|
|
{
|
|
case ShaderEncoding::DXBC: filter = tr("DXBC Shader files (*.dxbc)"); break;
|
|
case ShaderEncoding::HLSL: filter = tr("HLSL files (*.hlsl)"); break;
|
|
case ShaderEncoding::GLSL: filter = tr("GLSL files (*.glsl)"); break;
|
|
case ShaderEncoding::SPIRV:
|
|
case ShaderEncoding::OpenGLSPIRV: filter = tr("SPIR-V files (*.spv)"); break;
|
|
case ShaderEncoding::SPIRVAsm:
|
|
case ShaderEncoding::OpenGLSPIRVAsm: filter = tr("SPIR-V assembly files (*.spvasm)"); break;
|
|
case ShaderEncoding::DXIL: filter = tr("DXIL Shader files (*.dxbc)"); break;
|
|
case ShaderEncoding::Unknown:
|
|
case ShaderEncoding::Count: filter = tr("All files (*.*)"); break;
|
|
}
|
|
|
|
QString filename = RDDialog::getSaveFileName(this, tr("Save Shader As"), QString(), filter);
|
|
|
|
if(!filename.isEmpty())
|
|
{
|
|
ANALYTIC_SET(Export.Shader, true);
|
|
|
|
QDir dirinfo = QFileInfo(filename).dir();
|
|
if(dirinfo.exists())
|
|
{
|
|
QFile f(filename);
|
|
if(f.open(QIODevice::WriteOnly | QIODevice::Truncate))
|
|
{
|
|
f.write((const char *)shader->rawBytes.data(), (qint64)shader->rawBytes.size());
|
|
}
|
|
else
|
|
{
|
|
RDDialog::critical(
|
|
this, tr("Error saving shader"),
|
|
tr("Couldn't open path %1 for write.\n%2").arg(filename).arg(f.errorString()));
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
RDDialog::critical(this, tr("Invalid directory"),
|
|
tr("Cannot find target directory to save to"));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void PipelineStateViewer::SelectPipelineStage(PipelineStage stage)
|
|
{
|
|
if(m_D3D11)
|
|
m_D3D11->SelectPipelineStage(stage);
|
|
else if(m_D3D12)
|
|
m_D3D12->SelectPipelineStage(stage);
|
|
else if(m_GL)
|
|
m_GL->SelectPipelineStage(stage);
|
|
else if(m_Vulkan)
|
|
m_Vulkan->SelectPipelineStage(stage);
|
|
}
|