]> git.cworth.org Git - apitrace/blob - scripts/retracediff.py
retracediff: Dump mismtached float images to stdout.
[apitrace] / scripts / retracediff.py
1 #!/usr/bin/env python
2 ##########################################################################
3 #
4 # Copyright 2011 Jose Fonseca
5 # All Rights Reserved.
6 #
7 # Permission is hereby granted, free of charge, to any person obtaining a copy
8 # of this software and associated documentation files (the 'Software'), to deal
9 # in the Software without restriction, including without limitation the rights
10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 # copies of the Software, and to permit persons to whom the Software is
12 # furnished to do so, subject to the following conditions:
13 #
14 # The above copyright notice and this permission notice shall be included in
15 # all copies or substantial portions of the Software.
16 #
17 # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 # THE SOFTWARE.
24 #
25 ##########################################################################/
26
27 '''Run two retrace instances in parallel, comparing generated snapshots.
28 '''
29
30
31 import optparse
32 import os.path
33 import subprocess
34 import platform
35 import sys
36
37 from PIL import Image
38
39 from snapdiff import Comparer
40 from highlight import AutoHighlighter
41 import jsondiff
42
43
44 # Null file, to use when we're not interested in subprocesses output
45 if platform.system() == 'Windows':
46     NULL = open('NUL:', 'wb')
47 else:
48     NULL = open('/dev/null', 'wb')
49
50
51 class RetraceRun:
52
53     def __init__(self, process):
54         self.process = process
55
56     def nextSnapshot(self):
57         image, comment = read_pnm(self.process.stdout)
58         if image is None:
59             return None, None
60
61         callNo = int(comment.strip())
62
63         return image, callNo
64
65     def terminate(self):
66         try:
67             self.process.terminate()
68         except OSError:
69             # Avoid http://bugs.python.org/issue14252
70             pass
71
72
73 class Retracer:
74
75     def __init__(self, retraceExe, args, env=None):
76         self.retraceExe = retraceExe
77         self.args = args
78         self.env = env
79
80     def _retrace(self, args, stdout=subprocess.PIPE):
81         cmd = [
82             self.retraceExe,
83         ] + args + self.args
84         if self.env:
85             for name, value in self.env.iteritems():
86                 sys.stderr.write('%s=%s ' % (name, value))
87         sys.stderr.write(' '.join(cmd) + '\n')
88         try:
89             return subprocess.Popen(cmd, env=self.env, stdout=stdout, stderr=NULL)
90         except OSError, ex:
91             sys.stderr.write('error: failed to execute %s: %s\n' % (cmd[0], ex.strerror))
92             sys.exit(1)
93
94     def retrace(self, args):
95         p = self._retrace([])
96         p.wait()
97         return p.returncode
98
99     def snapshot(self, call_nos):
100         process = self._retrace([
101             '-s', '-',
102             '-S', call_nos,
103         ])
104         return RetraceRun(process)
105
106     def dump_state(self, call_no):
107         '''Get the state dump at the specified call no.'''
108
109         p = self._retrace([
110             '-D', str(call_no),
111         ])
112         state = jsondiff.load(p.stdout)
113         p.wait()
114         return state.get('parameters', {})
115
116     def diff_state(self, ref_call_no, src_call_no, stream):
117         '''Compare the state between two calls.'''
118
119         ref_state = self.dump_state(ref_call_no)
120         src_state = self.dump_state(src_call_no)
121
122         stream.flush()
123         differ = jsondiff.Differ(stream)
124         differ.visit(ref_state, src_state)
125         stream.write('\n')
126
127
128 def read_pnm(stream):
129     '''Read a PNM from the stream, and return the image object, and the comment.'''
130
131     magic = stream.readline()
132     if not magic:
133         return None, None
134     magic = magic.rstrip()
135     if magic == 'P5':
136         channels = 1
137         bytesPerChannel = 1
138         mode = 'L'
139     elif magic == 'P6':
140         channels = 3
141         bytesPerChannel = 1
142         mode = 'RGB'
143     elif magic == 'Pf':
144         channels = 1
145         bytesPerChannel = 4
146         mode = 'R'
147     elif magic == 'PF':
148         channels = 3
149         bytesPerChannel = 4
150         mode = 'RGB'
151     elif magic == 'PX':
152         channels = 4
153         bytesPerChannel = 4
154         mode = 'RGB'
155     else:
156         raise Exception('Unsupported magic `%s`' % magic)
157     comment = ''
158     line = stream.readline()
159     while line.startswith('#'):
160         comment += line[1:]
161         line = stream.readline()
162     width, height = map(int, line.strip().split())
163     maximum = int(stream.readline().strip())
164     if bytesPerChannel == 1:
165         assert maximum == 255
166     else:
167         assert maximum == 1
168     data = stream.read(height * width * channels * bytesPerChannel)
169     if bytesPerChannel == 4:
170         # Image magic only supports single channel floating point images, so
171         # represent the image as numpy arrays
172
173         import numpy
174         pixels = numpy.fromstring(data, dtype=numpy.float32)
175         pixels.resize((height, width, channels))
176         return pixels, comment
177
178     image = Image.frombuffer(mode, (width, height), data, 'raw', mode, 0, 1)
179     return image, comment
180
181
182 def dumpNumpyImage(output, pixels):
183     height, width, channels = pixels.shape
184     for y in range(height):
185         output.write('  ')
186         for x in range(width):
187             for c in range(channels):
188                 output.write('%0.9g,' % pixels[y, x, c])
189             output.write('  ')
190         output.write('\n')
191
192
193 def parse_env(optparser, entries):
194     '''Translate a list of NAME=VALUE entries into an environment dictionary.'''
195
196     if not entries:
197         return None
198
199     env = os.environ.copy()
200     for entry in entries:
201         try:
202             name, var = entry.split('=', 1)
203         except Exception:
204             optparser.error('invalid environment entry %r' % entry)
205         env[name] = var
206     return env
207
208
209 def main():
210     '''Main program.
211     '''
212
213     global options
214
215     # Parse command line options
216     optparser = optparse.OptionParser(
217         usage='\n\t%prog [options] -- [glretrace options] <trace>',
218         version='%%prog')
219     optparser.add_option(
220         '-r', '--retrace', metavar='PROGRAM',
221         type='string', dest='retrace', default='glretrace',
222         help='retrace command [default: %default]')
223     optparser.add_option(
224         '--ref-driver', metavar='DRIVER',
225         type='string', dest='ref_driver', default=None,
226         help='force reference driver')
227     optparser.add_option(
228         '--src-driver', metavar='DRIVER',
229         type='string', dest='src_driver', default=None,
230         help='force source driver')
231     optparser.add_option(
232         '--ref-arg', metavar='OPTION',
233         type='string', action='append', dest='ref_args', default=[],
234         help='pass argument to reference retrace')
235     optparser.add_option(
236         '--src-arg', metavar='OPTION',
237         type='string', action='append', dest='src_args', default=[],
238         help='pass argument to source retrace')
239     optparser.add_option(
240         '--ref-env', metavar='NAME=VALUE',
241         type='string', action='append', dest='ref_env', default=[],
242         help='add variable to reference environment')
243     optparser.add_option(
244         '--src-env', metavar='NAME=VALUE',
245         type='string', action='append', dest='src_env', default=[],
246         help='add variable to source environment')
247     optparser.add_option(
248         '--diff-prefix', metavar='PATH',
249         type='string', dest='diff_prefix', default='.',
250         help='prefix for the difference images')
251     optparser.add_option(
252         '-t', '--threshold', metavar='BITS',
253         type="float", dest="threshold", default=12.0,
254         help="threshold precision  [default: %default]")
255     optparser.add_option(
256         '-S', '--snapshot-frequency', metavar='CALLSET',
257         type="string", dest="snapshot_frequency", default='draw',
258         help="calls to compare [default: %default]")
259     optparser.add_option(
260         '--diff-state',
261         action='store_true', dest='diff_state', default=False,
262         help='diff state between failing calls')
263     optparser.add_option(
264         '-o', '--output', metavar='FILE',
265         type="string", dest="output",
266         help="output file [default: stdout]")
267
268     (options, args) = optparser.parse_args(sys.argv[1:])
269     ref_env = parse_env(optparser, options.ref_env)
270     src_env = parse_env(optparser, options.src_env)
271     if not args:
272         optparser.error("incorrect number of arguments")
273     
274     if options.ref_driver:
275         options.ref_args.insert(0, '--driver=' + options.ref_driver)
276     if options.src_driver:
277         options.src_args.insert(0, '--driver=' + options.src_driver)
278
279     refRetracer = Retracer(options.retrace, options.ref_args + args, ref_env)
280     srcRetracer = Retracer(options.retrace, options.src_args + args, src_env)
281
282     if options.output:
283         output = open(options.output, 'wt')
284     else:
285         output = sys.stdout
286
287     highligher = AutoHighlighter(output)
288
289     highligher.write('call\tprecision\n')
290
291     last_bad = -1
292     last_good = 0
293     refRun = refRetracer.snapshot(options.snapshot_frequency)
294     try:
295         srcRun = srcRetracer.snapshot(options.snapshot_frequency)
296         try:
297             while True:
298                 # Get the reference image
299                 refImage, refCallNo = refRun.nextSnapshot()
300                 if refImage is None:
301                     break
302
303                 # Get the source image
304                 srcImage, srcCallNo = srcRun.nextSnapshot()
305                 if srcImage is None:
306                     break
307
308                 assert refCallNo == srcCallNo
309                 callNo = refCallNo
310
311                 # Compare the two images
312                 if isinstance(refImage, Image.Image) and isinstance(srcImage, Image.Image):
313                     # Using PIL
314                     numpyImages = False
315                     comparer = Comparer(refImage, srcImage)
316                     precision = comparer.precision()
317                 else:
318                     # Using numpy (for floating point images)
319                     # TODO: drop PIL when numpy path becomes general enough
320                     import numpy
321                     assert not isinstance(refImage, Image.Image)
322                     assert not isinstance(srcImage, Image.Image)
323                     numpyImages = True
324                     assert refImage.shape == srcImage.shape
325                     diffImage = numpy.square(srcImage - refImage)
326                     match = numpy.all(diffImage == 0)
327                     if match:
328                         precision = 24
329                     else:
330                         precision = 0
331
332                 mismatch = precision < options.threshold
333
334                 if mismatch:
335                     highligher.color(highligher.red)
336                     highligher.bold()
337                 highligher.write('%u\t%f\n' % (callNo, precision))
338                 if mismatch:
339                     highligher.normal()
340
341                 if mismatch:
342                     if numpyImages:
343                         dumpNumpyImage(output, refImage)
344                         output.write("->\n")
345                         dumpNumpyImage(output, srcImage)
346                     if options.diff_prefix and not numpyImages:
347                         prefix = os.path.join(options.diff_prefix, '%010u' % callNo)
348                         prefix_dir = os.path.dirname(prefix)
349                         if not os.path.isdir(prefix_dir):
350                             os.makedirs(prefix_dir)
351                         refImage.save(prefix + '.ref.png')
352                         srcImage.save(prefix + '.src.png')
353                         comparer.write_diff(prefix + '.diff.png')
354                     if last_bad < last_good and options.diff_state:
355                         srcRetracer.diff_state(last_good, callNo, output)
356                     last_bad = callNo
357                 else:
358                     last_good = callNo
359
360                 highligher.flush()
361         finally:
362             srcRun.terminate()
363     finally:
364         refRun.terminate()
365
366
367 if __name__ == '__main__':
368     main()