]> git.cworth.org Git - apitrace/commitdiff
Merge remote-tracking branch 'origin/master' into on-demand-loading
authorZack Rusin <zack@kde.org>
Tue, 6 Sep 2011 21:45:34 +0000 (17:45 -0400)
committerZack Rusin <zack@kde.org>
Tue, 6 Sep 2011 21:45:34 +0000 (17:45 -0400)
INSTALL.markdown [new file with mode: 0644]
README.markdown
TODO.markdown
glretrace_main.cpp
image.hpp
image_pnm.cpp
scripts/README [deleted file]
scripts/highlight.py [new file with mode: 0644]
scripts/retracediff.py
scripts/snapdiff.py

diff --git a/INSTALL.markdown b/INSTALL.markdown
new file mode 100644 (file)
index 0000000..b50fb78
--- /dev/null
@@ -0,0 +1,69 @@
+Building from source
+====================
+
+
+Requirements common for all platforms:
+
+* Python (requires version 2.6)
+
+* CMake (tested with version 2.8)
+
+Requirements to build the GUI (optional):
+
+* Qt (tested with version 4.7)
+
+* QJSON (tested with version 0.7.1)
+
+
+Linux / Mac OS X
+----------------
+
+Build as:
+
+    cmake -H. -Bbuild
+    make -C build
+
+You can also build the 32bit GL wrapper on 64bit distro with a multilib gcc by
+doing:
+
+    cmake -H. -Bbuild32 -DCMAKE_C_FLAGS=-m32 -DCMAKE_CXX_FLAGS=-m32 -DCMAKE_EXE_LINKER_FLAGS=-m32
+    make -C build32 glxtrace
+
+
+Windows
+-------
+
+Additional requirements:
+
+* Microsoft Visual Studio (tested with 2008 version) or MinGW (tested with gcc version 4.4)
+
+* Microsoft DirectX SDK (tested with August 2007 release)
+
+To build with Visual Studio first invoke CMake GUI as:
+
+    cmake-gui -H. -B%cd%\build
+
+and press the _Configure_ button.
+
+It will try to detect most required/optional dependencies automatically.  When
+not found automatically, you can manually specify the location of the
+dependencies from the GUI.
+
+If you are building with GUI support (i.e, with QT and QJSON), it should detect
+the official QT sdk automatically, but you will need to build QJSON yourself
+and also set the `QJSON_INCLUDE_DIR` and `QJSON_LIBRARIES` variables in the
+generated `CMakeCache.txt` when building apitrace and repeat the above
+sequence.
+
+After you've succesfully configured, you can start the build by opening the
+generated `build\apitrace.sln` solution file, or invoking `cmake` as:
+
+    cmake --build build --config MinSizeRel
+
+The steps to build 64bit version are similar, but choosing _Visual Studio 9
+2008 Win64_ instead of _Visual Studio 9 2008_.
+
+It's also possible to instruct `cmake` build Windows binaries on Linux with
+[MinGW cross compilers](http://www.cmake.org/Wiki/CmakeMingw).
+
+
index 5b34dec786cb67c10a104e7435f37963977d2519..f56412aea7c02914173ca6d9e0af3752ba08495c 100644 (file)
@@ -10,77 +10,8 @@ About **apitrace**
 * visualize trace files, and inspect state.
 
 
-Building from source
-====================
-
-
-Requirements common for all platforms:
-
-* Python (requires version 2.6)
-
-* CMake (tested with version 2.8)
-
-Requirements to build the GUI (optional):
-
-* Qt (tested with version 4.7)
-
-* QJSON (tested with version 0.7.1)
-
-
-Linux / Mac OS X
-----------------
-
-Build as:
-
-    cmake -H. -Bbuild
-    make -C build
-
-You can also build the 32bit GL wrapper on 64bit distro with a multilib gcc by
-doing:
-
-    cmake -H. -Bbuild32 -DCMAKE_C_FLAGS=-m32 -DCMAKE_CXX_FLAGS=-m32 -DCMAKE_EXE_LINKER_FLAGS=-m32
-    make -C build32 glxtrace
-
-
-Windows
--------
-
-Additional requirements:
-
-* Microsoft Visual Studio (tested with 2008 version) or MinGW (tested with gcc version 4.4)
-
-* Microsoft DirectX SDK (tested with August 2007 release)
-
-To build with Visual Studio first invoke CMake GUI as:
-
-    cmake-gui -H. -B%cd%\build
-
-and press the _Configure_ button.
-
-It will try to detect most required/optional dependencies automatically.  When
-not found automatically, you can manually specify the location of the
-dependencies from the GUI.
-
-If you are building with GUI support (i.e, with QT and QJSON), it should detect
-the official QT sdk automatically, but you will need to build QJSON yourself
-and also set the `QJSON_INCLUDE_DIR` and `QJSON_LIBRARIES` variables in the
-generated `CMakeCache.txt` when building apitrace and repeat the above
-sequence.
-
-After you've succesfully configured, you can start the build by opening the
-generated `build\apitrace.sln` solution file, or invoking `cmake` as:
-
-    cmake --build build --config MinSizeRel
-
-The steps to build 64bit version are similar, but choosing _Visual Studio 9
-2008 Win64_ instead of _Visual Studio 9 2008_.
-
-It's also possible to instruct `cmake` build Windows binaries on Linux with
-[MinGW cross compilers](http://www.cmake.org/Wiki/CmakeMingw).
-
-
-Usage
-=====
+Basic usage
+===========
 
 
 Linux
@@ -129,11 +60,6 @@ to trace by using `glxtrace.so` as an ordinary `libGL.so` and injecting into
 See the `ld.so` man page for more information about `LD_PRELOAD` and
 `LD_LIBRARY_PATH` environment flags.
 
-You can make a video of the output by doing
-
-    /path/to/glretrace -s - application.trace \
-    | ffmpeg -r 30 -f image2pipe -vcodec ppm -i pipe: -vcodec mpeg4 -y output.mp4
-
 
 
 Mac OS X
@@ -167,6 +93,151 @@ Windows
         \path\to\glretrace application.trace
 
 
+Advanced command line usage
+===========================
+
+
+Dump GL state at a particular call
+----------------------------------
+
+You can get a dump of the bound GL state at call 12345 by doing:
+
+    /path/to/glretrace -D 12345 application.trace > 12345.json
+
+This is precisely the mechanism the GUI obtains its own state.
+
+You can compare two state dumps with the jsondiff.py script:
+
+    ./scripts/jsondiff.py 12345.json 67890.json
+
+
+Comparing two traces side by side
+---------------------------------
+
+    ./scripts/tracediff.sh trace1.trace trace2.trace
+
+This works only on Unices, and it will truncate the traces due to performance
+limitations.
+
+
+Recording a video with FFmpeg
+-----------------------------
+
+You can make a video of the output by doing
+
+    /path/to/glretrace -s - application.trace \
+    | ffmpeg -r 30 -f image2pipe -vcodec ppm -i pipe: -vcodec mpeg4 -y output.mp4
+
+
+Advanced usage for OpenGL implementors
+======================================
+
+There are several avanced usage examples meant for OpenGL implementors.
+
+
+Regression testing
+------------------
+
+These are the steps to create a regression testsuite around apitrace:
+
+* obtain a trace
+
+* obtain reference snapshots, by doing:
+
+        mkdir /path/to/snapshots/
+        /path/to/glretrace -s /path/to/reference/snapshots/ application.trace
+
+  on reference system.
+
+* prune the snapshots which are not interesting
+
+* to do a regression test, do:
+
+        /path/to/glretrace -c /path/to/reference/snapshots/ application.trace
+
+  Alternatively, for a HTML summary, use the snapdiff script:
+
+        /path/to/glretrace -s /path/to/current/snapshots/ application.trace
+        ./scripts/snapdiff.py --output summary.html /path/to/reference/snapshots/ /path/to/current/snapshots/
+
+
+Automated git-bisection
+-----------------------
+
+With tracecheck.py it is possible to automate git bisect and pinpoint the
+commit responsible for a regression.
+
+Below is an example of using tracecheck.py to bisect a regression in the
+Mesa-based Intel 965 driver.  But the procedure could be applied to any GL
+driver hosted on a git repository.
+
+First, create a build script, named build-script.sh, containing:
+
+    #!/bin/sh
+    set -e
+    export PATH=/usr/lib/ccache:$PATH
+    export CFLAGS='-g'
+    export CXXFLAGS='-g'
+    ./autogen.sh --disable-egl --disable-gallium --disable-glut --disable-glu --disable-glw --with-dri-drivers=i965
+    make clean
+    make "$@"
+
+It is important that builds are both robust, and efficient.  Due to broken
+dependency discovery in Mesa's makefile system, it was necessary invoke `make
+clean` in every iteration step.  `ccache` should be installed to avoid
+recompiling unchanged source files.
+
+Then do:
+
+    cd /path/to/mesa
+    export LIBGL_DEBUG=verbose
+    export LD_LIBRARY_PATH=$PWD/lib
+    export LIBGL_DRIVERS_DIR=$PWD/lib
+    git bisect start \
+        6491e9593d5cbc5644eb02593a2f562447efdcbb 71acbb54f49089b03d3498b6f88c1681d3f649ac \
+        -- src/mesa/drivers/dri/intel src/mesa/drivers/dri/i965/
+    git bisect run /path/to/tracecheck.py \
+        --precision-threshold 8.0 \
+        --build /path/to/build-script.sh \
+        --gl-renderer '.*Mesa.*Intel.*' \
+        --retrace=/path/to/glretrace \
+        -c /path/to/reference/snapshots/ \
+        topogun-1.06-orc-84k.trace
+
+The trace-check.py script will skip automatically when there are build
+failures.
+
+The `--gl-renderer` option will also cause a commit to be skipped if the
+`GL_RENDERER` is unexpected (e.g., when a software renderer or another GL
+driver is unintentianlly loaded due to missing symbol in the DRI driver, or
+another runtime fault).
+
+
+Side by side retracing
+----------------------
+
+In order to determine which draw call a regression first manifests one could
+generate snapshots for every draw call, using the -S option.  That is, however,
+very inefficient for big traces with many draw calls.
+
+A faster approach is to run both the bad and a good GL driver side-by-side.
+The latter can be either a preivously known good build of the GL driver, or a
+reference software renderer.
+
+This can be achieved with retracediff.py script, which invokes glretrace with
+different environments, allowing to choose the desired GL driver by
+manipulating variables such as `LD_LIBRARY_PATH` or `LIBGL_DRIVERS_DIR`.
+
+For example:
+
+    ./scripts/retracediff.py \
+        --ref-env LD_LIBRARY_PATH=/path/to/reference/GL/implementation \
+        -r ./glretrace \
+        --diff-prefix=/path/to/output/diffs \
+        application.trace
+
+
+
 Links
 =====
 
index fb447668722225fe1afffc198f44f47c32e3bb9d..b54cf65bd7e6085eb12cdd2f94c88568c0e975ac 100644 (file)
@@ -50,9 +50,6 @@ Retracing
 
 * Plug memory leaks.
 
-* Allow to retrace with two libGL.so in parallel, and output differences in
-  rendered frames / draw calls.
-
 * D3D support.
 
 
@@ -64,12 +61,11 @@ GUI
 * Visualize meshes in draw commands.
 
 
-Other:
+Other
+-----
 
 * Side-by-side trace diffing; either as a separate tool on or the GUI.
 
-* Side-by-side state diffing.
-
 * Ability to extract just a single frame from a trace, and all previous calls
   that contributed to it:
 
index 56ee763210b4e3848f4b359549cb9e72c8f42580..3a900927a128a3d6d46ec0055002d03f964f7fe6 100644 (file)
@@ -131,7 +131,9 @@ void snapshot(unsigned call_no) {
 
     if (snapshot_prefix) {
         if (snapshot_prefix[0] == '-' && snapshot_prefix[1] == 0) {
-            src->writePNM(std::cout);
+            char comment[21];
+            snprintf(comment, sizeof comment, "%u", call_no);
+            src->writePNM(std::cout, comment);
         } else {
             char filename[PATH_MAX];
             snprintf(filename, sizeof filename, "%s%010u.png", snapshot_prefix, call_no);
index 44fc4f4352252b148699097c08cef57afa16d523..dc53ec990ee3a19f931f5e9160269c7e0c875169 100644 (file)
--- a/image.hpp
+++ b/image.hpp
@@ -83,14 +83,14 @@ public:
 
     bool writeBMP(const char *filename) const;
 
-    void writePNM(std::ostream &os) const;
+    void writePNM(std::ostream &os, const char *comment = NULL) const;
 
-    inline bool writePNM(const char *filename) const {
+    inline bool writePNM(const char *filename, const char *comment = NULL) const {
         std::ofstream os(filename, std::ofstream::binary);
         if (!os) {
             return false;
         }
-        writePNM(os);
+        writePNM(os, comment);
         return true;
     }
 
index 6625f3ddc6b9846c1876bab53fd8f0afb8000fdb..5397a1a604d78d7c4afc45ad38ef500f2688a1ce 100644 (file)
@@ -26,6 +26,7 @@
 
 
 #include <assert.h>
+#include <string.h>
 #include <stdint.h>
 
 #include "image.hpp"
@@ -35,12 +36,16 @@ namespace Image {
 
 /**
  * http://en.wikipedia.org/wiki/Netpbm_format
+ * http://netpbm.sourceforge.net/doc/ppm.html
  */
 void
-Image::writePNM(std::ostream &os) const {
+Image::writePNM(std::ostream &os, const char *comment) const {
     assert(channels == 1 || channels >= 3);
 
     os << (channels == 1 ? "P5" : "P6") << "\n";
+    if (comment) {
+        os << "#" << comment << "\n";
+    }
     os << width << " " << height << "\n";
     os << "255" << "\n";
 
@@ -51,15 +56,55 @@ Image::writePNM(std::ostream &os) const {
             os.write((const char *)row, width*channels);
         }
     } else {
-        unsigned char pixel[3] = {0, 0, 0};
-        for (row = start(); row != end(); row += stride()) {
-            for (unsigned x = 0; x < width; ++x) {
-                for (unsigned channel = 0; channel < channels; ++channel) {
-                    pixel[channel] = row[x*channels + channel];
+        unsigned char *tmp = new unsigned char[width*3];
+        if (channels == 4) {
+            for (row = start(); row != end(); row += stride()) {
+                const uint32_t *src = (const uint32_t *)row;
+                uint32_t *dst = (uint32_t *)tmp;
+                unsigned x;
+                for (x = 0; x + 4 <= width; x += 4) {
+                    /*
+                     * It's much faster to access dwords than bytes.
+                     *
+                     * FIXME: Big-endian version.
+                     */
+
+                    uint32_t rgba0 = *src++ & 0xffffff;
+                    uint32_t rgba1 = *src++ & 0xffffff;
+                    uint32_t rgba2 = *src++ & 0xffffff;
+                    uint32_t rgba3 = *src++ & 0xffffff;
+                    uint32_t rgb0 = rgba0
+                                  | (rgba1 << 24);
+                    uint32_t rgb1 = (rgba1 >> 8)
+                                  | (rgba2 << 16);
+                    uint32_t rgb2 = (rgba2 >> 16)
+                                  | (rgba3 << 8);
+                    *dst++ = rgb0;
+                    *dst++ = rgb1;
+                    *dst++ = rgb2;
+                }
+                for (; x < width; ++x) {
+                    tmp[x*3 + 0] = row[x*4 + 0];
+                    tmp[x*3 + 1] = row[x*4 + 1];
+                    tmp[x*3 + 2] = row[x*4 + 2];
+                }
+                os.write((const char *)tmp, width*3);
+            }
+        } else if (channels == 2) {
+            for (row = start(); row != end(); row += stride()) {
+                const unsigned char *src = row;
+                unsigned char *dst = tmp;
+                for (unsigned x = 0; x < width; ++x) {
+                    *dst++ = *src++;
+                    *dst++ = *src++;
+                    *dst++ = 0;
                 }
-                os.write((const char *)pixel, sizeof pixel);
+                os.write((const char *)tmp, width*3);
             }
+        } else {
+            assert(0);
         }
+        delete [] tmp;
     }
 }
 
diff --git a/scripts/README b/scripts/README
deleted file mode 100644 (file)
index 576e48f..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-This directory contains several utilitarian scripts for common development
-tasks using apitrace tools.
-
-See their code for more details about their usage.
diff --git a/scripts/highlight.py b/scripts/highlight.py
new file mode 100644 (file)
index 0000000..986bd1d
--- /dev/null
@@ -0,0 +1,194 @@
+##########################################################################
+#
+# Copyright 2011 Jose Fonseca
+# Copyright 2008-2009 VMware, Inc.
+# All Rights Reserved.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+##########################################################################/
+
+
+import sys
+import platform
+
+
+class PlainHighlighter:
+    '''Plain formatter'''
+
+    black = None
+    red = None
+    green = None
+    yellow = None
+    blue = None
+    magenta = None
+    cyan = None
+    white = None
+
+    def __init__(self, stream):
+        self.stream = stream
+
+    def write(self, text):
+        self.stream.write(text)
+
+    def flush(self):
+        self.stream.flush()
+
+    def normal(self):
+        pass
+
+    def color(self, color):
+        pass
+
+    def bold(self):
+        pass
+
+    def italic(self):
+        pass
+
+
+class AnsiHighlighter(PlainHighlighter):
+    '''Highlighter for plain-text files which outputs ANSI escape codes. See
+    http://en.wikipedia.org/wiki/ANSI_escape_code for more information
+    concerning ANSI escape codes.
+    '''
+
+    _csi = '\33['
+
+    _normal = '0m'
+    _bold = '1m'
+    _italic = '3m'
+
+    black = 0
+    red = 1
+    green = 2
+    yellow = 3
+    blue = 4
+    magenta = 5
+    cyan = 6
+    white = 7
+
+    def __init__(self, stream):
+        PlainHighlighter.__init__(self, stream)
+        self.isatty = stream.isatty()
+
+    def _escape(self, code):
+        if self.isatty:
+            self.stream.write(self._csi + code)
+
+    def normal(self):
+        self._escape(self._normal)
+
+    def color(self, color):
+        self._escape(str(30 + color) + 'm')
+
+    def bold(self):
+        self._escape(self._bold)
+
+    def italic(self):
+        self._escape(self._italic)
+
+
+class WindowsConsoleHighlighter(PlainHighlighter):
+    '''Highlighter for the Windows Console. See 
+    http://code.activestate.com/recipes/496901/ for more information.
+    '''
+
+    INVALID_HANDLE_VALUE = -1
+    STD_INPUT_HANDLE  = -10
+    STD_OUTPUT_HANDLE = -11
+    STD_ERROR_HANDLE  = -12
+
+    FOREGROUND_BLUE      = 0x01
+    FOREGROUND_GREEN     = 0x02
+    FOREGROUND_RED       = 0x04
+    FOREGROUND_INTENSITY = 0x08
+    BACKGROUND_BLUE      = 0x10
+    BACKGROUND_GREEN     = 0x20
+    BACKGROUND_RED       = 0x40
+    BACKGROUND_INTENSITY = 0x80
+
+    COMMON_LVB_LEADING_BYTE = 0x0100
+    COMMON_LVB_TRAILING_BYTE = 0x0200
+    COMMON_LVB_GRID_HORIZONTAL = 0x0400
+    COMMON_LVB_GRID_LVERTICAL = 0x0800
+    COMMON_LVB_GRID_RVERTICAL = 0x1000
+    COMMON_LVB_REVERSE_VIDEO = 0x4000
+    COMMON_LVB_UNDERSCORE = 0x8000
+
+    _normal = FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED
+    _bold   = FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_INTENSITY
+    _italic = FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED
+
+    black   = 0
+    red     =                                      FOREGROUND_RED
+    green   =                   FOREGROUND_GREEN                 
+    blue    = FOREGROUND_BLUE                                    
+    white   = FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED
+
+    def __init__(self, stream):
+        PlainHighlighter.__init__(self, stream)
+
+        if stream is sys.stdin:
+            nStdHandle = self.STD_INPUT_HANDLE
+        elif stream is sys.stdout:
+            nStdHandle = self.STD_OUTPUT_HANDLE
+        elif stream is sys.stderr:
+            nStdHandle = self.STD_ERROR_HANDLE
+        else:
+            nStdHandle = None
+
+        if nStdHandle is not None:
+            import ctypes
+            self._handle = ctypes.windll.kernel32.GetStdHandle(nStdHandle)
+        else:
+            self._handle = INVALID_HANDLE_VALUE
+
+        self._attribute = self.white
+
+    def _setAttribute(self, attr):
+        if self._handle != INVALID_HANDLE_VALUE:
+            import ctypes
+            ctypes.windll.kernel32.SetConsoleTextAttribute(self._handle, attr)
+        self._attribute = attr
+
+    def normal(self):
+        self._setAttribute(self._normal)
+
+    def color(self, color):
+        intensity = self._attribute & FOREGROUND_INTENSITY
+        self._setAttribute(color | intensity)
+
+    def bold(self):
+        self._setAttribute(self._attribute | FOREGROUND_INTENSITY)
+
+    def italic(self):
+        pass
+
+
+def Highlighter(stream = sys.stdout):
+    if platform.system() == 'Windows':
+        return WindowsConsoleHighlighter(stream)
+    else:
+        return AnsiHighlighter(stream)
+
+
+__all__ = [
+    'Highlighter',
+]
index b4bad08db73588b93237cfc6b494ad0f24dbd8a5..0b6a3b09f5713402b8ae8d249891ed31c7d76b5c 100755 (executable)
 
 import optparse
 import os.path
-import re
-import shutil
 import subprocess
 import platform
 import sys
-import tempfile
+
+from PIL import Image
 
 from snapdiff import Comparer
+from highlight import Highlighter
 import jsondiff
 
 
@@ -54,10 +54,10 @@ class Setup:
         self.args = args
         self.env = env
 
-    def retrace(self, snapshot_dir):
+    def retrace(self):
         cmd = [
             options.retrace,
-            '-s', snapshot_dir + os.path.sep,
+            '-s', '-',
             '-S', options.snapshot_frequency,
         ] + self.args
         p = subprocess.Popen(cmd, env=self.env, stdout=subprocess.PIPE, stderr=NULL)
@@ -73,16 +73,38 @@ class Setup:
         p = subprocess.Popen(cmd, env=self.env, stdout=subprocess.PIPE, stderr=NULL)
         state = jsondiff.load(p.stdout)
         p.wait()
-        return state
+        return state.get('parameters', {})
+
+    def diff_state(self, ref_call_no, src_call_no):
+        '''Compare the state between two calls.'''
+
+        ref_state = self.dump_state(ref_call_no)
+        src_state = self.dump_state(src_call_no)
 
+        sys.stdout.flush()
+        differ = jsondiff.Differ(sys.stdout)
+        differ.visit(ref_state, src_state)
+        sys.stdout.write('\n')
 
-def diff_state(setup, ref_call_no, src_call_no):
-    ref_state = setup.dump_state(ref_call_no)
-    src_state = setup.dump_state(src_call_no)
-    sys.stdout.flush()
-    differ = jsondiff.Differ(sys.stdout)
-    differ.visit(ref_state, src_state)
-    sys.stdout.write('\n')
+
+def read_pnm(stream):
+    '''Read a PNM from the stream, and return the image object, and the comment.'''
+
+    magic = stream.readline()
+    if not magic:
+        return None, None
+    assert magic.rstrip() == 'P6'
+    comment = ''
+    line = stream.readline()
+    while line.startswith('#'):
+        comment += line[1:]
+        line = stream.readline()
+    width, height = map(int, line.strip().split())
+    maximum = int(stream.readline().strip())
+    assert maximum == 255
+    data = stream.read(height * width * 3)
+    image = Image.frombuffer('RGB', (width, height), data, 'raw', 'RGB', 0, 1)
+    return image, comment
 
 
 def parse_env(optparser, entries):
@@ -115,15 +137,15 @@ def main():
     optparser.add_option(
         '--ref-env', metavar='NAME=VALUE',
         type='string', action='append', dest='ref_env', default=[],
-        help='reference environment variable')
+        help='add variable to reference environment')
     optparser.add_option(
         '--src-env', metavar='NAME=VALUE',
         type='string', action='append', dest='src_env', default=[],
-        help='reference environment variable')
+        help='add variable to source environment')
     optparser.add_option(
         '--diff-prefix', metavar='PATH',
         type='string', dest='diff_prefix', default='.',
-        help='reference environment variable')
+        help='prefix for the difference images')
     optparser.add_option(
         '-t', '--threshold', metavar='BITS',
         type="float", dest="threshold", default=12.0,
@@ -131,7 +153,7 @@ def main():
     optparser.add_option(
         '-S', '--snapshot-frequency', metavar='FREQUENCY',
         type="string", dest="snapshot_frequency", default='draw',
-        help="snapshot frequency  [default: %default]")
+        help="snapshot frequency: frame, framebuffer, or draw  [default: %default]")
 
     (options, args) = optparser.parse_args(sys.argv[1:])
     ref_env = parse_env(optparser, options.ref_env)
@@ -142,62 +164,64 @@ def main():
     ref_setup = Setup(args, ref_env)
     src_setup = Setup(args, src_env)
 
-    image_re = re.compile('^Wrote (.*\.png)$')
+    highligher = Highlighter(sys.stdout)
+
+    highligher.write('call\tprecision\n')
 
-    last_good = -1
     last_bad = -1
-    ref_snapshot_dir = tempfile.mkdtemp()
+    last_good = 0
+    ref_proc = ref_setup.retrace()
     try:
-        src_snapshot_dir = tempfile.mkdtemp()
+        src_proc = src_setup.retrace()
         try:
-            ref_proc = ref_setup.retrace(ref_snapshot_dir)
-            try:
-                src_proc = src_setup.retrace(src_snapshot_dir)
-                try:
-                    for ref_line in ref_proc.stdout:
-                        # Get the reference image
-                        ref_line = ref_line.rstrip()
-                        mo = image_re.match(ref_line)
-                        if mo:
-                            ref_image = mo.group(1)
-                            for src_line in src_proc.stdout:
-                                # Get the source image
-                                src_line = src_line.rstrip()
-                                mo = image_re.match(src_line)
-                                if mo:
-                                    src_image = mo.group(1)
-                                    
-                                    root, ext = os.path.splitext(os.path.basename(src_image))
-                                    call_no = int(root)
-
-                                    # Compare the two images
-                                    comparer = Comparer(ref_image, src_image)
-                                    precision = comparer.precision()
-
-                                    sys.stdout.write('%u %f\n' % (call_no, precision))
-                                    
-                                    if precision < options.threshold:
-                                        if options.diff_prefix:
-                                            comparer.write_diff(os.path.join(options.diff_prefix, root + '.diff.png'))
-                                        if last_bad < last_good:
-                                            diff_state(src_setup, last_good, call_no)
-                                        last_bad = call_no
-                                    else:
-                                        last_good = call_no
-
-                                    sys.stdout.flush()
-                                   
-                                    os.unlink(src_image)
-                                    break
-                            os.unlink(ref_image)
-                finally:
-                    src_proc.terminate()
-            finally:
-                ref_proc.terminate()
+            while True:
+                # Get the reference image
+                ref_image, ref_comment = read_pnm(ref_proc.stdout)
+                if ref_image is None:
+                    break
+
+                # Get the source image
+                src_image, src_comment = read_pnm(src_proc.stdout)
+                if src_image is None:
+                    break
+
+                assert ref_comment == src_comment
+
+                call_no = int(ref_comment.strip())
+
+                # Compare the two images
+                comparer = Comparer(ref_image, src_image)
+                precision = comparer.precision()
+
+                mismatch = precision < options.threshold
+
+                if mismatch:
+                    highligher.color(highligher.red)
+                    highligher.bold()
+                highligher.write('%u\t%f\n' % (call_no, precision))
+                if mismatch:
+                    highligher.normal()
+
+                if mismatch:
+                    if options.diff_prefix:
+                        prefix = os.path.join(options.diff_prefix, '%010u' % call_no)
+                        prefix_dir = os.path.dirname(prefix)
+                        if not os.path.isdir(prefix_dir):
+                            os.makedirs(prefix_dir)
+                        ref_image.save(prefix + '.ref.png')
+                        src_image.save(prefix + '.src.png')
+                        comparer.write_diff(prefix + '.diff.png')
+                    if last_bad < last_good:
+                        src_setup.diff_state(last_good, call_no)
+                    last_bad = call_no
+                else:
+                    last_good = call_no
+
+                highligher.flush()
         finally:
-            shutil.rmtree(ref_snapshot_dir)
+            src_proc.terminate()
     finally:
-        shutil.rmtree(src_snapshot_dir)
+        ref_proc.terminate()
 
 
 if __name__ == '__main__':
index 60d9ae97ca740b26b5820f76567a4d414c6055d3..0ed3937a8f8e669accf577c4b06d6472784e3c57 100755 (executable)
@@ -48,8 +48,15 @@ class Comparer:
     '''Image comparer.'''
 
     def __init__(self, ref_image, src_image, alpha = False):
-        self.ref_im = Image.open(ref_image)
-        self.src_im = Image.open(src_image)
+        if isinstance(ref_image, basestring):
+            self.ref_im = Image.open(ref_image)
+        else:
+            self.ref_im = ref_image
+
+        if isinstance(src_image, basestring):
+            self.src_im = Image.open(src_image)
+        else:
+            self.src_im = src_image
 
         # Ignore
         if not alpha: