Merge branch 'perf' of https://github.com/CendioOssman/tigervnc
diff --git a/cmake/StaticBuild.cmake b/cmake/StaticBuild.cmake
index 1ac4f6e..0ec3361 100644
--- a/cmake/StaticBuild.cmake
+++ b/cmake/StaticBuild.cmake
@@ -47,14 +47,18 @@
 
     set(GNUTLS_LIBRARIES "${GNUTLS_LIBRARIES} -Wl,-Bdynamic")
 
-    # GnuTLS uses various crypto-api stuff
     if (WIN32)
+      # GnuTLS uses various crypto-api stuff
       set(GNUTLS_LIBRARIES "${GNUTLS_LIBRARIES} -lcrypt32")
+      # And sockets
+      set(GNUTLS_LIBRARIES "${GNUTLS_LIBRARIES} -lws2_32")
     endif()
 
-    # nanosleep() lives here on Solaris
     if(${CMAKE_SYSTEM_NAME} MATCHES "SunOS")
+      # nanosleep() lives here on Solaris
       set(GNUTLS_LIBRARIES "${GNUTLS_LIBRARIES} -lrt")
+      # and socket functions are hidden here
+      set(GNUTLS_LIBRARIES "${GNUTLS_LIBRARIES} -lsocket")
     endif()
 
     # GnuTLS uses gettext and zlib, so make sure those are always
diff --git a/common/rdr/CMakeLists.txt b/common/rdr/CMakeLists.txt
index cc45f91..989ba2f 100644
--- a/common/rdr/CMakeLists.txt
+++ b/common/rdr/CMakeLists.txt
@@ -4,6 +4,7 @@
   Exception.cxx
   FdInStream.cxx
   FdOutStream.cxx
+  FileInStream.cxx
   HexInStream.cxx
   HexOutStream.cxx
   InStream.cxx
diff --git a/common/rdr/FileInStream.cxx b/common/rdr/FileInStream.cxx
new file mode 100644
index 0000000..18a9169
--- /dev/null
+++ b/common/rdr/FileInStream.cxx
@@ -0,0 +1,87 @@
+/* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
+ * Copyright (C) 2013 D. R. Commander.  All Rights Reserved.
+ * Copyright 2015 Pierre Ossman for Cendio AB
+ *
+ * This is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this software; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
+ * USA.
+ */
+
+#include <errno.h>
+
+#include <rdr/Exception.h>
+#include <rdr/FileInStream.h>
+
+using namespace rdr;
+
+FileInStream::FileInStream(const char *fileName)
+{
+  file = fopen(fileName, "rb");
+  if (!file)
+    throw SystemException("fopen", errno);
+  ptr = end = b;
+}
+
+FileInStream::~FileInStream(void) {
+  if (file) {
+    fclose(file);
+    file = NULL;
+  }
+}
+
+void FileInStream::reset(void) {
+  if (!file)
+    throw Exception("File is not open");
+  if (fseek(file, 0, SEEK_SET) != 0)
+    throw SystemException("fseek", errno);
+  ptr = end = b;
+}
+
+int FileInStream::pos()
+{
+  if (!file)
+    throw Exception("File is not open");
+
+  return ftell(file) + ptr - b;
+}
+
+int FileInStream::overrun(int itemSize, int nItems, bool wait)
+{
+  if (itemSize > sizeof(b))
+    throw Exception("FileInStream overrun: max itemSize exceeded");
+
+  if (end - ptr != 0)
+    memmove(b, ptr, end - ptr);
+
+  end -= ptr - b;
+  ptr = b;
+
+
+  while (end < b + itemSize) {
+    size_t n = fread((U8 *)end, b + sizeof(b) - end, 1, file);
+    if (n < 1) {
+      if (n < 0 || ferror(file))
+        throw Exception(strerror(errno));
+      if (feof(file))
+        throw EndOfStream();
+      if (n == 0) { return 0; }
+    }
+    end += b + sizeof(b) - end;
+  }
+
+  if (itemSize * nItems > end - ptr)
+    nItems = (end - ptr) / itemSize;
+
+  return nItems;
+}
diff --git a/common/rdr/FileInStream.h b/common/rdr/FileInStream.h
new file mode 100644
index 0000000..ace04f3
--- /dev/null
+++ b/common/rdr/FileInStream.h
@@ -0,0 +1,50 @@
+/* Copyright (C) 2013 D. R. Commander.  All Rights Reserved.
+ * Copyright 2015 Pierre Ossman for Cendio AB
+ *
+ * This is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this software; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
+ * USA.
+ */
+
+#ifndef __RDR_FILEINSTREAM_H__
+#define __RDR_FILEINSTREAM_H__
+
+#include <stdio.h>
+
+#include <rdr/InStream.h>
+
+namespace rdr {
+
+  class FileInStream : public InStream {
+
+  public:
+
+    FileInStream(const char *fileName);
+    ~FileInStream(void);
+
+    void reset(void);
+
+    int pos();
+
+  protected:
+    int overrun(int itemSize, int nItems, bool wait = true);
+
+  private:
+    U8 b[131072];
+    FILE *file;
+  };
+
+} // end of namespace rdr
+
+#endif
diff --git a/common/rfb/CConnection.h b/common/rfb/CConnection.h
index 8bd886a..0109fe8 100644
--- a/common/rfb/CConnection.h
+++ b/common/rfb/CConnection.h
@@ -136,6 +136,9 @@
   protected:
     void setState(stateEnum s) { state_ = s; }
 
+    void setReader(CMsgReader *r) { reader_ = r; }
+    void setWriter(CMsgWriter *w) { writer_ = w; }
+
   private:
     // This is a default implementation of fences that automatically
     // responds to requests, stating no support for synchronisation.
diff --git a/common/rfb/CMakeLists.txt b/common/rfb/CMakeLists.txt
index 6cd321e..e9b027a 100644
--- a/common/rfb/CMakeLists.txt
+++ b/common/rfb/CMakeLists.txt
@@ -67,7 +67,7 @@
   set(RFB_SOURCES ${RFB_SOURCES} WinPasswdValidator.cxx)
 endif(WIN32)
 
