]> git.cworth.org Git - apitrace/blobdiff - gui/retracer.cpp
qapitrace: Adjust PATH only once and for all.
[apitrace] / gui / retracer.cpp
index f28f479b9e3a4b0af9db4d6a9665f51fd8661dbf..2928ed63f321f31e34667e2448e1b8f765f2fdbf 100644 (file)
 #include "retracer.h"
 
 #include "apitracecall.h"
+#include "thumbnail.h"
+
+#include "image/image.hpp"
+
+#include "trace_profiler.hpp"
 
 #include <QDebug>
 #include <QVariant>
+#include <QList>
+#include <QImage>
 
 #include <qjson/parser.h>
 
+/**
+ * Wrapper around a QProcess which enforces IO to block .
+ *
+ * Several QIODevice users (notably QJSON) expect blocking semantics, e.g.,
+ * they expect that QIODevice::read() will blocked until the requested ammount
+ * of bytes is read or end of file is reached. But by default QProcess, does
+ * not block.  And passing QIODevice::Unbuffered mitigates but does not fully
+ * address the problem either.
+ *
+ * This class wraps around QProcess, providing QIODevice interface, while
+ * ensuring that all reads block.
+ *
+ * This class also works around a bug in QProcess::atEnd() implementation.
+ *
+ * See also:
+ * - http://qt-project.org/wiki/Simple_Crypt_IO_Device
+ * - http://qt-project.org/wiki/Custom_IO_Device
+ */
+class BlockingIODevice : public QIODevice
+{
+    /* We don't use the Q_OBJECT in this class given we don't declare any
+     * signals and slots or use any other services provided by Qt's meta-object
+     * system. */
+public:
+    BlockingIODevice(QProcess * io);
+    bool isSequential() const;
+    bool atEnd() const;
+    bool waitForReadyRead(int msecs = -1);
+
+protected:
+    qint64 readData(char * data, qint64 maxSize);
+    qint64 writeData(const char * data, qint64 maxSize);
+
+private:
+    QProcess *m_device;
+};
+
+BlockingIODevice::BlockingIODevice(QProcess * io) :
+    m_device(io)
+{
+    /*
+     * We pass QIODevice::Unbuffered to prevent the base QIODevice class to do
+     * its own buffering on top of the overridden readData() method.
+     *
+     * The only buffering used will be to satisfy QIODevice::peek() and
+     * QIODevice::ungetChar().
+     */
+    setOpenMode(ReadOnly | Unbuffered);
+}
+
+bool BlockingIODevice::isSequential() const
+{
+    return true;
+}
+
+bool BlockingIODevice::atEnd() const
+{
+    /*
+     * XXX: QProcess::atEnd() documentation is wrong -- it will return true
+     * even when the process is running --, so we try to workaround that here.
+     */
+    if (m_device->atEnd()) {
+        if (m_device->state() == QProcess::Running) {
+            if (!m_device->waitForReadyRead(-1)) {
+                return true;
+            }
+        }
+    }
+    return false;
+}
+
+bool BlockingIODevice::waitForReadyRead(int msecs)
+{
+    Q_UNUSED(msecs);
+    return true;
+}
+
+qint64 BlockingIODevice::readData(char * data, qint64 maxSize)
+{
+    qint64 bytesToRead = maxSize;
+    qint64 readSoFar = 0;
+    do {
+        qint64 chunkSize = m_device->read(data + readSoFar, bytesToRead);
+        if (chunkSize < 0) {
+            if (readSoFar) {
+                return readSoFar;
+            } else {
+                return chunkSize;
+            }
+        }
+        Q_ASSERT(chunkSize <= bytesToRead);
+        bytesToRead -= chunkSize;
+        readSoFar += chunkSize;
+        if (bytesToRead) {
+            if (!m_device->waitForReadyRead(-1)) {
+                qDebug() << "waitForReadyRead failed\n";
+                break;
+            }
+        }
+    } while(bytesToRead);
+
+    return readSoFar;
+}
+
+qint64 BlockingIODevice::writeData(const char * data, qint64 maxSize)
+{
+    Q_ASSERT(false);
+    return -1;
+}
+
+Q_DECLARE_METATYPE(QList<ApiTraceError>);
+
 Retracer::Retracer(QObject *parent)
     : QThread(parent),
-      m_benchmarking(true),
+      m_benchmarking(false),
       m_doubleBuffered(true),
       m_captureState(false),
