]> git.cworth.org Git - apitrace/blob - gui/retracer.cpp
image: Support reading PFM images.
[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_singlethread(false),
134       m_captureState(false),
135       m_captureCall(0),
136       m_profileGpu(false),
137       m_profileCpu(false),
138       m_profilePixels(false)
139 {
140     qRegisterMetaType<QList<ApiTraceError> >();
141 }
142
143 QString Retracer::fileName() const
144 {
145     return m_fileName;
146 }
147
148 void Retracer::setFileName(const QString &name)
149 {
150     m_fileName = name;
151 }
152
153 QString Retracer::remoteTarget() const
154 {
155     return m_remoteTarget;
156 }
157
158 void Retracer::setRemoteTarget(const QString &host)
159 {
160     m_remoteTarget = host;
161 }
162
163 void Retracer::setAPI(trace::API api)
164 {
165     m_api = api;
166 }
167
168 bool Retracer::isBenchmarking() const
169 {
170     return m_benchmarking;
171 }
172
173 void Retracer::setBenchmarking(bool bench)
174 {
175     m_benchmarking = bench;
176 }
177
178 bool Retracer::isDoubleBuffered() const
179 {
180     return m_doubleBuffered;
181 }
182
183 void Retracer::setDoubleBuffered(bool db)
184 {
185     m_doubleBuffered = db;
186 }
187
188 bool Retracer::isSinglethread() const
189 {
190     return m_singlethread;
191 }
192
193 void Retracer::setSinglethread(bool singlethread)
194 {
195     m_singlethread = singlethread;
196 }
197
198 bool Retracer::isProfilingGpu() const
199 {
200     return m_profileGpu;
201 }
202
203 bool Retracer::isProfilingCpu() const
204 {
205     return m_profileCpu;
206 }
207
208 bool Retracer::isProfilingPixels() const
209 {
210     return m_profilePixels;
211 }
212
213 bool Retracer::isProfiling() const
214 {
215     return m_profileGpu || m_profileCpu || m_profilePixels;
216 }
217
218 void Retracer::setProfiling(bool gpu, bool cpu, bool pixels)
219 {
220     m_profileGpu = gpu;
221     m_profileCpu = cpu;
222     m_profilePixels = pixels;
223 }
224
225 void Retracer::setCaptureAtCallNumber(qlonglong num)
226 {
227     m_captureCall = num;
228 }
229
230 qlonglong Retracer::captureAtCallNumber() const
231 {
232     return m_captureCall;
233 }
234
235 bool Retracer::captureState() const
236 {
237     return m_captureState;
238 }
239
240 void Retracer::setCaptureState(bool enable)
241 {
242     m_captureState = enable;
243 }
244
245 bool Retracer::captureThumbnails() const
246 {
247     return m_captureThumbnails;
248 }
249
250 void Retracer::setCaptureThumbnails(bool enable)
251 {
252     m_captureThumbnails = enable;
253 }
254
255 /**
256  * Starting point for the retracing thread.
257  *
258  * Overrides QThread::run().
259  */
260 void Retracer::run()
261 {
262     QString msg = QLatin1String("Replay finished!");
263
264     /*
265      * Construct command line
266      */
267
268     QString prog;
269     QStringList arguments;
270
271     switch (m_api) {
272     case trace::API_GL:
273         prog = QLatin1String("glretrace");
274         break;
275     case trace::API_EGL:
276         prog = QLatin1String("eglretrace");
277         break;
278     case trace::API_DX:
279     case trace::API_D3D7:
280     case trace::API_D3D8:
281     case trace::API_D3D9:
282     case trace::API_DXGI:
283 #ifdef Q_OS_WIN
284         prog = QLatin1String("d3dretrace");
285 #else
286         prog = QLatin1String("wine");
287         arguments << QLatin1String("d3dretrace.exe");
288 #endif
289         break;
290     default:
291         emit finished(QLatin1String("Unsupported API"));
292         return;
293     }
294
295     if (m_singlethread) {
296         arguments << QLatin1String("--singlethread");
297     }
298
299     if (m_captureState) {
300         arguments << QLatin1String("-D");
301         arguments << QString::number(m_captureCall);
302     } else if (m_captureThumbnails) {
303         arguments << QLatin1String("-s"); // emit snapshots
304         arguments << QLatin1String("-"); // emit to stdout
305     } else if (isProfiling()) {
306         if (m_profileGpu) {
307             arguments << QLatin1String("--pgpu");
308         }
309
310         if (m_profileCpu) {
311             arguments << QLatin1String("--pcpu");
312         }
313
314         if (m_profilePixels) {
315             arguments << QLatin1String("--ppd");
316         }
317     } else {
318         if (m_doubleBuffered) {
319             arguments << QLatin1String("--db");
320         } else {
321             arguments << QLatin1String("--sb");
322         }
323
324         if (m_benchmarking) {
325             arguments << QLatin1String("-b");
326         }
327     }
328
329     arguments << m_fileName;
330
331     /*
332      * Support remote execution on a separate target.
333      */
334
335     if (m_remoteTarget.length() != 0) {
336         arguments.prepend(prog);
337         arguments.prepend(m_remoteTarget);
338         prog = QLatin1String("ssh");
339     }
340
341     /*
342      * Start the process.
343      */
344
345     QProcess process;
346
347     process.start(prog, arguments, QIODevice::ReadOnly);
348     if (!process.waitForStarted(-1)) {
349         emit finished(QLatin1String("Could not start process"));
350         return;
351     }
352
353     /*
354      * Process standard output
355      */
356
357     QList<QImage> thumbnails;
358     QVariantMap parsedJson;
359     trace::Profile* profile = NULL;
360
361     process.setReadChannel(QProcess::StandardOutput);
362     if (process.waitForReadyRead(-1)) {
363         BlockingIODevice io(&process);
364
365         if (m_captureState) {
366             /*
367              * Parse JSON from the output.
368              *
369              * XXX: QJSON's scanner is inneficient as it abuses single
370              * character QIODevice::peek (not cheap), instead of maintaining a
371              * lookahead character on its own.
372              */
373
374             bool ok = false;
375             QJson::Parser jsonParser;
376
377             // Allow Nan/Infinity
378             jsonParser.allowSpecialNumbers(true);
379 #if 0
380             parsedJson = jsonParser.parse(&io, &ok).toMap();
381 #else
382             /*
383              * XXX: QJSON expects blocking IO, and it looks like
384              * BlockingIODevice does not work reliably in all cases.
385              */
386             process.waitForFinished(-1);
387             parsedJson = jsonParser.parse(&process, &ok).toMap();
388 #endif
389             if (!ok) {
390                 msg = QLatin1String("failed to parse JSON");
391             }
392         } else if (m_captureThumbnails) {
393             /*
394              * Parse concatenated PNM images from output.
395              */
396
397             while (!io.atEnd()) {
398                 image::PNMInfo info;
399
400                 char header[512];
401                 qint64 headerSize = 0;
402                 int headerLines = 3; // assume no optional comment line
403
404                 for (int headerLine = 0; headerLine < headerLines; ++headerLine) {
405                     qint64 headerRead = io.readLine(&header[headerSize], sizeof(header) - headerSize);
406
407                     // if header actually contains optional comment line, ...
408                     if (headerLine == 1 && header[headerSize] == '#') {
409                         ++headerLines;
410                     }
411
412                     headerSize += headerRead;
413                 }
414
415                 const char *headerEnd = image::readPNMHeader(header, headerSize, info);
416
417                 // if invalid PNM header was encountered, ...
418                 if (headerEnd == NULL ||
419                     info.channelType != image::TYPE_UNORM8) {
420                     qDebug() << "error: invalid snapshot stream encountered";
421                     break;
422                 }
423
424                 unsigned channels = info.channels;
425                 unsigned width = info.width;
426                 unsigned height = info.height;
427
428                 // qDebug() << "channels: " << channels << ", width: " << width << ", height: " << height";
429
430                 QImage snapshot = QImage(width, height, channels == 1 ? QImage::Format_Mono : QImage::Format_RGB888);
431
432                 int rowBytes = channels * width;
433                 for (int y = 0; y < height; ++y) {
434                     unsigned char *scanLine = snapshot.scanLine(y);
435                     qint64 readBytes = io.read((char *) scanLine, rowBytes);
436                     Q_ASSERT(readBytes == rowBytes);
437                     (void)readBytes;
438                 }
439
440                 QImage thumb = thumbnail(snapshot);
441                 thumbnails.append(thumb);
442             }
443
444             Q_ASSERT(process.state() != QProcess::Running);
445         } else if (isProfiling()) {
446             profile = new trace::Profile();
447
448             while (!io.atEnd()) {
449                 char line[256];
450                 qint64 lineLength;
451
452                 lineLength = io.readLine(line, 256);
453
454                 if (lineLength == -1)
455                     break;
456
457                 trace::Profiler::parseLine(line, profile);
458             }
459         } else {
460             QByteArray output;
461             output = process.readAllStandardOutput();
462             if (output.length() < 80) {
463                 msg = QString::fromUtf8(output);
464             }
465         }
466     }
467
468     /*
469      * Wait for process termination
470      */
471
472     process.waitForFinished(-1);
473
474     if (process.exitStatus() != QProcess::NormalExit) {
475         msg = QLatin1String("Process crashed");
476     } else if (process.exitCode() != 0) {
477         msg = QLatin1String("Process exited with non zero exit code");
478     }
479
480     /*
481      * Parse errors.
482      */
483
484     QList<ApiTraceError> errors;
485     process.setReadChannel(QProcess::StandardError);
486     QRegExp regexp("(^\\d+): +(\\b\\w+\\b): ([^\\r\\n]+)[\\r\\n]*$");
487     while (!process.atEnd()) {
488         QString line = process.readLine();
489         if (regexp.indexIn(line) != -1) {
490             ApiTraceError error;
491             error.callIndex = regexp.cap(1).toInt();
492             error.type = regexp.cap(2);
493             error.message = regexp.cap(3);
494             errors.append(error);
495         } else if (!errors.isEmpty()) {
496             // Probably a multiligne message
497             ApiTraceError &previous = errors.last();
498             if (line.endsWith("\n")) {
499                 line.chop(1);
500             }
501             previous.message.append('\n');
502             previous.message.append(line);
503         }
504     }
505
506     /*
507      * Emit signals
508      */
509
510     if (m_captureState) {
511         ApiTraceState *state = new ApiTraceState(parsedJson);
512         emit foundState(state);
513     }
514
515     if (m_captureThumbnails && !thumbnails.isEmpty()) {
516         emit foundThumbnails(thumbnails);
517     }
518
519     if (isProfiling() && profile) {
520         emit foundProfile(profile);
521     }
522
523     if (!errors.isEmpty()) {
524         emit retraceErrors(errors);
525     }
526
527     emit finished(msg);
528 }
529
530 #include "retracer.moc"