]> git.cworth.org Git - apitrace/blob - gui/retracer.cpp
738367e1830c394f2808ab8a234355dd8de8e48a
[apitrace] / gui / retracer.cpp
1 #include "retracer.h"
2
3 #include "apitracecall.h"
4 #include "thumbnail.h"
5
6 #include "image/image.hpp"
7
8 #include "trace_profiler.hpp"
9
10 #include <QDebug>
11 #include <QVariant>
12 #include <QList>
13 #include <QImage>
14
15 #include <qjson/parser.h>
16
17 /**
18  * Wrapper around a QProcess which enforces IO to block .
19  *
20  * Several QIODevice users (notably QJSON) expect blocking semantics, e.g.,
21  * they expect that QIODevice::read() will blocked until the requested ammount
22  * of bytes is read or end of file is reached. But by default QProcess, does
23  * not block.  And passing QIODevice::Unbuffered mitigates but does not fully
24  * address the problem either.
25  *
26  * This class wraps around QProcess, providing QIODevice interface, while
27  * ensuring that all reads block.
28  *
29  * This class also works around a bug in QProcess::atEnd() implementation.
30  *
31  * See also:
32  * - http://qt-project.org/wiki/Simple_Crypt_IO_Device
33  * - http://qt-project.org/wiki/Custom_IO_Device
34  */
35 class BlockingIODevice : public QIODevice
36 {
37     /* We don't use the Q_OBJECT in this class given we don't declare any
38      * signals and slots or use any other services provided by Qt's meta-object
39      * system. */
40 public:
41     BlockingIODevice(QProcess * io);
42     bool isSequential() const;
43     bool atEnd() const;
44     bool waitForReadyRead(int msecs = -1);
45
46 protected:
47     qint64 readData(char * data, qint64 maxSize);
48     qint64 writeData(const char * data, qint64 maxSize);
49
50 private:
51     QProcess *m_device;
52 };
53
54 BlockingIODevice::BlockingIODevice(QProcess * io) :
55     m_device(io)
56 {
57     /*
58      * We pass QIODevice::Unbuffered to prevent the base QIODevice class to do
59      * its own buffering on top of the overridden readData() method.
60      *
61      * The only buffering used will be to satisfy QIODevice::peek() and
62      * QIODevice::ungetChar().
63      */
64     setOpenMode(ReadOnly | Unbuffered);
65 }
66
67 bool BlockingIODevice::isSequential() const
68 {
69     return true;
70 }
71
72 bool BlockingIODevice::atEnd() const
73 {
74     /*
75      * XXX: QProcess::atEnd() documentation is wrong -- it will return true
76      * even when the process is running --, so we try to workaround that here.
77      */
78     if (m_device->atEnd()) {
79         if (m_device->state() == QProcess::Running) {
80             if (!m_device->waitForReadyRead(-1)) {
81                 return true;
82             }
83         }
84     }
85     return false;
86 }
87
88 bool BlockingIODevice::waitForReadyRead(int msecs)
89 {
90     Q_UNUSED(msecs);
91     return true;
92 }
93
94 qint64 BlockingIODevice::readData(char * data, qint64 maxSize)
95 {
96     qint64 bytesToRead = maxSize;
97     qint64 readSoFar = 0;
98     do {
99         qint64 chunkSize = m_device->read(data + readSoFar, bytesToRead);
100         if (chunkSize < 0) {
101             if (readSoFar) {
102                 return readSoFar;
103             } else {
104                 return chunkSize;
105             }
106         }
107         Q_ASSERT(chunkSize <= bytesToRead);
108         bytesToRead -= chunkSize;
109         readSoFar += chunkSize;
110         if (bytesToRead) {
111             if (!m_device->waitForReadyRead(-1)) {
112                 qDebug() << "waitForReadyRead failed\n";
113                 break;
114             }
115         }
116     } while(bytesToRead);
117
118     return readSoFar;
119 }
120
121 qint64 BlockingIODevice::writeData(const char * data, qint64 maxSize)
122 {
123     Q_ASSERT(false);
124     return -1;
125 }
126
127 Q_DECLARE_METATYPE(QList<ApiTraceError>);
128
129 Retracer::Retracer(QObject *parent)
130     : QThread(parent),
131       m_benchmarking(false),
132       m_doubleBuffered(true),
133       m_captureState(false),
134       m_captureCall(0),
135       m_profileGpu(false),
136       m_profileCpu(false),
137       m_profilePixels(false)
138 {
139     qRegisterMetaType<QList<ApiTraceError> >();
140
141 #ifdef Q_OS_WIN
142     QString format = QLatin1String("%1;");
143 #else
144     QString format = QLatin1String("%1:");
145 #endif
146     QString buildPath = format.arg(APITRACE_BINARY_DIR);
147     m_processEnvironment = QProcessEnvironment::systemEnvironment();
148     m_processEnvironment.insert("PATH", buildPath +
149                                 m_processEnvironment.value("PATH"));
150
151     qputenv("PATH",
152             m_processEnvironment.value("PATH").toLatin1());
153 }
154
155 QString Retracer::fileName() const
156 {
157     return m_fileName;
158 }
159
160 void Retracer::setFileName(const QString &name)
161 {
162     m_fileName = name;
163 }
164
165 void Retracer::setAPI(trace::API api)
166 {
167     m_api = api;
168 }
169
170 bool Retracer::isBenchmarking() const
171 {
172     return m_benchmarking;
173 }
174
175 void Retracer::setBenchmarking(bool bench)
176 {
177     m_benchmarking = bench;
178 }
179
180 bool Retracer::isDoubleBuffered() const
181 {
182     return m_doubleBuffered;
183 }
184
185 void Retracer::setDoubleBuffered(bool db)
186 {
187     m_doubleBuffered = db;
188 }
189
190 bool Retracer::isProfilingGpu() const
191 {
192     return m_profileGpu;
193 }
194
195 bool Retracer::isProfilingCpu() const
196 {
197     return m_profileCpu;
198 }
199
200 bool Retracer::isProfilingPixels() const
201 {
202     return m_profilePixels;
203 }
204
205 bool Retracer::isProfiling() const
206 {
207     return m_profileGpu || m_profileCpu || m_profilePixels;
208 }
209
210 void Retracer::setProfiling(bool gpu, bool cpu, bool pixels)
211 {
212     m_profileGpu = gpu;
213     m_profileCpu = cpu;
214     m_profilePixels = pixels;
215 }
216
217 void Retracer::setCaptureAtCallNumber(qlonglong num)
218 {
219     m_captureCall = num;
220 }
221
222 qlonglong Retracer::captureAtCallNumber() const
223 {
224     return m_captureCall;
225 }
226
227 bool Retracer::captureState() const
228 {
229     return m_captureState;
230 }
231
232 void Retracer::setCaptureState(bool enable)
233 {
234     m_captureState = enable;
235 }
236
237 bool Retracer::captureThumbnails() const
238 {
239     return m_captureThumbnails;
240 }
241
242 void Retracer::setCaptureThumbnails(bool enable)
243 {
244     m_captureThumbnails = enable;
245 }
246
247 /**
248  * Starting point for the retracing thread.
249  *
250  * Overrides QThread::run().
251  */
252 void Retracer::run()
253 {
254     QString msg = QLatin1String("Replay finished!");
255
256     /*
257      * Construct command line
258      */
259
260     QString prog;
261     QStringList arguments;
262
263     switch (m_api) {
264     case trace::API_GL:
265         prog = QLatin1String("glretrace");
266         break;
267     case trace::API_EGL:
268         prog = QLatin1String("eglretrace");
269         break;
270     case trace::API_DX:
271     case trace::API_D3D7:
272     case trace::API_D3D8:
273     case trace::API_D3D9:
274     case trace::API_DXGI:
275 #ifdef Q_OS_WIN
276         prog = QLatin1String("d3dretrace");
277 #else
278         prog = QLatin1String("wine");
279         arguments << QLatin1String("d3dretrace.exe");
280 #endif
281         break;
282     default:
283         emit finished(QLatin1String("Unsupported API"));
284         return;
285     }
286
287     if (m_captureState) {
288         arguments << QLatin1String("-D");
289         arguments << QString::number(m_captureCall);
290     } else if (m_captureThumbnails) {
291         arguments << QLatin1String("-s"); // emit snapshots
292         arguments << QLatin1String("-"); // emit to stdout
293     } else if (isProfiling()) {
294         if (m_profileGpu) {
295             arguments << QLatin1String("--pgpu");
296         }
297
298         if (m_profileCpu) {
299             arguments << QLatin1String("--pcpu");
300         }
301
302         if (m_profilePixels) {
303             arguments << QLatin1String("--ppd");
304         }
305     } else {
306         if (m_doubleBuffered) {
307             arguments << QLatin1String("--db");
308         } else {
309             arguments << QLatin1String("--sb");
310         }
311
312         if (m_benchmarking) {
313             arguments << QLatin1String("-b");
314         }
315     }
316
317     arguments << m_fileName;
318
319     /*
320      * Start the process.
321      */
322
323     QProcess process;
324
325     process.start(prog, arguments, QIODevice::ReadOnly);
326     if (!process.waitForStarted(-1)) {
327         emit finished(QLatin1String("Could not start process"));
328         return;
329     }
330
331     /*
332      * Process standard output
333      */
334
335     QList<QImage> thumbnails;
336     QVariantMap parsedJson;
337     trace::Profile* profile = NULL;
338
339     process.setReadChannel(QProcess::StandardOutput);
340     if (process.waitForReadyRead(-1)) {
341         BlockingIODevice io(&process);
342
343         if (m_captureState) {
344             /*
345              * Parse JSON from the output.
346              *
347              * XXX: QJSON's scanner is inneficient as it abuses single
348              * character QIODevice::peek (not cheap), instead of maintaining a
349              * lookahead character on its own.
350              */
351
352             bool ok = false;
353             QJson::Parser jsonParser;
354
355             // Allow Nan/Infinity
356             jsonParser.allowSpecialNumbers(true);
357 #if 0
358             parsedJson = jsonParser.parse(&io, &ok).toMap();
359 #else
360             /*
361              * XXX: QJSON expects blocking IO, and it looks like
362              * BlockingIODevice does not work reliably in all cases.
363              */
364             process.waitForFinished(-1);
365             parsedJson = jsonParser.parse(&process, &ok).toMap();
366 #endif
367             if (!ok) {
368                 msg = QLatin1String("failed to parse JSON");
369             }
370         } else if (m_captureThumbnails) {
371             /*
372              * Parse concatenated PNM images from output.
373              */
374
375             while (!io.atEnd()) {
376                 unsigned channels = 0;
377                 unsigned width = 0;
378                 unsigned height = 0;
379
380                 char header[512];
381                 qint64 headerSize = 0;
382                 int headerLines = 3; // assume no optional comment line
383
384                 for (int headerLine = 0; headerLine < headerLines; ++headerLine) {
385                     qint64 headerRead = io.readLine(&header[headerSize], sizeof(header) - headerSize);
386
387                     // if header actually contains optional comment line, ...
388                     if (headerLine == 1 && header[headerSize] == '#') {
389                         ++headerLines;
390                     }
391
392                     headerSize += headerRead;
393                 }
394
395                 const char *headerEnd = image::readPNMHeader(header, headerSize, &channels, &width, &height);
396
397                 // if invalid PNM header was encountered, ...
398                 if (header == headerEnd) {
399                     qDebug() << "error: invalid snapshot stream encountered";
400                     break;
401                 }
402
403                 // qDebug() << "channels: " << channels << ", width: " << width << ", height: " << height";
404
405                 QImage snapshot = QImage(width, height, channels == 1 ? QImage::Format_Mono : QImage::Format_RGB888);
406
407                 int rowBytes = channels * width;
408                 for (int y = 0; y < height; ++y) {
409                     unsigned char *scanLine = snapshot.scanLine(y);
410                     qint64 readBytes = io.read((char *) scanLine, rowBytes);
411                     Q_ASSERT(readBytes == rowBytes);
412                     (void)readBytes;
413                 }
414
415                 QImage thumb = thumbnail(snapshot);
416                 thumbnails.append(thumb);
417             }
418
419             Q_ASSERT(process.state() != QProcess::Running);
420         } else if (isProfiling()) {
421             profile = new trace::Profile();
422
423             while (!io.atEnd()) {
424                 char line[256];
425                 qint64 lineLength;
426
427                 lineLength = io.readLine(line, 256);
428
429                 if (lineLength == -1)
430                     break;
431
432                 trace::Profiler::parseLine(line, profile);
433             }
434         } else {
435             QByteArray output;
436             output = process.readAllStandardOutput();
437             if (output.length() < 80) {
438                 msg = QString::fromUtf8(output);
439             }
440         }
441     }
442
443     /*
444      * Wait for process termination
445      */
446
447     process.waitForFinished(-1);
448
449     if (process.exitStatus() != QProcess::NormalExit) {
450         msg = QLatin1String("Process crashed");
451     } else if (process.exitCode() != 0) {
452         msg = QLatin1String("Process exited with non zero exit code");
453     }
454
455     /*
456      * Parse errors.
457      */
458
459     QList<ApiTraceError> errors;
460     process.setReadChannel(QProcess::StandardError);
461     QRegExp regexp("(^\\d+): +(\\b\\w+\\b): ([^\\r\\n]+)[\\r\\n]*$");
462     while (!process.atEnd()) {
463         QString line = process.readLine();
464         if (regexp.indexIn(line) != -1) {
465             ApiTraceError error;
466             error.callIndex = regexp.cap(1).toInt();
467             error.type = regexp.cap(2);
468             error.message = regexp.cap(3);
469             errors.append(error);
470         } else if (!errors.isEmpty()) {
471             // Probably a multiligne message
472             ApiTraceError &previous = errors.last();
473             if (line.endsWith("\n")) {
474                 line.chop(1);
475             }
476             previous.message.append('\n');
477             previous.message.append(line);
478         }
479     }
480
481     /*
482      * Emit signals
483      */
484
485     if (m_captureState) {
486         ApiTraceState *state = new ApiTraceState(parsedJson);
487         emit foundState(state);
488         msg = QLatin1String("State fetched.");
489     }
490
491     if (m_captureThumbnails && !thumbnails.isEmpty()) {
492         emit foundThumbnails(thumbnails);
493     }
494
495     if (isProfiling() && profile) {
496         emit foundProfile(profile);
497     }
498
499     if (!errors.isEmpty()) {
500         emit retraceErrors(errors);
501     }
502
503     emit finished(msg);
504 }
505
506 #include "retracer.moc"