Files
renderdoc/qrenderdoc/Code/RGPInterop.cpp
T
2022-02-17 17:38:32 +00:00

465 lines
12 KiB
C++

/******************************************************************************
* The MIT License (MIT)
*
* Copyright (c) 2019-2022 Baldur Karlsson
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
******************************************************************************/
#include "RGPInterop.h"
#include <QApplication>
#include <QTcpServer>
#include <QTcpSocket>
bool RGPInterop::RGPSupportsInterop(const QString &RGPPath)
{
uint32_t majorVersion = 0;
uint32_t minorVersion = 0;
const char *searchString = "RGPVersion=";
const int searchStringLength = (int)strlen(searchString);
// look for an embedded string in the exe
QFile f(RGPPath);
if(f.open(QIODevice::ReadOnly))
{
QByteArray contents = f.readAll();
int search = 0;
do
{
int needle = contents.indexOf("RGPVersion=", search);
if(needle == -1)
break;
search = needle + searchStringLength;
// bail if there isn't enough room for the string plus X.Y
if(contents.size() - needle < searchStringLength + 3)
break;
// get the major version number
const char *major = contents.data() + search;
const char *sep = major;
// find the separator
while(*sep >= '0' && *sep <= '9')
sep++;
// get the minor version number
const char *minor = sep + 1;
const char *end = minor;
// find the end
while(*end >= '0' && *end <= '9')
end++;
// convert the strings to integers
QByteArray majorStr(major, sep - major);
QByteArray minorStr(minor, end - minor);
bool ok = false;
majorVersion = majorStr.toUInt(&ok);
if(!ok)
{
majorVersion = 0;
continue;
}
minorVersion = minorStr.toUInt(&ok);
if(!ok)
{
majorVersion = minorVersion = 0;
continue;
}
// found the version
break;
} while(search >= 0);
}
// interop supported in RGP V1.2 and higher
if(majorVersion > 1 || (majorVersion == 1 && minorVersion > 1))
{
return true;
}
return false;
}
template <>
rdcstr DoStringise(const RGPCommand &el)
{
BEGIN_ENUM_STRINGISE(RGPCommand);
{
STRINGISE_ENUM_CLASS_NAMED(Initialize, "initialize");
STRINGISE_ENUM_CLASS_NAMED(SetEvent, "set_event");
STRINGISE_ENUM_CLASS_NAMED(Terminate, "terminate");
}
END_ENUM_STRINGISE();
}
RGPInterop::RGPInterop(ICaptureContext &ctx) : m_Ctx(ctx)
{
m_Server = new QTcpServer(NULL);
m_Server->listen(QHostAddress::Any, Port);
QObject::connect(m_Server, &QTcpServer::newConnection, [this]() {
if(m_Socket == NULL)
{
m_Socket = m_Server->nextPendingConnection();
ConnectionEstablished();
}
else
{
// close any other connections while we already have one
delete m_Server->nextPendingConnection();
}
});
}
RGPInterop::~RGPInterop()
{
RGPInteropTerminate terminate;
QString encoded = EncodeCommand(RGPCommand::Terminate, terminate.toParams(m_Version));
if(m_Socket)
{
m_Socket->write(encoded.trimmed().toUtf8().data());
m_Socket->waitForBytesWritten();
}
m_Server->close();
delete m_Server;
}
void RGPInterop::InitializeRGP()
{
RGPInteropInit init;
init.interop_version = 1;
init.interop_name = lit("RenderDoc");
QString encoded = EncodeCommand(RGPCommand::Initialize, init.toParams(m_Version));
if(m_Socket)
{
m_Socket->write(encoded.trimmed().toUtf8().data());
}
}
bool RGPInterop::HasRGPEvent(uint32_t eventId)
{
if(m_Version == 0)
return false;
if(m_Socket == NULL)
return false;
return m_Event2RGP[eventId].interoplinearid != 0;
}
bool RGPInterop::SelectRGPEvent(uint32_t eventId)
{
if(m_Version == 0)
return false;
RGPInteropEvent ev = m_Event2RGP[eventId];
if(ev.interoplinearid == 0)
return false;
QString encoded = EncodeCommand(RGPCommand::SetEvent, ev.toParams(m_Version));
if(m_Socket)
{
m_Socket->write(encoded.trimmed().toUtf8().data());
return true;
}
return false;
}
void RGPInterop::EventSelected(RGPInteropEvent event)
{
uint32_t eventId = m_RGP2Event[event.interoplinearid];
if(eventId == 0)
{
qWarning() << "RGP Event " << event.interoplinearid << event.cmdbufid << event.eventname
<< " did not correspond to a known eventId";
return;
}
const ActionDescription *action = m_Ctx.GetAction(eventId);
const SDFile &file = m_Ctx.GetStructuredFile();
if(action && QString(file.chunks[action->events.back().chunkIndex]->name) != event.eventname)
qWarning() << "Action name mismatch. Expected " << event.eventname << " but got "
<< QString(file.chunks[action->events.back().chunkIndex]->name);
m_Ctx.SetEventID({}, eventId, eventId);
BringToForeground(m_Ctx.GetMainWindow()->Widget());
}
void RGPInterop::ConnectionEstablished()
{
QObject::connect(m_Socket, &QAbstractSocket::disconnected, [this]() {
m_Socket->deleteLater();
m_Socket = NULL;
});
// initial handshake and protocol version
InitializeRGP();
// TODO: negotiate mapping version
uint32_t version = 1;
CreateMapping(version);
// add a handler that appends all data to the read buffer and processes each time more comes in.
QObject::connect(m_Socket, &QIODevice::readyRead, [this]() {
// append all available data
m_ReadBuffer += m_Socket->readAll();
// process the read buffer
ProcessReadBuffer();
});
}
void RGPInterop::CreateMapping(const rdcarray<ActionDescription> &actions)
{
const SDFile &file = m_Ctx.GetStructuredFile();
for(const ActionDescription &action : actions)
{
for(const APIEvent &ev : action.events)
{
if(ev.chunkIndex == 0 || ev.chunkIndex == APIEvent::NoChunk ||
ev.chunkIndex >= file.chunks.size())
continue;
const SDChunk *chunk = file.chunks[ev.chunkIndex];
if(m_EventNames.contains(chunk->name, Qt::CaseSensitive))
{
m_Event2RGP[ev.eventId].interoplinearid = (uint32_t)m_RGP2Event.size();
rdcstr n = chunk->name;
if(n.contains(':'))
n.erase(0, n.find_last_of(":") + 1);
n += "(";
bool first = true;
for(size_t i = 0; i < chunk->NumChildren(); i++)
{
const SDObject *o = chunk->GetChild(i);
if(o->type.flags & SDTypeFlags::Important)
{
if(!first)
n += ", ";
first = false;
n += ToStr(o->AsUInt32());
}
}
n += ")";
m_Event2RGP[ev.eventId].eventname = n;
m_RGP2Event.push_back(ev.eventId);
}
}
// if we have children, step into them first before going to our next sibling
if(!action.children.empty())
CreateMapping(action.children);
}
}
void RGPInterop::CreateMapping(uint32_t version)
{
m_Version = version;
if(m_Ctx.APIProps().pipelineType == GraphicsAPI::Vulkan)
{
if(version == 1)
{
m_EventNames << lit("vkCmdDispatch") << lit("vkCmdDraw") << lit("vkCmdDrawIndexed");
}
}
else if(m_Ctx.APIProps().pipelineType == GraphicsAPI::D3D12)
{
// these names must match those in DoStringise(const D3D12Chunk &el) for the chunks
if(version == 1)
{
m_EventNames << lit("ID3D12GraphicsCommandList::Dispatch")
<< lit("ID3D12GraphicsCommandList::DrawInstanced")
<< lit("ID3D12GraphicsCommandList::DrawIndexedInstanced");
}
}
// if we don't have any event names, this API doesn't have a mapping or this was an unrecognised
// version.
if(m_EventNames.isEmpty())
return;
m_Event2RGP.resize(m_Ctx.GetLastAction()->eventId + 1);
// linearId 0 is invalid, so map to eventId 0.
// the first real event will be linearId 1
m_RGP2Event.push_back(0);
CreateMapping(m_Ctx.CurRootActions());
}
QString RGPInterop::EncodeCommand(RGPCommand command, QVariantList params)
{
QString ret;
QString cmd = ToQStr(command);
ret += lit("command=%1\n").arg(cmd);
// iterate params in pair, name and value
for(int i = 0; i + 1 < params.count(); i += 2)
ret += QFormatStr("%1.%2=%3\n").arg(cmd).arg(params[i].toString()).arg(params[i + 1].toString());
ret += lit("endcommand=%1\n").arg(cmd);
return ret;
}
bool RGPInterop::DecodeCommand(QString command)
{
QStringList lines = command.trimmed().split(QLatin1Char('\n'));
if(lines[0].indexOf(lit("command=")) != 0 || lines.last().indexOf(lit("endcommand=")) != 0)
{
qWarning() << "Malformed RGP command:\n" << command;
return false;
}
QString commandName = lines[0].split(QLatin1Char('='))[1];
if(lines.last().split(QLatin1Char('='))[1] != commandName)
{
qWarning() << "Mismatch between command and endcommand:\n" << command;
return false;
}
lines.pop_front();
lines.pop_back();
QVariantList params;
QString prefix = commandName + lit(".");
for(QString &param : lines)
{
int eq = param.indexOf(QLatin1Char('='));
if(eq < 0)
{
qWarning() << "Malformed param: " << param;
continue;
}
QString key = param.left(eq);
QString value = param.mid(eq + 1);
if(!key.startsWith(prefix))
{
qWarning() << "Malformed param key for" << commandName << ": " << key;
continue;
}
key = key.mid(prefix.count());
params << key << value;
}
if(commandName == ToQStr(RGPCommand::SetEvent))
{
RGPInteropEvent ev;
ev.fromParams(m_Version, params);
EventSelected(ev);
return true;
}
else if(commandName == ToQStr(RGPCommand::Initialize))
{
RGPInteropInit init;
init.fromParams(m_Version, params);
// TODO: decode the params here. This will contain the interop
// version and the name of the tool connected to RenderDoc
return true;
}
else if(commandName == ToQStr(RGPCommand::Terminate))
{
// RGP has shut down so disconnect the socket etc
emit m_Socket->disconnected();
return true;
}
else
{
qWarning() << "Unrecognised command: " << commandName;
}
return false;
}
void RGPInterop::ProcessReadBuffer()
{
// we might have partial data, so wait until we have a full command
do
{
int idx = m_ReadBuffer.indexOf("endcommand=");
// if we don't have endcommand= yet, we don't have a full command
if(idx < 0)
return;
idx = m_ReadBuffer.indexOf('\n', idx);
// also break if we don't have the full line yet including newline.
if(idx < 0)
return;
// extract the command and decode as UTF-8
QString command = QString::fromUtf8(m_ReadBuffer.data(), idx + 1);
// remove the command from our buffer, to retain any partial subsequent command we might have
m_ReadBuffer.remove(0, idx + 1);
// process this command
DecodeCommand(command);
// loop again - we might have read multiple commands
} while(true);
}