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