-      m_captureCall(0)
+      m_captureCall(0),
+      m_profileGpu(false),
+      m_profileCpu(false),
+      m_profilePixels(false)
 {
-#ifdef Q_OS_WIN
-    QString format = QLatin1String("%1;");
-#else
-    QString format = QLatin1String("%1:");
-#endif
-    QString buildPath = format.arg(BUILD_DIR);
-    m_processEnvironment = QProcessEnvironment::systemEnvironment();
-    m_processEnvironment.insert("PATH", buildPath +
-                                m_processEnvironment.value("PATH"));
-
-    qputenv("PATH",
-            m_processEnvironment.value("PATH").toLatin1());
+    qRegisterMetaType<QList<ApiTraceError> >();
 }
 
 QString Retracer::fileName() const
@@ -38,6 +149,11 @@ void Retracer::setFileName(const QString &name)
     m_fileName = name;
 }
 
+void Retracer::setAPI(trace::API api)
+{
+    m_api = api;
+}
+
 bool Retracer::isBenchmarking() const
 {
     return m_benchmarking;
@@ -58,6 +174,33 @@ void Retracer::setDoubleBuffered(bool db)
     m_doubleBuffered = db;
 }
 
+bool Retracer::isProfilingGpu() const
+{
+    return m_profileGpu;
+}
+
+bool Retracer::isProfilingCpu() const
+{
+    return m_profileCpu;
+}
+
+bool Retracer::isProfilingPixels() const
+{
+    return m_profilePixels;
+}
+
+bool Retracer::isProfiling() const
+{
+    return m_profileGpu || m_profileCpu || m_profilePixels;
+}
+
+void Retracer::setProfiling(bool gpu, bool cpu, bool pixels)
+{
+    m_profileGpu = gpu;
+    m_profileCpu = cpu;
+    m_profilePixels = pixels;
+}
+
 void Retracer::setCaptureAtCallNumber(qlonglong num)
 {
     m_captureCall = num;
@@ -78,54 +221,81 @@ void Retracer::setCaptureState(bool enable)
     m_captureState = enable;
 }
 
-
-void Retracer::run()
+bool Retracer::captureThumbnails() const
 {
-    RetraceProcess *retrace = new RetraceProcess();
-    retrace->process()->setProcessEnvironment(m_processEnvironment);
-
-    retrace->setFileName(m_fileName);
-    retrace->setBenchmarking(m_benchmarking);
-    retrace->setDoubleBuffered(m_doubleBuffered);
-    retrace->setCaptureState(m_captureState);
-    retrace->setCaptureAtCallNumber(m_captureCall);
-
-    connect(retrace, SIGNAL(finished(const QString&)),
-            this, SLOT(cleanup()));
-    connect(retrace, SIGNAL(error(const QString&)),
-            this, SLOT(cleanup()));
-    connect(retrace, SIGNAL(finished(const QString&)),
-            this, SIGNAL(finished(const QString&)));
-    connect(retrace, SIGNAL(error(const QString&)),
-            this, SIGNAL(error(const QString&)));
-    connect(retrace, SIGNAL(foundState(const ApiTraceState&)),
-            this, SIGNAL(foundState(const ApiTraceState&)));
-
-    retrace->start();
-
-    exec();
-
-    /* means we need to kill the process */
-    if (retrace->process()->state() != QProcess::NotRunning) {
-        retrace->terminate();
-    }
-
-    delete retrace;
+    return m_captureThumbnails;
 }
 
+void Retracer::setCaptureThumbnails(bool enable)
+{
+    m_captureThumbnails = enable;
+}
 