-set(RFB_LIBRARIES ${JPEG_LIBRARIES} os rdr)
+set(RFB_LIBRARIES ${JPEG_LIBRARIES} os rdr Xregion)
 
 if(HAVE_PAM)
   set(RFB_SOURCES ${RFB_SOURCES} UnixPasswordValidator.cxx
diff --git a/common/rfb/CMsgHandler.cxx b/common/rfb/CMsgHandler.cxx
index eff4776..ce8b271 100644
--- a/common/rfb/CMsgHandler.cxx
+++ b/common/rfb/CMsgHandler.cxx
@@ -74,3 +74,11 @@
 {
   cp.supportsContinuousUpdates = true;
 }
+
+void CMsgHandler::framebufferUpdateStart()
+{
+}
+
+void CMsgHandler::framebufferUpdateEnd()
+{
+}
diff --git a/common/rfb/CMsgHandler.h b/common/rfb/CMsgHandler.h
index 8e3c84e..5e333d2 100644
--- a/common/rfb/CMsgHandler.h
+++ b/common/rfb/CMsgHandler.h
@@ -57,8 +57,8 @@
     virtual void endOfContinuousUpdates();
     virtual void serverInit() = 0;
 
-    virtual void framebufferUpdateStart() = 0;
-    virtual void framebufferUpdateEnd() = 0;
+    virtual void framebufferUpdateStart();
+    virtual void framebufferUpdateEnd();
     virtual void dataRect(const Rect& r, int encoding) = 0;
 
     virtual void setColourMapEntries(int firstColour, int nColours,
diff --git a/common/rfb/EncodeManager.cxx b/common/rfb/EncodeManager.cxx
index ca60da4..e632387 100644
--- a/common/rfb/EncodeManager.cxx
+++ b/common/rfb/EncodeManager.cxx
@@ -23,6 +23,7 @@
 #include <rfb/SConnection.h>
 #include <rfb/SMsgWriter.h>
 #include <rfb/UpdateTracker.h>
+#include <rfb/LogWriter.h>
 
 #include <rfb/RawEncoder.h>
 #include <rfb/RREEncoder.h>
@@ -33,6 +34,8 @@
 
 using namespace rfb;
 
+static LogWriter vlog("EncodeManager");
+
 // Split each rectangle into smaller ones no larger than this area,
 // and no wider than this width.
 static const int SubRectMaxArea = 65536;
@@ -73,8 +76,50 @@
 
 };
 
+static const char *encoderClassName(EncoderClass klass)
+{
+  switch (klass) {
+  case encoderRaw:
+    return "Raw";
+  case encoderRRE:
+    return "RRE";
+  case encoderHextile:
+    return "Hextile";
+  case encoderTight:
+    return "Tight";
+  case encoderTightJPEG:
+    return "Tight (JPEG)";
+  case encoderZRLE:
+    return "ZRLE";
+  }
+
+  return "Unknown Encoder Class";
+}
+
+static const char *encoderTypeName(EncoderType type)
+{
+  switch (type) {
+  case encoderSolid:
+    return "Solid";
+  case encoderBitmap:
+    return "Bitmap";
+  case encoderBitmapRLE:
+    return "Bitmap RLE";
+  case encoderIndexed:
+    return "Indexed";
+  case encoderIndexedRLE:
+    return "Indexed RLE";
+  case encoderFullColour:
+    return "Full Colour";
+  }
+
+  return "Unknown Encoder Type";
+}
+
 EncodeManager::EncodeManager(SConnection* conn_) : conn(conn_)
 {
+  StatsVector::iterator iter;
+
   encoders.resize(encoderClassMax, NULL);
   activeEncoders.resize(encoderTypeMax, encoderRaw);
 
@@ -84,16 +129,78 @@
   encoders[encoderTight] = new TightEncoder(conn);
   encoders[encoderTightJPEG] = new TightJPEGEncoder(conn);
   encoders[encoderZRLE] = new ZRLEEncoder(conn);
+
+  updates = 0;
+  stats.resize(encoderClassMax);
+  for (iter = stats.begin();iter != stats.end();++iter) {
+    StatsVector::value_type::iterator iter2;
+    iter->resize(encoderTypeMax);
+    for (iter2 = iter->begin();iter2 != iter->end();++iter2)
+      memset(&*iter2, 0, sizeof(EncoderStats));
+  }
 }
 
 EncodeManager::~EncodeManager()
 {
   std::vector<Encoder*>::iterator iter;
 
+  logStats();
+
   for (iter = encoders.begin();iter != encoders.end();iter++)
     delete *iter;
 }
 
+void EncodeManager::logStats()
+{
+  int i, j;
+
+  unsigned rects;
+  unsigned long long pixels, bytes, equivalent;
+
+  double ratio;
+
+  rects = 0;
+  pixels = bytes = equivalent = 0;
+
+  vlog.info("Framebuffer updates: %u", updates);
+
+  for (i = 0;i < stats.size();i++) {
+    // Did this class do anything at all?
+    for (j = 0;j < stats[i].size();j++) {
+      if (stats[i][j].rects != 0)
+        break;
+    }
+    if (j == stats[i].size())
+      continue;
+
+    vlog.info("  %s:", encoderClassName((EncoderClass)i));
+
+    for (j = 0;j < stats[i].size();j++) {
+      if (stats[i][j].rects == 0)
+        continue;
+
+      rects += stats[i][j].rects;
+      pixels += stats[i][j].pixels;
+      bytes += stats[i][j].bytes;
+      equivalent += stats[i][j].equivalent;
+
+      ratio = (double)stats[i][j].equivalent / stats[i][j].bytes;
+
+      vlog.info("    %s: %u rects, %llu pixels",
+                encoderTypeName((EncoderType)j),
+                stats[i][j].rects, stats[i][j].pixels);
+      vlog.info("    %*s  %llu bytes (%g ratio)",
+                strlen(encoderTypeName((EncoderType)j)), "",
+                stats[i][j].bytes, ratio);
+    }
+  }
+
+  ratio = (double)equivalent / bytes;
+
+  vlog.info("  Total: %u rects, %llu pixels", rects, pixels);
+  vlog.info("         %llu bytes (%g ratio)", bytes, ratio);
+}
+
 bool EncodeManager::supported(int encoding)
 {
   switch (encoding) {
@@ -114,6 +221,8 @@
     int nRects;
     Region changed;
 
+    updates++;
+
     prepareEncoders();
 
     if (conn->cp.supportsLastRect)
@@ -292,6 +401,40 @@
   return numRects;
 }
 
+Encoder *EncodeManager::startRect(const Rect& rect, int type)
+{
+  Encoder *encoder;
+  int klass, equiv;
+
+  activeType = type;
+  klass = activeEncoders[activeType];
+
+  beforeLength = conn->getOutStream()->length();
+
+  stats[klass][activeType].rects++;
+  stats[klass][activeType].pixels += rect.area();
+  equiv = 12 + rect.area() * conn->cp.pf().bpp/8;
+  stats[klass][activeType].equivalent += equiv;
+
+  encoder = encoders[klass];
+  conn->writer()->startRect(rect, encoder->encoding);
+
+  return encoder;
+}
+
+void EncodeManager::endRect()
+{
+  int klass;
+  int length;
+
+  conn->writer()->endRect();
+
+  length = conn->getOutStream()->length() - beforeLength;
+
+  klass = activeEncoders[activeType];
+  stats[klass][activeType].bytes += length;
+}
+
 void EncodeManager::writeCopyRects(const UpdateInfo& ui)
 {
   std::vector<Rect> rects;
@@ -309,84 +452,104 @@
   std::vector<Rect> rects;
   std::vector<Rect>::const_iterator rect;
 
-  // FIXME: This gives up after the first rect it finds. A large update
-  //        (like a whole screen refresh) might have lots of large solid
-  //        areas.
-
   changed->get_rects(&rects);
-  for (rect = rects.begin(); rect != rects.end(); ++rect) {
-    Rect sr;
-    int dx, dy, dw, dh;
+  for (rect = rects.begin(); rect != rects.end(); ++rect)
+    findSolidRect(*rect, changed, pb);
+}
 
-    // We start by finding a solid 16x16 block
-    for (dy = rect->tl.y; dy < rect->br.y; dy += SolidSearchBlock) {
+void EncodeManager::findSolidRect(const Rect& rect, Region *changed,
+                                  const PixelBuffer* pb)
+{
+  Rect sr;
+  int dx, dy, dw, dh;
 
-      dh = SolidSearchBlock;
-      if (dy + dh > rect->br.y)
-        dh = rect->br.y - dy;
+  // We start by finding a solid 16x16 block
+  for (dy = rect.tl.y; dy < rect.br.y; dy += SolidSearchBlock) {
 
-      for (dx = rect->tl.x; dx < rect->br.x; dx += SolidSearchBlock) {
-        // We define it like this to guarantee alignment
-        rdr::U32 _buffer;
-        rdr::U8* colourValue = (rdr::U8*)&_buffer;
+    dh = SolidSearchBlock;
+    if (dy + dh > rect.br.y)
+      dh = rect.br.y - dy;
 
-        dw = SolidSearchBlock;
-        if (dx + dw > rect->br.x)
-          dw = rect->br.x - dx;
+    for (dx = rect.tl.x; dx < rect.br.x; dx += SolidSearchBlock) {
+      // We define it like this to guarantee alignment
+      rdr::U32 _buffer;
+      rdr::U8* colourValue = (rdr::U8*)&_buffer;
 
-        pb->getImage(colourValue, Rect(dx, dy, dx+1, dy+1));
+      dw = SolidSearchBlock;
+      if (dx + dw > rect.br.x)
+        dw = rect.br.x - dx;
 
-        sr.setXYWH(dx, dy, dw, dh);
-        if (checkSolidTile(sr, colourValue, pb)) {
-          Rect erb, erp;
+      pb->getImage(colourValue, Rect(dx, dy, dx+1, dy+1));
 
-          Encoder *encoder;
+      sr.setXYWH(dx, dy, dw, dh);
+      if (checkSolidTile(sr, colourValue, pb)) {
+        Rect erb, erp;
 
-          // We then try extending the area by adding more blocks
-          // in both directions and pick the combination that gives
-          // the largest area.
-          sr.setXYWH(dx, dy, rect->br.x - dx, rect->br.y - dy);
-          extendSolidAreaByBlock(sr, colourValue, pb, &erb);
+        Encoder *encoder;
 
-          // Did we end up getting the entire rectangle?
-          if (erb.equals(*rect))
-            erp = erb;
-          else {
-            // Don't bother with sending tiny rectangles
-            if (erb.area() < SolidBlockMinArea)
-              continue;
+        // We then try extending the area by adding more blocks
+        // in both directions and pick the combination that gives
+        // the largest area.
+        sr.setXYWH(dx, dy, rect.br.x - dx, rect.br.y - dy);
+        extendSolidAreaByBlock(sr, colourValue, pb, &erb);
 
-            // Extend the area again, but this time one pixel
-            // row/column at a time.
-            extendSolidAreaByPixel(*rect, erb, colourValue, pb, &erp);
-          }
+        // Did we end up getting the entire rectangle?
+        if (erb.equals(rect))
+          erp = erb;
+        else {
+          // Don't bother with sending tiny rectangles
+          if (erb.area() < SolidBlockMinArea)
+            continue;
 
-          // Send solid-color rectangle.
-          encoder = encoders[activeEncoders[encoderSolid]];
-          conn->writer()->startRect(erp, encoder->encoding);
-          if (encoder->flags & EncoderUseNativePF) {
-            encoder->writeSolidRect(erp.width(), erp.height(),
-                                    pb->getPF(), colourValue);
-          } else {
-            rdr::U32 _buffer2;
-            rdr::U8* converted = (rdr::U8*)&_buffer2;
-
-            conn->cp.pf().bufferFromBuffer(converted, pb->getPF(),
-                                           colourValue, 1);
-
-            encoder->writeSolidRect(erp.width(), erp.height(),
-                                    conn->cp.pf(), converted);
-          }
-          conn->writer()->endRect();
-
-          changed->assign_subtract(Region(erp));
-
-          break;
+          // Extend the area again, but this time one pixel
+          // row/column at a time.
+          extendSolidAreaByPixel(rect, erb, colourValue, pb, &erp);
         }
-      }
 
-      if (dx < rect->br.x)
-        break;
+        // Send solid-color rectangle.
+        encoder = startRect(erp, encoderSolid);
+        if (encoder->flags & EncoderUseNativePF) {
+          encoder->writeSolidRect(erp.width(), erp.height(),
+                                  pb->getPF(), colourValue);
+        } else {
+          rdr::U32 _buffer2;
+          rdr::U8* converted = (rdr::U8*)&_buffer2;
+
+          conn->cp.pf().bufferFromBuffer(converted, pb->getPF(),
+                                         colourValue, 1);
+
+          encoder->writeSolidRect(erp.width(), erp.height(),
+                                  conn->cp.pf(), converted);
+        }
+        endRect();
+
+        changed->assign_subtract(Region(erp));
+
+        // Search remaining areas by recursion
+        // FIXME: Is this the best way to divide things up?
+
+        // Left? (Note that we've already searched a SolidSearchBlock
+        //        pixels high strip here)
+        if ((erp.tl.x != rect.tl.x) && (erp.height() > SolidSearchBlock)) {
+          sr.setXYWH(rect.tl.x, erp.tl.y + SolidSearchBlock,
+                     erp.tl.x - rect.tl.x, erp.height() - SolidSearchBlock);
+          findSolidRect(sr, changed, pb);
+        }
+
+        // Right?
+        if (erp.br.x != rect.br.x) {
+          sr.setXYWH(erp.br.x, erp.tl.y, rect.br.x - erp.br.x, erp.height());
+          findSolidRect(sr, changed, pb);
+        }
+
+        // Below?
+        if (erp.br.y != rect.br.y) {
+          sr.setXYWH(rect.tl.x, erp.br.y, rect.width(), rect.br.y - erp.br.y);
+          findSolidRect(sr, changed, pb);
+        }
+
+        return;
+      }
     }
   }
 }
@@ -461,7 +624,7 @@
 
   // Special exception inherited from the Tight encoder
   if (activeEncoders[encoderFullColour] == encoderTightJPEG) {
-    if (conn->cp.compressLevel < 2)
+    if ((conn->cp.compressLevel != -1) && (conn->cp.compressLevel < 2))
       maxColours = 24;
     else
       maxColours = 96;
@@ -507,14 +670,14 @@
       type = encoderIndexed;
   }
 
-  encoder = encoders[activeEncoders[type]];
+  encoder = startRect(rect, type);
 
   if (encoder->flags & EncoderUseNativePF)
     ppb = preparePixelBuffer(rect, pb, false);
 
-  conn->writer()->startRect(rect, encoder->encoding);
   encoder->writeRect(ppb, info.palette);
-  conn->writer()->endRect();
+
+  endRect();
 }
 
 bool EncodeManager::checkSolidTile(const Rect& r, const rdr::U8* colourValue,
diff --git a/common/rfb/EncodeManager.h b/common/rfb/EncodeManager.h
index df0275c..a3df8f7 100644
--- a/common/rfb/EncodeManager.h
+++ b/common/rfb/EncodeManager.h
@@ -41,6 +41,8 @@
     EncodeManager(SConnection* conn);
     ~EncodeManager();
 
+    void logStats();
+
     // Hack to let ConnParams calculate the client's preferred encoding
     static bool supported(int encoding);
 
@@ -52,8 +54,12 @@
 
     int computeNumRects(const Region& changed);
 
+    Encoder *startRect(const Rect& rect, int type);
+    void endRect();
+
     void writeCopyRects(const UpdateInfo& ui);
     void writeSolidRects(Region *changed, const PixelBuffer* pb);
+    void findSolidRect(const Rect& rect, Region *changed, const PixelBuffer* pb);
     void writeRects(const Region& changed, const PixelBuffer* pb);
 
     void writeSubRect(const Rect& rect, const PixelBuffer *pb);
@@ -97,6 +103,19 @@
     std::vector<Encoder*> encoders;
     std::vector<int> activeEncoders;
 
+    struct EncoderStats {
+      unsigned rects;
+      unsigned long long bytes;
+      unsigned long long pixels;
+      unsigned long long equivalent;
+    };
+    typedef std::vector< std::vector<struct EncoderStats> > StatsVector;
+
+    unsigned updates;
+    StatsVector stats;
+    int activeType;
+    int beforeLength;
+
     class OffsetPixelBuffer : public FullFramePixelBuffer {
     public:
       OffsetPixelBuffer() {}
diff --git a/common/rfb/PixelBuffer.cxx b/common/rfb/PixelBuffer.cxx
index b1359c2..b03af1a 100644
--- a/common/rfb/PixelBuffer.cxx
+++ b/common/rfb/PixelBuffer.cxx
@@ -99,23 +99,43 @@
 void ModifiablePixelBuffer::fillRect(const Rect& r, Pixel pix)
 {
   int stride;
-  U8 *buf, pixbuf[4];
+  U8 *buf;
   int w, h, b;
 
-  buf = getBufferRW(r, &stride);
   w = r.width();
   h = r.height();
   b = format.bpp/8;
 
-  format.bufferFromPixel(pixbuf, pix);
+  if (h == 0)
+    return;
 
-  while (h--) {
-    int w_ = w;
-    while (w_--) {
+  buf = getBufferRW(r, &stride);
+
+  if (b == 1) {
+    while (h--) {
+      memset(buf, pix, w);
+      buf += stride * b;
+    }
+  } else {
+    U8 pixbuf[4], *start;
+    int w1;
+
+    start = buf;
+
+    format.bufferFromPixel(pixbuf, pix);
+
+    w1 = w;
+    while (w1--) {
       memcpy(buf, pixbuf, b);
       buf += b;
     }
     buf += (stride - w) * b;
+    h--;
+
+    while (h--) {
+      memcpy(buf, start, w * b);
+      buf += stride * b;
+    }
   }
 
   commitBufferRW(r);
diff --git a/common/rfb/SConnection.cxx b/common/rfb/SConnection.cxx
index 99a4850..bfff70c 100644
--- a/common/rfb/SConnection.cxx
+++ b/common/rfb/SConnection.cxx
@@ -48,11 +48,10 @@
 const SConnection::AccessRights SConnection::AccessFull           = 0xffff;
 
 
-SConnection::SConnection(bool reverseConnection_)
+SConnection::SConnection()
   : readyForSetColourMapEntries(false),
     is(0), os(0), reader_(0), writer_(0),
     security(0), ssecurity(0), state_(RFBSTATE_UNINITIALISED),
-    reverseConnection(reverseConnection_),
     preferredEncoding(encodingRaw)
 {
   defaultMajorVersion = 3;
@@ -271,7 +270,7 @@
   os->flush();
 }
 
-void SConnection::setEncodings(int nEncodings, rdr::S32* encodings)
+void SConnection::setEncodings(int nEncodings, const rdr::S32* encodings)
 {
   int i;
 
diff --git a/common/rfb/SConnection.h b/common/rfb/SConnection.h
index 005a7a8..ef1de2b 100644
--- a/common/rfb/SConnection.h
+++ b/common/rfb/SConnection.h
@@ -38,7 +38,7 @@
   class SConnection : public SMsgHandler {
   public:
 
-    SConnection(bool reverseConnection_);
+    SConnection();
     virtual ~SConnection();
 
     // Methods to initialise the connection
@@ -71,7 +71,7 @@
 
     // Overridden from SMsgHandler
 
-    virtual void setEncodings(int nEncodings, rdr::S32* encodings);
+    virtual void setEncodings(int nEncodings, const rdr::S32* encodings);
 
 
     // Methods to be overridden in a derived class
@@ -183,6 +183,11 @@
 
   protected:
     void setState(stateEnum s) { state_ = s; }
+
+    void setReader(SMsgReader *r) { reader_ = r; }
+    void setWriter(SMsgWriter *w) { writer_ = w; }
+
+  private:
     void writeFakeColourMap(void);
 
     bool readyForSetColourMapEntries;
@@ -201,7 +206,6 @@
     SecurityServer *security;
     SSecurity* ssecurity;
     stateEnum state_;
-    bool reverseConnection;
     rdr::S32 preferredEncoding;
   };
 }
diff --git a/common/rfb/SMsgHandler.cxx b/common/rfb/SMsgHandler.cxx
index d4046b1..388b21f 100644
--- a/common/rfb/SMsgHandler.cxx
+++ b/common/rfb/SMsgHandler.cxx
@@ -39,7 +39,7 @@
   cp.setPF(pf);
 }
 
-void SMsgHandler::setEncodings(int nEncodings, rdr::S32* encodings)
+void SMsgHandler::setEncodings(int nEncodings, const rdr::S32* encodings)
 {
   bool firstFence, firstContinuousUpdates;
 
diff --git a/common/rfb/SMsgHandler.h b/common/rfb/SMsgHandler.h
index c21cf52..74509e0 100644
--- a/common/rfb/SMsgHandler.h
+++ b/common/rfb/SMsgHandler.h
@@ -46,7 +46,7 @@
     virtual void clientInit(bool shared);
 
     virtual void setPixelFormat(const PixelFormat& pf);
-    virtual void setEncodings(int nEncodings, rdr::S32* encodings);
+    virtual void setEncodings(int nEncodings, const rdr::S32* encodings);
     virtual void framebufferUpdateRequest(const Rect& r, bool incremental) = 0;
     virtual void setDesktopSize(int fb_width, int fb_height,
                                 const ScreenSet& layout) = 0;
diff --git a/common/rfb/SMsgWriter.cxx b/common/rfb/SMsgWriter.cxx
index 0d61292..9aee96d 100644
--- a/common/rfb/SMsgWriter.cxx
+++ b/common/rfb/SMsgWriter.cxx
@@ -33,31 +33,15 @@
 static LogWriter vlog("SMsgWriter");
 
 SMsgWriter::SMsgWriter(ConnParams* cp_, rdr::OutStream* os_)
-  : cp(cp_), os(os_), currentEncoding(0),
+  : cp(cp_), os(os_),
     nRectsInUpdate(0), nRectsInHeader(0),
     needSetDesktopSize(false), needExtendedDesktopSize(false),
-    needSetDesktopName(false), needSetCursor(false), needSetXCursor(false),
-    lenBeforeRect(0), updatesSent(0), rawBytesEquivalent(0)
+    needSetDesktopName(false), needSetCursor(false), needSetXCursor(false)
 {
-  for (int i = 0; i <= encodingMax; i++) {
-    bytesSent[i] = 0;
-    rectsSent[i] = 0;
-  }
 }
 
 SMsgWriter::~SMsgWriter()
 {
-  vlog.info("framebuffer updates %d",updatesSent);
-  int bytes = 0;
-  for (int i = 0; i <= encodingMax; i++) {
-    if (i != encodingCopyRect)
-      bytes += bytesSent[i];
-    if (rectsSent[i])
-      vlog.info("  %s rects %d, bytes %d",
-                encodingName(i), rectsSent[i], bytesSent[i]);
-  }
-  vlog.info("  raw bytes equivalent %llu, compression ratio %f",
-          rawBytesEquivalent, (double)rawBytesEquivalent / bytes);
 }
 
 void SMsgWriter::writeServerInit()
@@ -276,7 +260,6 @@
     os->writeU32(pseudoEncodingLastRect);
   }
 
-  updatesSent++;
   endMsg();
 }
 
@@ -293,11 +276,6 @@
   if (++nRectsInUpdate > nRectsInHeader && nRectsInHeader)
     throw Exception("SMsgWriter::startRect: nRects out of sync");
 
-  currentEncoding = encoding;
-  lenBeforeRect = os->length();
-  if (encoding != encodingCopyRect)
-    rawBytesEquivalent += 12 + r.width() * r.height() * (cp->pf().bpp/8);
-
   os->writeS16(r.tl.x);
   os->writeS16(r.tl.y);
   os->writeU16(r.width());
@@ -307,10 +285,6 @@
 
 void SMsgWriter::endRect()
 {
-  if (currentEncoding <= encodingMax) {
-    bytesSent[currentEncoding] += os->length() - lenBeforeRect;
-    rectsSent[currentEncoding]++;
-  }
 }
 
 void SMsgWriter::startMsg(int type)
diff --git a/common/rfb/SMsgWriter.h b/common/rfb/SMsgWriter.h
index deddd3c..917b933 100644
--- a/common/rfb/SMsgWriter.h
+++ b/common/rfb/SMsgWriter.h
@@ -109,11 +109,6 @@
     void startRect(const Rect& r, int enc);
     void endRect();
 
-    int getUpdatesSent()           { return updatesSent; }
-    int getRectsSent(int encoding) { return rectsSent[encoding]; }
-    int getBytesSent(int encoding) { return bytesSent[encoding]; }
-    rdr::U64 getRawBytesEquivalent()    { return rawBytesEquivalent; }
-
   protected:
     void startMsg(int type);
     void endMsg();
@@ -137,8 +132,6 @@
     ConnParams* cp;
     rdr::OutStream* os;
 
-    int currentEncoding;
-
     int nRectsInUpdate;
     int nRectsInHeader;
 
@@ -149,12 +142,6 @@
     bool needSetCursor;
     bool needSetXCursor;
 
-    int lenBeforeRect;
-    int updatesSent;
-    int bytesSent[encodingMax+1];
-    int rectsSent[encodingMax+1];
-    rdr::U64 rawBytesEquivalent;
-
     typedef struct {
       rdr::U16 reason, result;
       int fb_width, fb_height;
diff --git a/common/rfb/VNCSConnectionST.cxx b/common/rfb/VNCSConnectionST.cxx
index 746bb90..e261cfb 100644
--- a/common/rfb/VNCSConnectionST.cxx
+++ b/common/rfb/VNCSConnectionST.cxx
@@ -66,7 +66,7 @@
 
 VNCSConnectionST::VNCSConnectionST(VNCServerST* server_, network::Socket *s,
                                    bool reverse)
-  : SConnection(reverse), sock(s),
+  : sock(s), reverseConnection(reverse),
     queryConnectTimer(this), inProcessMessages(false),
     pendingSyncFence(false), syncFence(false), fenceFlags(0),
     fenceDataLen(0), fenceData(NULL),
diff --git a/common/rfb/VNCSConnectionST.h b/common/rfb/VNCSConnectionST.h
index 7b25570..978879f 100644
--- a/common/rfb/VNCSConnectionST.h
+++ b/common/rfb/VNCSConnectionST.h
@@ -173,6 +173,7 @@
 
     network::Socket* sock;
     CharArray peerEndpoint;
+    bool reverseConnection;
 
     Timer queryConnectTimer;
 
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index afab3e2..e99c825 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -7,3 +7,9 @@
 
 add_executable(conv conv.cxx)
 target_link_libraries(conv rfb)
+
+add_executable(decperf decperf.cxx)
+target_link_libraries(decperf test_util rfb)
+
+add_executable(encperf encperf.cxx)
+target_link_libraries(encperf test_util rfb)
diff --git a/tests/decperf.cxx b/tests/decperf.cxx
new file mode 100644
index 0000000..6714cb1
--- /dev/null
+++ b/tests/decperf.cxx
@@ -0,0 +1,217 @@
+/* Copyright 2015 Pierre Ossman <ossman@cendio.se> for Cendio AB
+ * 
+ * This is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this software; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
+ * USA.
+ */
+
+/*
+ * This program reads files produced by TightVNC's/TurboVNC's
+ * compare-encodings. It is basically a dump of the RFB protocol
+ * from the server side from the ServerInit message and forward.
+ * It is assumed that the client is using a bgr888 (LE) pixel
+ * format.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <math.h>
+
+#include <rdr/Exception.h>
+#include <rdr/FileInStream.h>
+
+#include <rfb/CConnection.h>
+#include <rfb/CMsgReader.h>
+#include <rfb/Decoder.h>
+#include <rfb/PixelBuffer.h>
+#include <rfb/PixelFormat.h>
+
+#include "util.h"
+
+// FIXME: Files are always in this format
+static const rfb::PixelFormat filePF(32, 24, false, true, 255, 255, 255, 0, 8, 16);
+
+class CConn : public rfb::CConnection {
+public:
+  CConn(const char *filename);
+  ~CConn();
+
+  virtual void setDesktopSize(int w, int h);
+  virtual void setPixelFormat(const rfb::PixelFormat& pf);
+  virtual void setCursor(int, int, const rfb::Point&, void*, void*);
+  virtual void dataRect(const rfb::Rect&, int);
+  virtual void setColourMapEntries(int, int, rdr::U16*);
+  virtual void bell();
+  virtual void serverCutText(const char*, rdr::U32);
+
+public:
+  double cpuTime;
+
+protected:
+  rdr::FileInStream *in;
+  rfb::Decoder *decoders[rfb::encodingMax+1];
+  rfb::ManagedPixelBuffer pb;
+};
+
+CConn::CConn(const char *filename)
+{
+  int i;
+
+  cpuTime = 0.0;
+
+  in = new rdr::FileInStream(filename);
+  setStreams(in, NULL);
+
+  memset(decoders, 0, sizeof(decoders));
+  for (i = 0;i < rfb::encodingMax;i++) {
+    if (!rfb::Decoder::supported(i))
+      continue;
+
+    decoders[i] = rfb::Decoder::createDecoder(i, this);
+  }
+
+  // Need to skip the initial handshake
+  setState(RFBSTATE_INITIALISATION);
+  // That also means that the reader and writer weren't setup
+  setReader(new rfb::CMsgReader(this, in));
+}
+
+CConn::~CConn()
+{
+  int i;
+
+  delete in;
+
+  for (i = 0;i < rfb::encodingMax;i++)
+    delete decoders[i];
+}
+
+void CConn::setDesktopSize(int w, int h)
+{
+  CConnection::setDesktopSize(w, h);
+
+  pb.setSize(cp.width, cp.height);
+}
+
+void CConn::setPixelFormat(const rfb::PixelFormat& pf)
+{
+  // Override format
+  CConnection::setPixelFormat(filePF);
+
+  pb.setPF(cp.pf());
+}
+
+void CConn::setCursor(int, int, const rfb::Point&, void*, void*)
+{
+}
+
+void CConn::dataRect(const rfb::Rect &r, int encoding)
+{
+  if (!decoders[encoding])
+    throw rdr::Exception("Unknown encoding");
+
+  startCpuCounter();
+  decoders[encoding]->readRect(r, &pb);
+  endCpuCounter();
+
+  cpuTime += getCpuCounter();
+}
+
+void CConn::setColourMapEntries(int, int, rdr::U16*)
+{
+}
+
+void CConn::bell()
+{
+}
+
+void CConn::serverCutText(const char*, rdr::U32)
+{
+}
+
+static double runTest(const char *fn)
+{
+  CConn *cc;
+  double time;
+
+  cc = new CConn(fn);
+
+  try {
+    while (true)
+      cc->processMsg();
+  } catch (rdr::EndOfStream e) {
+  } catch (rdr::Exception e) {
+    fprintf(stderr, "Failed to run rfb file: %s\n", e.str());
+    exit(1);
+  }
+
+  time = cc->cpuTime;
+
+  delete cc;
+
+  return time;
+}
+
+static void sort(double *array, int count)
+{
+  bool sorted;
+  int i;
+  do {
+    sorted = true;
+    for (i = 1;i < count;i++) {
+      if (array[i-1] > array[i]) {
+        double d;
+        d = array[i];
+        array[i] = array[i-1];
+        array[i-1] = d;
+        sorted = false;
+      }
+    }
+  } while (!sorted);
+}
+
+static const int runCount = 9;
+
+int main(int argc, char **argv)
+{
+  int i;
+  double times[runCount], dev[runCount];
+  double median, meddev;
+
+  if (argc != 2) {
+    printf("Syntax: %s <rfb file>\n", argv[0]);
+    return 1;
+  }
+
+  // Warmup
+  runTest(argv[1]);
+
+  // Multiple runs to get a good average
+  for (i = 0;i < runCount;i++)
+    times[i] = runTest(argv[1]);
+
+  // Calculate median and median deviation
+  sort(times, runCount);
+  median = times[runCount/2];
+
+  for (i = 0;i < runCount;i++)
+    dev[i] = fabs((times[i] - median) / median) * 100;
+
+  sort(dev, runCount);
+  meddev = dev[runCount/2];
+
+  printf("CPU time: %g s (+/- %g %)\n", median, meddev);
+
+  return 0;
+}
diff --git a/tests/encperf.cxx b/tests/encperf.cxx
new file mode 100644
index 0000000..60905c8
--- /dev/null
+++ b/tests/encperf.cxx
@@ -0,0 +1,439 @@
+/* Copyright 2015 Pierre Ossman <ossman@cendio.se> for Cendio AB
+ * 
+ * This is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ * 
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this software; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
+ * USA.
+ */
+
+/*
+ * This program reads files produced by TightVNC's/TurboVNC's
+ * fbs-dump, which in turn takes files from rfbproxy. It is
+ * basically a dump of the RFB protocol from the server side after
+ * the ServerInit message. Mostly this consists of FramebufferUpdate
+ * message using the HexTile encoding. Screen size and pixel format
+ * are not encoded in the file and must be specified by the user.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <math.h>
+
+#include <rdr/Exception.h>
+#include <rdr/OutStream.h>
+#include <rdr/FileInStream.h>
+
+#include <rfb/PixelFormat.h>
+
+#include <rfb/CConnection.h>
+#include <rfb/CMsgReader.h>
+#include <rfb/Decoder.h>
+#include <rfb/UpdateTracker.h>
+
+#include <rfb/EncodeManager.h>
+#include <rfb/SConnection.h>
+#include <rfb/SMsgWriter.h>
+
+#include "util.h"
+
+static rfb::IntParameter width("width", "Frame buffer width", 0);
+static rfb::IntParameter height("height", "Frame buffer height", 0);
+
+static rfb::StringParameter format("format", "Pixel format (e.g. bgr888)", "");
+
+// The frame buffer (and output) is always this format
+static const rfb::PixelFormat fbPF(32, 24, false, true, 255, 255, 255, 0, 8, 16);
+
+// Encodings to use
+static const rdr::S32 encodings[] = {
+  rfb::encodingTight, rfb::encodingCopyRect, rfb::encodingRRE,
+  rfb::encodingHextile, rfb::encodingZRLE, rfb::pseudoEncodingLastRect,
+  rfb::pseudoEncodingQualityLevel0 + 8 };
+
+class DummyOutStream : public rdr::OutStream {
+public:
+  DummyOutStream();
+
+  virtual int length();
+  virtual void flush();
+
+private:
+  virtual int overrun(int itemSize, int nItems);
+
+  int offset;
+  rdr::U8 buf[131072];
+};
+
+class CConn : public rfb::CConnection {
+public:
+  CConn(const char *filename);
+  ~CConn();
+
+  double getRatio();
+
+  virtual void setDesktopSize(int w, int h);
+  virtual void setCursor(int, int, const rfb::Point&, void*, void*);
+  virtual void framebufferUpdateStart();
+  virtual void framebufferUpdateEnd();
+  virtual void dataRect(const rfb::Rect&, int);
+  virtual void setColourMapEntries(int, int, rdr::U16*);
+  virtual void bell();
+  virtual void serverCutText(const char*, rdr::U32);
+
+public:
+  double decodeTime;
+  double encodeTime;
+
+protected:
+  rdr::FileInStream *in;
+  rfb::Decoder *decoders[rfb::encodingMax+1];
+  rfb::ManagedPixelBuffer pb;
+  rfb::SimpleUpdateTracker updates;
+  class SConn *sc;
+};
+
+class Manager : public rfb::EncodeManager {
+public:
+  Manager(class rfb::SConnection *conn);
+
+  double getRatio();
+};
+
+class SConn : public rfb::SConnection {
+public:
+  SConn();
+  ~SConn();
+
+  void writeUpdate(const rfb::UpdateInfo& ui, const rfb::PixelBuffer* pb);
+
+  double getRatio();
+
+  virtual void setAccessRights(AccessRights ar);
+
+  virtual void setDesktopSize(int fb_width, int fb_height,
+                              const rfb::ScreenSet& layout);
+
+protected:
+  DummyOutStream *out;
+  Manager *manager;
+};
+
+DummyOutStream::DummyOutStream()
+{
+  offset = 0;
+  ptr = buf;
+  end = buf + sizeof(buf);
+}
+
+int DummyOutStream::length()
+{
+  flush();
+  return offset;
+}
+
+void DummyOutStream::flush()
+{
+  offset += ptr - buf;
+  ptr = buf;
+}
+
+int DummyOutStream::overrun(int itemSize, int nItems)
+{
+  flush();
+}
+
+CConn::CConn(const char *filename)
+{
+  int i;
+
+  decodeTime = 0.0;
+  encodeTime = 0.0;
+
+  in = new rdr::FileInStream(filename);
+  setStreams(in, NULL);
+
+  memset(decoders, 0, sizeof(decoders));
+  for (i = 0;i < rfb::encodingMax;i++) {
+    if (!rfb::Decoder::supported(i))
+      continue;
+
+    decoders[i] = rfb::Decoder::createDecoder(i, this);
+  }
+
+  pb.setPF(fbPF);
+
+  // Need to skip the initial handshake and ServerInit
+  setState(RFBSTATE_NORMAL);
+  // That also means that the reader and writer weren't setup
+  setReader(new rfb::CMsgReader(this, in));
+  // Nor the frame buffer size and format
+  setDesktopSize(width, height);
+  rfb::PixelFormat pf;
+  pf.parse(format);
+  setPixelFormat(pf);
+
+  sc = new SConn();
+  sc->cp.setPF(pb.getPF());
+  sc->setEncodings(sizeof(encodings)/sizeof(*encodings), encodings);
+}
+
+CConn::~CConn()
+{
+  int i;
+
+  delete sc;
+
+  delete in;
+
+  for (i = 0;i < rfb::encodingMax;i++)
+    delete decoders[i];
+}
+
+double CConn::getRatio()
+{
+  return sc->getRatio();
+}
+
+void CConn::setDesktopSize(int w, int h)
+{
+  CConnection::setDesktopSize(w, h);
+
+  pb.setSize(cp.width, cp.height);
+}
+
+void CConn::setCursor(int, int, const rfb::Point&, void*, void*)
+{
+}
+
+void CConn::framebufferUpdateStart()
+{
+  updates.clear();
+}
+
+void CConn::framebufferUpdateEnd()
+{
+  rfb::UpdateInfo ui;
+  rfb::Region clip(pb.getRect());
+
+  updates.getUpdateInfo(&ui, clip);
+
+  startCpuCounter();
+  sc->writeUpdate(ui, &pb);
+  endCpuCounter();
+
+  encodeTime += getCpuCounter();
+}
+
+void CConn::dataRect(const rfb::Rect &r, int encoding)
+{
+  if (!decoders[encoding])
+    throw rdr::Exception("Unknown encoding");
+
+  startCpuCounter();
+  decoders[encoding]->readRect(r, &pb);
+  endCpuCounter();
+
+  decodeTime += getCpuCounter();
+
+  if (encoding != rfb::encodingCopyRect) // FIXME
+    updates.add_changed(rfb::Region(r));
+}
+
+void CConn::setColourMapEntries(int, int, rdr::U16*)
+{
+}
+
+void CConn::bell()
+{
+}
+
+void CConn::serverCutText(const char*, rdr::U32)
+{
+}
+
+Manager::Manager(class rfb::SConnection *conn) :
+  EncodeManager(conn)
+{
+}
+
+double Manager::getRatio()
+{
+  StatsVector::iterator iter;
+  unsigned long long bytes, equivalent;
+
+  bytes = equivalent = 0;
+  for (iter = stats.begin();iter != stats.end();++iter) {
+    StatsVector::value_type::iterator iter2;
+    for (iter2 = iter->begin();iter2 != iter->end();++iter2) {
+      bytes += iter2->bytes;
+      equivalent += iter2->equivalent;
+    }
+  }
+
+  return (double)equivalent / bytes;
+}
+
+SConn::SConn()
+{
+  out = new DummyOutStream;
+  setStreams(NULL, out);
+
+  setWriter(new rfb::SMsgWriter(&cp, out));
+
+  manager = new Manager(this);
+}
+
+SConn::~SConn()
+{
+  delete manager;
+  delete out;
+}
+
+void SConn::writeUpdate(const rfb::UpdateInfo& ui, const rfb::PixelBuffer* pb)
+{
+  manager->writeUpdate(ui, pb, NULL);
+}
+
+double SConn::getRatio()
+{
+  return manager->getRatio();
+}
+
+void SConn::setAccessRights(AccessRights ar)
+{
+}
+
+void SConn::setDesktopSize(int fb_width, int fb_height,
+                           const rfb::ScreenSet& layout)
+{
+}
+
+static double runTest(const char *fn, double *ratio)
+{
+  CConn *cc;
+  double time;
+
+  cc = new CConn(fn);
+
+  try {
+    while (true)
+      cc->processMsg();
+  } catch (rdr::EndOfStream e) {
+  } catch (rdr::Exception e) {
+    fprintf(stderr, "Failed to run rfb file: %s\n", e.str());
+    exit(1);
+  }
+
+  time = cc->encodeTime;
+  *ratio = cc->getRatio();
+
+  delete cc;
+
+  return time;
+}
+
+static void sort(double *array, int count)
+{
+  bool sorted;
+  int i;
+  do {
+    sorted = true;
+    for (i = 1;i < count;i++) {
+      if (array[i-1] > array[i]) {
+        double d;
+        d = array[i];
+        array[i] = array[i-1];
+        array[i-1] = d;
+        sorted = false;
+      }
+    }
+  } while (!sorted);
+}
+
+static const int runCount = 9;
+
+static void usage(const char *argv0)
+{
+  fprintf(stderr, "Syntax: %s [options] <rfb file>\n", argv0);
+  fprintf(stderr, "Options:\n");
+  rfb::Configuration::listParams(79, 14);
+  exit(1);
+}
+
+int main(int argc, char **argv)
+{
+  int i;
+
+  const char *fn;
+
+  double times[runCount], dev[runCount];
+  double median, meddev, ratio;
+
+  fn = NULL;
+  for (i = 1; i < argc; i++) {
+    if (rfb::Configuration::setParam(argv[i]))
+      continue;
+
+    if (argv[i][0] == '-') {
+      if (i+1 < argc) {
+        if (rfb::Configuration::setParam(&argv[i][1], argv[i+1])) {
+          i++;
+          continue;
+        }
+      }
+      usage(argv[0]);
+    }
+
+    if (fn != NULL)
+      usage(argv[0]);
+
+    fn = argv[i];
+  }
+
+  if (fn == NULL) {
+    fprintf(stderr, "No file specified!\n\n");
+    usage(argv[0]);
+  }
+
+  if (!format.hasBeenSet()) {
+    fprintf(stderr, "Pixel format not specified!\n\n");
+    usage(argv[0]);
+  }
+
+  if (!width.hasBeenSet() || !height.hasBeenSet()) {
+    fprintf(stderr, "Frame buffer size not specified!\n\n");
+    usage(argv[0]);
+  }
+
+  // Warmup
+  runTest(fn, &ratio);
+
+  // Multiple runs to get a good average
+  for (i = 0;i < runCount;i++)
+    times[i] = runTest(fn, &ratio);
+
+  // Calculate median and median deviation
+  sort(times, runCount);
+  median = times[runCount/2];
+
+  for (i = 0;i < runCount;i++)
+    dev[i] = fabs((times[i] - median) / median) * 100;
+
+  sort(dev, runCount);
+  meddev = dev[runCount/2];
+
+  printf("CPU time: %g s (+/- %g %)\n", median, meddev);
+  printf("Ratio: %g\n", ratio);
+
+  return 0;
+}
diff --git a/tests/util.cxx b/tests/util.cxx
index c2685ab..52a2c61 100644
--- a/tests/util.cxx
+++ b/tests/util.cxx
@@ -17,6 +17,8 @@
  */
 
 #include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
 
 #ifdef WIN32
 #include <windows.h>
@@ -24,52 +26,93 @@
 #include <sys/resource.h>
 #endif
 
+#include "util.h"
+
 #ifdef WIN32
-static FILETIME cpuCounters[2];
+typedef FILETIME syscounter_t;
 #else
-struct rusage cpuCounters[2];
+typedef struct rusage syscounter_t;
 #endif
 
-static void measureCpu(void *counter)
+static syscounter_t _globalCounter[2];
+static cpucounter_t globalCounter = _globalCounter;
+
+void startCpuCounter(void)
+{
+  startCpuCounter(globalCounter);
+}
+
+void endCpuCounter(void)
+{
+  endCpuCounter(globalCounter);
+}
+
+double getCpuCounter(void)
+{
+  return getCpuCounter(globalCounter);
+}
+
+cpucounter_t newCpuCounter(void)
+{
+  syscounter_t *c;
+
+  c = (syscounter_t*)malloc(sizeof(syscounter_t) * 2);
+  if (c == NULL)
+    return NULL;
+
+  memset(c, 0, sizeof(syscounter_t) * 2);
+
+  return c;
+}
+
+void freeCpuCounter(cpucounter_t c)
+{
+  free(c);
+}
+
+static void measureCpu(syscounter_t *counter)
 {
 #ifdef WIN32
   FILETIME dummy1, dummy2, dummy3;
 
   GetProcessTimes(GetCurrentProcess(), &dummy1, &dummy2,
-                  &dummy3, (FILETIME*)counter);
+                  &dummy3, counter);
 #else
-  getrusage(RUSAGE_SELF, (struct rusage*)counter);
+  getrusage(RUSAGE_SELF, counter);
 #endif
 }
 
-void startCpuCounter(void)
+void startCpuCounter(cpucounter_t c)
 {
-  measureCpu(&cpuCounters[0]);
+  syscounter_t *s = (syscounter_t*)c;
+  measureCpu(&s[0]);
 }
 
-void endCpuCounter(void)
+void endCpuCounter(cpucounter_t c)
 {
-  measureCpu(&cpuCounters[1]);
+  syscounter_t *s = (syscounter_t*)c;
+  measureCpu(&s[1]);
 }
 
-double getCpuCounter(void)
+double getCpuCounter(cpucounter_t c)
 {
+  syscounter_t *s = (syscounter_t*)c;
   double seconds;
 
 #ifdef WIN32
   uint64_t counters[2];
 
-  counters[0] = (uint64_t)cpuCounters[0].dwHighDateTime << 32 |
-                cpuCounters[0].dwLowDateTime;
-  counters[1] = (uint64_t)cpuCounters[1].dwHighDateTime << 32 |
-                cpuCounters[1].dwLowDateTime;
+  counters[0] = (uint64_t)s[0].dwHighDateTime << 32 |
+                s[0].dwLowDateTime;
+  counters[1] = (uint64_t)s[1].dwHighDateTime << 32 |
+                s[1].dwLowDateTime;
 
   seconds = (double)(counters[1] - counters[2]) / 10000000.0;
 #else
-  seconds = (double)(cpuCounters[1].ru_utime.tv_sec -
-                     cpuCounters[0].ru_utime.tv_sec);
-  seconds += (double)(cpuCounters[1].ru_utime.tv_usec -
-                      cpuCounters[0].ru_utime.tv_usec) / 1000000.0;
+  seconds = (double)(s[1].ru_utime.tv_sec -
+                     s[0].ru_utime.tv_sec);
+  seconds += (double)(s[1].ru_utime.tv_usec -
+                      s[0].ru_utime.tv_usec) / 1000000.0;
 #endif
 
   return seconds;
diff --git a/tests/util.h b/tests/util.h
index ebeadeb..16f2ba2 100644
--- a/tests/util.h
+++ b/tests/util.h
@@ -19,9 +19,19 @@
 #ifndef __TESTS_UTIL_H__
 #define __TESTS_UTIL_H__
 
+typedef void* cpucounter_t;
+
 void startCpuCounter(void);
 void endCpuCounter(void);
 
 double getCpuCounter(void);
 
+cpucounter_t newCpuCounter(void);
+void freeCpuCounter(cpucounter_t c);
+
+void startCpuCounter(cpucounter_t c);
+void endCpuCounter(cpucounter_t c);
+
+double getCpuCounter(cpucounter_t c);
+
 #endif