mirror of
https://github.com/baldurk/renderdoc.git
synced 2026-05-05 01:20:42 +00:00
ebe0ab1b64
* Error logs are less likely to be noticed by users when things go wrong.
2580 lines
74 KiB
C++
2580 lines
74 KiB
C++
/******************************************************************************
|
|
* The MIT License (MIT)
|
|
*
|
|
* Copyright (c) 2019-2020 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 "QRDUtils.h"
|
|
#include <QAbstractTextDocumentLayout>
|
|
#include <QApplication>
|
|
#include <QCollator>
|
|
#include <QDesktopServices>
|
|
#include <QElapsedTimer>
|
|
#include <QFileSystemModel>
|
|
#include <QFontDatabase>
|
|
#include <QGridLayout>
|
|
#include <QGuiApplication>
|
|
#include <QJsonDocument>
|
|
#include <QKeyEvent>
|
|
#include <QLabel>
|
|
#include <QMenu>
|
|
#include <QMetaMethod>
|
|
#include <QMouseEvent>
|
|
#include <QPainter>
|
|
#include <QProcess>
|
|
#include <QProgressBar>
|
|
#include <QProgressDialog>
|
|
#include <QRegularExpression>
|
|
#include <QRegularExpressionMatch>
|
|
#include <QStandardPaths>
|
|
#include <QTextBlock>
|
|
#include <QTextDocument>
|
|
#include <QtMath>
|
|
#include "Code/Resources.h"
|
|
#include "Widgets/Extended/RDTreeWidget.h"
|
|
|
|
// normally this is in the renderdoc core library, but it's needed for the 'unknown enum' path,
|
|
// so we implement it here using QString. It's inefficient, but this is a very uncommon path -
|
|
// either for invalid values or for when a new enum is added and the code isn't updated
|
|
template <>
|
|
rdcstr DoStringise(const uint32_t &el)
|
|
{
|
|
return QString::number(el);
|
|
}
|
|
|
|
// these ones we do by hand as it requires formatting
|
|
template <>
|
|
rdcstr DoStringise(const ResourceId &el)
|
|
{
|
|
uint64_t num;
|
|
memcpy(&num, &el, sizeof(num));
|
|
return lit("ResourceId::%1").arg(num);
|
|
}
|
|
|
|
QMap<QPair<ResourceId, uint32_t>, uint32_t> PointerTypeRegistry::typeMapping;
|
|
rdcarray<ShaderVariableType> PointerTypeRegistry::typeDescriptions;
|
|
|
|
static const uint32_t TypeIDBit = 0x80000000;
|
|
|
|
void PointerTypeRegistry::Init()
|
|
{
|
|
typeMapping.clear();
|
|
|
|
// type ID 0 is reserved as a NULL/empty descriptor
|
|
typeDescriptions.resize(1);
|
|
typeDescriptions[0].descriptor.name = "<Unknown>";
|
|
}
|
|
|
|
uint32_t PointerTypeRegistry::GetTypeID(ResourceId shader, uint32_t pointerTypeId)
|
|
{
|
|
return typeMapping[qMakePair(shader, pointerTypeId)];
|
|
}
|
|
|
|
uint32_t PointerTypeRegistry::GetTypeID(const ShaderVariableType &structDef)
|
|
{
|
|
// see if the type is already registered, return its existing ID
|
|
for(uint32_t i = 1; i < typeDescriptions.size(); i++)
|
|
{
|
|
if(structDef == typeDescriptions[i])
|
|
return TypeIDBit | i;
|
|
}
|
|
|
|
uint32_t id = TypeIDBit | (uint32_t)typeDescriptions.size();
|
|
|
|
// otherwise register the new type
|
|
typeDescriptions.push_back(structDef);
|
|
typeMapping[qMakePair(ResourceId(), id)] = id;
|
|
|
|
return id;
|
|
}
|
|
|
|
const ShaderVariableType &PointerTypeRegistry::GetTypeDescriptor(uint32_t typeId)
|
|
{
|
|
return typeDescriptions[typeId & ~TypeIDBit];
|
|
}
|
|
|
|
void PointerTypeRegistry::CacheSubTypes(const ShaderReflection *reflection,
|
|
ShaderVariableType &structDef)
|
|
{
|
|
if((structDef.descriptor.pointerTypeID & TypeIDBit) == 0)
|
|
structDef.descriptor.pointerTypeID =
|
|
PointerTypeRegistry::GetTypeID(reflection->pointerTypes[structDef.descriptor.pointerTypeID]);
|
|
|
|
for(ShaderConstant &member : structDef.members)
|
|
CacheSubTypes(reflection, member.type);
|
|
}
|
|
|
|
void PointerTypeRegistry::CacheShader(const ShaderReflection *reflection)
|
|
{
|
|
// nothing to do if there are no pointer types
|
|
if(reflection->pointerTypes.isEmpty())
|
|
return;
|
|
|
|
// check if we've already cached this shader (we know there's at least one pointer type)
|
|
if(typeMapping.contains(qMakePair(reflection->resourceId, 0)))
|
|
return;
|
|
|
|
for(uint32_t i = 0; i < reflection->pointerTypes.size(); i++)
|
|
{
|
|
ShaderVariableType typeDesc = reflection->pointerTypes[i];
|
|
|
|
// first recursively cache all subtypes needed by the root struct types
|
|
CacheSubTypes(reflection, typeDesc);
|
|
|
|
// then look up the Type ID for this struct
|
|
typeMapping[qMakePair(reflection->resourceId, i)] = GetTypeID(typeDesc);
|
|
}
|
|
}
|
|
|
|
template <>
|
|
rdcstr DoStringise(const PointerVal &el)
|
|
{
|
|
if(el.pointerTypeID != ~0U)
|
|
{
|
|
uint32_t ptrTypeId = PointerTypeRegistry::GetTypeID(el.shader, el.pointerTypeID);
|
|
|
|
return QFormatStr("GPUAddress::%1::%2").arg(el.pointer).arg(ptrTypeId);
|
|
}
|
|
else
|
|
{
|
|
return QFormatStr("GPUAddress::%1").arg(el.pointer);
|
|
}
|
|
}
|
|
|
|
QString GetTruncatedResourceName(ICaptureContext &ctx, ResourceId id)
|
|
{
|
|
rdcstr name = ctx.GetResourceName(id);
|
|
if(name.length() > 64)
|
|
return name.substr(0, 64) + "...";
|
|
|
|
return name;
|
|
}
|
|
|
|
// this is an opaque struct that contains the data to render, hit-test, etc for some text that
|
|
// contains links to resources. It will update and cache the names of the resources.
|
|
struct RichResourceText
|
|
{
|
|
QVector<QVariant> fragments;
|
|
|
|
// cached formatted document. We use cacheId to check if it needs to be updated
|
|
QTextDocument doc;
|
|
int cacheId = 0;
|
|
|
|
// a plain-text version of the document, suitable for e.g. copy-paste
|
|
QString text;
|
|
|
|
// the ideal width for the document
|
|
int idealWidth = 0;
|
|
|
|
// cache the context once we've obtained it.
|
|
ICaptureContext *ctxptr = NULL;
|
|
|
|
void cacheDocument(const QWidget *widget)
|
|
{
|
|
if(!ctxptr)
|
|
ctxptr = getCaptureContext(widget);
|
|
|
|
if(!ctxptr)
|
|
return;
|
|
|
|
ICaptureContext &ctx = *(ICaptureContext *)ctxptr;
|
|
|
|
int refCache = ctx.ResourceNameCacheID();
|
|
|
|
if(cacheId == refCache)
|
|
return;
|
|
|
|
cacheId = refCache;
|
|
|
|
// use a table to ensure images don't screw up the baseline for text. DON'T JUDGE ME.
|
|
QString html = lit("<table><tr>");
|
|
|
|
int i = 0;
|
|
|
|
bool highdpi = widget->devicePixelRatioF() > 1.0;
|
|
|
|
QVector<int> fragmentIndexFromBlockIndex;
|
|
|
|
// there's an empty block at the start.
|
|
fragmentIndexFromBlockIndex.push_back(-1);
|
|
|
|
text.clear();
|
|
|
|
for(const QVariant &v : fragments)
|
|
{
|
|
if(v.userType() == qMetaTypeId<ResourceId>())
|
|
{
|
|
QString resname = GetTruncatedResourceName(ctx, v.value<ResourceId>()).toHtmlEscaped();
|
|
html += lit("<td valign=\"middle\"><b>%1</b></td>"
|
|
"<td><img width=\"16\" src=':/link%3.png'></td>")
|
|
.arg(resname)
|
|
.arg(highdpi ? lit("@2x") : QString());
|
|
text += resname;
|
|
|
|
// these generate two blocks (one for each cell)
|
|
fragmentIndexFromBlockIndex.push_back(i);
|
|
fragmentIndexFromBlockIndex.push_back(i);
|
|
}
|
|
else
|
|
{
|
|
html += lit("<td valign=\"middle\">%1</td>").arg(v.toString().toHtmlEscaped());
|
|
text += v.toString();
|
|
|
|
// this only generates one block
|
|
fragmentIndexFromBlockIndex.push_back(i);
|
|
}
|
|
|
|
i++;
|
|
}
|
|
|
|
// there's another empty block at the end
|
|
fragmentIndexFromBlockIndex.push_back(-1);
|
|
|
|
html += lit("</tr></table>");
|
|
|
|
doc.setDocumentMargin(0);
|
|
doc.setHtml(html);
|
|
doc.setDefaultFont(widget->font());
|
|
|
|
if(doc.blockCount() != fragmentIndexFromBlockIndex.count())
|
|
{
|
|
qCritical() << "Block count is not what's expected!" << doc.blockCount()
|
|
<< fragmentIndexFromBlockIndex.count();
|
|
|
|
for(i = 0; i < doc.blockCount(); i++)
|
|
doc.findBlockByNumber(i).setUserState(-1);
|
|
|
|
return;
|
|
}
|
|
|
|
for(i = 0; i < doc.blockCount(); i++)
|
|
doc.findBlockByNumber(i).setUserState(fragmentIndexFromBlockIndex[i]);
|
|
|
|
doc.setTextWidth(-1);
|
|
idealWidth = doc.idealWidth();
|
|
doc.setTextWidth(10000);
|
|
}
|
|
};
|
|
|
|
// we use QSharedPointer to refer to the text since the lifetime management of these objects would
|
|
// get quite complicated. There's not necessarily an obvious QObject parent to assign to if the text
|
|
// is being initialised before being assigned to a widget and we want the most seamless interface we
|
|
// can get.
|
|
typedef QSharedPointer<RichResourceText> RichResourceTextPtr;
|
|
|
|
Q_DECLARE_METATYPE(RichResourceTextPtr);
|
|
|
|
void GPUAddress::cacheAddress(const QWidget *widget)
|
|
{
|
|
if(!ctxptr)
|
|
ctxptr = getCaptureContext(widget);
|
|
|
|
// bail out if we don't have a context
|
|
if(!ctxptr)
|
|
return;
|
|
|
|
// bail if we're already cached
|
|
if(base != ResourceId())
|
|
return;
|
|
|
|
// find the first matching buffer
|
|
for(const BufferDescription &b : ctxptr->GetBuffers())
|
|
{
|
|
if(b.gpuAddress && b.gpuAddress <= val.pointer && b.gpuAddress + b.length > val.pointer)
|
|
{
|
|
base = b.resourceId;
|
|
offset = val.pointer - b.gpuAddress;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// for the same reason as above we use a shared pointer for GPU addresses too. This ensures the
|
|
// cached data doesn't keep getting re-cached in copies.
|
|
typedef QSharedPointer<GPUAddress> GPUAddressPtr;
|
|
|
|
Q_DECLARE_METATYPE(GPUAddressPtr);
|
|
|
|
QString ResIdTextToString(RichResourceTextPtr ptr)
|
|
{
|
|
return ptr->text;
|
|
}
|
|
|
|
QString ResIdToString(ResourceId ptr)
|
|
{
|
|
return ToQStr(ptr);
|
|
}
|
|
|
|
QString GPUAddressToString(GPUAddressPtr addr)
|
|
{
|
|
if(addr->base != ResourceId())
|
|
return QFormatStr("%1+%2").arg(ToQStr(addr->base)).arg(addr->offset);
|
|
else
|
|
return QFormatStr("0x%1").arg(addr->val.pointer, 0, 16);
|
|
}
|
|
|
|
void RegisterMetatypeConversions()
|
|
{
|
|
QMetaType::registerConverter<RichResourceTextPtr, QString>(&ResIdTextToString);
|
|
QMetaType::registerConverter<ResourceId, QString>(&ResIdToString);
|
|
QMetaType::registerConverter<GPUAddressPtr, QString>(&GPUAddressToString);
|
|
}
|
|
|
|
void RichResourceTextInitialise(QVariant &var)
|
|
{
|
|
// we only upconvert from strings, any other type with a string representation is not expected to
|
|
// contain ResourceIds. In particular if the variant is already a ResourceId we can return.
|
|
if(GetVariantMetatype(var) != QMetaType::QString)
|
|
return;
|
|
|
|
// we trim the string because that will happen naturally when rendering as HTML, and it makes it
|
|
// easier to detect strings where the only contents are ResourceId text.
|
|
QString text = var.toString().trimmed();
|
|
|
|
// do a simple string search first before using regular expressions
|
|
if(!text.contains(lit("ResourceId::")) && !text.contains(lit("GPUAddress::")))
|
|
return;
|
|
|
|
// two forms: GPUAddress::012345 - typeless
|
|
// GPUAddress::012345::991 - using type 991 from PointerTypeRegistry
|
|
static QRegularExpression addrRE(lit("GPUAddress::([0-9]*)(::([0-9]*))?"));
|
|
|
|
QRegularExpressionMatch match = addrRE.match(text);
|
|
|
|
if(match.hasMatch())
|
|
{
|
|
// don't support mixed text & addresses. Only do the replacement if we matched the whole string
|
|
if(match.capturedStart(0) == 0 && match.capturedLength(0) == text.length())
|
|
{
|
|
GPUAddressPtr addr(new GPUAddress);
|
|
addr->val.pointer = match.captured(1).toULongLong();
|
|
|
|
// we deliberately set this to ResourceId() to indicate that we're using an ID from the
|
|
// registry, not a shader-relative index
|
|
addr->val.shader = ResourceId();
|
|
addr->val.pointerTypeID = match.captured(3).toULong();
|
|
|
|
var = QVariant::fromValue(addr);
|
|
return;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// use regexp to split up into fragments of text and resourceid. The resourceid is then
|
|
// formatted on the fly in RichResourceText::cacheDocument
|
|
static QRegularExpression resRE(lit("(ResourceId::)([0-9]*)"));
|
|
|
|
match = resRE.match(text);
|
|
|
|
if(match.hasMatch())
|
|
{
|
|
// if the match is the whole string, this is just a plain ResourceId on its own, so make that
|
|
// the variant without being rich resource text, so we can process it faster.
|
|
if(match.capturedStart(0) == 0 && match.capturedLength(0) == text.length())
|
|
{
|
|
qulonglong idnum = match.captured(2).toULongLong();
|
|
ResourceId id;
|
|
memcpy(&id, &idnum, sizeof(id));
|
|
|
|
var = id;
|
|
return;
|
|
}
|
|
|
|
RichResourceTextPtr linkedText(new RichResourceText);
|
|
|
|
while(match.hasMatch())
|
|
{
|
|
qulonglong idnum = match.captured(2).toULongLong();
|
|
ResourceId id;
|
|
memcpy(&id, &idnum, sizeof(id));
|
|
|
|
// push any text that preceeded the ResourceId.
|
|
if(match.capturedStart(1) > 0)
|
|
linkedText->fragments.push_back(text.left(match.capturedStart(1)));
|
|
|
|
text.remove(0, match.capturedEnd(2));
|
|
|
|
linkedText->fragments.push_back(id);
|
|
|
|
match = resRE.match(text);
|
|
}
|
|
|
|
if(!text.isEmpty())
|
|
linkedText->fragments.push_back(text);
|
|
|
|
linkedText->doc.setHtml(text);
|
|
|
|
var = QVariant::fromValue(linkedText);
|
|
}
|
|
}
|
|
|
|
bool RichResourceTextCheck(const QVariant &var)
|
|
{
|
|
return var.userType() == qMetaTypeId<RichResourceTextPtr>() ||
|
|
var.userType() == qMetaTypeId<GPUAddressPtr>() ||
|
|
var.userType() == qMetaTypeId<ResourceId>();
|
|
}
|
|
|
|
// I'm not sure if this should come from the style or not - the QTextDocument handles this in
|
|
// the rich text case.
|
|
static const int RichResourceTextMargin = 2;
|
|
|
|
void RichResourceTextPaint(const QWidget *owner, QPainter *painter, QRect rect, QFont font,
|
|
QPalette palette, bool mouseOver, QPoint mousePos, const QVariant &var)
|
|
{
|
|
// special case handling for ResourceId/GPUAddress on its own
|
|
if(var.userType() == qMetaTypeId<ResourceId>() || var.userType() == qMetaTypeId<GPUAddressPtr>())
|
|
{
|
|
painter->save();
|
|
|
|
QFont f = painter->font();
|
|
f.setBold(true);
|
|
painter->setFont(f);
|
|
|
|
static const int margin = RichResourceTextMargin;
|
|
|
|
rect.adjust(margin, 0, -margin * 2, 0);
|
|
|
|
QString name;
|
|
|
|
bool valid = false;
|
|
|
|
if(var.userType() == qMetaTypeId<ResourceId>())
|
|
{
|
|
ICaptureContext *ctxptr = getCaptureContext(owner);
|
|
|
|
ResourceId id = var.value<ResourceId>();
|
|
|
|
valid = (id != ResourceId());
|
|
|
|
if(ctxptr)
|
|
name = GetTruncatedResourceName(*ctxptr, id);
|
|
else
|
|
name = ToQStr(id);
|
|
}
|
|
else
|
|
{
|
|
GPUAddressPtr ptr = var.value<GPUAddressPtr>();
|
|
|
|
ptr->cacheAddress(owner);
|
|
|
|
valid = (ptr->val.pointer != 0);
|
|
|
|
if(valid)
|
|
{
|
|
if(ptr->base != ResourceId())
|
|
{
|
|
name =
|
|
QFormatStr("%1+%2").arg(GetTruncatedResourceName(*ptr->ctxptr, ptr->base)).arg(ptr->offset);
|
|
}
|
|
else
|
|
{
|
|
name = QFormatStr("Unknown 0x%1").arg(ptr->val.pointer, 16, 16, QLatin1Char('0'));
|
|
valid = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
name = lit("NULL");
|
|
}
|
|
}
|
|
|
|
painter->drawText(rect, Qt::AlignLeft | Qt::AlignVCenter, name);
|
|
|
|
QRect textRect =
|
|
painter->fontMetrics().boundingRect(rect, Qt::AlignLeft | Qt::AlignVCenter, name);
|
|
|
|
const QPixmap &px = Pixmaps::link(owner->devicePixelRatio());
|
|
|
|
painter->setClipRect(rect);
|
|
|
|
textRect.setLeft(rect.left());
|
|
textRect.setWidth(textRect.width() + margin + px.width());
|
|
textRect.setHeight(qMax(textRect.height(), px.height()));
|
|
|
|
QPoint pos;
|
|
pos.setX(textRect.right() - px.width() + 1);
|
|
pos.setY(textRect.center().y() - px.height() / 2);
|
|
|
|
painter->drawPixmap(pos, px, px.rect());
|
|
|
|
if(mouseOver && textRect.contains(mousePos) && valid)
|
|
{
|
|
int underline_y = textRect.bottom() - margin;
|
|
|
|
painter->setPen(QPen(palette.brush(QPalette::WindowText), 1.0));
|
|
painter->drawLine(QPoint(textRect.left(), underline_y), QPoint(textRect.right(), underline_y));
|
|
}
|
|
|
|
painter->restore();
|
|
|
|
return;
|
|
}
|
|
|
|
RichResourceTextPtr linkedText = var.value<RichResourceTextPtr>();
|
|
|
|
linkedText->cacheDocument(owner);
|
|
|
|
painter->translate(rect.left(), rect.top());
|
|
|
|
if(font != linkedText->doc.defaultFont())
|
|
linkedText->doc.setDefaultFont(font);
|
|
|
|
// vertical align to the centre, if there's spare room.
|
|
int diff = rect.height() - linkedText->doc.size().height();
|
|
|
|
if(diff > 0)
|
|
painter->translate(1, diff / 2);
|
|
else
|
|
painter->translate(1, 0);
|
|
|
|
linkedText->doc.drawContents(painter, QRectF(0, 0, rect.width() - 1, rect.height()));
|
|
|
|
if(mouseOver)
|
|
{
|
|
painter->setPen(QPen(palette.brush(QPalette::WindowText), 1.0));
|
|
|
|
QAbstractTextDocumentLayout *layout = linkedText->doc.documentLayout();
|
|
|
|
QPoint p = mousePos - rect.topLeft();
|
|
if(diff > 0)
|
|
p -= QPoint(1, diff / 2);
|
|
else
|
|
p -= QPoint(1, 0);
|
|
|
|
int pos = layout->hitTest(p, Qt::FuzzyHit);
|
|
|
|
if(pos >= 0)
|
|
{
|
|
QTextBlock block = linkedText->doc.findBlock(pos);
|
|
|
|
int frag = block.userState();
|
|
if(frag >= 0)
|
|
{
|
|
QVariant v = linkedText->fragments[frag];
|
|
if(v.userType() == qMetaTypeId<ResourceId>() && v.value<ResourceId>() != ResourceId())
|
|
{
|
|
layout->blockBoundingRect(block);
|
|
QRectF blockrect = layout->blockBoundingRect(block);
|
|
|
|
if(block.previous().userState() == frag)
|
|
{
|
|
blockrect = blockrect.united(layout->blockBoundingRect(block.previous()));
|
|
}
|
|
|
|
if(block.next().userState() == frag)
|
|
{
|
|
blockrect = blockrect.united(layout->blockBoundingRect(block.next()));
|
|
}
|
|
|
|
blockrect.translate(0.0, -2.0);
|
|
|
|
blockrect.setRight(qMin(blockrect.right(), (qreal)rect.width()));
|
|
|
|
painter->drawLine(blockrect.bottomLeft(), blockrect.bottomRight());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int RichResourceTextWidthHint(const QWidget *owner, const QFont &font, const QVariant &var)
|
|
{
|
|
// special case handling for ResourceId/GPUAddress on its own
|
|
if(var.userType() == qMetaTypeId<ResourceId>() || var.userType() == qMetaTypeId<GPUAddressPtr>())
|
|
{
|
|
QFont f = font;
|
|
f.setBold(true);
|
|
|
|
static const int margin = RichResourceTextMargin;
|
|
|
|
QFontMetrics metrics(f);
|
|
|
|
QString name;
|
|
|
|
if(var.userType() == qMetaTypeId<ResourceId>())
|
|
{
|
|
ICaptureContext *ctxptr = getCaptureContext(owner);
|
|
|
|
ResourceId id = var.value<ResourceId>();
|
|
|
|
if(ctxptr)
|
|
name = GetTruncatedResourceName(*ctxptr, id);
|
|
else
|
|
name = ToQStr(id);
|
|
}
|
|
else
|
|
{
|
|
GPUAddressPtr ptr = var.value<GPUAddressPtr>();
|
|
|
|
ptr->cacheAddress(owner);
|
|
|
|
if(ptr->val.pointer != 0)
|
|
name =
|
|
QFormatStr("%1+%2").arg(GetTruncatedResourceName(*ptr->ctxptr, ptr->base)).arg(ptr->offset);
|
|
else
|
|
name = lit("NULL");
|
|
}
|
|
|
|
const QPixmap &px = Pixmaps::link(owner->devicePixelRatio());
|
|
|
|
int ret = margin + metrics.boundingRect(name).width() + margin + px.width() + margin;
|
|
return ret;
|
|
}
|
|
|
|
RichResourceTextPtr linkedText = var.value<RichResourceTextPtr>();
|
|
|
|
linkedText->cacheDocument(owner);
|
|
|
|
return linkedText->idealWidth;
|
|
}
|
|
|
|
bool RichResourceTextMouseEvent(const QWidget *owner, const QVariant &var, QRect rect,
|
|
const QFont &font, QMouseEvent *event)
|
|
{
|
|
// only process clicks or moves
|
|
if(event->type() != QEvent::MouseButtonRelease && event->type() != QEvent::MouseMove)
|
|
return false;
|
|
|
|
// only process left button clicks
|
|
if(event->type() == QEvent::MouseButtonRelease && event->button() != Qt::LeftButton)
|
|
return false;
|
|
|
|
// special case handling for ResourceId/GPUAddress on its own
|
|
if(var.userType() == qMetaTypeId<ResourceId>() || var.userType() == qMetaTypeId<GPUAddressPtr>())
|
|
{
|
|
ResourceId id;
|
|
GPUAddressPtr ptr;
|
|
ICaptureContext *ctxptr = NULL;
|
|
|
|
if(var.userType() == qMetaTypeId<ResourceId>())
|
|
{
|
|
id = var.value<ResourceId>();
|
|
|
|
// empty resource ids are not clickable or hover-highlighted.
|
|
if(id == ResourceId())
|
|
return false;
|
|
}
|
|
|
|
if(var.userType() == qMetaTypeId<GPUAddressPtr>())
|
|
{
|
|
ptr = var.value<GPUAddressPtr>();
|
|
|
|
ptr->cacheAddress(owner);
|
|
|
|
// NULL or unknown addresses also are not clickable
|
|
if(ptr->val.pointer == 0 || ptr->base == ResourceId())
|
|
return false;
|
|
}
|
|
|
|
QFont f = font;
|
|
f.setBold(true);
|
|
|
|
static const int margin = RichResourceTextMargin;
|
|
|
|
rect.adjust(margin, 0, -margin * 2, 0);
|
|
|
|
QString name;
|
|
|
|
if(var.userType() == qMetaTypeId<ResourceId>())
|
|
{
|
|
ctxptr = getCaptureContext(owner);
|
|
|
|
if(ctxptr)
|
|
name = GetTruncatedResourceName(*ctxptr, id);
|
|
else
|
|
name = ToQStr(id);
|
|
}
|
|
else
|
|
{
|
|
ctxptr = ptr->ctxptr;
|
|
|
|
name =
|
|
QFormatStr("%1+%2").arg(GetTruncatedResourceName(*ptr->ctxptr, ptr->base)).arg(ptr->offset);
|
|
}
|
|
|
|
QRect textRect = QFontMetrics(f).boundingRect(rect, Qt::AlignLeft | Qt::AlignVCenter, name);
|
|
|
|
const QPixmap &px = Pixmaps::link(owner->devicePixelRatio());
|
|
|
|
rect.setTop(textRect.top());
|
|
rect.setWidth(textRect.width() + margin + px.width());
|
|
rect.setHeight(qMax(textRect.height(), px.height()));
|
|
|
|
if(rect.contains(event->pos()))
|
|
{
|
|
if(var.userType() == qMetaTypeId<ResourceId>())
|
|
{
|
|
if(event->type() == QEvent::MouseButtonRelease && ctxptr)
|
|
{
|
|
ICaptureContext &ctx = *(ICaptureContext *)ctxptr;
|
|
|
|
if(!ctx.HasResourceInspector())
|
|
ctx.ShowResourceInspector();
|
|
|
|
ctx.GetResourceInspector()->Inspect(id);
|
|
|
|
ctx.RaiseDockWindow(ctx.GetResourceInspector()->Widget());
|
|
}
|
|
|
|
return true;
|
|
}
|
|
else if(var.userType() == qMetaTypeId<GPUAddressPtr>())
|
|
{
|
|
if(event->type() == QEvent::MouseButtonRelease && ctxptr)
|
|
{
|
|
ICaptureContext &ctx = *(ICaptureContext *)ctxptr;
|
|
|
|
const ShaderVariableType &ptrType = PointerTypeRegistry::GetTypeDescriptor(ptr->val);
|
|
|
|
QString formatter;
|
|
|
|
if(!ptrType.members.isEmpty())
|
|
formatter = BufferFormatter::DeclareStruct(ptrType.descriptor.name, ptrType.members,
|
|
ptrType.descriptor.arrayByteStride);
|
|
|
|
IBufferViewer *view = ctx.ViewBuffer(ptr->offset, ~0ULL, ptr->base, formatter);
|
|
|
|
ctx.AddDockWindow(view->Widget(), DockReference::MainToolArea, NULL);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
RichResourceTextPtr linkedText = var.value<RichResourceTextPtr>();
|
|
|
|
linkedText->cacheDocument(owner);
|
|
|
|
QAbstractTextDocumentLayout *layout = linkedText->doc.documentLayout();
|
|
|
|
// vertical align to the centre, if there's spare room.
|
|
int diff = rect.height() - linkedText->doc.size().height();
|
|
|
|
QPoint p = event->pos() - rect.topLeft();
|
|
if(diff > 0)
|
|
p -= QPoint(1, diff / 2);
|
|
else
|
|
p -= QPoint(1, 0);
|
|
|
|
int pos = layout->hitTest(p, Qt::FuzzyHit);
|
|
|
|
if(pos >= 0)
|
|
{
|
|
QTextBlock block = linkedText->doc.findBlock(pos);
|
|
|
|
int frag = block.userState();
|
|
if(frag >= 0)
|
|
{
|
|
QVariant v = linkedText->fragments[frag];
|
|
if(v.userType() == qMetaTypeId<ResourceId>())
|
|
{
|
|
// empty resource ids are not clickable or hover-highlighted.
|
|
ResourceId res = v.value<ResourceId>();
|
|
if(res == ResourceId())
|
|
return false;
|
|
|
|
if(event->type() == QEvent::MouseButtonRelease && linkedText->ctxptr)
|
|
{
|
|
ICaptureContext &ctx = *(ICaptureContext *)linkedText->ctxptr;
|
|
|
|
if(!ctx.HasResourceInspector())
|
|
ctx.ShowResourceInspector();
|
|
|
|
ctx.GetResourceInspector()->Inspect(res);
|
|
|
|
ctx.RaiseDockWindow(ctx.GetResourceInspector()->Widget());
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
QString RichResourceTextFormat(ICaptureContext &ctx, QVariant var)
|
|
{
|
|
RichResourceTextInitialise(var);
|
|
if(var.userType() == qMetaTypeId<ResourceId>())
|
|
return GetTruncatedResourceName(ctx, var.value<ResourceId>());
|
|
|
|
// either it's something else and wasn't rich resource, in which case just return the string
|
|
// representation, or it's a fully formatted rich resource document, where the cached text will do
|
|
// the trick with ResIdTextToString.
|
|
return var.toString();
|
|
}
|
|
|
|
RichTextViewDelegate::RichTextViewDelegate(QAbstractItemView *parent)
|
|
: m_widget(parent), ForwardingDelegate(parent)
|
|
{
|
|
}
|
|
|
|
RichTextViewDelegate::~RichTextViewDelegate()
|
|
{
|
|
}
|
|
|
|
void RichTextViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
|
|
const QModelIndex &index) const
|
|
{
|
|
if(index.isValid())
|
|
{
|
|
QVariant v = index.data();
|
|
|
|
if(RichResourceTextCheck(v))
|
|
{
|
|
// draw the item without text, so we get the proper background/selection/etc.
|
|
// we'd like to be able to use the parent delegate's paint here, but either it calls to
|
|
// QStyledItemDelegate which will re-fetch the text (bleh), or it calls to the manual
|
|
// delegate which could do anything. So for this case we just use the style and skip the
|
|
// delegate and hope it works out.
|
|
QStyleOptionViewItem opt = option;
|
|
QStyledItemDelegate::initStyleOption(&opt, index);
|
|
opt.text.clear();
|
|
m_widget->style()->drawControl(QStyle::CE_ItemViewItem, &opt, painter, m_widget);
|
|
|
|
painter->save();
|
|
|
|
QRect rect = option.rect;
|
|
if(!opt.icon.isNull())
|
|
{
|
|
QIcon::Mode mode;
|
|
if((opt.state & QStyle::State_Enabled) == 0)
|
|
mode = QIcon::Disabled;
|
|
else if(opt.state & QStyle::State_Selected)
|
|
mode = QIcon::Selected;
|
|
else
|
|
mode = QIcon::Normal;
|
|
QIcon::State state = opt.state & QStyle::State_Open ? QIcon::On : QIcon::Off;
|
|
rect.setX(rect.x() + opt.icon.actualSize(opt.decorationSize, mode, state).width() + 4);
|
|
}
|
|
|
|
RichResourceTextPaint(m_widget, painter, rect, opt.font, option.palette,
|
|
option.state & QStyle::State_MouseOver,
|
|
m_widget->viewport()->mapFromGlobal(QCursor::pos()), v);
|
|
|
|
painter->restore();
|
|
return;
|
|
}
|
|
}
|
|
|
|
return ForwardingDelegate::paint(painter, option, index);
|
|
}
|
|
|
|
QSize RichTextViewDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
|
|
{
|
|
if(index.isValid())
|
|
{
|
|
QVariant v = index.data();
|
|
|
|
if(RichResourceTextCheck(v))
|
|
return QSize(RichResourceTextWidthHint(m_widget, option.font, v), option.fontMetrics.height());
|
|
}
|
|
|
|
return ForwardingDelegate::sizeHint(option, index);
|
|
}
|
|
|
|
bool RichTextViewDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
|
|
const QStyleOptionViewItem &option, const QModelIndex &index)
|
|
{
|
|
if(event->type() == QEvent::MouseButtonRelease && index.isValid())
|
|
{
|
|
QVariant v = index.data();
|
|
|
|
if(RichResourceTextCheck(v))
|
|
{
|
|
QRect rect = option.rect;
|
|
|
|
QIcon icon = index.data(Qt::DecorationRole).value<QIcon>();
|
|
|
|
if(!icon.isNull())
|
|
{
|
|
rect.setX(rect.x() +
|
|
icon.actualSize(option.decorationSize, QIcon::Normal, QIcon::On).width() + 4);
|
|
}
|
|
|
|
// ignore the return value, we always consume clicks on this cell
|
|
RichResourceTextMouseEvent(m_widget, v, rect, option.font, (QMouseEvent *)event);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return ForwardingDelegate::editorEvent(event, model, option, index);
|
|
}
|
|
|
|
bool RichTextViewDelegate::linkHover(QMouseEvent *e, const QFont &font, const QModelIndex &index)
|
|
{
|
|
if(index.isValid())
|
|
{
|
|
QVariant v = index.data();
|
|
|
|
if(RichResourceTextCheck(v))
|
|
{
|
|
QRect rect = m_widget->visualRect(index);
|
|
|
|
QIcon icon = index.data(Qt::DecorationRole).value<QIcon>();
|
|
|
|
if(!icon.isNull())
|
|
{
|
|
rect.setX(
|
|
rect.x() +
|
|
icon.actualSize(QSize(rect.height(), rect.height()), QIcon::Normal, QIcon::On).width() +
|
|
4);
|
|
}
|
|
|
|
return RichResourceTextMouseEvent(m_widget, v, rect, font, e);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
#include "renderdoc_tostr.inl"
|
|
|
|
QString ToQStr(const ResourceUsage usage, const GraphicsAPI apitype)
|
|
{
|
|
if(IsD3D(apitype))
|
|
{
|
|
switch(usage)
|
|
{
|
|
case ResourceUsage::Unused: return lit("Unused");
|
|
|
|
case ResourceUsage::VertexBuffer: return lit("Vertex Buffer");
|
|
case ResourceUsage::IndexBuffer: return lit("Index Buffer");
|
|
|
|
case ResourceUsage::VS_Constants: return lit("VS - Constant Buffer");
|
|
case ResourceUsage::GS_Constants: return lit("GS - Constant Buffer");
|
|
case ResourceUsage::HS_Constants: return lit("HS - Constant Buffer");
|
|
case ResourceUsage::DS_Constants: return lit("DS - Constant Buffer");
|
|
case ResourceUsage::CS_Constants: return lit("CS - Constant Buffer");
|
|
case ResourceUsage::PS_Constants: return lit("PS - Constant Buffer");
|
|
case ResourceUsage::All_Constants: return lit("All - Constant Buffer");
|
|
|
|
case ResourceUsage::StreamOut: return lit("Stream Out");
|
|
|
|
case ResourceUsage::VS_Resource: return lit("VS - Resource");
|
|
case ResourceUsage::GS_Resource: return lit("GS - Resource");
|
|
case ResourceUsage::HS_Resource: return lit("HS - Resource");
|
|
case ResourceUsage::DS_Resource: return lit("DS - Resource");
|
|
case ResourceUsage::CS_Resource: return lit("CS - Resource");
|
|
case ResourceUsage::PS_Resource: return lit("PS - Resource");
|
|
case ResourceUsage::All_Resource: return lit("All - Resource");
|
|
|
|
case ResourceUsage::VS_RWResource: return lit("VS - UAV");
|
|
case ResourceUsage::HS_RWResource: return lit("HS - UAV");
|
|
case ResourceUsage::DS_RWResource: return lit("DS - UAV");
|
|
case ResourceUsage::GS_RWResource: return lit("GS - UAV");
|
|
case ResourceUsage::PS_RWResource: return lit("PS - UAV");
|
|
case ResourceUsage::CS_RWResource: return lit("CS - UAV");
|
|
case ResourceUsage::All_RWResource: return lit("All - UAV");
|
|
|
|
case ResourceUsage::InputTarget: return lit("Color Input");
|
|
case ResourceUsage::ColorTarget: return lit("Rendertarget");
|
|
case ResourceUsage::DepthStencilTarget: return lit("Depthstencil");
|
|
|
|
case ResourceUsage::Indirect: return lit("Indirect argument");
|
|
|
|
case ResourceUsage::Clear: return lit("Clear");
|
|
|
|
case ResourceUsage::GenMips: return lit("Generate Mips");
|
|
case ResourceUsage::Resolve: return lit("Resolve");
|
|
case ResourceUsage::ResolveSrc: return lit("Resolve - Source");
|
|
case ResourceUsage::ResolveDst: return lit("Resolve - Dest");
|
|
case ResourceUsage::Copy: return lit("Copy");
|
|
case ResourceUsage::CopySrc: return lit("Copy - Source");
|
|
case ResourceUsage::CopyDst: return lit("Copy - Dest");
|
|
|
|
case ResourceUsage::Barrier: return lit("Barrier");
|
|
|
|
case ResourceUsage::CPUWrite: return lit("CPU Write");
|
|
}
|
|
}
|
|
else if(apitype == GraphicsAPI::OpenGL || apitype == GraphicsAPI::Vulkan)
|
|
{
|
|
const bool vk = (apitype == GraphicsAPI::Vulkan);
|
|
|
|
switch(usage)
|
|
{
|
|
case ResourceUsage::Unused: return lit("Unused");
|
|
|
|
case ResourceUsage::VertexBuffer: return lit("Vertex Buffer");
|
|
case ResourceUsage::IndexBuffer: return lit("Index Buffer");
|
|
|
|
case ResourceUsage::VS_Constants: return lit("VS - Uniform Buffer");
|
|
case ResourceUsage::GS_Constants: return lit("GS - Uniform Buffer");
|
|
case ResourceUsage::HS_Constants: return lit("HS - Uniform Buffer");
|
|
case ResourceUsage::DS_Constants: return lit("DS - Uniform Buffer");
|
|
case ResourceUsage::CS_Constants: return lit("CS - Uniform Buffer");
|
|
case ResourceUsage::PS_Constants: return lit("PS - Uniform Buffer");
|
|
case ResourceUsage::All_Constants: return lit("All - Uniform Buffer");
|
|
|
|
case ResourceUsage::StreamOut: return lit("Transform Feedback");
|
|
|
|
case ResourceUsage::VS_Resource: return lit("VS - Texture");
|
|
case ResourceUsage::GS_Resource: return lit("GS - Texture");
|
|
case ResourceUsage::HS_Resource: return lit("HS - Texture");
|
|
case ResourceUsage::DS_Resource: return lit("DS - Texture");
|
|
case ResourceUsage::CS_Resource: return lit("CS - Texture");
|
|
case ResourceUsage::PS_Resource: return lit("PS - Texture");
|
|
case ResourceUsage::All_Resource: return lit("All - Texture");
|
|
|
|
case ResourceUsage::VS_RWResource: return lit("VS - Image/SSBO");
|
|
case ResourceUsage::HS_RWResource: return lit("HS - Image/SSBO");
|
|
case ResourceUsage::DS_RWResource: return lit("DS - Image/SSBO");
|
|
case ResourceUsage::GS_RWResource: return lit("GS - Image/SSBO");
|
|
case ResourceUsage::PS_RWResource: return lit("PS - Image/SSBO");
|
|
case ResourceUsage::CS_RWResource: return lit("CS - Image/SSBO");
|
|
case ResourceUsage::All_RWResource: return lit("All - Image/SSBO");
|
|
|
|
case ResourceUsage::InputTarget: return lit("FBO Input");
|
|
case ResourceUsage::ColorTarget: return lit("FBO Color");
|
|
case ResourceUsage::DepthStencilTarget: return lit("FBO Depthstencil");
|
|
|
|
case ResourceUsage::Indirect: return lit("Indirect argument");
|
|
|
|
case ResourceUsage::Clear: return lit("Clear");
|
|
|
|
case ResourceUsage::GenMips: return lit("Generate Mips");
|
|
case ResourceUsage::Resolve: return vk ? lit("Resolve") : lit("Framebuffer blit");
|
|
case ResourceUsage::ResolveSrc:
|
|
return vk ? lit("Resolve - Source") : lit("Framebuffer blit - Source");
|
|
case ResourceUsage::ResolveDst:
|
|
return vk ? lit("Resolve - Dest") : lit("Framebuffer blit - Dest");
|
|
case ResourceUsage::Copy: return lit("Copy");
|
|
case ResourceUsage::CopySrc: return lit("Copy - Source");
|
|
case ResourceUsage::CopyDst: return lit("Copy - Dest");
|
|
|
|
case ResourceUsage::Barrier: return lit("Barrier");
|
|
|
|
case ResourceUsage::CPUWrite: return lit("CPU Write");
|
|
}
|
|
}
|
|
|
|
return lit("Unknown");
|
|
}
|
|
|
|
QString ToQStr(const ShaderStage stage, const GraphicsAPI apitype)
|
|
{
|
|
if(IsD3D(apitype))
|
|
{
|
|
switch(stage)
|
|
{
|
|
case ShaderStage::Vertex: return lit("Vertex");
|
|
case ShaderStage::Hull: return lit("Hull");
|
|
case ShaderStage::Domain: return lit("Domain");
|
|
case ShaderStage::Geometry: return lit("Geometry");
|
|
case ShaderStage::Pixel: return lit("Pixel");
|
|
case ShaderStage::Compute: return lit("Compute");
|
|
default: break;
|
|
}
|
|
}
|
|
else if(apitype == GraphicsAPI::OpenGL || apitype == GraphicsAPI::Vulkan)
|
|
{
|
|
switch(stage)
|
|
{
|
|
case ShaderStage::Vertex: return lit("Vertex");
|
|
case ShaderStage::Tess_Control: return lit("Tess. Control");
|
|
case ShaderStage::Tess_Eval: return lit("Tess. Eval");
|
|
case ShaderStage::Geometry: return lit("Geometry");
|
|
case ShaderStage::Fragment: return lit("Fragment");
|
|
case ShaderStage::Compute: return lit("Compute");
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
return lit("Unknown");
|
|
}
|
|
|
|
QString ToQStr(const AddressMode addr, const GraphicsAPI apitype)
|
|
{
|
|
if(IsD3D(apitype))
|
|
{
|
|
switch(addr)
|
|
{
|
|
case AddressMode::Wrap: return lit("Wrap");
|
|
case AddressMode::Mirror: return lit("Mirror");
|
|
case AddressMode::MirrorOnce: return lit("MirrorOnce");
|
|
case AddressMode::ClampEdge: return lit("ClampEdge");
|
|
case AddressMode::ClampBorder: return lit("ClampBorder");
|
|
default: break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
switch(addr)
|
|
{
|
|
case AddressMode::Repeat: return lit("Repeat");
|
|
case AddressMode::MirrorRepeat: return lit("MirrorRepeat");
|
|
case AddressMode::MirrorClamp: return lit("MirrorClamp");
|
|
case AddressMode::ClampEdge: return lit("ClampEdge");
|
|
case AddressMode::ClampBorder: return lit("ClampBorder");
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
return lit("Unknown");
|
|
}
|
|
|
|
QString TypeString(const SigParameter &sig)
|
|
{
|
|
QString ret = ToQStr(sig.varType);
|
|
|
|
if(sig.compCount > 1)
|
|
ret += QString::number(sig.compCount);
|
|
|
|
return ret;
|
|
}
|
|
|
|
QString D3DSemanticString(const SigParameter &sig)
|
|
{
|
|
if(sig.systemValue == ShaderBuiltin::Undefined)
|
|
return sig.semanticIdxName;
|
|
|
|
QString sysValues[] = {
|
|
lit("SV_Undefined"),
|
|
lit("SV_Position"),
|
|
lit("Unsupported (PointSize)"),
|
|
lit("SV_ClipDistance"),
|
|
lit("SV_CullDistance"),
|
|
lit("SV_RenderTargetIndex"),
|
|
lit("SV_ViewportIndex"),
|
|
lit("SV_VertexID"),
|
|
lit("SV_PrimitiveID"),
|
|
lit("SV_InstanceID"),
|
|
lit("Unsupported (DispatchSize)"),
|
|
lit("SV_DispatchThreadID"),
|
|
lit("SV_GroupID"),
|
|
lit("Unsupported (GroupSize)"),
|
|
lit("SV_GroupIndex"),
|
|
lit("SV_GroupThreadID"),
|
|
lit("SV_GSInstanceID"),
|
|
lit("SV_OutputControlPointID"),
|
|
lit("SV_DomainLocation"),
|
|
lit("SV_IsFrontFace"),
|
|
lit("SV_Coverage"),
|
|
lit("Unsupported (SamplePosition)"),
|
|
lit("SV_SampleIndex"),
|
|
lit("Unsupported (PatchNumVertices)"),
|
|
lit("SV_TessFactor"),
|
|
lit("SV_InsideTessFactor"),
|
|
lit("SV_Target"),
|
|
lit("SV_Depth"),
|
|
lit("SV_DepthGreaterEqual"),
|
|
lit("SV_DepthLessEqual"),
|
|
lit("Unsupported (BaseVertex)"),
|
|
lit("Unsupported (BaseInstance)"),
|
|
lit("Unsupported (DrawIndex)"),
|
|
lit("Unsupported (StencilReference)"),
|
|
lit("Unsupported (PointCoord)"),
|
|
lit("Unsupported (IsHelper)"),
|
|
lit("Unsupported (SubgroupSize)"),
|
|
lit("Unsupported (NumSubgroups)"),
|
|
lit("Unsupported (SubgroupIndexInWorkgroup)"),
|
|
lit("Unsupported (IndexInSubgroup)"),
|
|
lit("Unsupported (SubgroupEqualMask)"),
|
|
lit("Unsupported (SubgroupGreaterEqualMask)"),
|
|
lit("Unsupported (SubgroupGreaterMask)"),
|
|
lit("Unsupported (SubgroupLessEqualMask)"),
|
|
lit("Unsupported (SubgroupLessMask)"),
|
|
lit("Unsupported (DeviceIndex)"),
|
|
lit("Unsupported (IsFullyCovered)"),
|
|
lit("Unsupported (FragAreaSize)"),
|
|
lit("Unsupported (FragInvocationCount)"),
|
|
};
|
|
|
|
static_assert(arraydim<ShaderBuiltin>() == ARRAY_COUNT(sysValues),
|
|
"System values have changed - update HLSL stub generation");
|
|
|
|
QString ret = sysValues[size_t(sig.systemValue)];
|
|
|
|
// need to include the index if it's a system value semantic that's numbered
|
|
if(sig.systemValue == ShaderBuiltin::ColorOutput ||
|
|
sig.systemValue == ShaderBuiltin::CullDistance || sig.systemValue == ShaderBuiltin::ClipDistance)
|
|
ret += QString::number(sig.semanticIndex);
|
|
|
|
return ret;
|
|
}
|
|
|
|
QString GetComponentString(byte mask)
|
|
{
|
|
QString ret;
|
|
|
|
if((mask & 0x1) > 0)
|
|
ret += lit("R");
|
|
if((mask & 0x2) > 0)
|
|
ret += lit("G");
|
|
if((mask & 0x4) > 0)
|
|
ret += lit("B");
|
|
if((mask & 0x8) > 0)
|
|
ret += lit("A");
|
|
|
|
return ret;
|
|
}
|
|
|
|
void CombineUsageEvents(ICaptureContext &ctx, const rdcarray<EventUsage> &usage,
|
|
std::function<void(uint32_t startEID, uint32_t endEID, ResourceUsage use)> callback)
|
|
{
|
|
uint32_t start = 0;
|
|
uint32_t end = 0;
|
|
ResourceUsage us = ResourceUsage::IndexBuffer;
|
|
|
|
for(const EventUsage u : usage)
|
|
{
|
|
if(start == 0)
|
|
{
|
|
start = end = u.eventId;
|
|
us = u.usage;
|
|
continue;
|
|
}
|
|
|
|
const DrawcallDescription *draw = ctx.GetDrawcall(u.eventId);
|
|
|
|
bool distinct = false;
|
|
|
|
// if the usage is different from the last, add a new entry,
|
|
// or if the previous draw link is broken.
|
|
if(u.usage != us || draw == NULL || draw->previous == 0)
|
|
{
|
|
distinct = true;
|
|
}
|
|
else
|
|
{
|
|
// otherwise search back through real draws, to see if the
|
|
// last event was where we were - otherwise it's a new
|
|
// distinct set of drawcalls and should have a separate
|
|
// entry in the context menu
|
|
const DrawcallDescription *prev = draw->previous;
|
|
|
|
while(prev != NULL && prev->eventId > end)
|
|
{
|
|
if(!(prev->flags & (DrawFlags::Dispatch | DrawFlags::Drawcall | DrawFlags::CmdList)))
|
|
{
|
|
prev = prev->previous;
|
|
}
|
|
else
|
|
{
|
|
distinct = true;
|
|
break;
|
|
}
|
|
|
|
if(prev == NULL)
|
|
distinct = true;
|
|
}
|
|
}
|
|
|
|
if(distinct)
|
|
{
|
|
callback(start, end, us);
|
|
start = end = u.eventId;
|
|
us = u.usage;
|
|
}
|
|
|
|
end = u.eventId;
|
|
}
|
|
|
|
if(start != 0)
|
|
callback(start, end, us);
|
|
}
|
|
|
|
void addStructuredObjects(RDTreeWidgetItem *parent, const StructuredObjectList &objs,
|
|
bool parentIsArray)
|
|
{
|
|
for(const SDObject *obj : objs)
|
|
{
|
|
if(obj->type.flags & SDTypeFlags::Hidden)
|
|
continue;
|
|
|
|
QVariant param;
|
|
|
|
if(parentIsArray)
|
|
param = QFormatStr("[%1]").arg(parent->childCount());
|
|
else
|
|
param = obj->name;
|
|
|
|
RDTreeWidgetItem *item = new RDTreeWidgetItem({param, QString()});
|
|
|
|
// we don't identify via the type name as many types could be serialised as a ResourceId -
|
|
// e.g. ID3D11Resource* or ID3D11Buffer* which would be the actual typename. We want to preserve
|
|
// that for the best raw structured data representation instead of flattening those out to just
|
|
// "ResourceId", and we also don't want to store two types ('fake' and 'real'), so instead we
|
|
// check the custom string.
|
|
if(obj->type.basetype == SDBasic::Resource)
|
|
{
|
|
ResourceId id;
|
|
static_assert(sizeof(id) == sizeof(obj->data.basic.u), "ResourceId is no longer uint64_t!");
|
|
memcpy(&id, &obj->data.basic.u, sizeof(id));
|
|
|
|
param = id;
|
|
}
|
|
else if(obj->type.flags & SDTypeFlags::NullString)
|
|
{
|
|
param = lit("NULL");
|
|
}
|
|
else if(obj->type.flags & SDTypeFlags::HasCustomString)
|
|
{
|
|
param = obj->data.str;
|
|
}
|
|
else
|
|
{
|
|
switch(obj->type.basetype)
|
|
{
|
|
case SDBasic::Chunk:
|
|
case SDBasic::Struct:
|
|
param = QFormatStr("%1()").arg(obj->type.name);
|
|
addStructuredObjects(item, obj->data.children, false);
|
|
break;
|
|
case SDBasic::Array:
|
|
param = QFormatStr("%1[]").arg(obj->type.name);
|
|
addStructuredObjects(item, obj->data.children, true);
|
|
break;
|
|
case SDBasic::Null: param = lit("NULL"); break;
|
|
case SDBasic::Buffer: param = lit("(%1 bytes)").arg(obj->type.byteSize); break;
|
|
case SDBasic::String:
|
|
{
|
|
QStringList lines = QString(obj->data.str).split(QLatin1Char('\n'));
|
|
QString trimmedStr;
|
|
for(int i = 0; i < 3 && i < lines.count(); i++)
|
|
trimmedStr += lines[i] + QLatin1Char('\n');
|
|
if(lines.count() > 3)
|
|
trimmedStr += lit("...");
|
|
param = trimmedStr.trimmed();
|
|
break;
|
|
}
|
|
case SDBasic::Resource:
|
|
case SDBasic::Enum:
|
|
case SDBasic::UnsignedInteger: param = Formatter::HumanFormat(obj->data.basic.u); break;
|
|
case SDBasic::SignedInteger: param = Formatter::Format(obj->data.basic.i); break;
|
|
case SDBasic::Float: param = Formatter::Format(obj->data.basic.d); break;
|
|
case SDBasic::Boolean: param = (obj->data.basic.b ? lit("True") : lit("False")); break;
|
|
case SDBasic::Character: param = QString(QLatin1Char(obj->data.basic.c)); break;
|
|
}
|
|
}
|
|
|
|
item->setText(1, param);
|
|
|
|
parent->addChild(item);
|
|
}
|
|
}
|
|
|
|
bool SaveToJSON(QVariantMap &data, QIODevice &f, const char *magicIdentifier, uint32_t magicVersion)
|
|
{
|
|
// marker that this data is valid
|
|
if(magicIdentifier)
|
|
data[QString::fromLatin1(magicIdentifier)] = magicVersion;
|
|
|
|
QJsonDocument doc = QJsonDocument::fromVariant(data);
|
|
|
|
if(doc.isEmpty() || doc.isNull())
|
|
{
|
|
qCritical() << "Failed to convert data to JSON document";
|
|
return false;
|
|
}
|
|
|
|
QByteArray jsontext = doc.toJson(QJsonDocument::Indented);
|
|
|
|
qint64 ret = f.write(jsontext);
|
|
|
|
if(ret != jsontext.size())
|
|
{
|
|
qCritical() << "Failed to write JSON data: " << ret << " " << f.errorString();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool LoadFromJSON(QVariantMap &data, QIODevice &f, const char *magicIdentifier, uint32_t magicVersion)
|
|
{
|
|
QByteArray json = f.readAll();
|
|
|
|
if(json.isEmpty())
|
|
{
|
|
qCritical() << "Read invalid empty JSON data from file " << f.errorString();
|
|
return false;
|
|
}
|
|
|
|
QJsonDocument doc = QJsonDocument::fromJson(json);
|
|
|
|
if(doc.isEmpty() || doc.isNull())
|
|
{
|
|
qCritical() << "Failed to convert file to JSON document";
|
|
return false;
|
|
}
|
|
|
|
data = doc.toVariant().toMap();
|
|
|
|
QString ident = QString::fromLatin1(magicIdentifier);
|
|
|
|
if(data.isEmpty() || !data.contains(ident))
|
|
{
|
|
qCritical() << "Converted config data is invalid or unrecognised";
|
|
return false;
|
|
}
|
|
|
|
if(data[ident].toUInt() != magicVersion)
|
|
{
|
|
qCritical() << "Converted config data is not the right version";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
QString VariantToJSON(const QVariantMap &data)
|
|
{
|
|
return QString::fromUtf8(QJsonDocument::fromVariant(data).toJson(QJsonDocument::Indented));
|
|
}
|
|
|
|
QVariantMap JSONToVariant(const QString &json)
|
|
{
|
|
return QJsonDocument::fromJson(json.toUtf8()).toVariant().toMap();
|
|
}
|
|
|
|
int GUIInvoke::methodIndex = -1;
|
|
|
|
void GUIInvoke::init()
|
|
{
|
|
GUIInvoke *invoke = new GUIInvoke(NULL, {});
|
|
methodIndex = invoke->metaObject()->indexOfMethod(QMetaObject::normalizedSignature("doInvoke()"));
|
|
invoke->deleteLater();
|
|
}
|
|
|
|
void GUIInvoke::call(QObject *obj, const std::function<void()> &f)
|
|
{
|
|
if(!obj)
|
|
qCritical() << "GUIInvoke::call called with NULL object";
|
|
|
|
if(onUIThread())
|
|
{
|
|
if(obj)
|
|
f();
|
|
return;
|
|
}
|
|
|
|
defer(obj, f);
|
|
}
|
|
|
|
void GUIInvoke::defer(QObject *obj, const std::function<void()> &f)
|
|
{
|
|
if(!obj)
|
|
qCritical() << "GUIInvoke::defer called with NULL object";
|
|
|
|
GUIInvoke *invoke = new GUIInvoke(obj, f);
|
|
invoke->moveToThread(qApp->thread());
|
|
invoke->metaObject()->method(methodIndex).invoke(invoke, Qt::QueuedConnection);
|
|
}
|
|
|
|
void GUIInvoke::blockcall(QObject *obj, const std::function<void()> &f)
|
|
{
|
|
if(!obj)
|
|
qCritical() << "GUIInvoke::blockcall called with NULL object";
|
|
|
|
if(onUIThread())
|
|
{
|
|
if(obj)
|
|
f();
|
|
return;
|
|
}
|
|
|
|
GUIInvoke *invoke = new GUIInvoke(obj, f);
|
|
invoke->moveToThread(qApp->thread());
|
|
invoke->metaObject()->method(methodIndex).invoke(invoke, Qt::BlockingQueuedConnection);
|
|
}
|
|
|
|
bool GUIInvoke::onUIThread()
|
|
{
|
|
return qApp->thread() == QThread::currentThread();
|
|
}
|
|
|
|
QString RDDialog::DefaultBrowsePath;
|
|
|
|
const QMessageBox::StandardButtons RDDialog::YesNoCancel =
|
|
QMessageBox::StandardButtons(QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
|
|
|
|
void RDDialog::show(QMenu *menu, QPoint pos)
|
|
{
|
|
// menus aren't always visible immediately, so we need to listen for aboutToHide to exit the event
|
|
// loop. As a safety precaution because I don't trust the damn signals, if we loop for over a
|
|
// second then we'll quit as soon as the menu is not visible
|
|
volatile bool menuHiding = false;
|
|
auto connection =
|
|
QObject::connect(menu, &QMenu::aboutToHide, [&menuHiding]() { menuHiding = true; });
|
|
|
|
menu->setWindowModality(Qt::ApplicationModal);
|
|
menu->popup(pos);
|
|
|
|
QElapsedTimer elapsed;
|
|
elapsed.start();
|
|
|
|
QEventLoop loop;
|
|
for(;;)
|
|
{
|
|
// stop processing once aboutToHide has been signalled
|
|
if(menuHiding)
|
|
break;
|
|
|
|
// stop processing if 1s has passed and the menu isn't visible anymore.
|
|
if(elapsed.hasExpired(1000) && !menu->isVisible())
|
|
break;
|
|
|
|
loop.processEvents(QEventLoop::WaitForMoreEvents);
|
|
QCoreApplication::sendPostedEvents();
|
|
}
|
|
|
|
QObject::disconnect(connection);
|
|
}
|
|
|
|
int RDDialog::show(QDialog *dialog)
|
|
{
|
|
// workaround for QTBUG-56382 needed on windows only - it can break on other platforms
|
|
#if defined(Q_OS_WIN32)
|
|
dialog->setWindowModality(Qt::ApplicationModal);
|
|
dialog->show();
|
|
QEventLoop loop;
|
|
while(dialog->isVisible())
|
|
{
|
|
loop.processEvents(QEventLoop::WaitForMoreEvents);
|
|
QCoreApplication::sendPostedEvents();
|
|
}
|
|
#else
|
|
dialog->exec();
|
|
#endif
|
|
|
|
return dialog->result();
|
|
}
|
|
|
|
QMessageBox::StandardButton RDDialog::messageBox(QMessageBox::Icon icon, QWidget *parent,
|
|
const QString &title, const QString &text,
|
|
QMessageBox::StandardButtons buttons,
|
|
QMessageBox::StandardButton defaultButton)
|
|
{
|
|
QMessageBox::StandardButton ret = defaultButton;
|
|
|
|
QObject *parentObj = parent;
|
|
|
|
if(parentObj == NULL)
|
|
{
|
|
// for 'global' message boxes with no parents, just use the app as the parent pointer
|
|
parentObj = qApp;
|
|
}
|
|
|
|
// if we're already on the right thread, this boils down to a function call
|
|
GUIInvoke::blockcall(parentObj, [&]() {
|
|
QMessageBox mb(icon, title, text, buttons, parent);
|
|
mb.setDefaultButton(defaultButton);
|
|
show(&mb);
|
|
ret = mb.standardButton(mb.clickedButton());
|
|
});
|
|
return ret;
|
|
}
|
|
|
|
QMessageBox::StandardButton RDDialog::messageBoxChecked(QMessageBox::Icon icon, QWidget *parent,
|
|
const QString &title, const QString &text,
|
|
QCheckBox *checkBox, bool &checked,
|
|
QMessageBox::StandardButtons buttons,
|
|
QMessageBox::StandardButton defaultButton)
|
|
{
|
|
QMessageBox::StandardButton ret = defaultButton;
|
|
|
|
// if we're already on the right thread, this boils down to a function call
|
|
GUIInvoke::blockcall(parent, [&]() {
|
|
QMessageBox mb(icon, title, text, buttons, parent);
|
|
mb.setDefaultButton(defaultButton);
|
|
mb.setCheckBox(checkBox);
|
|
show(&mb);
|
|
checked = mb.checkBox()->isChecked();
|
|
ret = mb.standardButton(mb.clickedButton());
|
|
});
|
|
|
|
return ret;
|
|
}
|
|
|
|
QString RDDialog::getExistingDirectory(QWidget *parent, const QString &caption, const QString &dir,
|
|
QFileDialog::Options options)
|
|
{
|
|
QFileDialog fd(parent, caption, dir, QString());
|
|
fd.setAcceptMode(QFileDialog::AcceptOpen);
|
|
fd.setFileMode(QFileDialog::DirectoryOnly);
|
|
fd.setOptions(options);
|
|
show(&fd);
|
|
|
|
if(fd.result() == QFileDialog::Accepted)
|
|
{
|
|
QStringList files = fd.selectedFiles();
|
|
if(!files.isEmpty())
|
|
return files[0];
|
|
}
|
|
|
|
return QString();
|
|
}
|
|
|
|
QString RDDialog::getOpenFileName(QWidget *parent, const QString &caption, const QString &dir,
|
|
const QString &filter, QString *selectedFilter,
|
|
QFileDialog::Options options)
|
|
{
|
|
QString d = dir;
|
|
if(d.isEmpty())
|
|
d = DefaultBrowsePath;
|
|
|
|
QFileDialog fd(parent, caption, d, filter);
|
|
fd.setFileMode(QFileDialog::ExistingFile);
|
|
fd.setAcceptMode(QFileDialog::AcceptOpen);
|
|
fd.setOptions(options);
|
|
show(&fd);
|
|
|
|
if(fd.result() == QFileDialog::Accepted)
|
|
{
|
|
if(selectedFilter)
|
|
*selectedFilter = fd.selectedNameFilter();
|
|
|
|
QStringList files = fd.selectedFiles();
|
|
if(!files.isEmpty())
|
|
{
|
|
DefaultBrowsePath = QFileInfo(files[0]).dir().absolutePath();
|
|
return files[0];
|
|
}
|
|
}
|
|
|
|
return QString();
|
|
}
|
|
|
|
QString RDDialog::getExecutableFileName(QWidget *parent, const QString &caption, const QString &dir,
|
|
const QString &defaultExe, QFileDialog::Options options)
|
|
{
|
|
QString d = dir;
|
|
if(d.isEmpty())
|
|
d = DefaultBrowsePath;
|
|
|
|
QString filter;
|
|
|
|
#if defined(Q_OS_WIN32)
|
|
// can't filter by executable bit on windows, but we have extensions
|
|
filter = QApplication::translate("RDDialog", "Executables (*.exe);;All Files (*)");
|
|
#endif
|
|
|
|
QFileDialog fd(parent, caption, d, filter);
|
|
fd.setOptions(options);
|
|
fd.setAcceptMode(QFileDialog::AcceptOpen);
|
|
fd.setFileMode(QFileDialog::ExistingFile);
|
|
{
|
|
QFileFilterModel *fileProxy = new QFileFilterModel(parent);
|
|
fileProxy->setRequirePermissions(QDir::Executable);
|
|
fd.setProxyModel(fileProxy);
|
|
}
|
|
if(!defaultExe.isEmpty())
|
|
fd.selectFile(defaultExe);
|
|
show(&fd);
|
|
|
|
if(fd.result() == QFileDialog::Accepted)
|
|
{
|
|
QStringList files = fd.selectedFiles();
|
|
if(!files.isEmpty())
|
|
{
|
|
DefaultBrowsePath = QFileInfo(files[0]).dir().absolutePath();
|
|
return files[0];
|
|
}
|
|
}
|
|
|
|
return QString();
|
|
}
|
|
|
|
static QStringList getDefaultSuffixesFromFilter(const QString &filter)
|
|
{
|
|
// capture the first suffix found and discard the rest
|
|
static const QRegularExpression regex(lit("\\*\\.([\\w.]+).*"));
|
|
|
|
QStringList suffixes;
|
|
for(const QString &s : filter.split(lit(";;")))
|
|
{
|
|
suffixes << regex.match(s).captured(1);
|
|
}
|
|
return suffixes;
|
|
}
|
|
|
|
QString RDDialog::getSaveFileName(QWidget *parent, const QString &caption, const QString &dir,
|
|
const QString &filter, QString *selectedFilter,
|
|
QFileDialog::Options options)
|
|
{
|
|
QString d = dir;
|
|
if(d.isEmpty())
|
|
d = DefaultBrowsePath;
|
|
|
|
QFileDialog fd(parent, caption, d, filter);
|
|
fd.setAcceptMode(QFileDialog::AcceptSave);
|
|
fd.setOptions(options);
|
|
const QStringList &defaultSuffixes = getDefaultSuffixesFromFilter(filter);
|
|
if(!defaultSuffixes.isEmpty())
|
|
fd.setDefaultSuffix(defaultSuffixes.first());
|
|
QObject::connect(&fd, &QFileDialog::filterSelected, [&](const QString &filter) {
|
|
int i = fd.nameFilters().indexOf(filter);
|
|
fd.setDefaultSuffix(defaultSuffixes.value(i));
|
|
});
|
|
show(&fd);
|
|
|
|
if(fd.result() == QFileDialog::Accepted)
|
|
{
|
|
if(selectedFilter)
|
|
*selectedFilter = fd.selectedNameFilter();
|
|
|
|
QStringList files = fd.selectedFiles();
|
|
if(!files.isEmpty())
|
|
{
|
|
DefaultBrowsePath = QFileInfo(files[0]).dir().absolutePath();
|
|
return files[0];
|
|
}
|
|
}
|
|
|
|
return QString();
|
|
}
|
|
|
|
bool QFileFilterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
|
|
{
|
|
QModelIndex idx = sourceModel()->index(source_row, 0, source_parent);
|
|
|
|
QFileSystemModel *fs = qobject_cast<QFileSystemModel *>(sourceModel());
|
|
|
|
if(!fs)
|
|
{
|
|
qCritical() << "Expected a QFileSystemModel as the source model!";
|
|
return true;
|
|
}
|
|
|
|
if(fs->isDir(idx))
|
|
return true;
|
|
|
|
QFile::Permissions permissions =
|
|
(QFile::Permissions)sourceModel()->data(idx, QFileSystemModel::FilePermissions).toInt();
|
|
|
|
if((m_requireMask & QDir::Readable) && !(permissions & QFile::ReadUser))
|
|
return false;
|
|
if((m_requireMask & QDir::Writable) && !(permissions & QFile::WriteUser))
|
|
return false;
|
|
if((m_requireMask & QDir::Executable) && !(permissions & QFile::ExeUser))
|
|
return false;
|
|
|
|
if((m_excludeMask & QDir::Readable) && (permissions & QFile::ReadUser))
|
|
return false;
|
|
if((m_excludeMask & QDir::Writable) && (permissions & QFile::WriteUser))
|
|
return false;
|
|
if((m_excludeMask & QDir::Executable) && (permissions & QFile::ExeUser))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
QCollatorSortFilterProxyModel::QCollatorSortFilterProxyModel(QObject *parent)
|
|
: QSortFilterProxyModel(parent)
|
|
{
|
|
m_collator = new QCollator();
|
|
}
|
|
|
|
QCollatorSortFilterProxyModel::~QCollatorSortFilterProxyModel()
|
|
{
|
|
delete m_collator;
|
|
}
|
|
|
|
bool QCollatorSortFilterProxyModel::lessThan(const QModelIndex &source_left,
|
|
const QModelIndex &source_right) const
|
|
{
|
|
return m_collator->compare(sourceModel()->data(source_left, sortRole()).toString(),
|
|
sourceModel()->data(source_right, sortRole()).toString()) < 0;
|
|
}
|
|
|
|
void addGridLines(QGridLayout *grid, QColor gridColor)
|
|
{
|
|
QString style =
|
|
QFormatStr("border: solid #%1%2%3; border-bottom-width: 1px; border-right-width: 1px;")
|
|
.arg(gridColor.red(), 2, 16, QLatin1Char('0'))
|
|
.arg(gridColor.green(), 2, 16, QLatin1Char('0'))
|
|
.arg(gridColor.blue(), 2, 16, QLatin1Char('0'));
|
|
|
|
for(int y = 0; y < grid->rowCount(); y++)
|
|
{
|
|
for(int x = 0; x < grid->columnCount(); x++)
|
|
{
|
|
QLayoutItem *item = grid->itemAtPosition(y, x);
|
|
|
|
if(item == NULL)
|
|
continue;
|
|
|
|
QWidget *w = item->widget();
|
|
|
|
if(w == NULL)
|
|
continue;
|
|
|
|
QString cellStyle = style;
|
|
|
|
if(x == 0)
|
|
cellStyle += lit("border-left-width: 1px;");
|
|
if(y == 0)
|
|
cellStyle += lit("border-top-width: 1px;");
|
|
|
|
w->setStyleSheet(cellStyle);
|
|
}
|
|
}
|
|
}
|
|
|
|
int Formatter::m_minFigures = 2, Formatter::m_maxFigures = 5, Formatter::m_expNegCutoff = 5,
|
|
Formatter::m_expPosCutoff = 7;
|
|
double Formatter::m_expNegValue = 0.00001; // 10^(-5)
|
|
double Formatter::m_expPosValue = 10000000.0; // 10^7
|
|
QFont *Formatter::m_Font = NULL;
|
|
float Formatter::m_FontBaseSize = 10.0f; // this should always be overridden below, but just in
|
|
// case let's pick a sensible value
|
|
QColor Formatter::m_DarkChecker, Formatter::m_LightChecker;
|
|
|
|
void Formatter::setParams(const PersistantConfig &config)
|
|
{
|
|
m_minFigures = qMax(0, config.Formatter_MinFigures);
|
|
m_maxFigures = qMax(2, config.Formatter_MaxFigures);
|
|
m_expNegCutoff = qMax(0, config.Formatter_NegExp);
|
|
m_expPosCutoff = qMax(0, config.Formatter_PosExp);
|
|
|
|
m_expNegValue = qPow(10.0, -config.Formatter_NegExp);
|
|
m_expPosValue = qPow(10.0, config.Formatter_PosExp);
|
|
|
|
if(!m_Font)
|
|
{
|
|
m_Font = new QFont();
|
|
m_FontBaseSize = QApplication::font().pointSizeF();
|
|
}
|
|
|
|
*m_Font =
|
|
config.Font_PreferMonospaced ? QFontDatabase::systemFont(QFontDatabase::FixedFont) : QFont();
|
|
|
|
m_Font->setPointSizeF(m_FontBaseSize * config.Font_GlobalScale);
|
|
QFont f = QApplication::font();
|
|
f.setPointSizeF(m_FontBaseSize * config.Font_GlobalScale);
|
|
QApplication::setFont(f);
|
|
|
|
Formatter::setPalette(QApplication::palette());
|
|
}
|
|
|
|
void Formatter::setPalette(QPalette palette)
|
|
{
|
|
m_DarkChecker = palette.color(QPalette::Mid);
|
|
m_LightChecker = m_DarkChecker.lighter(150);
|
|
|
|
RENDERDOC_SetColors(m_DarkChecker, m_LightChecker, IsDarkTheme());
|
|
}
|
|
|
|
void Formatter::shutdown()
|
|
{
|
|
delete m_Font;
|
|
}
|
|
|
|
QString Formatter::Format(double f, bool)
|
|
{
|
|
if(f != 0.0 && (qAbs(f) < m_expNegValue || qAbs(f) > m_expPosValue))
|
|
return QFormatStr("%1").arg(f, -m_minFigures, 'E', m_maxFigures);
|
|
|
|
QString ret = QFormatStr("%1").arg(f, 0, 'f', m_maxFigures);
|
|
|
|
// trim excess trailing 0s
|
|
int decimal = ret.lastIndexOf(QLatin1Char('.'));
|
|
if(decimal > 0)
|
|
{
|
|
decimal += m_minFigures;
|
|
|
|
const int len = ret.count();
|
|
|
|
int remove = 0;
|
|
while(len - remove - 1 > decimal && ret.at(len - remove - 1) == QLatin1Char('0'))
|
|
remove++;
|
|
|
|
if(remove > 0)
|
|
ret.chop(remove);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
QString Formatter::HumanFormat(uint64_t u)
|
|
{
|
|
if(u == UINT16_MAX)
|
|
return lit("UINT16_MAX");
|
|
if(u == UINT32_MAX)
|
|
return lit("UINT32_MAX");
|
|
if(u == UINT64_MAX)
|
|
return lit("UINT64_MAX");
|
|
|
|
// format as hex when over a certain threshold
|
|
if(u > 0xffffff)
|
|
return lit("0x") + Format(u, true);
|
|
|
|
return Format(u);
|
|
}
|
|
|
|
class RDProgressDialog : public QProgressDialog
|
|
{
|
|
public:
|
|
RDProgressDialog(const QString &labelText, QWidget *parent)
|
|
// we add 1 so that the progress value never hits maximum until we are actually finished
|
|
: QProgressDialog(labelText, QString(), 0, maxProgress + 1, parent),
|
|
m_Label(this)
|
|
{
|
|
setWindowTitle(tr("Please Wait"));
|
|
setWindowFlags(Qt::CustomizeWindowHint | Qt::Dialog | Qt::WindowTitleHint);
|
|
setWindowIcon(QIcon());
|
|
setMinimumSize(QSize(250, 0));
|
|
setMaximumSize(QSize(250, 10000));
|
|
setCancelButton(NULL);
|
|
setMinimumDuration(0);
|
|
setWindowModality(Qt::ApplicationModal);
|
|
setValue(0);
|
|
|
|
m_Label.setText(labelText);
|
|
m_Label.setAlignment(Qt::AlignCenter);
|
|
m_Label.setWordWrap(true);
|
|
|
|
setLabel(&m_Label);
|
|
}
|
|
|
|
void enableCancel() { setCancelButtonText(tr("Cancel")); }
|
|
void setPercentage(float percent) { setValue(int(maxProgress * percent)); }
|
|
void setInfinite(bool infinite)
|
|
{
|
|
if(infinite)
|
|
{
|
|
setMinimum(0);
|
|
setMaximum(0);
|
|
setValue(0);
|
|
}
|
|
else
|
|
{
|
|
setMinimum(0);
|
|
setMaximum(maxProgress + 1);
|
|
setValue(0);
|
|
}
|
|
}
|
|
|
|
void closeAndReset()
|
|
{
|
|
setValue(maxProgress);
|
|
hide();
|
|
reset();
|
|
}
|
|
|
|
protected:
|
|
void keyPressEvent(QKeyEvent *e) override
|
|
{
|
|
if(e->key() == Qt::Key_Escape)
|
|
return;
|
|
|
|
QProgressDialog::keyPressEvent(e);
|
|
}
|
|
|
|
QLabel m_Label;
|
|
|
|
static const int maxProgress = 1000;
|
|
};
|
|
|
|
#if defined(Q_OS_WIN32)
|
|
|
|
#include <windows.h>
|
|
|
|
#include <shellapi.h>
|
|
|
|
typedef LSTATUS(APIENTRY *PFN_RegCreateKeyExA)(HKEY hKey, LPCSTR lpSubKey, DWORD Reserved,
|
|
LPSTR lpClass, DWORD dwOptions, REGSAM samDesired,
|
|
CONST LPSECURITY_ATTRIBUTES lpSecurityAttributes,
|
|
PHKEY phkResult, LPDWORD lpdwDisposition);
|
|
|
|
typedef LSTATUS(APIENTRY *PFN_RegCloseKey)(HKEY hKey);
|
|
|
|
#else
|
|
|
|
#include <unistd.h>
|
|
|
|
#endif
|
|
|
|
bool IsRunningAsAdmin()
|
|
{
|
|
#if defined(Q_OS_WIN32)
|
|
// try to open HKLM\Software for write.
|
|
HKEY key = NULL;
|
|
|
|
// access dynamically to get around the pain of trying to link to extra window libs in qt
|
|
HMODULE mod = LoadLibraryA("advapi32.dll");
|
|
|
|
if(mod == NULL)
|
|
return false;
|
|
|
|
PFN_RegCreateKeyExA create = (PFN_RegCreateKeyExA)GetProcAddress(mod, "RegCreateKeyExA");
|
|
PFN_RegCloseKey close = (PFN_RegCloseKey)GetProcAddress(mod, "RegCloseKey");
|
|
|
|
LSTATUS ret = ERROR_PROC_NOT_FOUND;
|
|
|
|
if(create && close)
|
|
{
|
|
ret = create(HKEY_LOCAL_MACHINE, "SOFTWARE", 0, NULL, 0, KEY_READ | KEY_WRITE, NULL, &key, NULL);
|
|
|
|
if(key)
|
|
close(key);
|
|
}
|
|
|
|
FreeLibrary(mod);
|
|
|
|
return (ret == ERROR_SUCCESS);
|
|
|
|
#else
|
|
|
|
// this isn't ideal, we should check something else since a user may have permissions to do what
|
|
// we want to do
|
|
return geteuid() == 0;
|
|
|
|
#endif
|
|
}
|
|
|
|
bool RunProcessAsAdmin(const QString &fullExecutablePath, const QStringList ¶ms,
|
|
QWidget *parent, bool hidden, std::function<void()> finishedCallback)
|
|
{
|
|
#if defined(Q_OS_WIN32)
|
|
|
|
std::wstring wideExe = QDir::toNativeSeparators(fullExecutablePath).toStdWString();
|
|
std::wstring wideParams;
|
|
|
|
for(QString p : params)
|
|
{
|
|
wideParams += L"\"";
|
|
wideParams += p.toStdWString();
|
|
wideParams += L"\" ";
|
|
}
|
|
|
|
SHELLEXECUTEINFOW info = {};
|
|
info.cbSize = sizeof(info);
|
|
info.fMask = SEE_MASK_NOCLOSEPROCESS;
|
|
info.lpVerb = L"runas";
|
|
info.lpFile = wideExe.c_str();
|
|
info.lpParameters = wideParams.c_str();
|
|
info.nShow = hidden ? SW_HIDE : SW_SHOWNORMAL;
|
|
|
|
ShellExecuteExW(&info);
|
|
|
|
if((uintptr_t)info.hInstApp > 32 && info.hProcess != NULL)
|
|
{
|
|
if(finishedCallback)
|
|
{
|
|
HANDLE h = info.hProcess;
|
|
|
|
// do the wait on another thread
|
|
LambdaThread *thread = new LambdaThread([h, parent, finishedCallback]() {
|
|
WaitForSingleObject(h, 30000);
|
|
CloseHandle(h);
|
|
GUIInvoke::call(parent, finishedCallback);
|
|
});
|
|
thread->selfDelete(true);
|
|
thread->start();
|
|
}
|
|
else
|
|
{
|
|
CloseHandle(info.hProcess);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
|
|
#else
|
|
// try to find a way to run the application elevated.
|
|
const QString graphicalSudo[] = {
|
|
lit("kdesudo"), lit("gksudo"), lit("beesu"),
|
|
};
|
|
|
|
// if none of the graphical options, then look for sudo and either
|
|
const QString termEmulator[] = {
|
|
lit("x-terminal-emulator"), lit("gnome-terminal"), lit("konsole"), lit("xterm"),
|
|
};
|
|
|
|
for(const QString &sudo : graphicalSudo)
|
|
{
|
|
QString inPath = QStandardPaths::findExecutable(sudo);
|
|
|
|
// can't find in path
|
|
if(inPath.isEmpty())
|
|
continue;
|
|
|
|
QProcess *process = new QProcess;
|
|
|
|
QStringList sudoParams;
|
|
// these programs need a -- to indicate the end of their options, before the program
|
|
if(sudo == lit("kdesudo") || sudo == lit("gksudo"))
|
|
sudoParams << lit("--");
|
|
|
|
sudoParams << fullExecutablePath;
|
|
for(const QString &p : params)
|
|
sudoParams << p;
|
|
|
|
qInfo() << "Running" << sudo << "with params" << sudoParams;
|
|
|
|
// run with sudo
|
|
process->start(sudo, sudoParams);
|
|
|
|
// when the process exits, call the callback and delete
|
|
QObject::connect(process, OverloadedSlot<int, QProcess::ExitStatus>::of(&QProcess::finished),
|
|
[parent, process, finishedCallback](int exitCode) {
|
|
process->deleteLater();
|
|
GUIInvoke::call(parent, finishedCallback);
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
QString sudo = QStandardPaths::findExecutable(lit("sudo"));
|
|
|
|
if(sudo.isEmpty())
|
|
{
|
|
RDDialog::critical(parent, lit("Error running program as root"),
|
|
lit("Couldn't find graphical or terminal sudo program!\n"
|
|
"Please run '%1' with args '%2' manually.")
|
|
.arg(fullExecutablePath)
|
|
.arg(params.join(QLatin1Char(' '))));
|
|
return false;
|
|
}
|
|
|
|
for(const QString &term : termEmulator)
|
|
{
|
|
QString inPath = QStandardPaths::findExecutable(term);
|
|
|
|
// can't find in path
|
|
if(inPath.isEmpty())
|
|
continue;
|
|
|
|
QProcess *process = new QProcess;
|
|
|
|
// run terminal sudo with emulator
|
|
QStringList termParams;
|
|
termParams << lit("-e")
|
|
<< lit("bash -c 'echo Running \"%1 %2\" as root.;echo;sudo %1 %2'")
|
|
.arg(fullExecutablePath)
|
|
.arg(params.join(QLatin1Char(' ')));
|
|
|
|
process->start(term, termParams);
|
|
|
|
// when the process exits, call the callback and delete
|
|
QObject::connect(process, OverloadedSlot<int, QProcess::ExitStatus>::of(&QProcess::finished),
|
|
[parent, process, finishedCallback](int exitCode) {
|
|
process->deleteLater();
|
|
GUIInvoke::call(parent, finishedCallback);
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
RDDialog::critical(parent, lit("Error running program as root"),
|
|
lit("Couldn't find graphical or terminal emulator to launch sudo!\n"
|
|
"Please manually run: sudo \"%1\" %2")
|
|
.arg(fullExecutablePath)
|
|
.arg(params.join(QLatin1Char(' '))));
|
|
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
void RevealFilenameInExternalFileBrowser(const QString &filePath)
|
|
{
|
|
#if defined(Q_OS_WIN32)
|
|
// on windows we can ask explorer to highlight the exact file.
|
|
QProcess::startDetached(lit("explorer.exe"), QStringList() << lit("/select,")
|
|
<< QDir::toNativeSeparators(filePath));
|
|
#else
|
|
// on all other platforms, we just use QDesktopServices to invoke the external file browser on the
|
|
// directory and hope that's close enough.
|
|
QDesktopServices::openUrl(QFileInfo(filePath).absoluteDir().absolutePath());
|
|
#endif
|
|
}
|
|
|
|
QStringList ParseArgsList(const QString &args)
|
|
{
|
|
QStringList ret;
|
|
|
|
if(args.isEmpty())
|
|
return ret;
|
|
|
|
// on windows just use the function provided by the system
|
|
#if defined(Q_OS_WIN32)
|
|
std::wstring wargs = args.toStdWString();
|
|
|
|
int argc = 0;
|
|
wchar_t **argv = CommandLineToArgvW(wargs.c_str(), &argc);
|
|
|
|
for(int i = 0; i < argc; i++)
|
|
ret << QString::fromWCharArray(argv[i]);
|
|
|
|
LocalFree(argv);
|
|
#else
|
|
rdcstr argString = args;
|
|
|
|
// perform some kind of sane parsing
|
|
bool dquot = false, squot = false; // are we inside ''s or ""s
|
|
|
|
// current character
|
|
char *c = &argString[0];
|
|
|
|
// current argument we're building
|
|
rdcstr a;
|
|
|
|
while(*c)
|
|
{
|
|
if(!dquot && !squot && (*c == ' ' || *c == '\t'))
|
|
{
|
|
if(!a.empty())
|
|
ret << QString(a);
|
|
|
|
a = "";
|
|
}
|
|
else if(!dquot && *c == '"')
|
|
{
|
|
dquot = true;
|
|
}
|
|
else if(!squot && *c == '\'')
|
|
{
|
|
squot = true;
|
|
}
|
|
else if(dquot && *c == '"')
|
|
{
|
|
dquot = false;
|
|
}
|
|
else if(squot && *c == '\'')
|
|
{
|
|
squot = false;
|
|
}
|
|
else if(squot)
|
|
{
|
|
// single quotes don't escape, just copy literally until we leave single quote mode
|
|
a.push_back(*c);
|
|
}
|
|
else if(dquot)
|
|
{
|
|
// handle escaping
|
|
if(*c == '\\')
|
|
{
|
|
c++;
|
|
if(*c)
|
|
{
|
|
a.push_back(*c);
|
|
}
|
|
else
|
|
{
|
|
qCritical() << "Malformed args list:" << args;
|
|
return ret;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
a.push_back(*c);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
a.push_back(*c);
|
|
}
|
|
|
|
c++;
|
|
}
|
|
|
|
// if we were building an argument when we hit the end of the string
|
|
if(!a.empty())
|
|
ret << QString(a);
|
|
#endif
|
|
|
|
return ret;
|
|
}
|
|
|
|
void ShowProgressDialog(QWidget *window, const QString &labelText, ProgressFinishedMethod finished,
|
|
ProgressUpdateMethod update, ProgressCancelMethod cancel)
|
|
{
|
|
if(finished())
|
|
return;
|
|
|
|
RDProgressDialog dialog(labelText, window);
|
|
|
|
if(cancel)
|
|
dialog.enableCancel();
|
|
|
|
// if we don't have an update function, set the progress display to be 'infinite spinner'
|
|
dialog.setInfinite(!update);
|
|
|
|
QSemaphore tickerSemaphore(1);
|
|
|
|
// start a lambda thread to tick our functions and close the progress dialog when we're done.
|
|
LambdaThread progressTickerThread([finished, update, &dialog, &tickerSemaphore]() {
|
|
while(tickerSemaphore.available())
|
|
{
|
|
QThread::msleep(30);
|
|
|
|
if(update)
|
|
GUIInvoke::call(&dialog, [update, &dialog]() { dialog.setPercentage(update()); });
|
|
|
|
GUIInvoke::call(&dialog, [finished, &tickerSemaphore]() {
|
|
if(finished())
|
|
tickerSemaphore.tryAcquire();
|
|
});
|
|
}
|
|
|
|
GUIInvoke::call(&dialog, [&dialog]() { dialog.closeAndReset(); });
|
|
});
|
|
progressTickerThread.setName(lit("Progress Dialog"));
|
|
progressTickerThread.start();
|
|
|
|
// show the dialog
|
|
RDDialog::show(&dialog);
|
|
|
|
// signal the thread to exit if somehow we got here without it finishing, then wait for it thread
|
|
// to clean itself up
|
|
tickerSemaphore.tryAcquire();
|
|
progressTickerThread.wait();
|
|
|
|
if(cancel && dialog.wasCanceled())
|
|
cancel();
|
|
}
|
|
|
|
void UpdateTransferProgress(qint64 xfer, qint64 total, QElapsedTimer *timer,
|
|
QProgressBar *progressBar, QLabel *progressLabel, QString progressText)
|
|
{
|
|
if(xfer >= total)
|
|
{
|
|
progressBar->setMaximum(10000);
|
|
progressBar->setValue(10000);
|
|
return;
|
|
}
|
|
|
|
if(total <= 0)
|
|
{
|
|
progressBar->setMaximum(10000);
|
|
progressBar->setValue(0);
|
|
return;
|
|
}
|
|
|
|
progressBar->setMaximum(10000);
|
|
progressBar->setValue(int(10000.0 * (double(xfer) / double(total))));
|
|
|
|
double xferMB = double(xfer) / 1000000.0;
|
|
double totalMB = double(total) / 1000000.0;
|
|
|
|
double secondsElapsed = double(timer->nsecsElapsed()) * 1.0e-9;
|
|
|
|
double speedMBS = xferMB / secondsElapsed;
|
|
|
|
qulonglong secondsRemaining = qulonglong(double(totalMB - xferMB) / speedMBS);
|
|
|
|
if(secondsElapsed > 1.0)
|
|
{
|
|
QString remainString;
|
|
|
|
qulonglong minutesRemaining = (secondsRemaining / 60) % 60;
|
|
qulonglong hoursRemaining = (secondsRemaining / 3600);
|
|
secondsRemaining %= 60;
|
|
|
|
if(hoursRemaining > 0)
|
|
remainString = QFormatStr("%1:%2:%3")
|
|
.arg(hoursRemaining, 2, 10, QLatin1Char('0'))
|
|
.arg(minutesRemaining, 2, 10, QLatin1Char('0'))
|
|
.arg(secondsRemaining, 2, 10, QLatin1Char('0'));
|
|
else if(minutesRemaining > 0)
|
|
remainString = QFormatStr("%1:%2")
|
|
.arg(minutesRemaining, 2, 10, QLatin1Char('0'))
|
|
.arg(secondsRemaining, 2, 10, QLatin1Char('0'));
|
|
else
|
|
remainString = QApplication::translate("qrenderdoc", "%1 seconds").arg(secondsRemaining);
|
|
|
|
double speed = speedMBS;
|
|
|
|
bool MBs = true;
|
|
if(speedMBS < 1)
|
|
{
|
|
MBs = false;
|
|
speed *= 1000;
|
|
}
|
|
|
|
progressLabel->setText(
|
|
QApplication::translate("qrenderdoc", "%1\n%2 MB / %3 MB. %4 remaining (%5 %6)")
|
|
.arg(progressText)
|
|
.arg(xferMB, 0, 'f', 2)
|
|
.arg(totalMB, 0, 'f', 2)
|
|
.arg(remainString)
|
|
.arg(speed, 0, 'f', 2)
|
|
.arg(MBs ? lit("MB/s") : lit("KB/s")));
|
|
}
|
|
}
|
|
|
|
void setEnabledMultiple(const QList<QWidget *> &widgets, bool enabled)
|
|
{
|
|
for(QWidget *w : widgets)
|
|
w->setEnabled(enabled);
|
|
}
|
|
|
|
QString GetSystemUsername()
|
|
{
|
|
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
|
|
|
QString username = env.value(lit("USER"));
|
|
if(username == QString())
|
|
username = env.value(lit("USERNAME"));
|
|
if(username == QString())
|
|
username = lit("Unknown_User");
|
|
|
|
return username;
|
|
}
|
|
|
|
void BringToForeground(QWidget *window)
|
|
{
|
|
#ifdef Q_OS_WIN
|
|
SetWindowPos((HWND)window->winId(), HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
|
|
window->setWindowState(Qt::WindowActive);
|
|
window->raise();
|
|
window->showNormal();
|
|
window->show();
|
|
SetWindowPos((HWND)window->winId(), HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
|
|
#else
|
|
window->activateWindow();
|
|
window->raise();
|
|
window->showNormal();
|
|
#endif
|
|
}
|
|
|
|
bool IsDarkTheme()
|
|
{
|
|
float baseLum = getLuminance(QApplication::palette().color(QPalette::Base));
|
|
float textLum = getLuminance(QApplication::palette().color(QPalette::Text));
|
|
|
|
// if the base is dark than the text, then it's a light-on-dark theme (aka dark theme)
|
|
return (baseLum < textLum);
|
|
}
|
|
|
|
float getLuminance(const QColor &col)
|
|
{
|
|
return (float)(0.2126 * qPow(col.redF(), 2.2) + 0.7152 * qPow(col.greenF(), 2.2) +
|
|
0.0722 * qPow(col.blueF(), 2.2));
|
|
}
|
|
|
|
QColor contrastingColor(const QColor &col, const QColor &defaultCol)
|
|
{
|
|
float backLum = getLuminance(col);
|
|
float textLum = getLuminance(defaultCol);
|
|
|
|
bool backDark = backLum < 0.2f;
|
|
bool textDark = textLum < 0.2f;
|
|
|
|
// if they're contrasting, use the text colour desired
|
|
if(backDark != textDark)
|
|
return defaultCol;
|
|
|
|
// otherwise pick a contrasting colour
|
|
if(backDark)
|
|
return QColor(Qt::white);
|
|
else
|
|
return QColor(Qt::black);
|
|
}
|
|
|
|
// we declare this partial class to get the accessors. THIS IS DANGEROUS as the ABI is unstable and
|
|
// this is a private class. The first few functions have been stable for a while so we hope that it
|
|
// will remain so. If a stable interface is added in future like QX11Info we should definitely use
|
|
// it instead.
|
|
//
|
|
// Unfortunately we need this for Wayland, so we only ever use it when we are absolutely forced to
|
|
// because we're running under the Wayland Qt platform.
|
|
class QOpenGLContext;
|
|
|
|
class Q_GUI_EXPORT QPlatformNativeInterface : public QObject
|
|
{
|
|
Q_OBJECT
|
|
public:
|
|
virtual void *nativeResourceForIntegration(const QByteArray &resource);
|
|
virtual void *nativeResourceForContext(const QByteArray &resource, QOpenGLContext *context);
|
|
virtual void *nativeResourceForScreen(const QByteArray &resource, QScreen *screen);
|
|
virtual void *nativeResourceForWindow(const QByteArray &resource, QWindow *window);
|
|
};
|
|
|
|
void *AccessWaylandPlatformInterface(const QByteArray &resource, QWindow *window)
|
|
{
|
|
QPlatformNativeInterface *native = QGuiApplication::platformNativeInterface();
|
|
return native->nativeResourceForWindow(resource, window);
|
|
}
|
|
|
|
// Default Qt doesn't do this in release Qt builds, which is all we use
|
|
#if defined(Q_OS_WIN32)
|
|
|
|
#include <windows.h>
|
|
|
|
typedef HRESULT(WINAPI *PFN_SetThreadDescription)(HANDLE hThread, PCWSTR lpThreadDescription);
|
|
|
|
const DWORD MS_VC_EXCEPTION = 0x406D1388;
|
|
#pragma pack(push, 8)
|
|
typedef struct tagTHREADNAME_INFO
|
|
{
|
|
DWORD dwType; // Must be 0x1000.
|
|
LPCSTR szName; // Pointer to name (in user addr space).
|
|
DWORD dwThreadID; // Thread ID (-1=caller thread).
|
|
DWORD dwFlags; // Reserved for future use, must be zero.
|
|
} THREADNAME_INFO;
|
|
#pragma pack(pop)
|
|
|
|
static void SetThreadNameWithException(const char *name)
|
|
{
|
|
THREADNAME_INFO info;
|
|
info.dwType = 0x1000;
|
|
info.szName = name;
|
|
info.dwThreadID = GetCurrentThreadId();
|
|
info.dwFlags = 0;
|
|
__try
|
|
{
|
|
RaiseException(MS_VC_EXCEPTION, 0, sizeof(info) / sizeof(ULONG_PTR), (ULONG_PTR *)(&info));
|
|
}
|
|
__except(EXCEPTION_CONTINUE_EXECUTION)
|
|
{
|
|
}
|
|
}
|
|
|
|
void LambdaThread::windowsSetName()
|
|
{
|
|
// try to use the fancy modern API
|
|
static PFN_SetThreadDescription setThreadDesc = (PFN_SetThreadDescription)GetProcAddress(
|
|
GetModuleHandleA("kernel32.dll"), "SetThreadDescription");
|
|
|
|
if(setThreadDesc)
|
|
{
|
|
setThreadDesc(GetCurrentThread(), m_Name.toStdWString().c_str());
|
|
}
|
|
else
|
|
{
|
|
// don't throw the exception if there's no debugger present
|
|
if(!IsDebuggerPresent())
|
|
return;
|
|
|
|
SetThreadNameWithException(m_Name.toStdString().c_str());
|
|
}
|
|
}
|
|
|
|
#else
|
|
|
|
void LambdaThread::windowsSetName()
|
|
{
|
|
}
|
|
|
|
#endif
|