-void RetraceProcess::start()
+/**
+ * Starting point for the retracing thread.
+ *
+ * Overrides QThread::run().
+ */
+void Retracer::run()
 {
+    QString msg = QLatin1String("Replay finished!");
+
+    /*
+     * Construct command line
+     */
+
+    QString prog;
     QStringList arguments;
 
-    if (m_doubleBuffered) {
-        arguments << QLatin1String("-db");
+    switch (m_api) {
+    case trace::API_GL:
+        prog = QLatin1String("glretrace");
+        break;
+    case trace::API_EGL:
+        prog = QLatin1String("eglretrace");
+        break;
+    case trace::API_DX:
+    case trace::API_D3D7:
+    case trace::API_D3D8:
+    case trace::API_D3D9:
+    case trace::API_DXGI:
+#ifdef Q_OS_WIN
+        prog = QLatin1String("d3dretrace");
+#else
+        prog = QLatin1String("wine");
+        arguments << QLatin1String("d3dretrace.exe");
+#endif
+        break;
+    default:
+        emit finished(QLatin1String("Unsupported API"));
+        return;
     }
 
     if (m_captureState) {
         arguments << QLatin1String("-D");
         arguments << QString::number(m_captureCall);
+    } else if (m_captureThumbnails) {
+        arguments << QLatin1String("-s"); // emit snapshots
+        arguments << QLatin1String("-"); // emit to stdout
+    } else if (isProfiling()) {
+        if (m_profileGpu) {
+            arguments << QLatin1String("--pgpu");
+        }
+
+        if (m_profileCpu) {
+            arguments << QLatin1String("--pcpu");
+        }
+
+        if (m_profilePixels) {
+            arguments << QLatin1String("--ppd");
+        }
     } else {
+        if (m_doubleBuffered) {
+            arguments << QLatin1String("--db");
+        } else {
+            arguments << QLatin1String("--sb");
+        }
+
         if (m_benchmarking) {
             arguments << QLatin1String("-b");
         }
@@ -133,127 +303,191 @@ void RetraceProcess::start()
 
     arguments << m_fileName;
 
-    m_process->start(QLatin1String("glretrace"), arguments);
-}
+    /*
+     * Start the process.
+     */
 
+    QProcess process;
 
-void RetraceProcess::replayFinished()
-{
-    QByteArray output = m_process->readAllStandardOutput();
-    QString msg;
-
-#if 0
-    qDebug()<<"Process finished = ";
-    qDebug()<<"\terr = "<<m_process->readAllStandardError();
-    qDebug()<<"\tout = "<<output;
-#endif
-    if (m_captureState) {
-        bool ok = false;
-        QVariantMap parsedJson = m_jsonParser->parse(output, &ok).toMap();
-        ApiTraceState state(parsedJson);
-        emit foundState(state);
-        msg = tr("State fetched.");
-    } else {
-        msg = QString::fromUtf8(output);
+    process.start(prog, arguments, QIODevice::ReadOnly);
+    if (!process.waitForStarted(-1)) {
+        emit finished(QLatin1String("Could not start process"));
+        return;
     }
 
-    emit finished(msg);
-}
+    /*
+     * Process standard output
+     */
 
-void RetraceProcess::replayError(QProcess::ProcessError err)
-{
-    qDebug()<<"Process error = "<<err;
-    qDebug()<<"\terr = "<<m_process->readAllStandardError();
-    qDebug()<<"\tout = "<<m_process->readAllStandardOutput();
-
-    emit error(
-        tr("Couldn't execute the replay file '%1'").arg(m_fileName));
-}
-
-
-RetraceProcess::RetraceProcess(QObject *parent)
-    : QObject(parent)
-{
-    m_process = new QProcess(this);
-    m_jsonParser = new QJson::Parser();
-
-    connect(m_process, SIGNAL(finished(int, QProcess::ExitStatus)),
-            this, SLOT(replayFinished()));
-    connect(m_process, SIGNAL(error(QProcess::ProcessError)),
-            this, SLOT(replayError(QProcess::ProcessError)));
-}
+    QList<QImage> thumbnails;
+    QVariantMap parsedJson;
+    trace::Profile* profile = NULL;
 
-QProcess * RetraceProcess::process() const
-{
-    return m_process;
-}
+    process.setReadChannel(QProcess::StandardOutput);
+    if (process.waitForReadyRead(-1)) {
+        BlockingIODevice io(&process);
 
-QString RetraceProcess::fileName() const
-{
-    return m_fileName;
-}
+        if (m_captureState) {
+            /*
+             * Parse JSON from the output.
+             *
+             * XXX: QJSON's scanner is inneficient as it abuses single
+             * character QIODevice::peek (not cheap), instead of maintaining a
+             * lookahead character on its own.
+             */
 
-void RetraceProcess::setFileName(const QString &name)
-{
-    m_fileName = name;
-}
+            bool ok = false;
+            QJson::Parser jsonParser;
 
-bool RetraceProcess::isBenchmarking() const
-{
-    return m_benchmarking;
-}
+            // Allow Nan/Infinity
+            jsonParser.allowSpecialNumbers(true);
+#if 0
+            parsedJson = jsonParser.parse(&io, &ok).toMap();
+#else
+            /*
+             * XXX: QJSON expects blocking IO, and it looks like
+             * BlockingIODevice does not work reliably in all cases.
+             */
+            process.waitForFinished(-1);
+            parsedJson = jsonParser.parse(&process, &ok).toMap();
+#endif
+            if (!ok) {
+                msg = QLatin1String("failed to parse JSON");
+            }
+        } else if (m_captureThumbnails) {
+            /*
+             * Parse concatenated PNM images from output.
+             */
+
+            while (!io.atEnd()) {
+                unsigned channels = 0;
+                unsigned width = 0;
+                unsigned height = 0;
+
+                char header[512];
+                qint64 headerSize = 0;
+                int headerLines = 3; // assume no optional comment line
+
+                for (int headerLine = 0; headerLine < headerLines; ++headerLine) {
+                    qint64 headerRead = io.readLine(&header[headerSize], sizeof(header) - headerSize);
+
+                    // if header actually contains optional comment line, ...
+                    if (headerLine == 1 && header[headerSize] == '#') {
+                        ++headerLines;
+                    }
+
+                    headerSize += headerRead;
+                }
+
+                const char *headerEnd = image::readPNMHeader(header, headerSize, &channels, &width, &height);
+
+                // if invalid PNM header was encountered, ...
+                if (header == headerEnd) {
+                    qDebug() << "error: invalid snapshot stream encountered";
+                    break;
+                }
+
+                // qDebug() << "channels: " << channels << ", width: " << width << ", height: " << height";
+
+                QImage snapshot = QImage(width, height, channels == 1 ? QImage::Format_Mono : QImage::Format_RGB888);
+
+                int rowBytes = channels * width;
+                for (int y = 0; y < height; ++y) {
+                    unsigned char *scanLine = snapshot.scanLine(y);
+                    qint64 readBytes = io.read((char *) scanLine, rowBytes);
+                    Q_ASSERT(readBytes == rowBytes);
+                    (void)readBytes;
+                }
+
+                QImage thumb = thumbnail(snapshot);
+                thumbnails.append(thumb);
+            }
+
+            Q_ASSERT(process.state() != QProcess::Running);
+        } else if (isProfiling()) {
+            profile = new trace::Profile();
+
+            while (!io.atEnd()) {
+                char line[256];
+                qint64 lineLength;
+
+                lineLength = io.readLine(line, 256);
+
+                if (lineLength == -1)
+                    break;
+
+                trace::Profiler::parseLine(line, profile);
+            }
+        } else {
+            QByteArray output;
+            output = process.readAllStandardOutput();
+            if (output.length() < 80) {
+                msg = QString::fromUtf8(output);
+            }
+        }
+    }
 
-void RetraceProcess::setBenchmarking(bool bench)
-{
-    m_benchmarking = bench;
-}
+    /*
+     * Wait for process termination
+     */
 
-bool RetraceProcess::isDoubleBuffered() const
-{
-    return m_doubleBuffered;
-}
+    process.waitForFinished(-1);
 
-void RetraceProcess::setDoubleBuffered(bool db)
-{
-    m_doubleBuffered = db;
-}
+    if (process.exitStatus() != QProcess::NormalExit) {
+        msg = QLatin1String("Process crashed");
+    } else if (process.exitCode() != 0) {
+        msg = QLatin1String("Process exited with non zero exit code");
+    }
 
-void RetraceProcess::setCaptureAtCallNumber(qlonglong num)
-{
-    m_captureCall = num;
-}
+    /*
+     * Parse errors.
+     */
+
+    QList<ApiTraceError> errors;
+    process.setReadChannel(QProcess::StandardError);
+    QRegExp regexp("(^\\d+): +(\\b\\w+\\b): ([^\\r\\n]+)[\\r\\n]*$");
+    while (!process.atEnd()) {
+        QString line = process.readLine();
+        if (regexp.indexIn(line) != -1) {
+            ApiTraceError error;
+            error.callIndex = regexp.cap(1).toInt();
+            error.type = regexp.cap(2);
+            error.message = regexp.cap(3);
+            errors.append(error);
+        } else if (!errors.isEmpty()) {
+            // Probably a multiligne message
+            ApiTraceError &previous = errors.last();
+            if (line.endsWith("\n")) {
+                line.chop(1);
+            }
+            previous.message.append('\n');
+            previous.message.append(line);
+        }
+    }
 
-qlonglong RetraceProcess::captureAtCallNumber() const
-{
-    return m_captureCall;
-}
+    /*
+     * Emit signals
+     */
 
-bool RetraceProcess::captureState() const
-{
-    return m_captureState;
-}
+    if (m_captureState) {
+        ApiTraceState *state = new ApiTraceState(parsedJson);
+        emit foundState(state);
+        msg = QLatin1String("State fetched.");
+    }
 
-void RetraceProcess::setCaptureState(bool enable)
-{
-    m_captureState = enable;
-}
+    if (m_captureThumbnails && !thumbnails.isEmpty()) {
+        emit foundThumbnails(thumbnails);
+    }
 
-void RetraceProcess::terminate()
-{
-    if (m_process) {
-        m_process->terminate();
-        emit finished(tr("Process terminated."));
+    if (isProfiling() && profile) {
+        emit foundProfile(profile);
     }
-}
 
-void Retracer::cleanup()
-{
-    quit();
-}
+    if (!errors.isEmpty()) {
+        emit retraceErrors(errors);
+    }
 
-RetraceProcess::~RetraceProcess()
-{
-    delete m_jsonParser;
+    emit finished(msg);
 }
 
 #include "retracer.moc"