| #include "retracer.h" |
| |
| #include "apitracecall.h" |
| #include "thumbnail.h" |
| |
| #include "image.hpp" |
| |
| #include "trace_profiler.hpp" |
| |
| #include <QDebug> |
| #include <QVariant> |
| #include <QList> |
| #include <QImage> |
| |
| #include "qubjson.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 override; |
| bool atEnd() const override; |
| bool waitForReadyRead(int msecs = -1) override; |
| |
| protected: |
| qint64 readData(char * data, qint64 maxSize) override; |
| qint64 writeData(const char * data, qint64 maxSize) override; |
| |
| 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(false), |
| m_doubleBuffered(true), |
| m_singlethread(false), |
| m_useCoreProfile(false), |
| m_msaaResolve(true), |
| m_captureState(false), |
| m_captureThumbnails(false), |
| m_captureCall(0), |
| m_profileGpu(false), |
| m_profileCpu(false), |
| m_profilePixels(false) |
| { |
| qRegisterMetaType<QList<ApiTraceError> >(); |
| } |
| |
| QString Retracer::fileName() const |
| { |
| return m_fileName; |
| } |
| |
| void Retracer::setFileName(const QString &name) |
| { |
| m_fileName = name; |
| } |
| |
| QString Retracer::remoteTarget() const |
| { |
| return m_remoteTarget; |
| } |
| |
| void Retracer::setRemoteTarget(const QString &host) |
| { |
| m_remoteTarget = host; |
| } |
| |
| void Retracer::setAPI(trace::API api) |
| { |
| m_api = api; |
| } |
| |
| bool Retracer::isBenchmarking() const |
| { |
| return m_benchmarking; |
| } |
| |
| void Retracer::setBenchmarking(bool bench) |
| { |
| m_benchmarking = bench; |
| } |
| |
| bool Retracer::isDoubleBuffered() const |
| { |
| return m_doubleBuffered; |
| } |
| |
| void Retracer::setDoubleBuffered(bool db) |
| { |
| m_doubleBuffered = db; |
| } |
| |
| bool Retracer::isSinglethread() const |
| { |
| return m_singlethread; |
| } |
| |
| void Retracer::setSinglethread(bool singlethread) |
| { |
| m_singlethread = singlethread; |
| } |
| |
| bool Retracer::isCoreProfile() const |
| { |
| return m_useCoreProfile; |
| } |
| |
| void Retracer::setCoreProfile(bool coreprofile) |
| { |
| m_useCoreProfile = coreprofile; |
| } |
| |
| 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; |
| } |
| |
| bool Retracer::isMsaaResolve() const |
| { |
| return m_msaaResolve; |
| } |
| |
| void Retracer::setMsaaResolve(bool resolve) |
| { |
| m_msaaResolve = resolve; |
| } |
| |
| void Retracer::setCaptureAtCallNumber(qlonglong num) |
| { |
| m_captureCall = num; |
| } |
| |
| qlonglong Retracer::captureAtCallNumber() const |
| { |
| return m_captureCall; |
| } |
| |
| bool Retracer::captureState() const |
| { |
| return m_captureState; |
| } |
| |
| void Retracer::setCaptureState(bool enable) |
| { |
| m_captureState = enable; |
| } |
| |
| bool Retracer::captureThumbnails() const |
| { |
| return m_captureThumbnails; |
| } |
| |
| void Retracer::setCaptureThumbnails(bool enable) |
| { |
| m_captureThumbnails = enable; |
| } |
| |
| void Retracer::addThumbnailToCapture(qlonglong num) |
| { |
| if (!m_thumbnailsToCapture.contains(num)) { |
| m_thumbnailsToCapture.append(num); |
| } |
| } |
| |
| void Retracer::resetThumbnailsToCapture() |
| { |
| m_thumbnailsToCapture.clear(); |
| } |
| |
| QString Retracer::thumbnailCallSet() |
| { |
| QString callSet; |
| |
| bool isFirst = true; |
| |
| foreach (qlonglong callIndex, m_thumbnailsToCapture) { |
| // TODO: detect contiguous ranges |
| if (!isFirst) { |
| callSet.append(QLatin1String(",")); |
| } else { |
| isFirst = false; |
| } |
| |
| //emit "callIndex" |
| callSet.append(QString::number(callIndex)); |
| } |
| |
| //qDebug() << QLatin1String("debug: call set to capture: ") << callSet; |
| return callSet; |
| } |
| |
| /** |
| * Starting point for the retracing thread. |
| * |
| * Overrides QThread::run(). |
| */ |
| void Retracer::run() |
| { |
| QString msg = QLatin1String("Replay finished!"); |
| |
| /* |
| * Construct command line |
| */ |
| |
| QString prog; |
| QStringList arguments; |
| |
| 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_singlethread) { |
| arguments << QLatin1String("--singlethread"); |
| } |
| |
| if (m_useCoreProfile) { |
| arguments << QLatin1String("--core"); |
| } |
| |
| if (!m_msaaResolve) { |
| arguments << QLatin1String("--msaa-no-resolve"); |
| } |
| |
| if (m_captureState) { |
| arguments << QLatin1String("-D"); |
| arguments << QString::number(m_captureCall); |
| arguments << QLatin1String("--dump-format"); |
| arguments << QLatin1String("ubjson"); |
| } else if (m_captureThumbnails) { |
| if (!m_thumbnailsToCapture.isEmpty()) { |
| arguments << QLatin1String("-S"); |
| arguments << thumbnailCallSet(); |
| } |
| 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("--sb"); |
| } |
| |
| if (m_benchmarking) { |
| arguments << QLatin1String("-b"); |
| } |
| } |
| |
| arguments << m_fileName; |
| |
| /* |
| * Support remote execution on a separate target. |
| */ |
| |
| if (m_remoteTarget.length() != 0) { |
| arguments.prepend(prog); |
| arguments.prepend(m_remoteTarget); |
| prog = QLatin1String("ssh"); |
| } |
| |
| /* |
| * Start the process. |
| */ |
| |
| { |
| QDebug debug(QtDebugMsg); |
| debug << "Running:"; |
| debug << prog; |
| foreach (const QString &argument, arguments) { |
| debug << argument; |
| } |
| } |
| |
| QProcess process; |
| |
| process.start(prog, arguments, QIODevice::ReadOnly); |
| if (!process.waitForStarted(-1)) { |
| emit finished(QLatin1String("Could not start process")); |
| return; |
| } |
| |
| /* |
| * Process standard output |
| */ |
| |
| ImageHash thumbnails; |
| QVariantMap parsedJson; |
| trace::Profile* profile = NULL; |
| |
| process.setReadChannel(QProcess::StandardOutput); |
| if (process.waitForReadyRead(-1)) { |
| BlockingIODevice io(&process); |
| |
| if (m_captureState) { |
| parsedJson = decodeUBJSONObject(&io).toMap(); |
| process.waitForFinished(-1); |
| } else if (m_captureThumbnails) { |
| /* |
| * Parse concatenated PNM images from output. |
| */ |
| |
| while (!io.atEnd()) { |
| image::PNMInfo info; |
| |
| 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, info); |
| |
| // if invalid PNM header was encountered, ... |
| if (headerEnd == NULL || |
| info.channelType != image::TYPE_UNORM8) { |
| qDebug() << "error: invalid snapshot stream encountered"; |
| break; |
| } |
| |
| unsigned channels = info.channels; |
| unsigned width = info.width; |
| unsigned height = info.height; |
| |
| // 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.insert(info.commentNumber, 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); |
| } |
| } |
| } |
| |
| /* |
| * Wait for process termination |
| */ |
| |
| process.waitForFinished(-1); |
| |
| if (process.exitStatus() != QProcess::NormalExit) { |
| msg = QLatin1String("Process crashed"); |
| } else if (process.exitCode() != 0) { |
| msg = QLatin1String("Process exited with non zero exit code"); |
| } |
| |
| /* |
| * 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); |
| } |
| } |
| |
| /* |
| * Emit signals |
| */ |
| |
| if (m_captureState) { |
| ApiTraceState *state = new ApiTraceState(parsedJson); |
| emit foundState(state); |
| } |
| |
| if (m_captureThumbnails && !thumbnails.isEmpty()) { |
| emit foundThumbnails(thumbnails); |
| } |
| |
| if (isProfiling() && profile) { |
| emit foundProfile(profile); |
| } |
| |
| if (!errors.isEmpty()) { |
| emit retraceErrors(errors); |
| } |
| |
| emit finished(msg); |
| } |
| |
| #include "retracer.moc" |