changeset 1146:74f2706995b7 3.0-integration

Merge work on unified spectrogram and colour 3d plot caching renderer
author Chris Cannam
date Fri, 05 Aug 2016 15:05:02 +0100
parents b4fd6c67fce5 (current diff) 17f999cd0a22 (diff)
children 1badacff7ab2
files view/LayerGeometryProvider.h
diffstat 42 files changed, 3681 insertions(+), 3154 deletions(-) [+]
line wrap: on
line diff
--- a/layer/Colour3DPlotLayer.cpp	Mon Jun 13 11:44:08 2016 +0100
+++ b/layer/Colour3DPlotLayer.cpp	Fri Aug 05 15:05:02 2016 +0100
@@ -15,11 +15,17 @@
 
 #include "Colour3DPlotLayer.h"
 
-#include "view/View.h"
 #include "base/Profiler.h"
 #include "base/LogRange.h"
 #include "base/RangeMapper.h"
+
 #include "ColourMapper.h"
+#include "LayerGeometryProvider.h"
+#include "PaintAssistant.h"
+
+#include "data/model/Dense3DModelPeakCache.h"
+
+#include "view/ViewManager.h"
 
 #include <QPainter>
 #include <QImage>
@@ -42,24 +48,22 @@
 
 Colour3DPlotLayer::Colour3DPlotLayer() :
     m_model(0),
-    m_cache(0),
-    m_peaksCache(0),
-    m_cacheValidStart(0),
-    m_cacheValidEnd(0),
-    m_colourScale(LinearScale),
+    m_colourScale(ColourScaleType::Linear),
     m_colourScaleSet(false),
     m_colourMap(0),
     m_gain(1.0),
-    m_binScale(LinearBinScale),
-    m_normalizeColumns(false),
+    m_binScale(BinScale::Linear),
+    m_normalization(ColumnNormalization::None),
     m_normalizeVisibleArea(false),
-    m_normalizeHybrid(false),
     m_invertVertical(false),
     m_opaque(false),
     m_smooth(false),
     m_peakResolution(256),
     m_miny(0),
-    m_maxy(0)
+    m_maxy(0),
+    m_synchronous(false),
+    m_peakCache(0),
+    m_peakCacheDivisor(8)
 {
     QSettings settings;
     settings.beginGroup("Preferences");
@@ -69,8 +73,67 @@
 
 Colour3DPlotLayer::~Colour3DPlotLayer()
 {
-    delete m_cache;
-    delete m_peaksCache;
+    invalidateRenderers();
+    delete m_peakCache;
+}
+
+ColourScaleType
+Colour3DPlotLayer::convertToColourScale(int value)
+{
+    switch (value) {
+    default:
+    case 0: return ColourScaleType::Linear;
+    case 1: return ColourScaleType::Log;
+    case 2: return ColourScaleType::PlusMinusOne;
+    case 3: return ColourScaleType::Absolute;
+    }
+}
+
+int
+Colour3DPlotLayer::convertFromColourScale(ColourScaleType scale)
+{
+    switch (scale) {
+    case ColourScaleType::Linear: return 0;
+    case ColourScaleType::Log: return 1;
+    case ColourScaleType::PlusMinusOne: return 2;
+    case ColourScaleType::Absolute: return 3;
+
+    case ColourScaleType::Meter:
+    case ColourScaleType::Phase:
+    default: return 0;
+    }
+}
+
+std::pair<ColumnNormalization, bool>
+Colour3DPlotLayer::convertToColumnNorm(int value)
+{
+    switch (value) {
+    default:
+    case 0: return { ColumnNormalization::None, false };
+    case 1: return { ColumnNormalization::Max1, false };
+    case 2: return { ColumnNormalization::None, true }; // visible area
+    case 3: return { ColumnNormalization::Hybrid, false };
+    }
+}
+
+int
+Colour3DPlotLayer::convertFromColumnNorm(ColumnNormalization norm, bool visible)
+{
+    if (visible) return 2;
+    switch (norm) {
+    case ColumnNormalization::None: return 0;
+    case ColumnNormalization::Max1: return 1;
+    case ColumnNormalization::Hybrid: return 3;
+
+    case ColumnNormalization::Sum1:
+    default: return 0;
+    }
+}
+
+void
+Colour3DPlotLayer::setSynchronousPainting(bool synchronous)
+{
+    m_synchronous = synchronous;
 }
 
 void
@@ -95,7 +158,11 @@
     } else if (model->getResolution() > 2) {
         m_peakResolution = 128;
     }
-    cacheInvalid();
+
+    delete m_peakCache;
+    m_peakCache = 0;
+
+    invalidateRenderers();
 
     emit modelReplaced();
     emit sliceableModelReplaced(oldModel, model);
@@ -104,34 +171,46 @@
 void
 Colour3DPlotLayer::cacheInvalid()
 {
-    delete m_cache;
-    delete m_peaksCache;
-    m_cache = 0;
-    m_peaksCache = 0;
-    m_cacheValidStart = 0;
-    m_cacheValidEnd = 0;
+    invalidateRenderers();
 }
 
 void
-Colour3DPlotLayer::cacheInvalid(sv_frame_t startFrame, sv_frame_t endFrame)
+Colour3DPlotLayer::cacheInvalid(sv_frame_t /* startFrame */,
+                                sv_frame_t /* endFrame */)
 {
-    if (!m_cache || !m_model) return;
+    //!!! should do this only if the range is visible
+    delete m_peakCache;
+    m_peakCache = 0;
+    
+    invalidateRenderers();
+}
 
-    int modelResolution = m_model->getResolution();
-    int start = int(startFrame / modelResolution);
-    int end = int(endFrame / modelResolution + 1);
-    if (m_cacheValidStart < end) m_cacheValidStart = end;
-    if (m_cacheValidEnd > start) m_cacheValidEnd = start;
-    if (m_cacheValidStart > m_cacheValidEnd) m_cacheValidEnd = m_cacheValidStart;
+void
+Colour3DPlotLayer::invalidateRenderers()
+{
+    for (ViewRendererMap::iterator i = m_renderers.begin();
+         i != m_renderers.end(); ++i) {
+        delete i->second;
+    }
+    m_renderers.clear();
+}
+
+Dense3DModelPeakCache *
+Colour3DPlotLayer::getPeakCache() const
+{
+    if (!m_peakCache) {
+        m_peakCache = new Dense3DModelPeakCache(m_model, m_peakCacheDivisor);
+    }
+    return m_peakCache;
 }
 
 void
 Colour3DPlotLayer::modelChanged()
 {
-    if (!m_colourScaleSet && m_colourScale == LinearScale) {
+    if (!m_colourScaleSet && m_colourScale == ColourScaleType::Linear) {
         if (m_model) {
             if (m_model->shouldUseLogValueScale()) {
-                setColourScale(LogScale);
+                setColourScale(ColourScaleType::Log);
             } else {
                 m_colourScaleSet = true;
             }
@@ -143,10 +222,10 @@
 void
 Colour3DPlotLayer::modelChangedWithin(sv_frame_t startFrame, sv_frame_t endFrame)
 {
-    if (!m_colourScaleSet && m_colourScale == LinearScale) {
+    if (!m_colourScaleSet && m_colourScale == ColourScaleType::Linear) {
         if (m_model && m_model->getWidth() > 50) {
             if (m_model->shouldUseLogValueScale()) {
-                setColourScale(LogScale);
+                setColourScale(ColourScaleType::Log);
             } else {
                 m_colourScaleSet = true;
             }
@@ -161,8 +240,7 @@
     PropertyList list;
     list.push_back("Colour");
     list.push_back("Colour Scale");
-    list.push_back("Normalize Columns");
-    list.push_back("Normalize Visible Area");
+    list.push_back("Normalization");
     list.push_back("Gain");
     list.push_back("Bin Scale");
     list.push_back("Invert Vertical Scale");
@@ -176,8 +254,7 @@
 {
     if (name == "Colour") return tr("Colour");
     if (name == "Colour Scale") return tr("Scale");
-    if (name == "Normalize Columns") return tr("Normalize Columns");
-    if (name == "Normalize Visible Area") return tr("Normalize Visible Area");
+    if (name == "Normalization") return tr("Normalization");
     if (name == "Invert Vertical Scale") return tr("Invert Vertical Scale");
     if (name == "Gain") return tr("Gain");
     if (name == "Opaque") return tr("Always Opaque");
@@ -189,8 +266,6 @@
 QString
 Colour3DPlotLayer::getPropertyIconName(const PropertyName &name) const
 {
-    if (name == "Normalize Columns") return "normalise-columns";
-    if (name == "Normalize Visible Area") return "normalise";
     if (name == "Invert Vertical Scale") return "invert-vertical";
     if (name == "Opaque") return "opaque";
     if (name == "Smooth") return "smooth";
@@ -201,8 +276,6 @@
 Colour3DPlotLayer::getPropertyType(const PropertyName &name) const
 {
     if (name == "Gain") return RangeProperty;
-    if (name == "Normalize Columns") return ToggleProperty;
-    if (name == "Normalize Visible Area") return ToggleProperty;
     if (name == "Invert Vertical Scale") return ToggleProperty;
     if (name == "Opaque") return ToggleProperty;
     if (name == "Smooth") return ToggleProperty;
@@ -212,9 +285,8 @@
 QString
 Colour3DPlotLayer::getPropertyGroupName(const PropertyName &name) const
 {
-    if (name == "Normalize Columns" ||
-        name == "Normalize Visible Area" ||
-	name == "Colour Scale" ||
+    if (name == "Normalization" ||
+        name == "Colour Scale" ||
         name == "Gain") return tr("Scale");
     if (name == "Bin Scale" ||
         name == "Invert Vertical Scale") return tr("Bins");
@@ -250,11 +322,12 @@
 
     } else if (name == "Colour Scale") {
 
+        // linear, log, +/-1, abs
 	*min = 0;
 	*max = 3;
-        *deflt = (int)LinearScale;
+        *deflt = 0;
 
-	val = (int)m_colourScale;
+	val = convertFromColourScale(m_colourScale);
 
     } else if (name == "Colour") {
 
@@ -264,15 +337,13 @@
 
 	val = m_colourMap;
 
-    } else if (name == "Normalize Columns") {
+    } else if (name == "Normalization") {
 	
+        *min = 0;
+        *max = 3;
         *deflt = 0;
-	val = (m_normalizeColumns ? 1 : 0);
 
-    } else if (name == "Normalize Visible Area") {
-	
-        *deflt = 0;
-	val = (m_normalizeVisibleArea ? 1 : 0);
+        val = convertFromColumnNorm(m_normalization, m_normalizeVisibleArea);
 
     } else if (name == "Invert Vertical Scale") {
 	
@@ -283,7 +354,7 @@
 
 	*min = 0;
 	*max = 1;
-        *deflt = int(LinearBinScale);
+        *deflt = int(BinScale::Linear);
 	val = (int)m_binScale;
 
     } else if (name == "Opaque") {
@@ -319,6 +390,9 @@
 	case 3: return tr("Absolute");
 	}
     }
+    if (name == "Normalization") {
+        return ""; // icon only
+    }
     if (name == "Bin Scale") {
 	switch (value) {
 	default:
@@ -329,6 +403,22 @@
     return tr("<unknown>");
 }
 
+QString
+Colour3DPlotLayer::getPropertyValueIconName(const PropertyName &name,
+                                            int value) const
+{
+    if (name == "Normalization") {
+        switch(value) {
+        default:
+        case 0: return "normalise-none";
+        case 1: return "normalise-columns";
+        case 2: return "normalise";
+        case 3: return "normalise-hybrid";
+        }
+    }
+    return "";
+}
+
 RangeMapper *
 Colour3DPlotLayer::getNewPropertyRangeMapper(const PropertyName &name) const
 {
@@ -344,19 +434,9 @@
     if (name == "Gain") {
 	setGain(float(pow(10, value/20.0)));
     } else if (name == "Colour Scale") {
-	switch (value) {
-	default:
-	case 0: setColourScale(LinearScale); break;
-	case 1: setColourScale(LogScale); break;
-	case 2: setColourScale(PlusMinusOneScale); break;
-	case 3: setColourScale(AbsoluteScale); break;
-	}
+        setColourScale(convertToColourScale(value));
     } else if (name == "Colour") {
         setColourMap(value);
-    } else if (name == "Normalize Columns") {
-	setNormalizeColumns(value ? true : false);
-    } else if (name == "Normalize Visible Area") {
-	setNormalizeVisibleArea(value ? true : false);
     } else if (name == "Invert Vertical Scale") {
 	setInvertVertical(value ? true : false);
     } else if (name == "Opaque") {
@@ -366,19 +446,23 @@
     } else if (name == "Bin Scale") {
 	switch (value) {
 	default:
-	case 0: setBinScale(LinearBinScale); break;
-	case 1: setBinScale(LogBinScale); break;
+	case 0: setBinScale(BinScale::Linear); break;
+	case 1: setBinScale(BinScale::Log); break;
 	}
+    } else if (name == "Normalization") {
+        auto n = convertToColumnNorm(value);
+        setNormalization(n.first);
+        setNormalizeVisibleArea(n.second);
     }
 }
 
 void
-Colour3DPlotLayer::setColourScale(ColourScale scale)
+Colour3DPlotLayer::setColourScale(ColourScaleType scale)
 {
     if (m_colourScale == scale) return;
     m_colourScale = scale;
     m_colourScaleSet = true;
-    cacheInvalid();
+    invalidateRenderers();
     emit layerParametersChanged();
 }
 
@@ -387,7 +471,7 @@
 {
     if (m_colourMap == map) return;
     m_colourMap = map;
-    cacheInvalid();
+    invalidateRenderers();
     emit layerParametersChanged();
 }
 
@@ -396,7 +480,7 @@
 {
     if (m_gain == gain) return;
     m_gain = gain;
-    cacheInvalid();
+    invalidateRenderers();
     emit layerParametersChanged();
 }
 
@@ -411,52 +495,41 @@
 {
     if (m_binScale == binScale) return;
     m_binScale = binScale;
-    cacheInvalid();
+    invalidateRenderers();
     emit layerParametersChanged();
 }
 
-Colour3DPlotLayer::BinScale
+BinScale
 Colour3DPlotLayer::getBinScale() const
 {
     return m_binScale;
 }
 
 void
-Colour3DPlotLayer::setNormalizeColumns(bool n)
+Colour3DPlotLayer::setNormalization(ColumnNormalization n)
 {
-    if (m_normalizeColumns == n) return;
-    m_normalizeColumns = n;
-    cacheInvalid();
+    if (m_normalization == n) return;
+
+    m_normalization = n;
+    invalidateRenderers();
+    
     emit layerParametersChanged();
 }
 
-bool
-Colour3DPlotLayer::getNormalizeColumns() const
+ColumnNormalization
+Colour3DPlotLayer::getNormalization() const
 {
-    return m_normalizeColumns;
-}
-
-void
-Colour3DPlotLayer::setNormalizeHybrid(bool n)
-{
-    if (m_normalizeHybrid == n) return;
-    m_normalizeHybrid = n;
-    cacheInvalid();
-    emit layerParametersChanged();
-}
-
-bool
-Colour3DPlotLayer::getNormalizeHybrid() const
-{
-    return m_normalizeHybrid;
+    return m_normalization;
 }
 
 void
 Colour3DPlotLayer::setNormalizeVisibleArea(bool n)
 {
     if (m_normalizeVisibleArea == n) return;
+
     m_normalizeVisibleArea = n;
-    cacheInvalid();
+    invalidateRenderers();
+    
     emit layerParametersChanged();
 }
 
@@ -471,7 +544,7 @@
 {
     if (m_invertVertical == n) return;
     m_invertVertical = n;
-    cacheInvalid();
+    invalidateRenderers();
     emit layerParametersChanged();
 }
 
@@ -480,6 +553,7 @@
 {
     if (m_opaque == n) return;
     m_opaque = n;
+    invalidateRenderers();
     emit layerParametersChanged();
 }
 
@@ -488,6 +562,7 @@
 {
     if (m_smooth == n) return;
     m_smooth = n;
+    invalidateRenderers();
     emit layerParametersChanged();
 }
 
@@ -534,16 +609,19 @@
 }
 
 bool
-Colour3DPlotLayer::isLayerScrollable(const LayerGeometryProvider *v) const
+Colour3DPlotLayer::isLayerScrollable(const LayerGeometryProvider */* v */) const
 {
     if (m_normalizeVisibleArea) {
         return false;
     }
-    if (shouldPaintDenseIn(v)) {
-        return true;
-    }
-    QPoint discard;
-    return !v->shouldIlluminateLocalFeatures(this, discard);
+    //!!! ah hang on, if we're potentially rendering incrementally
+    //!!! they we can't be scrollable
+    return false;
+//    if (getRenderer(v)->willRenderOpaque(v)) {
+//        return true;
+//    }
+//    QPoint discard;
+//    return !v->shouldIlluminateLocalFeatures(this, discard);
 }
 
 bool
@@ -588,6 +666,8 @@
     m_miny = int(lrint(min));
     m_maxy = int(lrint(max));
     
+    invalidateRenderers();
+    
     emit layerParametersChanged();
     return true;
 }
@@ -634,6 +714,8 @@
     m_maxy = m_miny + dist;
     if (m_maxy > m_model->getHeight()) m_maxy = m_model->getHeight();
 
+    invalidateRenderers();
+    
 //    SVDEBUG << "Colour3DPlotLayer::setVerticalZoomStep(" <<step <<"):  after: miny = " << m_miny << ", maxy = " << m_maxy << endl;
     
     emit layerParametersChanged();
@@ -649,14 +731,14 @@
 }
 
 double
-Colour3DPlotLayer::getYForBin(LayerGeometryProvider *v, double bin) const
+Colour3DPlotLayer::getYForBin(const LayerGeometryProvider *v, double bin) const
 {
     double y = bin;
     if (!m_model) return y;
     double mn = 0, mx = m_model->getHeight();
     getDisplayExtents(mn, mx);
     double h = v->getPaintHeight();
-    if (m_binScale == LinearBinScale) {
+    if (m_binScale == BinScale::Linear) {
         y = h - (((bin - mn) * h) / (mx - mn));
     } else {
         double logmin = mn + 1, logmax = mx + 1;
@@ -666,21 +748,15 @@
     return y;
 }
 
-int
-Colour3DPlotLayer::getIYForBin(LayerGeometryProvider *v, int bin) const
-{
-    return int(round(getYForBin(v, bin)));
-}
-
 double
-Colour3DPlotLayer::getBinForY(LayerGeometryProvider *v, double y) const
+Colour3DPlotLayer::getBinForY(const LayerGeometryProvider *v, double y) const
 {
     double bin = y;
     if (!m_model) return bin;
     double mn = 0, mx = m_model->getHeight();
     getDisplayExtents(mn, mx);
     double h = v->getPaintHeight();
-    if (m_binScale == LinearBinScale) {
+    if (m_binScale == BinScale::Linear) {
         bin = mn + ((h - y) * (mx - mn)) / h;
     } else {
         double logmin = mn + 1, logmax = mx + 1;
@@ -690,12 +766,6 @@
     return bin;
 }
 
-int
-Colour3DPlotLayer::getIBinForY(LayerGeometryProvider *v, int y) const
-{
-    return int(floor(getBinForY(v, y)));
-}
-
 QString
 Colour3DPlotLayer::getFeatureDescription(LayerGeometryProvider *v, QPoint &pos) const
 {
@@ -798,49 +868,20 @@
     int cw = getColourScaleWidth(paint);
     
     int ch = h - 20;
-    if (ch > 20 && m_cache) {
+    if (ch > 20) {
 
-        double min = m_model->getMinimumLevel();
-        double max = m_model->getMaximumLevel();
+        double min = m_viewMags[v->getId()].getMin();
+        double max = m_viewMags[v->getId()].getMax();
 
-        double mmin = min;
-        double mmax = max;
+        if (max <= min) max = min + 0.1;
 
-        if (m_colourScale == LogScale) {
-            LogRange::mapRange(mmin, mmax);
-        } else if (m_colourScale == PlusMinusOneScale) {
-            mmin = -1.f;
-            mmax = 1.f;
-        } else if (m_colourScale == AbsoluteScale) {
-            if (mmin < 0) {
-                if (fabs(mmin) > fabs(mmax)) mmax = fabs(mmin);
-                else mmax = fabs(mmax);
-                mmin = 0;
-            } else {
-                mmin = fabs(mmin);
-                mmax = fabs(mmax);
-            }
-        }
-    
-        if (max == min) max = min + 1.f;
-        if (mmax == mmin) mmax = mmin + 1.f;
-    
         paint.setPen(v->getForeground());
         paint.drawRect(4, 10, cw - 8, ch+1);
 
         for (int y = 0; y < ch; ++y) {
             double value = ((max - min) * (double(ch-y) - 1.0)) / double(ch) + min;
-            if (m_colourScale == LogScale) {
-                value = LogRange::map(value);
-            }
-            int pixel = int(((value - mmin) * 256) / (mmax - mmin));
-            if (pixel >= 0 && pixel < 256) {
-                QRgb c = m_cache->color(pixel);
-                paint.setPen(QColor(qRed(c), qGreen(c), qBlue(c)));
-                paint.drawLine(5, 11 + y, cw - 5, 11 + y);
-            } else {
-                cerr << "WARNING: Colour3DPlotLayer::paintVerticalScale: value " << value << ", mmin " << mmin << ", mmax " << mmax << " leads to invalid pixel " << pixel << endl;
-            }
+            paint.setPen(getRenderer(v)->getColour(value));
+            paint.drawLine(5, 11 + y, cw - 5, 11 + y);
         }
 
         QString minstr = QString("%1").arg(min);
@@ -864,12 +905,12 @@
 
         paint.setWorldMatrix(m);
 
-        v->drawVisibleText(paint, 2, 0, minstr, View::OutlinedText);
+        PaintAssistant::drawVisibleText(v, paint, 2, 0, minstr, PaintAssistant::OutlinedText);
 
         m.translate(ch - msw - 2, 0);
         paint.setWorldMatrix(m);
 
-        v->drawVisibleText(paint, 0, 0, maxstr, View::OutlinedText);
+        PaintAssistant::drawVisibleText(v, paint, 0, 0, maxstr, PaintAssistant::OutlinedText);
 
         paint.restore();
     }
@@ -943,7 +984,10 @@
 
     DenseThreeDimensionalModel::Column values = m_model->getColumn(col);
     values.resize(m_model->getHeight(), 0.f);
-    if (!m_normalizeColumns && !m_normalizeHybrid) return values;
+    if (m_normalization != ColumnNormalization::Max1 &&
+        m_normalization != ColumnNormalization::Hybrid) {
+        return values;
+    }
 
     double colMax = 0.f, colMin = 0.f;
     double min = 0.f, max = 0.f;
@@ -968,7 +1012,8 @@
         if (value != newvalue) values[y] = float(newvalue);
     }
 
-    if (m_normalizeHybrid && (colMax > 0.0)) {
+    if (m_normalization == ColumnNormalization::Hybrid
+        && (colMax > 0.0)) {
         double logmax = log10(colMax);
         for (int y = 0; y < nv; ++y) {
             values[y] = float(values[y] * logmax);
@@ -977,304 +1022,92 @@
 
     return values;
 }
-    
-void
-Colour3DPlotLayer::fillCache(int firstColumn, int lastColumn) const
+
+Colour3DPlotRenderer *
+Colour3DPlotLayer::getRenderer(const LayerGeometryProvider *v) const
 {
-    // This call requests a (perhaps partial) fill of the cache
-    // between model columns firstColumn and lastColumn inclusive.
-    // The cache itself always has size sufficient to contain the
-    // whole model, but its validity may be less, depending on which
-    // regions have been requested via calls to this function.  Note
-    // that firstColumn and lastColumn are *model* column numbers. If
-    // the model starts at a frame > 0, a firstColumn of zero still
-    // corresponds to the first column in the model, not the first
-    // column on the resulting rendered layer.
+    if (m_renderers.find(v->getId()) == m_renderers.end()) {
 
-    Profiler profiler("Colour3DPlotLayer::fillCache", true);
+        Colour3DPlotRenderer::Sources sources;
+        sources.verticalBinLayer = this;
+        sources.fft = 0;
+        sources.source = m_model;
+        sources.peaks = getPeakCache();
 
-    int cacheWidth = m_model->getWidth();
-    int cacheHeight = m_model->getHeight();
+        ColourScale::Parameters cparams;
+        cparams.colourMap = m_colourMap;
+        cparams.scaleType = m_colourScale;
+        cparams.gain = m_gain;
 
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-    cerr << "Colour3DPlotLayer::fillCache: range " << firstColumn << " -> " << lastColumn << " (cache size will be " << cacheWidth << " x " << cacheHeight << ")" << endl;
-#endif
+        if (m_normalization == ColumnNormalization::None) {
+            cparams.minValue = m_model->getMinimumLevel();
+            cparams.maxValue = m_model->getMaximumLevel();
+        } else if (m_normalization == ColumnNormalization::Hybrid) {
+            cparams.minValue = 0;
+            cparams.maxValue = log10(m_model->getMaximumLevel() + 1.0);
+        }
 
-    if (m_cache && m_cache->height() != cacheHeight) {
-        // height has changed: delete everything rather than resizing
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-        cerr << "Colour3DPlotLayer::fillCache: Cache height has changed, recreating" << endl;
-#endif
-        delete m_cache;
-        delete m_peaksCache;
-        m_cache = 0;
-        m_peaksCache = 0;
-    } 
+        if (cparams.maxValue <= cparams.minValue) {
+            cparams.maxValue = cparams.minValue + 0.1;
+        }
+        
+        Colour3DPlotRenderer::Parameters params;
+        params.colourScale = ColourScale(cparams);
+        params.normalization = m_normalization;
+        params.binScale = m_binScale;
+        params.alwaysOpaque = m_opaque;
+        params.invertVertical = m_invertVertical;
+        params.interpolate = m_smooth;
 
-    if (m_cache && m_cache->width() != cacheWidth) {
-        // width has changed and we have an existing cache: resize it
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-        cerr << "Colour3DPlotLayer::fillCache: Cache width has changed, resizing existing cache" << endl;
-#endif
-        QImage *newCache =
-            new QImage(m_cache->copy(0, 0, cacheWidth, cacheHeight));
-        delete m_cache;
-        m_cache = newCache;
-        if (m_peaksCache) {
-            QImage *newPeaksCache =
-                new QImage(m_peaksCache->copy
-                           (0, 0, cacheWidth / m_peakResolution + 1, cacheHeight));
-            delete m_peaksCache;
-            m_peaksCache = newPeaksCache;
-        }
+        m_renderers[v->getId()] = new Colour3DPlotRenderer(sources, params);
     }
 
-    if (!m_cache) {
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-        cerr << "Colour3DPlotLayer::fillCache: Have no cache, making one" << endl;
-#endif
-        m_cache = new QImage(cacheWidth, cacheHeight, QImage::Format_Indexed8);
-        m_cache->setColorCount(256);
-        m_cache->fill(0);
-        if (!m_normalizeVisibleArea) {
-            m_peaksCache = new QImage
-                (cacheWidth / m_peakResolution + 1, cacheHeight,
-                 QImage::Format_Indexed8);
-            m_peaksCache->setColorCount(256);
-            m_peaksCache->fill(0);
-        } else if (m_peaksCache) {
-            delete m_peaksCache;
-            m_peaksCache = 0;
-        }
-        m_cacheValidStart = 0;
-        m_cacheValidEnd = 0;
-    }
+    return m_renderers[v->getId()];
+}
 
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-    cerr << "cache size = " << m_cache->width() << "x" << m_cache->height()
-         << " peaks cache size = " << m_peaksCache->width() << "x" << m_peaksCache->height() << endl;
-#endif
+void
+Colour3DPlotLayer::paintWithRenderer(LayerGeometryProvider *v,
+                                     QPainter &paint, QRect rect) const
+{
+    Colour3DPlotRenderer *renderer = getRenderer(v);
 
-    if (m_cacheValidStart <= firstColumn && m_cacheValidEnd >= lastColumn) {
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-        cerr << "Cache is valid in this region already" << endl;
-#endif
-        return;
+    Colour3DPlotRenderer::RenderResult result;
+    MagnitudeRange magRange;
+    int viewId = v->getId();
+
+    if (!renderer->geometryChanged(v)) {
+        magRange = m_viewMags[viewId];
     }
     
-    int fillStart = firstColumn;
-    int fillEnd = lastColumn;
+    if (m_synchronous) {
 
-    if (fillStart >= cacheWidth) fillStart = cacheWidth-1;
-    if (fillStart < 0) fillStart = 0;
-    if (fillEnd >= cacheWidth) fillEnd = cacheWidth-1;
-    if (fillEnd < 0) fillEnd = 0;
-    if (fillEnd < fillStart) fillEnd = fillStart;
-
-    bool normalizeVisible = (m_normalizeVisibleArea && !m_normalizeColumns);
-
-    if (!normalizeVisible && (m_cacheValidStart < m_cacheValidEnd)) {
-        
-        if (m_cacheValidEnd < fillStart) {
-            fillStart = m_cacheValidEnd + 1;
-        }
-        if (m_cacheValidStart > fillEnd) {
-            fillEnd = m_cacheValidStart - 1;
-        }
-        
-        m_cacheValidStart = std::min(fillStart, m_cacheValidStart);
-        m_cacheValidEnd = std::max(fillEnd, m_cacheValidEnd);
+        result = renderer->render(v, paint, rect);
 
     } else {
 
-        // when normalising the visible area, the only valid area,
-        // ever, is the currently visible one
+        result = renderer->renderTimeConstrained(v, paint, rect);
 
-        m_cacheValidStart = fillStart;
-        m_cacheValidEnd = fillEnd;
-    }
-
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-    cerr << "Cache size " << cacheWidth << "x" << cacheHeight << " will be valid from " << m_cacheValidStart << " to " << m_cacheValidEnd << " (fillStart = " << fillStart << ", fillEnd = " << fillEnd << ")" << endl;
-#endif
-
-    DenseThreeDimensionalModel::Column values;
-
-    double min = m_model->getMinimumLevel();
-    double max = m_model->getMaximumLevel();
-
-    if (m_colourScale == LogScale) {
-        LogRange::mapRange(min, max);
-    } else if (m_colourScale == PlusMinusOneScale) {
-        min = -1.f;
-        max = 1.f;
-    } else if (m_colourScale == AbsoluteScale) {
-        if (min < 0) {
-            if (fabs(min) > fabs(max)) max = fabs(min);
-            else max = fabs(max);
-            min = 0;
-        } else {
-            min = fabs(min);
-            max = fabs(max);
+        QRect uncached = renderer->getLargestUncachedRect(v);
+        if (uncached.width() > 0) {
+            v->updatePaintRect(uncached);
         }
     }
     
-    if (max == min) max = min + 1.f;
-    
-    ColourMapper mapper(m_colourMap, 0.f, 255.f);
-    
-    for (int index = 0; index < 256; ++index) {
-        QColor colour = mapper.map(index);
-        m_cache->setColor
-            (index, qRgb(colour.red(), colour.green(), colour.blue()));
-        if (m_peaksCache) {
-            m_peaksCache->setColor
-                (index, qRgb(colour.red(), colour.green(), colour.blue()));
+    magRange.sample(result.range);
+
+    if (magRange.isSet()) {
+        if (!(m_viewMags[viewId] == magRange)) {
+            m_viewMags[viewId] = magRange;
+    //!!! now need to do the normalise-visible thing
         }
     }
     
-    double visibleMax = 0.f, visibleMin = 0.f;
-
-    if (normalizeVisible) {
+    cerr << "mag range in this view: "
+         << m_viewMags[v->getId()].getMin()
+         << " -> "
+         << m_viewMags[v->getId()].getMax()
+         << endl;
         
-        for (int c = fillStart; c <= fillEnd; ++c) {
-	
-            values = getColumn(c);
-
-            double colMax = 0.f, colMin = 0.f;
-
-            for (int y = 0; y < cacheHeight; ++y) {
-                if (!in_range_for(values, y)) break;
-                if (y == 0 || values[y] > colMax) colMax = values[y];
-                if (y == 0 || values[y] < colMin) colMin = values[y];
-            }
-
-            if (c == fillStart || colMax > visibleMax) visibleMax = colMax;
-            if (c == fillStart || colMin < visibleMin) visibleMin = colMin;
-        }
-
-        if (m_colourScale == LogScale) {
-            visibleMin = LogRange::map(visibleMin);
-            visibleMax = LogRange::map(visibleMax);
-            if (visibleMin > visibleMax) std::swap(visibleMin, visibleMax);
-        } else if (m_colourScale == AbsoluteScale) {
-            if (visibleMin < 0) {
-                if (fabs(visibleMin) > fabs(visibleMax)) visibleMax = fabs(visibleMin);
-                else visibleMax = fabs(visibleMax);
-                visibleMin = 0;
-            } else {
-                visibleMin = fabs(visibleMin);
-                visibleMax = fabs(visibleMax);
-            }
-        }
-    }
-    
-    if (visibleMin == visibleMax) visibleMax = visibleMin + 1;
-
-    int *peaks = 0;
-    if (m_peaksCache) {
-        peaks = new int[cacheHeight];
-        for (int y = 0; y < cacheHeight; ++y) {
-            peaks[y] = 0;
-        }
-    }
-
-    Profiler profiler2("Colour3DPlotLayer::fillCache: filling", true);
-
-    for (int c = fillStart; c <= fillEnd; ++c) {
-	
-        values = getColumn(c);
-
-        if (c >= m_cache->width()) {
-            cerr << "ERROR: column " << c << " >= cache width "
-                 << m_cache->width() << endl;
-            continue;
-        }
-
-        for (int y = 0; y < cacheHeight; ++y) {
-
-            double value = min;
-            if (in_range_for(values, y)) {
-                value = values.at(y);
-            }
-
-            value = value * m_gain;
-
-            if (m_colourScale == LogScale) {
-                value = LogRange::map(value);
-            } else if (m_colourScale == AbsoluteScale) {
-                value = fabs(value);
-            }
-            
-            if (normalizeVisible) {
-                double norm = (value - visibleMin) / (visibleMax - visibleMin);
-                value = min + (max - min) * norm;
-            }
-
-            int pixel = int(((value - min) * 256) / (max - min));
-            if (pixel < 0) pixel = 0;
-            if (pixel > 255) pixel = 255;
-            if (peaks && (pixel > peaks[y])) peaks[y] = pixel;
-
-            if (m_invertVertical) {
-                m_cache->setPixel(c, cacheHeight - y - 1, pixel);
-            } else {
-                if (y >= m_cache->height()) {
-                    cerr << "ERROR: row " << y << " >= cache height " << m_cache->height() << endl;
-                } else {
-                    m_cache->setPixel(c, y, pixel);
-                }
-            }
-        }
-
-        if (peaks) {
-            int notch = (c % m_peakResolution);
-            if (notch == m_peakResolution-1 || c == fillEnd) {
-                int pc = c / m_peakResolution;
-                if (pc >= m_peaksCache->width()) {
-                    cerr << "ERROR: peak column " << pc
-                         << " (from col " << c << ") >= peaks cache width "
-                         << m_peaksCache->width() << endl;
-                    continue;
-                }
-                for (int y = 0; y < cacheHeight; ++y) {
-                    if (m_invertVertical) {
-                        m_peaksCache->setPixel(pc, cacheHeight - y - 1, peaks[y]);
-                    } else {
-                        if (y >= m_peaksCache->height()) {
-                            cerr << "ERROR: row " << y
-                                 << " >= peaks cache height "
-                                 << m_peaksCache->height() << endl;
-                        } else {
-                            m_peaksCache->setPixel(pc, y, peaks[y]);
-                        }
-                    }
-                }
-                for (int y = 0; y < cacheHeight; ++y) {
-                    peaks[y] = 0;
-                }
-            }
-        }
-    }
-
-    delete[] peaks;
-}
-
-bool
-Colour3DPlotLayer::shouldPaintDenseIn(const LayerGeometryProvider *v) const
-{
-    if (!m_model || !v || !(v->getViewManager())) {
-        return false;
-    }
-    double srRatio =
-        v->getViewManager()->getMainModelSampleRate() / m_model->getSampleRate();
-    if (m_opaque || 
-        m_smooth ||
-        m_model->getHeight() >= v->getPaintHeight() ||
-        ((m_model->getResolution() * srRatio) / v->getZoomLevel()) < 2) {
-        return true;
-    }
-    return false;
 }
 
 void
@@ -1306,378 +1139,18 @@
 #endif
         return;
     }
+
+    //!!!???
     
-    if (m_normalizeVisibleArea && !m_normalizeColumns) rect = v->getPaintRect();
-
-    sv_frame_t modelStart = m_model->getStartFrame();
-    sv_frame_t modelEnd = m_model->getEndFrame();
-    int modelResolution = m_model->getResolution();
-
-    // The cache is from the model's start frame to the model's end
-    // frame at the model's window increment frames per pixel.  We
-    // want to draw from our start frame + x0 * zoomLevel to our start
-    // frame + x1 * zoomLevel at zoomLevel frames per pixel.
-
-    //  We have quite different paint mechanisms for rendering "large"
-    //  bins (more than one bin per pixel in both directions) and
-    //  "small".  This is "large"; see paintDense below for "small".
-
-    int x0 = rect.left();
-    int x1 = rect.right() + 1;
-
-    int h = v->getPaintHeight();
-
-    double srRatio =
-        v->getViewManager()->getMainModelSampleRate() / m_model->getSampleRate();
-
-    // the s-prefix values are source, i.e. model, column and bin numbers
-    int sx0 = int((double(v->getFrameForX(x0)) / srRatio - double(modelStart))
-                  / modelResolution);
-    int sx1 = int((double(v->getFrameForX(x1)) / srRatio - double(modelStart))
-                  / modelResolution);
-    int sh = m_model->getHeight();
-
-    int symin = m_miny;
-    int symax = m_maxy;
-    if (symax <= symin) {
-        symin = 0;
-        symax = sh;
-    }
-    if (symin < 0) symin = 0;
-    if (symax > sh) symax = sh;
-
-    if (sx0 > 0) --sx0;
-    fillCache(sx0 < 0 ? 0 : sx0,
-              sx1 < 0 ? 0 : sx1);
-
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-    cerr << "Colour3DPlotLayer::paint: height = "<< m_model->getHeight() << ", modelStart = " << modelStart << ", resolution = " << modelResolution << ", model rate = " << m_model->getSampleRate() << " (zoom level = " << v->getZoomLevel() << ", srRatio = " << srRatio << ")" << endl;
-#endif
-
-    if (shouldPaintDenseIn(v)) {
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-        cerr << "calling paintDense" << endl;
-#endif
-        paintDense(v, paint, rect);
-        return;
+    if (m_normalizeVisibleArea) {
+        rect = v->getPaintRect();
     }
 
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-    cerr << "Colour3DPlotLayer::paint: w " << x1-x0 << ", h " << h << ", sx0 " << sx0 << ", sx1 " << sx1 << ", sw " << sx1-sx0 << ", sh " << sh << endl;
-    cerr << "Colour3DPlotLayer: sample rate is " << m_model->getSampleRate() << ", resolution " << m_model->getResolution() << endl;
-#endif
+    //!!! why is the setLayerDormant(false) found here in
+    //!!! SpectrogramLayer not present in Colour3DPlotLayer?
+    //!!! unnecessary? vestigial? forgotten?
 
-    QPoint illuminatePos;
-    bool illuminate = v->shouldIlluminateLocalFeatures(this, illuminatePos);
-    
-    const int buflen = 40;
-    char labelbuf[buflen];
-
-    for (int sx = sx0; sx <= sx1; ++sx) {
-
-	sv_frame_t fx = sx * modelResolution + modelStart;
-
-	if (fx + modelResolution <= modelStart || fx > modelEnd) continue;
-
-        int rx0 = v->getXForFrame(int(double(fx) * srRatio));
-	int rx1 = v->getXForFrame(int(double(fx + modelResolution + 1) * srRatio));
-
-	int rw = rx1 - rx0;
-	if (rw < 1) rw = 1;
-
-	bool showLabel = (rw > 10 &&
-			  paint.fontMetrics().width("0.000000") < rw - 3 &&
-			  paint.fontMetrics().height() < (h / sh));
-        
-	for (int sy = symin; sy < symax; ++sy) {
-
-            int ry0 = getIYForBin(v, sy);
-            int ry1 = getIYForBin(v, sy + 1);
-            QRect r(rx0, ry1, rw, ry0 - ry1);
-
-	    QRgb pixel = qRgb(255, 255, 255);
-	    if (sx >= 0 && sx < m_cache->width() &&
-		sy >= 0 && sy < m_cache->height()) {
-		pixel = m_cache->pixel(sx, sy);
-	    }
-
-            if (rw == 1) {
-                paint.setPen(pixel);
-                paint.setBrush(Qt::NoBrush);
-                paint.drawLine(r.x(), r.y(), r.x(), r.y() + r.height() - 1);
-                continue;
-            }
-
-	    QColor pen(255, 255, 255, 80);
-	    QColor brush(pixel);
-
-            if (rw > 3 && r.height() > 3) {
-                brush.setAlpha(160);
-            }
-
-	    paint.setPen(Qt::NoPen);
-	    paint.setBrush(brush);
-
-	    if (illuminate) {
-		if (r.contains(illuminatePos)) {
-		    paint.setPen(v->getForeground());
-		}
-	    }
-            
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-//            cerr << "rect " << r.x() << "," << r.y() << " "
-//                      << r.width() << "x" << r.height() << endl;
-#endif
-
-	    paint.drawRect(r);
-
-	    if (showLabel) {
-		if (sx >= 0 && sx < m_cache->width() &&
-		    sy >= 0 && sy < m_cache->height()) {
-		    double value = m_model->getValueAt(sx, sy);
-		    snprintf(labelbuf, buflen, "%06f", value);
-		    QString text(labelbuf);
-		    v->drawVisibleText
-                        (paint,
-                         rx0 + 2,
-                         ry0 - h / sh - 1 + 2 + paint.fontMetrics().ascent(),
-                         text,
-                         View::OutlinedText);
-		}
-	    }
-	}
-    }
-}
-
-void
-Colour3DPlotLayer::paintDense(LayerGeometryProvider *v, QPainter &paint, QRect rect) const
-{
-    Profiler profiler("Colour3DPlotLayer::paintDense", true);
-    if (!m_cache) return;
-
-    double modelStart = double(m_model->getStartFrame());
-    double modelResolution = double(m_model->getResolution());
-
-    sv_samplerate_t mmsr = v->getViewManager()->getMainModelSampleRate();
-    sv_samplerate_t msr = m_model->getSampleRate();
-    double srRatio = mmsr / msr;
-
-    int x0 = rect.left();
-    int x1 = rect.right() + 1;
-
-    const int w = x1 - x0; // const so it can be used as array size below
-    int h = v->getPaintHeight(); // we always paint full height
-    int sh = m_model->getHeight();
-
-    int symin = m_miny;
-    int symax = m_maxy;
-    if (symax <= symin) {
-        symin = 0;
-        symax = sh;
-    }
-    if (symin < 0) symin = 0;
-    if (symax > sh) symax = sh;
-
-    QImage img(w, h, QImage::Format_Indexed8);
-    img.setColorTable(m_cache->colorTable());
-
-    uchar *peaks = new uchar[w];
-    memset(peaks, 0, w);
-
-    int zoomLevel = v->getZoomLevel();
-    
-    QImage *source = m_cache;
-
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT    
-    cerr << "modelResolution " << modelResolution << ", srRatio "
-         << srRatio << ", m_peakResolution " << m_peakResolution
-         << ", zoomLevel " << zoomLevel << ", result "
-         << ((modelResolution * srRatio * m_peakResolution) / zoomLevel)
-         << endl;
-#endif
-
-    if (m_peaksCache) {
-        if (((modelResolution * srRatio * m_peakResolution) / zoomLevel) < 1) {
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT    
-            cerr << "using peaks cache" << endl;
-#endif
-            source = m_peaksCache;
-            modelResolution *= m_peakResolution;
-        } else {
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT    
-            cerr << "not using peaks cache" << endl;
-#endif
-        }
-    } else {
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT    
-        cerr << "have no peaks cache" << endl;
-#endif
-    }
-
-    int sw = source->width();
-    
-    sv_frame_t xf = -1;
-    sv_frame_t nxf = v->getFrameForX(x0);
-
-    double epsilon = 0.000001;
-
-    vector<double> sxa(w*2);
-    
-    for (int x = 0; x < w; ++x) {
-
-        xf = nxf;
-        nxf = xf + zoomLevel;
-
-        double sx0 = (double(xf) / srRatio - modelStart) / modelResolution;
-        double sx1 = (double(nxf) / srRatio - modelStart) / modelResolution;
-
-        sxa[x*2] = sx0;
-        sxa[x*2 + 1] = sx1;
-    }
-
-    double logmin = symin+1, logmax = symax+1;
-    LogRange::mapRange(logmin, logmax);
-
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-    cerr << "m_smooth = " << m_smooth << ", w = " << w << ", h = " << h << endl;
-#endif
-
-    if (m_smooth) {
-        
-        for (int y = 0; y < h; ++y) {
-
-            double sy = getBinForY(v, y) - 0.5;
-            int syi = int(sy + epsilon);
-            if (syi < 0 || syi >= source->height()) continue;
-
-            uchar *targetLine = img.scanLine(y);
-            uchar *sourceLine = source->scanLine(syi);
-            uchar *nextSource;
-            if (syi + 1 < source->height()) {
-                nextSource = source->scanLine(syi + 1);
-            } else {
-                nextSource = sourceLine;
-            }
-
-            for (int x = 0; x < w; ++x) {
-
-                targetLine[x] = 0;
-
-                double sx0 = sxa[x*2];
-                if (sx0 < 0) continue;
-                int sx0i = int(sx0 + epsilon);
-                if (sx0i >= sw) break;
-
-                double a = sourceLine[sx0i];
-                double b = a;
-                double value;
-
-                double sx1 = sxa[x*2+1];
-                if (sx1 > sx0 + 1.f) {
-                    int sx1i = int(sx1);
-                    bool have = false;
-                    for (int sx = sx0i; sx <= sx1i; ++sx) {
-                        if (sx < 0 || sx >= sw) continue;
-                        if (!have) {
-                            a = sourceLine[sx];
-                            b = nextSource[sx];
-                            have = true;
-                        } else {
-                            a = std::max(a, double(sourceLine[sx]));
-                            b = std::max(b, double(nextSource[sx]));
-                        }
-                    }
-                    double yprop = sy - syi;
-                    value = (a * (1.f - yprop) + b * yprop);
-                } else {
-                    a = sourceLine[sx0i];
-                    b = nextSource[sx0i];
-                    double yprop = sy - syi;
-                    value = (a * (1.f - yprop) + b * yprop);
-                    int oi = sx0i + 1;
-                    double xprop = sx0 - sx0i;
-                    xprop -= 0.5;
-                    if (xprop < 0) {
-                        oi = sx0i - 1;
-                        xprop = -xprop;
-                    }
-                    if (oi < 0 || oi >= sw) oi = sx0i;
-                    a = sourceLine[oi];
-                    b = nextSource[oi];
-                    value = (value * (1.f - xprop) +
-                             (a * (1.f - yprop) + b * yprop) * xprop);
-                }
-                
-                int vi = int(lrint(value));
-                if (vi > 255) vi = 255;
-                if (vi < 0) vi = 0;
-                targetLine[x] = uchar(vi);
-            }
-        }
-    } else {
-
-        double sy0 = getBinForY(v, 0);
-
-        int psy0i = -1, psy1i = -1;
-
-        for (int y = 0; y < h; ++y) {
-
-            double sy1 = sy0;
-            sy0 = getBinForY(v, double(y + 1));
-
-            int sy0i = int(sy0 + epsilon);
-            int sy1i = int(sy1);
-
-            uchar *targetLine = img.scanLine(y);
-
-            if (sy0i == psy0i && sy1i == psy1i) {
-                // same source scan line as just computed
-                goto copy;
-            }
-
-            psy0i = sy0i;
-            psy1i = sy1i;
-
-            for (int x = 0; x < w; ++x) {
-                peaks[x] = 0;
-            }
-        
-            for (int sy = sy0i; sy <= sy1i; ++sy) {
-
-                if (sy < 0 || sy >= source->height()) continue;
-
-                uchar *sourceLine = source->scanLine(sy);
-            
-                for (int x = 0; x < w; ++x) {
-
-                    double sx1 = sxa[x*2 + 1];
-                    if (sx1 < 0) continue;
-                    int sx1i = int(sx1);
-
-                    double sx0 = sxa[x*2];
-                    if (sx0 < 0) continue;
-                    int sx0i = int(sx0 + epsilon);
-                    if (sx0i >= sw) break;
-
-                    uchar peak = 0;
-                    for (int sx = sx0i; sx <= sx1i; ++sx) {
-                        if (sx < 0 || sx >= sw) continue;
-                        if (sourceLine[sx] > peak) peak = sourceLine[sx];
-                    }
-                    peaks[x] = peak;
-                }
-            }
-        
-        copy:
-            for (int x = 0; x < w; ++x) {
-                targetLine[x] = peaks[x];
-            }
-        }
-    }
-
-    delete[] peaks;
-
-    paint.drawImage(x0, 0, img);
+    paintWithRenderer(v, paint, rect);
 }
 
 bool
@@ -1712,25 +1185,40 @@
 {
     QString s = QString("scale=\"%1\" "
                         "colourScheme=\"%2\" "
-                        "normalizeColumns=\"%3\" "
-                        "normalizeVisibleArea=\"%4\" "
-                        "minY=\"%5\" "
-                        "maxY=\"%6\" "
-                        "invertVertical=\"%7\" "
-                        "opaque=\"%8\" %9")
-	.arg((int)m_colourScale)
+                        "minY=\"%3\" "
+                        "maxY=\"%4\" "
+                        "invertVertical=\"%5\" "
+                        "opaque=\"%6\" %7")
+	.arg(convertFromColourScale(m_colourScale))
         .arg(m_colourMap)
-        .arg(m_normalizeColumns ? "true" : "false")
-        .arg(m_normalizeVisibleArea ? "true" : "false")
         .arg(m_miny)
         .arg(m_maxy)
         .arg(m_invertVertical ? "true" : "false")
         .arg(m_opaque ? "true" : "false")
         .arg(QString("binScale=\"%1\" smooth=\"%2\" gain=\"%3\" ")
-             .arg((int)m_binScale)
+             .arg(int(m_binScale))
              .arg(m_smooth ? "true" : "false")
              .arg(m_gain));
     
+    // New-style normalization attributes, allowing for more types of
+    // normalization in future: write out the column normalization
+    // type separately, and then whether we are normalizing visible
+    // area as well afterwards
+    
+    s += QString("columnNormalization=\"%1\" ")
+        .arg(m_normalization == ColumnNormalization::Max1 ? "peak" :
+             m_normalization == ColumnNormalization::Hybrid ? "hybrid" : "none");
+
+    // Old-style normalization attribute, for backward compatibility
+    
+    s += QString("normalizeColumns=\"%1\" ")
+	.arg(m_normalization == ColumnNormalization::Max1 ? "true" : "false");
+
+    // And this applies to both old- and new-style attributes
+    
+    s += QString("normalizeVisibleArea=\"%1\" ")
+        .arg(m_normalizeVisibleArea ? "true" : "false");
+    
     Layer::toXml(stream, indent, extraAttributes + " " + s);
 }
 
@@ -1739,22 +1227,16 @@
 {
     bool ok = false, alsoOk = false;
 
-    ColourScale scale = (ColourScale)attributes.value("scale").toInt(&ok);
-    if (ok) setColourScale(scale);
+    ColourScaleType colourScale = convertToColourScale
+        (attributes.value("colourScale").toInt(&ok));
+    if (ok) setColourScale(colourScale);
 
     int colourMap = attributes.value("colourScheme").toInt(&ok);
     if (ok) setColourMap(colourMap);
 
-    BinScale binscale = (BinScale)attributes.value("binScale").toInt(&ok);
-    if (ok) setBinScale(binscale);
-
-    bool normalizeColumns =
-        (attributes.value("normalizeColumns").trimmed() == "true");
-    setNormalizeColumns(normalizeColumns);
-
-    bool normalizeVisibleArea =
-        (attributes.value("normalizeVisibleArea").trimmed() == "true");
-    setNormalizeVisibleArea(normalizeVisibleArea);
+    BinScale binScale = (BinScale)
+	attributes.value("binScale").toInt(&ok);
+    if (ok) setBinScale(binScale);
 
     bool invertVertical =
         (attributes.value("invertVertical").trimmed() == "true");
@@ -1774,5 +1256,50 @@
     float min = attributes.value("minY").toFloat(&ok);
     float max = attributes.value("maxY").toFloat(&alsoOk);
     if (ok && alsoOk) setDisplayExtents(min, max);
+
+    bool haveNewStyleNormalization = false;
+    
+    QString columnNormalization = attributes.value("columnNormalization");
+
+    if (columnNormalization != "") {
+
+        haveNewStyleNormalization = true;
+
+        if (columnNormalization == "peak") {
+            setNormalization(ColumnNormalization::Max1);
+        } else if (columnNormalization == "hybrid") {
+            setNormalization(ColumnNormalization::Hybrid);
+        } else if (columnNormalization == "none") {
+            setNormalization(ColumnNormalization::None);
+        } else {
+            cerr << "NOTE: Unknown or unsupported columnNormalization attribute \""
+                 << columnNormalization << "\"" << endl;
+        }
+    }
+
+    if (!haveNewStyleNormalization) {
+
+        setNormalization(ColumnNormalization::None);
+
+        bool normalizeColumns =
+            (attributes.value("normalizeColumns").trimmed() == "true");
+        if (normalizeColumns) {
+            setNormalization(ColumnNormalization::Max1);
+        }
+
+        bool normalizeHybrid =
+            (attributes.value("normalizeHybrid").trimmed() == "true");
+        if (normalizeHybrid) {
+            setNormalization(ColumnNormalization::Hybrid);
+        }
+    }
+    
+    bool normalizeVisibleArea =
+        (attributes.value("normalizeVisibleArea").trimmed() == "true");
+    setNormalizeVisibleArea(normalizeVisibleArea);
+
+    //!!! todo: check save/reload scaling, compare with
+    //!!! SpectrogramLayer, compare with prior SV versions, compare
+    //!!! with Tony v1 and v2 and their save files
 }
 
--- a/layer/Colour3DPlotLayer.h	Mon Jun 13 11:44:08 2016 +0100
+++ b/layer/Colour3DPlotLayer.h	Fri Aug 05 15:05:02 2016 +0100
@@ -13,10 +13,14 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _COLOUR_3D_PLOT_H_
-#define _COLOUR_3D_PLOT_H_
+#ifndef COLOUR_3D_PLOT_LAYER_H
+#define COLOUR_3D_PLOT_LAYER_H
 
 #include "SliceableLayer.h"
+#include "VerticalBinLayer.h"
+
+#include "ColourScale.h"
+#include "Colour3DPlotRenderer.h"
 
 #include "data/model/DenseThreeDimensionalModel.h"
 
@@ -30,14 +34,11 @@
  * colour range.  Its source is a DenseThreeDimensionalModel.
  *
  * This was the original implementation for the spectrogram view, but
- * it was replaced with a more efficient implementation that derived
- * the spectrogram itself from a DenseTimeValueModel instead of using
- * a three-dimensional model.  This class is retained in case it
- * becomes useful, but it will probably need some cleaning up if it's
- * ever actually used.
+ * it was replaced for that purpose with a more efficient
+ * implementation that derived the spectrogram itself from a
+ * DenseTimeValueModel instead of using a three-dimensional model.
  */
-
-class Colour3DPlotLayer : public SliceableLayer
+class Colour3DPlotLayer : public VerticalBinLayer
 {
     Q_OBJECT
 
@@ -50,6 +51,7 @@
     }
     virtual const Model *getModel() const { return m_model; }
     virtual void paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const;
+    virtual void setSynchronousPainting(bool synchronous);
 
     virtual int getVerticalScaleWidth(LayerGeometryProvider *v, bool, QPainter &) const;
     virtual void paintVerticalScale(LayerGeometryProvider *v, bool, QPainter &paint, QRect rect) const;
@@ -81,19 +83,14 @@
                                          int *min, int *max, int *deflt) const;
     virtual QString getPropertyValueLabel(const PropertyName &,
 					  int value) const;
+    virtual QString getPropertyValueIconName(const PropertyName &,
+                                             int value) const;
     virtual RangeMapper *getNewPropertyRangeMapper(const PropertyName &) const;
     virtual void setProperty(const PropertyName &, int value);
     virtual void setProperties(const QXmlAttributes &);
     
-    enum ColourScale {
-        LinearScale,
-        LogScale,
-        PlusMinusOneScale,
-        AbsoluteScale
-    };
-
-    void setColourScale(ColourScale);
-    ColourScale getColourScale() const { return m_colourScale; }
+    void setColourScale(ColourScaleType);
+    ColourScaleType getColourScale() const { return m_colourScale; }
 
     void setColourMap(int map);
     int getColourMap() const;
@@ -104,11 +101,6 @@
      */
     void setGain(float gain);
     float getGain() const;
-
-    enum BinScale {
-	LinearBinScale,
-	LogBinScale
-    };
     
     /**
      * Specify the scale for the y axis.
@@ -117,25 +109,17 @@
     BinScale getBinScale() const;
 
     /**
-     * Normalize each column to its maximum value, independent of its
-     * neighbours.
+     * Specify the normalization mode for individual columns.
      */
-    void setNormalizeColumns(bool n);
-    bool getNormalizeColumns() const;
+    void setNormalization(ColumnNormalization);
+    ColumnNormalization getNormalization() const;
 
     /**
-     * Normalize each value against the maximum in the visible region.
+     * Specify whether to normalize the visible area.
      */
-    void setNormalizeVisibleArea(bool n);
+    void setNormalizeVisibleArea(bool);
     bool getNormalizeVisibleArea() const;
 
-    /**
-     * Normalize each column to its maximum value, and then scale by
-     * the log of the (absolute) maximum value.
-     */
-    void setNormalizeHybrid(bool n);
-    bool getNormalizeHybrid() const;
-
     void setInvertVertical(bool i);
     bool getInvertVertical() const;
 
@@ -173,41 +157,50 @@
 protected:
     const DenseThreeDimensionalModel *m_model; // I do not own this
     
-    mutable QImage *m_cache;
-    mutable QImage *m_peaksCache;
-    mutable int m_cacheValidStart;
-    mutable int m_cacheValidEnd;
-
-    ColourScale m_colourScale;
-    bool        m_colourScaleSet;
-    int         m_colourMap;
-    float       m_gain;
-    BinScale    m_binScale;
-    bool        m_normalizeColumns;
-    bool        m_normalizeVisibleArea;
-    bool        m_normalizeHybrid;
-    bool        m_invertVertical;
-    bool        m_opaque;
-    bool        m_smooth;
-    int         m_peakResolution;
+    ColourScaleType m_colourScale;
+    bool m_colourScaleSet;
+    int m_colourMap;
+    float m_gain;
+    BinScale m_binScale;
+    ColumnNormalization m_normalization; // of individual columns
+    bool m_normalizeVisibleArea;
+    bool m_invertVertical;
+    bool m_opaque;
+    bool m_smooth;
+    int m_peakResolution;
 
     // Minimum and maximum bin numbers visible within the view. We
     // always snap to whole bins at view edges.
-    int         m_miny;
-    int         m_maxy;
+    int m_miny;
+    int m_maxy;
 
+    bool m_synchronous;
+
+    static ColourScaleType convertToColourScale(int value);
+    static int convertFromColourScale(ColourScaleType);
+    static std::pair<ColumnNormalization, bool> convertToColumnNorm(int value);
+    static int convertFromColumnNorm(ColumnNormalization norm, bool visible);
+
+    mutable Dense3DModelPeakCache *m_peakCache;
+    const int m_peakCacheDivisor;
+    Dense3DModelPeakCache *getPeakCache() const;
+
+    typedef std::map<int, MagnitudeRange> ViewMagMap; // key is view id
+    mutable ViewMagMap m_viewMags;
+
+    typedef std::map<int, Colour3DPlotRenderer *> ViewRendererMap; // key is view id
+    mutable ViewRendererMap m_renderers;
+    
+    Colour3DPlotRenderer *getRenderer(const LayerGeometryProvider *) const;
+    void invalidateRenderers();
+        
     /**
      * Return the y coordinate at which the given bin "starts"
      * (i.e. at the bottom of the bin, if the given bin is an integer
      * and the vertical scale is the usual way up). Bin number may be
      * fractional, to obtain a position part-way through a bin.
      */
-    double getYForBin(LayerGeometryProvider *, double bin) const;
-
-    /**
-     * As getYForBin, but rounding to integer values.
-     */
-    int getIYForBin(LayerGeometryProvider *, int bin) const;
+    double getYForBin(const LayerGeometryProvider *, double bin) const;
     
     /**
      * Return the bin number, possibly fractional, at the given y
@@ -215,25 +208,13 @@
      * at which the bins "start" (i.e. the bottom of the visible bin,
      * if the vertical scale is the usual way up).
      */
-    double getBinForY(LayerGeometryProvider *, double y) const;
-
-    /**
-     * As getBinForY, but rounding to integer values.
-     */
-    int getIBinForY(LayerGeometryProvider *, int y) const;
+    double getBinForY(const LayerGeometryProvider *, double y) const;
     
     DenseThreeDimensionalModel::Column getColumn(int col) const;
 
-    /**
-     * True if we have the opaque or smooth flag set, or if the cells
-     * are so small you can't see their borders. False for big,
-     * translucent cells.
-     */
-    bool shouldPaintDenseIn(const LayerGeometryProvider *) const; 
+    int getColourScaleWidth(QPainter &) const;
 
-    int getColourScaleWidth(QPainter &) const;
-    void fillCache(int firstBin, int lastBin) const;
-    void paintDense(LayerGeometryProvider *v, QPainter &paint, QRect rect) const;
+    void paintWithRenderer(LayerGeometryProvider *v, QPainter &paint, QRect rect) const;
 };
 
 #endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/Colour3DPlotRenderer.cpp	Fri Aug 05 15:05:02 2016 +0100
@@ -0,0 +1,1051 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006-2016 Chris Cannam and QMUL.
+    
+    This program 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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "Colour3DPlotRenderer.h"
+#include "RenderTimer.h"
+
+#include "base/Profiler.h"
+
+#include "data/model/DenseThreeDimensionalModel.h"
+#include "data/model/Dense3DModelPeakCache.h"
+#include "data/model/FFTModel.h"
+
+#include "LayerGeometryProvider.h"
+#include "VerticalBinLayer.h"
+#include "PaintAssistant.h"
+#include "ImageRegionFinder.h"
+
+#include "view/ViewManager.h" // for main model sample rate. Pity
+
+#include <vector>
+
+//#define DEBUG_COLOUR_PLOT_REPAINT 1
+
+using namespace std;
+
+Colour3DPlotRenderer::RenderResult
+Colour3DPlotRenderer::render(const LayerGeometryProvider *v, QPainter &paint, QRect rect)
+{
+    return render(v, paint, rect, false);
+}
+
+Colour3DPlotRenderer::RenderResult
+Colour3DPlotRenderer::renderTimeConstrained(const LayerGeometryProvider *v,
+                                            QPainter &paint, QRect rect)
+{
+    return render(v, paint, rect, true);
+}
+
+QRect
+Colour3DPlotRenderer::getLargestUncachedRect(const LayerGeometryProvider *v)
+{
+    RenderType renderType = decideRenderType(v);
+
+    if (renderType == DirectTranslucent) {
+        return QRect(); // never cached
+    }
+
+    int h = m_cache.getSize().height();
+
+    QRect areaLeft(0, 0, m_cache.getValidLeft(), h);
+    QRect areaRight(m_cache.getValidRight(), 0,
+                    m_cache.getSize().width() - m_cache.getValidRight(), h);
+
+    if (areaRight.width() > areaLeft.width()) {
+        return areaRight;
+    } else {
+        return areaLeft;
+    }
+}
+
+bool
+Colour3DPlotRenderer::geometryChanged(const LayerGeometryProvider *v)
+{
+    RenderType renderType = decideRenderType(v);
+
+    if (renderType == DirectTranslucent) {
+        return true; // never cached
+    }
+
+    if (m_cache.getSize() == v->getPaintSize() &&
+        m_cache.getZoomLevel() == v->getZoomLevel() &&
+        m_cache.getStartFrame() == v->getStartFrame()) {
+        return false;
+    } else {
+        return true;
+    }
+}
+
+Colour3DPlotRenderer::RenderResult
+Colour3DPlotRenderer::render(const LayerGeometryProvider *v,
+                             QPainter &paint, QRect rect, bool timeConstrained)
+{
+    RenderType renderType = decideRenderType(v);
+
+    if (renderType != DrawBufferPixelResolution) {
+        // Rendering should be fast in bin-resolution and direct draw
+        // cases because we are quite well zoomed-in, and the sums are
+        // easier this way. Calculating boundaries later will be
+        // fiddly for partial paints otherwise.
+        timeConstrained = false;
+    }
+
+    int x0 = v->getXForViewX(rect.x());
+    int x1 = v->getXForViewX(rect.x() + rect.width());
+    if (x0 < 0) x0 = 0;
+    if (x1 > v->getPaintWidth()) x1 = v->getPaintWidth();
+
+    sv_frame_t startFrame = v->getStartFrame();
+    
+    m_cache.resize(v->getPaintSize());
+    m_cache.setZoomLevel(v->getZoomLevel());
+
+    m_magCache.resize(v->getPaintSize().width());
+    m_magCache.setZoomLevel(v->getZoomLevel());
+    
+    if (renderType == DirectTranslucent) {
+        MagnitudeRange range = renderDirectTranslucent(v, paint, rect);
+        return { rect, range };
+    }
+    
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+    cerr << "cache start " << m_cache.getStartFrame()
+         << " valid left " << m_cache.getValidLeft()
+         << " valid right " << m_cache.getValidRight()
+         << endl;
+    cerr << " view start " << startFrame
+         << " x0 " << x0
+         << " x1 " << x1
+         << endl;
+#endif
+    
+    if (m_cache.isValid()) { // some part of the cache is valid
+
+        if (v->getXForFrame(m_cache.getStartFrame()) ==
+            v->getXForFrame(startFrame) &&
+            m_cache.getValidLeft() <= x0 &&
+            m_cache.getValidRight() >= x1) {
+
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+            cerr << "cache hit" << endl;
+#endif
+            
+            // cache is valid for the complete requested area
+            paint.drawImage(rect, m_cache.getImage(), rect);
+
+            MagnitudeRange range = m_magCache.getRange(x0, x1 - x0);
+
+            return { rect, range };
+
+        } else {
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+            cerr << "cache partial hit" << endl;
+#endif
+            
+            // cache doesn't begin at the right frame or doesn't
+            // contain the complete view, but might be scrollable or
+            // partially usable
+            m_cache.scrollTo(v, startFrame);
+            m_magCache.scrollTo(v, startFrame);
+
+            // if we are not time-constrained, then we want to paint
+            // the whole area in one go; we don't return a partial
+            // paint. To avoid providing the more complex logic to
+            // handle painting discontiguous areas, if the only valid
+            // part of cache is in the middle, just make the whole
+            // thing invalid and start again.
+            if (!timeConstrained) {
+                if (m_cache.getValidLeft() > x0 &&
+                    m_cache.getValidRight() < x1) {
+                    m_cache.invalidate();
+                }
+            }
+        }
+    } else {
+        // cache is completely invalid
+        m_cache.setStartFrame(startFrame);
+        m_magCache.setStartFrame(startFrame);
+    }
+
+    bool rightToLeft = false;
+
+    int reqx0 = x0;
+    int reqx1 = x1;
+    
+    if (!m_cache.isValid() && timeConstrained) {
+        // When rendering the whole area, in a context where we might
+        // not be able to complete the work, start from somewhere near
+        // the middle so that the region of interest appears first
+
+        //!!! (perhaps we should avoid doing this if past repaints
+        //!!! have been fast enough to do the whole in one shot)
+        if (x0 == 0 && x1 == v->getPaintWidth()) {
+            x0 = int(x1 * 0.3);
+        }
+    }
+
+    if (m_cache.isValid()) {
+            
+        // When rendering only a part of the cache, we need to make
+        // sure that the part we're rendering is adjacent to (or
+        // overlapping) a valid area of cache, if we have one. The
+        // alternative is to ditch the valid area of cache and render
+        // only the requested area, but that's risky because this can
+        // happen when just waving the pointer over a small part of
+        // the view -- if we lose the partly-built cache every time
+        // the user does that, we'll never finish building it.
+        int left = x0;
+        int width = x1 - x0;
+        bool isLeftOfValidArea = false;
+        m_cache.adjustToTouchValidArea(left, width, isLeftOfValidArea);
+        x0 = left;
+        x1 = x0 + width;
+
+        // That call also told us whether we should be painting
+        // sub-regions of our target region in right-to-left order in
+        // order to ensure contiguity
+        rightToLeft = isLeftOfValidArea;
+    }
+    
+    // Note, we always paint the full height to cache. We want to
+    // ensure the cache is coherent without having to worry about
+    // vertical matching of required and valid areas as well as
+    // horizontal.
+
+    if (renderType == DrawBufferBinResolution) {
+
+        renderToCacheBinResolution(v, x0, x1 - x0);
+
+    } else { // must be DrawBufferPixelResolution, handled DirectTranslucent earlier
+
+        renderToCachePixelResolution(v, x0, x1 - x0, rightToLeft, timeConstrained);
+    }
+
+    QRect pr = rect & m_cache.getValidArea();
+    paint.drawImage(pr.x(), pr.y(), m_cache.getImage(),
+                    pr.x(), pr.y(), pr.width(), pr.height());
+
+    if (!timeConstrained && (pr != rect)) {
+        cerr << "WARNING: failed to render entire requested rect "
+             << "even when not time-constrained" << endl;
+    }
+
+    MagnitudeRange range = m_magCache.getRange(reqx0, reqx1 - reqx0);
+    
+    return { pr, range };
+}
+
+Colour3DPlotRenderer::RenderType
+Colour3DPlotRenderer::decideRenderType(const LayerGeometryProvider *v) const
+{
+    const DenseThreeDimensionalModel *model = m_sources.source;
+    if (!model || !v || !(v->getViewManager())) {
+        return DrawBufferPixelResolution; // or anything
+    }
+
+    int binResolution = model->getResolution();
+    int zoomLevel = v->getZoomLevel();
+    sv_samplerate_t modelRate = model->getSampleRate();
+
+    double rateRatio = v->getViewManager()->getMainModelSampleRate() / modelRate;
+    double relativeBinResolution = binResolution * rateRatio;
+
+    if (m_params.binDisplay == BinDisplay::PeakFrequencies) {
+        // no alternative works here
+        return DrawBufferPixelResolution;
+    }
+
+    if (!m_params.alwaysOpaque && !m_params.interpolate) {
+
+        // consider translucent option -- only if not smoothing & not
+        // explicitly requested opaque & sufficiently zoomed-in
+        
+        if (model->getHeight() * 3 < v->getPaintHeight() &&
+            relativeBinResolution >= 3 * zoomLevel) {
+            return DirectTranslucent;
+        }
+    }
+
+    if (relativeBinResolution > zoomLevel) {
+        return DrawBufferBinResolution;
+    } else {
+        return DrawBufferPixelResolution;
+    }
+}
+
+ColumnOp::Column
+Colour3DPlotRenderer::getColumn(int sx, int minbin, int nbins) const
+{
+    // order:
+    // get column -> scale -> normalise -> record extents ->
+    // peak pick -> distribute/interpolate -> apply display gain
+
+    // we do the first bit here:
+    // get column -> scale -> normalise
+
+    ColumnOp::Column column;
+                
+    if (m_params.colourScale.getScale() == ColourScaleType::Phase &&
+        m_sources.fft) {
+
+        ColumnOp::Column fullColumn = m_sources.fft->getPhases(sx);
+
+        column = vector<float>(fullColumn.data() + minbin,
+                               fullColumn.data() + minbin + nbins);
+
+    } else {
+                    
+        ColumnOp::Column fullColumn = m_sources.source->getColumn(sx);
+                
+        column = vector<float>(fullColumn.data() + minbin,
+                               fullColumn.data() + minbin + nbins);
+
+        column = ColumnOp::applyGain(column, m_params.scaleFactor);
+
+        column = ColumnOp::normalize(column, m_params.normalization);
+    }
+
+    return column;
+}
+
+MagnitudeRange
+Colour3DPlotRenderer::renderDirectTranslucent(const LayerGeometryProvider *v,
+                                              QPainter &paint,
+                                              QRect rect)
+{
+    Profiler profiler("Colour3DPlotRenderer::renderDirectTranslucent");
+
+    MagnitudeRange magRange;
+    
+    QPoint illuminatePos;
+    bool illuminate = v->shouldIlluminateLocalFeatures
+        (m_sources.verticalBinLayer, illuminatePos);
+
+    const DenseThreeDimensionalModel *model = m_sources.source;
+    
+    int x0 = rect.left();
+    int x1 = rect.right() + 1;
+
+    int h = v->getPaintHeight();
+
+    sv_frame_t modelStart = model->getStartFrame();
+    sv_frame_t modelEnd = model->getEndFrame();
+    int modelResolution = model->getResolution();
+
+    double rateRatio =
+        v->getViewManager()->getMainModelSampleRate() / model->getSampleRate();
+
+    // the s-prefix values are source, i.e. model, column and bin numbers
+    int sx0 = int((double(v->getFrameForX(x0)) / rateRatio - double(modelStart))
+                  / modelResolution);
+    int sx1 = int((double(v->getFrameForX(x1)) / rateRatio - double(modelStart))
+                  / modelResolution);
+
+    int sh = model->getHeight();
+
+    const int buflen = 40;
+    char labelbuf[buflen];
+
+    int minbin = m_sources.verticalBinLayer->getIBinForY(v, h);
+    if (minbin >= sh) minbin = sh - 1;
+    if (minbin < 0) minbin = 0;
+    
+    int nbins  = m_sources.verticalBinLayer->getIBinForY(v, 0) - minbin + 1;
+    if (minbin + nbins > sh) nbins = sh - minbin;
+
+    int psx = -1;
+
+    vector<float> preparedColumn;
+
+    int modelWidth = model->getWidth();
+
+    for (int sx = sx0; sx <= sx1; ++sx) {
+
+        if (sx < 0 || sx >= modelWidth) {
+            continue;
+        }
+
+        if (sx != psx) {
+
+            // order:
+            // get column -> scale -> normalise -> record extents ->
+            // peak pick -> distribute/interpolate -> apply display gain
+
+            // this does the first three:
+            preparedColumn = getColumn(sx, minbin, nbins);
+            
+            magRange.sample(preparedColumn);
+
+            if (m_params.binDisplay == BinDisplay::PeakBins) {
+                preparedColumn = ColumnOp::peakPick(preparedColumn);
+            }
+
+            // Display gain belongs to the colour scale and is
+            // applied by the colour scale object when mapping it
+
+            psx = sx;
+        }
+
+	sv_frame_t fx = sx * modelResolution + modelStart;
+
+	if (fx + modelResolution <= modelStart || fx > modelEnd) continue;
+
+        int rx0 = v->getXForFrame(int(double(fx) * rateRatio));
+	int rx1 = v->getXForFrame(int(double(fx + modelResolution + 1) * rateRatio));
+
+	int rw = rx1 - rx0;
+	if (rw < 1) rw = 1;
+
+	bool showLabel = (rw > 10 &&
+			  paint.fontMetrics().width("0.000000") < rw - 3 &&
+			  paint.fontMetrics().height() < (h / sh));
+        
+	for (int sy = minbin; sy < minbin + nbins; ++sy) {
+
+            int ry0 = m_sources.verticalBinLayer->getIYForBin(v, sy);
+            int ry1 = m_sources.verticalBinLayer->getIYForBin(v, sy + 1);
+
+            if (m_params.invertVertical) {
+                ry0 = h - ry0 - 1;
+                ry1 = h - ry1 - 1;
+            }
+                    
+            QRect r(rx0, ry1, rw, ry0 - ry1);
+
+            float value = preparedColumn[sy - minbin];
+            QColor colour = m_params.colourScale.getColour(value,
+                                                           m_params.colourRotation);
+
+            if (rw == 1) {
+                paint.setPen(colour);
+                paint.setBrush(Qt::NoBrush);
+                paint.drawLine(r.x(), r.y(), r.x(), r.y() + r.height() - 1);
+                continue;
+            }
+
+	    QColor pen(255, 255, 255, 80);
+	    QColor brush(colour);
+
+            if (rw > 3 && r.height() > 3) {
+                brush.setAlpha(160);
+            }
+
+	    paint.setPen(Qt::NoPen);
+	    paint.setBrush(brush);
+
+	    if (illuminate) {
+		if (r.contains(illuminatePos)) {
+		    paint.setPen(v->getForeground());
+		}
+	    }
+            
+#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
+//            cerr << "rect " << r.x() << "," << r.y() << " "
+//                      << r.width() << "x" << r.height() << endl;
+#endif
+
+	    paint.drawRect(r);
+
+	    if (showLabel) {
+                double value = model->getValueAt(sx, sy);
+                snprintf(labelbuf, buflen, "%06f", value);
+                QString text(labelbuf);
+                PaintAssistant::drawVisibleText
+                    (v,
+                     paint,
+                     rx0 + 2,
+                     ry0 - h / sh - 1 + 2 + paint.fontMetrics().ascent(),
+                     text,
+                     PaintAssistant::OutlinedText);
+	    }
+	}
+    }
+
+    return magRange;
+}
+
+void
+Colour3DPlotRenderer::renderToCachePixelResolution(const LayerGeometryProvider *v,
+                                                   int x0, int repaintWidth,
+                                                   bool rightToLeft,
+                                                   bool timeConstrained)
+{
+    Profiler profiler("Colour3DPlotRenderer::renderToCachePixelResolution");
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+    cerr << "renderToCachePixelResolution" << endl;
+#endif
+    
+    // Draw to the draw buffer, and then copy from there. The draw
+    // buffer is at the same resolution as the target in the cache, so
+    // no extra scaling needed.
+
+    const DenseThreeDimensionalModel *model = m_sources.source;
+    if (!model || !model->isOK() || !model->isReady()) {
+	throw std::logic_error("no source model provided, or model not ready");
+    }
+
+    int h = v->getPaintHeight();
+
+    clearDrawBuffer(repaintWidth, h);
+
+    vector<int> binforx(repaintWidth);
+    vector<double> binfory(h);
+    
+    bool usePeaksCache = false;
+    int binsPerPeak = 1;
+    int zoomLevel = v->getZoomLevel();
+    int binResolution = model->getResolution();
+
+    for (int x = 0; x < repaintWidth; ++x) {
+        sv_frame_t f0 = v->getFrameForX(x0 + x);
+        double s0 = double(f0 - model->getStartFrame()) / binResolution;
+        binforx[x] = int(s0 + 0.0001);
+    }
+
+    if (m_sources.peaks) { // peaks cache exists
+
+        binsPerPeak = m_sources.peaks->getColumnsPerPeak();
+        usePeaksCache = (binResolution * binsPerPeak) < zoomLevel;
+        
+        if (m_params.colourScale.getScale() ==
+            ColourScaleType::Phase) {
+            usePeaksCache = false;
+        }
+    }
+
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+    cerr << "[PIX] zoomLevel = " << zoomLevel
+         << ", binResolution " << binResolution 
+         << ", binsPerPeak " << binsPerPeak
+         << ", peak cache " << m_sources.peaks
+         << ", usePeaksCache = " << usePeaksCache
+         << endl;
+#endif
+    
+    for (int y = 0; y < h; ++y) {
+        binfory[y] = m_sources.verticalBinLayer->getBinForY(v, h - y - 1);
+    }
+
+    int attainedWidth;
+
+    if (m_params.binDisplay == BinDisplay::PeakFrequencies) {
+        attainedWidth = renderDrawBufferPeakFrequencies(v,
+                                                        repaintWidth,
+                                                        h,
+                                                        binforx,
+                                                        binfory,
+                                                        rightToLeft,
+                                                        timeConstrained);
+
+    } else {
+        attainedWidth = renderDrawBuffer(repaintWidth,
+                                         h,
+                                         binforx,
+                                         binfory,
+                                         usePeaksCache,
+                                         rightToLeft,
+                                         timeConstrained);
+    }
+
+    if (attainedWidth == 0) return;
+
+    // draw buffer is pixel resolution, no scaling factors or padding involved
+    
+    int paintedLeft = x0;
+    if (rightToLeft) {
+        paintedLeft += (repaintWidth - attainedWidth);
+    }
+
+    m_cache.drawImage(paintedLeft, attainedWidth,
+                      m_drawBuffer,
+                      paintedLeft - x0, attainedWidth);
+
+    for (int i = 0; in_range_for(m_magRanges, i); ++i) {
+        m_magCache.sampleColumn(i, m_magRanges.at(i));
+    }
+}
+
+void
+Colour3DPlotRenderer::renderToCacheBinResolution(const LayerGeometryProvider *v,
+                                                 int x0, int repaintWidth)
+{
+    Profiler profiler("Colour3DPlotRenderer::renderToCacheBinResolution");
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+    cerr << "renderToCacheBinResolution" << endl;
+#endif
+    
+    // Draw to the draw buffer, and then scale-copy from there. Draw
+    // buffer is at bin resolution, i.e. buffer x == source column
+    // number. We use toolkit smooth scaling for interpolation.
+
+    const DenseThreeDimensionalModel *model = m_sources.source;
+    if (!model || !model->isOK() || !model->isReady()) {
+	throw std::logic_error("no source model provided, or model not ready");
+    }
+
+    // The draw buffer will contain a fragment at bin resolution. We
+    // need to ensure that it starts and ends at points where a
+    // time-bin boundary occurs at an exact pixel boundary, and with a
+    // certain amount of overlap across existing pixels so that we can
+    // scale and draw from it without smoothing errors at the edges.
+
+    // If (getFrameForX(x) / increment) * increment ==
+    // getFrameForX(x), then x is a time-bin boundary.  We want two
+    // such boundaries at either side of the draw buffer -- one which
+    // we draw up to, and one which we subsequently crop at.
+
+    sv_frame_t leftBoundaryFrame = -1, leftCropFrame = -1;
+    sv_frame_t rightBoundaryFrame = -1, rightCropFrame = -1;
+
+    int drawBufferWidth;
+    int binResolution = model->getResolution();
+
+    for (int x = x0; ; --x) {
+        sv_frame_t f = v->getFrameForX(x);
+        if ((f / binResolution) * binResolution == f) {
+            if (leftCropFrame == -1) leftCropFrame = f;
+            else if (x < x0 - 2) {
+                leftBoundaryFrame = f;
+                break;
+            }
+        }
+    }
+    for (int x = x0 + repaintWidth; ; ++x) {
+        sv_frame_t f = v->getFrameForX(x);
+        if ((f / binResolution) * binResolution == f) {
+            if (rightCropFrame == -1) rightCropFrame = f;
+            else if (x > x0 + repaintWidth + 2) {
+                rightBoundaryFrame = f;
+                break;
+            }
+        }
+    }
+    drawBufferWidth = int
+        ((rightBoundaryFrame - leftBoundaryFrame) / binResolution);
+    
+    int h = v->getPaintHeight();
+
+    // For our purposes here, the draw buffer needs to be exactly our
+    // target size (so we recreate always rather than just clear it)
+    
+    recreateDrawBuffer(drawBufferWidth, h);
+
+    vector<int> binforx(drawBufferWidth);
+    vector<double> binfory(h);
+    
+    for (int x = 0; x < drawBufferWidth; ++x) {
+        binforx[x] = int(leftBoundaryFrame / binResolution) + x;
+    }
+
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+    cerr << "[BIN] binResolution " << binResolution 
+         << endl;
+#endif
+    
+    for (int y = 0; y < h; ++y) {
+        binfory[y] = m_sources.verticalBinLayer->getBinForY(v, h - y - 1);
+    }
+
+    int attainedWidth = renderDrawBuffer(drawBufferWidth,
+                                         h,
+                                         binforx,
+                                         binfory,
+                                         false,
+                                         false,
+                                         false);
+
+    if (attainedWidth == 0) return;
+
+    int scaledLeft = v->getXForFrame(leftBoundaryFrame);
+    int scaledRight = v->getXForFrame(rightBoundaryFrame);
+
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+    cerr << "scaling draw buffer from width " << m_drawBuffer.width()
+         << " to " << (scaledRight - scaledLeft) << " (nb drawBufferWidth = "
+         << drawBufferWidth << ")" << endl;
+#endif
+    
+    QImage scaled = m_drawBuffer.scaled
+        (scaledRight - scaledLeft, h,
+         Qt::IgnoreAspectRatio, (m_params.interpolate ?
+                                 Qt::SmoothTransformation :
+                                 Qt::FastTransformation));
+            
+    int scaledLeftCrop = v->getXForFrame(leftCropFrame);
+    int scaledRightCrop = v->getXForFrame(rightCropFrame);
+    
+    int targetLeft = scaledLeftCrop;
+    if (targetLeft < 0) {
+        targetLeft = 0;
+    }
+    
+    int targetWidth = scaledRightCrop - targetLeft;
+    if (targetLeft + targetWidth > m_cache.getSize().width()) {
+        targetWidth = m_cache.getSize().width() - targetLeft;
+    }
+    
+    int sourceLeft = targetLeft - scaledLeft;
+    if (sourceLeft < 0) {
+        sourceLeft = 0;
+    }
+    
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+    cerr << "repaintWidth = " << repaintWidth
+         << ", targetWidth = " << targetWidth << endl;
+#endif
+    
+    if (targetWidth > 0) {
+        // we are copying from an image that has already been scaled,
+        // hence using the same width in both geometries
+        m_cache.drawImage(targetLeft, targetWidth,
+                          scaled,
+                          sourceLeft, targetWidth);
+    }
+    
+    for (int i = 0; i < targetWidth; ++i) {
+        // but the mag range vector has not been scaled
+        int sourceIx = int((double(i + sourceLeft) / scaled.width())
+                           * int(m_magRanges.size()));
+        if (in_range_for(m_magRanges, sourceIx)) {
+            m_magCache.sampleColumn(i, m_magRanges.at(sourceIx));
+        }
+    }
+}
+
+int
+Colour3DPlotRenderer::renderDrawBuffer(int w, int h,
+                                       const vector<int> &binforx,
+                                       const vector<double> &binfory,
+                                       bool usePeaksCache,
+                                       bool rightToLeft,
+                                       bool timeConstrained)
+{
+    // Callers must have checked that the appropriate subset of
+    // Sources data members are set for the supplied flags (e.g. that
+    // peaks model exists if usePeaksCache)
+    
+    RenderTimer timer(timeConstrained ?
+                      RenderTimer::FastRender :
+                      RenderTimer::NoTimeout);
+
+    int divisor = 1;
+    const DenseThreeDimensionalModel *sourceModel = m_sources.source;
+    if (usePeaksCache) {
+        divisor = m_sources.peaks->getColumnsPerPeak();
+        sourceModel = m_sources.peaks;
+    }
+
+    int sh = sourceModel->getHeight();
+    
+    int minbin = int(binfory[0] + 0.0001);
+    if (minbin >= sh) minbin = sh - 1;
+    if (minbin < 0) minbin = 0;
+
+    int nbins  = int(binfory[h-1]) - minbin + 1;
+    if (minbin + nbins > sh) nbins = sh - minbin;
+    
+    int psx = -1;
+
+    int start = 0;
+    int finish = w;
+    int step = 1;
+
+    if (rightToLeft) {
+        start = w-1;
+        finish = -1;
+        step = -1;
+    }
+
+    int columnCount = 0;
+    
+    vector<float> preparedColumn;
+
+    int modelWidth = sourceModel->getWidth();
+
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+    cerr << "modelWidth " << modelWidth << ", divisor " << divisor << endl;
+#endif
+    
+    for (int x = start; x != finish; x += step) {
+
+        // x is the on-canvas pixel coord; sx (later) will be the
+        // source column index
+        
+        ++columnCount;
+
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+        cerr << "x = " << x << ", binforx[x] = " << binforx[x] << endl;
+#endif
+        
+        if (binforx[x] < 0) continue;
+
+        int sx0 = binforx[x] / divisor;
+        int sx1 = sx0;
+        if (x+1 < w) sx1 = binforx[x+1] / divisor;
+        if (sx0 < 0) sx0 = sx1 - 1;
+        if (sx0 < 0) continue;
+        if (sx1 <= sx0) sx1 = sx0 + 1;
+
+        vector<float> pixelPeakColumn;
+        MagnitudeRange magRange;
+        
+        for (int sx = sx0; sx < sx1; ++sx) {
+
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+            cerr << "sx = " << sx << endl;
+#endif
+
+            if (sx < 0 || sx >= modelWidth) {
+                continue;
+            }
+
+            if (sx != psx) {
+                
+                // order:
+                // get column -> scale -> normalise -> record extents ->
+                // peak pick -> distribute/interpolate -> apply display gain
+
+                // this does the first three:
+                ColumnOp::Column column = getColumn(sx, minbin, nbins);
+
+                magRange.sample(column);
+                
+                if (m_params.binDisplay == BinDisplay::PeakBins) {
+                    column = ColumnOp::peakPick(column);
+                }
+
+                preparedColumn =
+                    ColumnOp::distribute(column,
+                                         h,
+                                         binfory,
+                                         minbin,
+                                         m_params.interpolate);
+
+                // Display gain belongs to the colour scale and is
+                // applied by the colour scale object when mapping it
+                
+                psx = sx;
+            }
+
+            if (sx == sx0) {
+                pixelPeakColumn = preparedColumn;
+            } else {
+                for (int i = 0; in_range_for(pixelPeakColumn, i); ++i) {
+                    pixelPeakColumn[i] = std::max(pixelPeakColumn[i],
+                                                  preparedColumn[i]);
+                }
+            }
+        }
+
+        if (!pixelPeakColumn.empty()) {
+
+            for (int y = 0; y < h; ++y) {
+                int py;
+                if (m_params.invertVertical) {
+                    py = y;
+                } else {
+                    py = h - y - 1;
+                }
+                m_drawBuffer.setPixel
+                    (x,
+                     py,
+                     m_params.colourScale.getPixel(pixelPeakColumn[y]));
+            }
+            
+            m_magRanges.push_back(magRange);
+        }
+
+        double fractionComplete = double(columnCount) / double(w);
+        if (timer.outOfTime(fractionComplete)) {
+            cerr << "out of time" << endl;
+            return columnCount;
+        }
+    }
+
+    return columnCount;
+}
+
+int
+Colour3DPlotRenderer::renderDrawBufferPeakFrequencies(const LayerGeometryProvider *v,
+                                                      int w, int h,
+                                                      const vector<int> &binforx,
+                                                      const vector<double> &binfory,
+                                                      bool rightToLeft,
+                                                      bool timeConstrained)
+{
+    // Callers must have checked that the appropriate subset of
+    // Sources data members are set for the supplied flags (e.g. that
+    // fft model exists)
+    
+    RenderTimer timer(timeConstrained ?
+                      RenderTimer::FastRender :
+                      RenderTimer::NoTimeout);
+
+    const FFTModel *fft = m_sources.fft;
+
+    int sh = fft->getHeight();
+    
+    int minbin = int(binfory[0] + 0.0001);
+    if (minbin >= sh) minbin = sh - 1;
+    if (minbin < 0) minbin = 0;
+
+    int nbins  = int(binfory[h-1]) - minbin + 1;
+    if (minbin + nbins > sh) nbins = sh - minbin;
+
+    FFTModel::PeakSet peakfreqs;
+
+    int psx = -1;
+    
+    int start = 0;
+    int finish = w;
+    int step = 1;
+
+    if (rightToLeft) {
+        start = w-1;
+        finish = -1;
+        step = -1;
+    }
+    
+    int columnCount = 0;
+    
+    vector<float> preparedColumn;
+
+    int modelWidth = fft->getWidth();
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+    cerr << "modelWidth " << modelWidth << endl;
+#endif
+    
+    double minFreq =
+        (double(minbin) * fft->getSampleRate()) / fft->getFFTSize();
+    double maxFreq =
+        (double(minbin + nbins - 1) * fft->getSampleRate()) / fft->getFFTSize();
+
+    bool logarithmic = (m_params.binScale == BinScale::Log);
+    
+    for (int x = start; x != finish; x += step) {
+        
+        // x is the on-canvas pixel coord; sx (later) will be the
+        // source column index
+        
+        ++columnCount;
+        
+        if (binforx[x] < 0) continue;
+
+        int sx0 = binforx[x];
+        int sx1 = sx0;
+        if (x+1 < w) sx1 = binforx[x+1];
+        if (sx0 < 0) sx0 = sx1 - 1;
+        if (sx0 < 0) continue;
+        if (sx1 <= sx0) sx1 = sx0 + 1;
+
+        vector<float> pixelPeakColumn;
+        MagnitudeRange magRange;
+        
+        for (int sx = sx0; sx < sx1; ++sx) {
+
+            if (sx < 0 || sx >= modelWidth) {
+                continue;
+            }
+
+            if (sx != psx) {
+                preparedColumn = getColumn(sx, minbin, nbins);
+                magRange.sample(preparedColumn);
+                psx = sx;
+            }
+
+            if (sx == sx0) {
+                pixelPeakColumn = preparedColumn;
+                peakfreqs = fft->getPeakFrequencies(FFTModel::AllPeaks, sx,
+                                                    minbin, minbin + nbins - 1);
+            } else {
+                for (int i = 0; in_range_for(pixelPeakColumn, i); ++i) {
+                    pixelPeakColumn[i] = std::max(pixelPeakColumn[i],
+                                                  preparedColumn[i]);
+                }
+            }
+        }
+
+        if (!pixelPeakColumn.empty()) {
+            
+            for (FFTModel::PeakSet::const_iterator pi = peakfreqs.begin();
+                 pi != peakfreqs.end(); ++pi) {
+
+                int bin = pi->first;
+                double freq = pi->second;
+
+                if (bin < minbin) continue;
+                if (bin >= minbin + nbins) break;
+            
+                double value = pixelPeakColumn[bin - minbin];
+            
+                double y = v->getYForFrequency
+                    (freq, minFreq, maxFreq, logarithmic);
+            
+                int iy = int(y + 0.5);
+                if (iy < 0 || iy >= h) continue;
+
+                m_drawBuffer.setPixel
+                    (x,
+                     iy,
+                     m_params.colourScale.getPixel(value));
+            }
+
+            m_magRanges.push_back(magRange);
+        }
+
+        double fractionComplete = double(columnCount) / double(w);
+        if (timer.outOfTime(fractionComplete)) {
+            return columnCount;
+        }
+    }
+
+    return columnCount;
+}
+
+void
+Colour3DPlotRenderer::recreateDrawBuffer(int w, int h)
+{
+    m_drawBuffer = QImage(w, h, QImage::Format_Indexed8);
+
+    for (int pixel = 0; pixel < 256; ++pixel) {
+        m_drawBuffer.setColor
+            ((unsigned char)pixel,
+             m_params.colourScale.getColourForPixel
+             (pixel, m_params.colourRotation).rgb());
+    }
+
+    m_drawBuffer.fill(0);
+    m_magRanges.clear();
+}
+
+void
+Colour3DPlotRenderer::clearDrawBuffer(int w, int h)
+{
+    if (m_drawBuffer.width() < w || m_drawBuffer.height() != h) {
+        recreateDrawBuffer(w, h);
+    } else {
+        m_drawBuffer.fill(0);
+        m_magRanges.clear();
+    }
+}
+
+QRect
+Colour3DPlotRenderer::findSimilarRegionExtents(QPoint p) const
+{
+    QImage image = m_cache.getImage();
+    ImageRegionFinder finder;
+    QRect rect = finder.findRegionExtents(&image, p);
+    return rect;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/Colour3DPlotRenderer.h	Fri Aug 05 15:05:02 2016 +0100
@@ -0,0 +1,309 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006-2016 Chris Cannam and QMUL.
+    
+    This program 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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef COLOUR_3D_PLOT_RENDERER_H
+#define COLOUR_3D_PLOT_RENDERER_H
+
+#include "ColourScale.h"
+#include "ScrollableImageCache.h"
+#include "ScrollableMagRangeCache.h"
+
+#include "base/ColumnOp.h"
+#include "base/MagnitudeRange.h"
+
+#include <QRect>
+#include <QPainter>
+#include <QImage>
+
+class LayerGeometryProvider;
+class VerticalBinLayer;
+class DenseThreeDimensionalModel;
+class Dense3DModelPeakCache;
+class FFTModel;
+
+enum class BinDisplay {
+    AllBins,
+    PeakBins,
+    PeakFrequencies
+};
+
+enum class BinScale {
+    Linear,
+    Log
+};
+
+class Colour3DPlotRenderer
+{
+public:
+    struct Sources {
+        Sources() : verticalBinLayer(0), source(0), peaks(0), fft(0) { }
+        
+        // These must all outlive this class
+        const VerticalBinLayer *verticalBinLayer;  // always
+	const DenseThreeDimensionalModel *source;  // always
+	const Dense3DModelPeakCache *peaks;        // optionally
+	const FFTModel *fft;                       // optionally
+    };        
+
+    struct Parameters {
+	Parameters() :
+	    colourScale(ColourScale::Parameters()),
+	    normalization(ColumnNormalization::None),
+	    binDisplay(BinDisplay::AllBins),
+            binScale(BinScale::Linear),
+	    alwaysOpaque(false),
+            interpolate(false),
+            invertVertical(false),
+            scaleFactor(1.0),
+            colourRotation(0) { }
+
+        /** A complete ColourScale object by value, used for colour
+         *  map conversion. Note that the final display gain setting is
+         *  also encapsulated here. */
+	ColourScale colourScale;
+
+        /** Type of column normalization. */
+	ColumnNormalization normalization;
+
+        /** Selection of bins to display. */
+	BinDisplay binDisplay;
+
+        /** Scale for vertical bin spacing (linear or logarithmic). */
+	BinScale binScale;
+
+        /** Whether cells should always be opaque. If false, then
+         *  large cells (when zoomed in a long way) will be rendered
+         *  translucent in order not to obscure anything in a layer
+         *  beneath. */
+	bool alwaysOpaque;
+
+        /** Whether to apply smoothing when rendering cells at more
+         *  than one pixel per cell.  !!! todo: decide about separating
+         *  out x-interpolate and y-interpolate as the spectrogram
+         *  actually does (or used to)
+         */
+	bool interpolate;
+
+        /** Whether to render the whole caboodle upside-down. */
+	bool invertVertical;
+
+        /** Initial scale factor (e.g. for FFT scaling). This factor
+         *  is applied to all values read from the underlying model
+         *  *before* magnitude ranges are calculated, in contrast to
+         *  the display gain found in the ColourScale parameter. */
+        double scaleFactor;
+
+        /** Colourmap rotation, in the range 0-255. */
+        int colourRotation;
+    };
+    
+    Colour3DPlotRenderer(Sources sources, Parameters parameters) :
+        m_sources(sources),
+	m_params(parameters)
+    { }
+
+    struct RenderResult {
+        /**
+         * The rect that was actually rendered. May be equal to the
+         * rect that was requested to render, or may be smaller if
+         * time ran out and the complete flag was not set.
+         */
+        QRect rendered;
+
+        /**
+         * The magnitude range of the data in the rendered area.
+         */
+        MagnitudeRange range;
+    };
+
+    /**
+     * Render the requested area using the given painter, obtaining
+     * geometry (e.g. start frame) from the given
+     * LayerGeometryProvider.
+     *
+     * The whole of the supplied rect will be rendered and the
+     * returned QRect will be equal to the supplied QRect. (See
+     * renderTimeConstrained for an alternative that may render only
+     * part of the rect in cases where obtaining source data is slow
+     * and retaining responsiveness is important.)
+     *
+     * Note that Colour3DPlotRenderer retains internal cache state
+     * related to the size and position of the supplied
+     * LayerGeometryProvider. Although it is valid to call render()
+     * successively on the same Colour3DPlotRenderer with different
+     * LayerGeometryProviders, it will be much faster to use a
+     * dedicated Colour3DPlotRenderer for each LayerGeometryProvider.
+     *
+     * If the model to render from is not ready, this will throw a
+     * std::logic_error exception. The model must be ready and the
+     * layer requesting the render must not be dormant in its view, so
+     * that the LayerGeometryProvider returns valid results; it is the
+     * caller's responsibility to ensure these.
+     */
+    RenderResult render(const LayerGeometryProvider *v,
+                        QPainter &paint, QRect rect);
+    
+    /**
+     * Render the requested area using the given painter, obtaining
+     * geometry (e.g. start frame) from the stored
+     * LayerGeometryProvider.
+     *
+     * As much of the rect will be rendered as can be managed given
+     * internal time constraints (using a RenderTimer object
+     * internally). The returned QRect (the rendered field in the
+     * RenderResult struct) will contain the area that was
+     * rendered. Note that we always render the full requested height,
+     * it's only width that is time-constrained.
+     *
+     * Note that Colour3DPlotRenderer retains internal cache state
+     * related to the size and position of the supplied
+     * LayerGeometryProvider. Although it is valid to call render()
+     * successively on the same Colour3DPlotRenderer with different
+     * LayerGeometryProviders, it will be much faster to use a
+     * dedicated Colour3DPlotRenderer for each LayerGeometryProvider.
+     *
+     * If the model to render from is not ready, this will throw a
+     * std::logic_error exception. The model must be ready and the
+     * layer requesting the render must not be dormant in its view, so
+     * that the LayerGeometryProvider returns valid results; it is the
+     * caller's responsibility to ensure these.
+     */
+    RenderResult renderTimeConstrained(const LayerGeometryProvider *v,
+                                       QPainter &paint, QRect rect);
+
+    /**
+     * Return the area of the largest rectangle within the entire area
+     * of the cache that is unavailable in the cache. This is only
+     * valid in relation to a preceding render() call which is
+     * presumed to have set the area, start frame, and zoom level for
+     * the cache. It could be used to establish a suitable region for
+     * a subsequent paint request (because if an area is not in the
+     * cache, it cannot have been rendered since the cache was
+     * cleared).
+     *
+     * Returns an empty QRect if the cache is entirely valid.
+     */
+    QRect getLargestUncachedRect(const LayerGeometryProvider *v);
+
+    /**
+     * Return true if the provider's geometry differs from the cache,
+     * or if we are not using a cache. i.e. if the cache will be
+     * regenerated for the next render, or the next render performed
+     * from scratch.
+     */
+    bool geometryChanged(const LayerGeometryProvider *v);
+    
+    /**
+     * Return true if the rendering will be opaque. This may be used
+     * by the calling layer to determine whether it can scroll
+     * directly without regard to any other layers beneath.
+     */
+    bool willRenderOpaque(const LayerGeometryProvider *v) {
+        return decideRenderType(v) != DirectTranslucent;
+    }
+    
+    /**
+     * Return the colour corresponding to the given value.
+     * \see ColourScale::getPixel
+     * \see ColourScale::getColour
+     */
+    QColor getColour(double value) const {
+        return m_params.colourScale.getColour(value, m_params.colourRotation);
+    }
+
+    /**
+     * Return the enclosing rectangle for the region of similar colour
+     * to the given point within the cache. Return an empty QRect if
+     * this is not possible. \see ImageRegionFinder
+     */
+    QRect findSimilarRegionExtents(QPoint point) const;
+    
+private:
+    Sources m_sources;
+    Parameters m_params;
+
+    // Draw buffer is the target of each partial repaint. It is always
+    // at view height (not model height) and is cleared and repainted
+    // on each fragment render. The only reason it's stored as a data
+    // member is to avoid reallocation.
+    QImage m_drawBuffer;
+
+    // A temporary store of magnitude ranges per-column, used when
+    // rendering to the draw buffer. This always has the same length
+    // as the width of the draw buffer, and the x coordinates of the
+    // two containers are equivalent.
+    std::vector<MagnitudeRange> m_magRanges;
+    
+    // The image cache is our persistent record of the visible
+    // area. It is always the same size as the view (i.e. the paint
+    // size reported by the LayerGeometryProvider) and is scrolled and
+    // partially repainted internally as appropriate. A render request
+    // is carried out by repainting to cache (via the draw buffer) any
+    // area that is being requested but is not valid in the cache, and
+    // then repainting from cache to the requested painter.
+    ScrollableImageCache m_cache;
+
+    // The mag range cache is our record of the column magnitude
+    // ranges for each of the columns in the cache. It always has the
+    // same start frame and width as the image cache, and the column
+    // indices match up across both. Our cache update mechanism
+    // guarantees that every valid column in the image cache has a
+    // valid range in the magnitude cache, but not necessarily vice
+    // versa (as the image cache is limited to contiguous ranges).
+    ScrollableMagRangeCache m_magCache;
+    
+    RenderResult render(const LayerGeometryProvider *v,
+                        QPainter &paint, QRect rect, bool timeConstrained);
+
+    MagnitudeRange renderDirectTranslucent(const LayerGeometryProvider *v,
+                                           QPainter &paint, QRect rect);
+    
+    void renderToCachePixelResolution(const LayerGeometryProvider *v, int x0,
+                                      int repaintWidth, bool rightToLeft,
+                                      bool timeConstrained);
+
+    void renderToCacheBinResolution(const LayerGeometryProvider *v, int x0,
+                                    int repaintWidth);
+
+    int renderDrawBuffer(int w, int h,
+                         const std::vector<int> &binforx,
+                         const std::vector<double> &binfory,
+                         bool usePeaksCache,
+                         bool rightToLeft,
+                         bool timeConstrained);
+
+    int renderDrawBufferPeakFrequencies(const LayerGeometryProvider *v,
+                                        int w, int h,
+                                        const std::vector<int> &binforx,
+                                        const std::vector<double> &binfory,
+                                        bool rightToLeft,
+                                        bool timeConstrained);
+    
+    void recreateDrawBuffer(int w, int h);
+    void clearDrawBuffer(int w, int h);
+
+    enum RenderType {
+        DrawBufferPixelResolution,
+        DrawBufferBinResolution,
+        DirectTranslucent
+    };
+
+    RenderType decideRenderType(const LayerGeometryProvider *) const;
+
+    ColumnOp::Column getColumn(int sx, int minbin, int nbins) const;
+};
+
+#endif
+
--- a/layer/ColourMapper.cpp	Mon Jun 13 11:44:08 2016 +0100
+++ b/layer/ColourMapper.cpp	Fri Aug 05 15:05:02 2016 +0100
@@ -60,7 +60,6 @@
 }
 
 ColourMapper::ColourMapper(int map, double min, double max) :
-    QObject(),
     m_map(map),
     m_min(min),
     m_max(max)
@@ -85,25 +84,25 @@
 QString
 ColourMapper::getColourMapName(int n)
 {
-    if (n >= getColourMapCount()) return tr("<unknown>");
+    if (n >= getColourMapCount()) return QObject::tr("<unknown>");
     StandardMap map = (StandardMap)n;
 
     switch (map) {
-    case Green:            return tr("Green");
-    case WhiteOnBlack:     return tr("White on Black");
-    case BlackOnWhite:     return tr("Black on White");
-    case Cherry:           return tr("Cherry");
-    case Wasp:             return tr("Wasp");
-    case Ice:              return tr("Ice");
-    case Sunset:           return tr("Sunset");
-    case FruitSalad:       return tr("Fruit Salad");
-    case Banded:           return tr("Banded");
-    case Highlight:        return tr("Highlight");
-    case Printer:          return tr("Printer");
-    case HighGain:         return tr("High Gain");
+    case Green:            return QObject::tr("Green");
+    case WhiteOnBlack:     return QObject::tr("White on Black");
+    case BlackOnWhite:     return QObject::tr("Black on White");
+    case Cherry:           return QObject::tr("Cherry");
+    case Wasp:             return QObject::tr("Wasp");
+    case Ice:              return QObject::tr("Ice");
+    case Sunset:           return QObject::tr("Sunset");
+    case FruitSalad:       return QObject::tr("Fruit Salad");
+    case Banded:           return QObject::tr("Banded");
+    case Highlight:        return QObject::tr("Highlight");
+    case Printer:          return QObject::tr("Printer");
+    case HighGain:         return QObject::tr("High Gain");
     }
 
-    return tr("<unknown>");
+    return QObject::tr("<unknown>");
 }
 
 QColor
--- a/layer/ColourMapper.h	Mon Jun 13 11:44:08 2016 +0100
+++ b/layer/ColourMapper.h	Fri Aug 05 15:05:02 2016 +0100
@@ -23,14 +23,14 @@
 /**
  * A class for mapping intensity values onto various colour maps.
  */
-
-class ColourMapper : public QObject
+class ColourMapper
 {
-    Q_OBJECT
-
 public:
     ColourMapper(int map, double minValue, double maxValue);
-    virtual ~ColourMapper();
+    ~ColourMapper();
+
+    ColourMapper(const ColourMapper &) = default;
+    ColourMapper &operator=(const ColourMapper &) = default;
 
     enum StandardMap {
         Green,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/ColourScale.cpp	Fri Aug 05 15:05:02 2016 +0100
@@ -0,0 +1,160 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006-2016 Chris Cannam and QMUL.
+    
+    This program 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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "ColourScale.h"
+
+#include "base/AudioLevel.h"
+#include "base/LogRange.h"
+
+#include <cmath>
+#include <iostream>
+
+using namespace std;
+
+int ColourScale::m_maxPixel = 255;
+
+ColourScale::ColourScale(Parameters parameters) :
+    m_params(parameters),
+    m_mapper(m_params.colourMap, 1.f, double(m_maxPixel))
+{
+    if (m_params.minValue >= m_params.maxValue) {
+        cerr << "ERROR: ColourScale::ColourScale: minValue = "
+             << m_params.minValue << ", maxValue = " << m_params.maxValue << endl;
+	throw std::logic_error("maxValue must be greater than minValue");
+    }
+
+    m_mappedMin = m_params.minValue;
+    m_mappedMax = m_params.maxValue;
+
+    if (m_mappedMin < m_params.threshold) {
+        m_mappedMin = m_params.threshold;
+    }
+    
+    if (m_params.scaleType == ColourScaleType::Log) {
+
+	LogRange::mapRange(m_mappedMin, m_mappedMax);
+	
+    } else if (m_params.scaleType == ColourScaleType::PlusMinusOne) {
+	
+	m_mappedMin = -1.0;
+	m_mappedMax =  1.0;
+
+    } else if (m_params.scaleType == ColourScaleType::Absolute) {
+
+	m_mappedMin = fabs(m_mappedMin);
+	m_mappedMax = fabs(m_mappedMax);
+	if (m_mappedMin >= m_mappedMax) {
+	    std::swap(m_mappedMin, m_mappedMax);
+	}
+    }
+
+    if (m_mappedMin >= m_mappedMax) {
+        cerr << "ERROR: ColourScale::ColourScale: minValue = " << m_params.minValue
+             << ", maxValue = " << m_params.maxValue
+             << ", threshold = " << m_params.threshold
+             << ", scale = " << int(m_params.scaleType)
+             << " resulting in mapped minValue = " << m_mappedMin
+             << ", mapped maxValue = " << m_mappedMax << endl;
+	throw std::logic_error("maxValue must be greater than minValue [after mapping]");
+    }
+}
+
+ColourScale::~ColourScale()
+{
+}
+
+ColourScaleType
+ColourScale::getScale() const
+{
+    return m_params.scaleType;
+}
+
+int
+ColourScale::getPixel(double value) const
+{
+    double maxPixF = m_maxPixel;
+
+    if (m_params.scaleType == ColourScaleType::Phase) {
+	double half = (maxPixF - 1.f) / 2.f;
+        int pixel = 1 + int((value * half) / M_PI + half);
+//        cerr << "phase = " << value << " pixel = " << pixel << endl;
+        return pixel;
+    }
+    
+    value *= m_params.gain;
+    
+    if (value < m_params.threshold) return 0;
+
+    double mapped = value;
+
+    if (m_params.scaleType == ColourScaleType::Log) {
+	mapped = LogRange::map(value);
+    } else if (m_params.scaleType == ColourScaleType::PlusMinusOne) {
+	if (mapped < -1.f) mapped = -1.f;
+	if (mapped > 1.f) mapped = 1.f;
+    } else if (m_params.scaleType == ColourScaleType::Absolute) {
+	if (mapped < 0.f) mapped = -mapped;
+    }
+
+    mapped *= m_params.multiple;
+    
+    if (mapped < m_mappedMin) {
+	mapped = m_mappedMin;
+    }
+    if (mapped > m_mappedMax) {
+	mapped = m_mappedMax;
+    }
+
+    double proportion = (mapped - m_mappedMin) / (m_mappedMax - m_mappedMin);
+
+    int pixel = 0;
+
+    if (m_params.scaleType == ColourScaleType::Meter) {
+	pixel = AudioLevel::multiplier_to_preview(proportion, m_maxPixel-1) + 1;
+    } else {
+	pixel = int(proportion * maxPixF) + 1;
+    }
+
+    if (pixel < 0) {
+	pixel = 0;
+    }
+    if (pixel > m_maxPixel) {
+	pixel = m_maxPixel;
+    }
+    return pixel;
+}
+
+QColor
+ColourScale::getColourForPixel(int pixel, int rotation) const
+{
+    if (pixel < 0) {
+	pixel = 0;
+    }
+    if (pixel > m_maxPixel) {
+	pixel = m_maxPixel;
+    }
+    if (pixel == 0) {
+	if (m_mapper.hasLightBackground()) {
+	    return Qt::white;
+	} else {
+	    return Qt::black;
+	}
+    } else {
+	int target = int(pixel) + rotation;
+	while (target < 1) target += m_maxPixel;
+	while (target > m_maxPixel) target -= m_maxPixel;
+	return m_mapper.map(double(target));
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/ColourScale.h	Fri Aug 05 15:05:02 2016 +0100
@@ -0,0 +1,120 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006-2016 Chris Cannam and QMUL.
+    
+    This program 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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef COLOUR_SCALE_H
+#define COLOUR_SCALE_H
+
+#include "ColourMapper.h"
+
+enum class ColourScaleType {
+    Linear,
+    Meter,
+    Log,
+    Phase,
+    PlusMinusOne,
+    Absolute
+};
+
+/**
+ * Map values within a range onto a set of colours, with a given
+ * distribution (linear, log etc) and optional colourmap rotation.
+ */
+class ColourScale
+{
+public:
+    struct Parameters {
+	Parameters() : colourMap(0), scaleType(ColourScaleType::Linear),
+		       minValue(0.0), maxValue(1.0),
+		       threshold(0.0), gain(1.0), multiple(1.0) { }
+
+	/** A colour map index as used by ColourMapper */
+	int colourMap;
+	
+	/** Distribution for the scale */
+	ColourScaleType scaleType;
+	
+	/** Minimum value in source range */
+	double minValue;
+	
+	/** Maximum value in source range. Must be > minValue */
+	double maxValue;
+
+	/** Threshold below which every value is mapped to background
+	    pixel 0 */
+	double threshold;
+
+	/** Gain to apply before thresholding, mapping, and clamping */
+	double gain;
+
+        /** Multiple to apply after thresholding and mapping. In most
+         *  cases the gain parameter is the one you want instead of
+         *  this, but this can be used for example with Log scale to
+         *  produce the log of some power of the original value,
+         *  e.g. multiple = 2 gives log(x^2). */
+        double multiple;
+    };
+    
+    /**
+     * Create a ColourScale with the given parameters.
+     *
+     * Note that some parameters may be ignored for some scale
+     * distribution settings. For example, min and max are ignored for
+     * PlusMinusOneScale and PhaseColourScale and threshold and gain
+     * are ignored for PhaseColourScale.
+     */
+    ColourScale(Parameters parameters);
+    ~ColourScale();
+
+    ColourScale(const ColourScale &) = default;
+    ColourScale &operator=(const ColourScale &) = default;
+
+    /**
+     * Return the general type of scale this is.
+     */
+    ColourScaleType getScale() const;
+    
+    /**
+     * Return a pixel number (in the range 0-255 inclusive)
+     * corresponding to the given value.  The pixel 0 is used only for
+     * values below the threshold supplied in the constructor. All
+     * other values are mapped onto the range 1-255.
+     */
+    int getPixel(double value) const;
+
+    /**
+     * Return the colour for the given pixel number (which must be in
+     * the range 0-255). The pixel 0 is always the background
+     * colour. Other pixels are mapped taking into account the given
+     * colourmap rotation (which is also a value in the range 0-255).
+     */
+    QColor getColourForPixel(int pixel, int rotation) const;
+    
+    /**
+     * Return the colour corresponding to the given value. This is
+     * equivalent to getColourForPixel(getPixel(value), rotation).
+     */
+    QColor getColour(double value, int rotation) const {
+	return getColourForPixel(getPixel(value), rotation);
+    }
+
+private:
+    Parameters m_params;
+    ColourMapper m_mapper;
+    double m_mappedMin;
+    double m_mappedMax;
+    static int m_maxPixel;
+};
+
+#endif
--- a/layer/ColourScaleLayer.h	Mon Jun 13 11:44:08 2016 +0100
+++ b/layer/ColourScaleLayer.h	Fri Aug 05 15:05:02 2016 +0100
@@ -21,6 +21,13 @@
 
 class LayerGeometryProvider;
 
+/**
+ * Interface for layers in which a colour scale represents (or can
+ * sometimes represent, depending on the display mode) the sample
+ * value. For example, TimeValueLayer uses colour scale when in
+ * segment mode and so provides this interface for use by the
+ * LogColourScale or LinearColourScale scale renderers.
+ */
 class ColourScaleLayer
 {
 public:
--- a/layer/FlexiNoteLayer.cpp	Mon Jun 13 11:44:08 2016 +0100
+++ b/layer/FlexiNoteLayer.cpp	Fri Aug 05 15:05:02 2016 +0100
@@ -22,15 +22,18 @@
 #include "base/Pitch.h"
 #include "base/LogRange.h"
 #include "base/RangeMapper.h"
+
 #include "ColourDatabase.h"
-#include "view/View.h"
-
+#include "LayerGeometryProvider.h"
 #include "PianoScale.h"
 #include "LinearNumericalScale.h"
 #include "LogNumericalScale.h"
+#include "PaintAssistant.h"
 
 #include "data/model/FlexiNoteModel.h"
 
+#include "view/View.h"
+
 #include "widgets/ItemEditDialog.h"
 #include "widgets/TextAbbrev.h"
 
@@ -867,34 +870,34 @@
                 // paint.setBrush(v->getForeground());
         
                 QString vlabel = QString("freq: %1%2").arg(p.value).arg(m_model->getScaleUnits());
-                // v->drawVisibleText(paint, 
+                // PaintAssistant::drawVisibleText(v, paint, 
                 //                    x - paint.fontMetrics().width(vlabel) - 2,
                 //                    y + paint.fontMetrics().height()/2
                 //                      - paint.fontMetrics().descent(), 
-                //                    vlabel, View::OutlinedText);
-                v->drawVisibleText(paint, 
+                //                    vlabel, PaintAssistant::OutlinedText);
+                PaintAssistant::drawVisibleText(v, paint, 
                                    x,
                                    y - h/2 - 2 - paint.fontMetrics().height()
                                      - paint.fontMetrics().descent(), 
-                                   vlabel, View::OutlinedText);
+                                   vlabel, PaintAssistant::OutlinedText);
 
                 QString hlabel = "dur: " + QString(RealTime::frame2RealTime
                     (p.duration, m_model->getSampleRate()).toText(true).c_str());
-                v->drawVisibleText(paint, 
+                PaintAssistant::drawVisibleText(v, paint, 
                                    x,
                                    y - h/2 - paint.fontMetrics().descent() - 2,
-                                   hlabel, View::OutlinedText);
+                                   hlabel, PaintAssistant::OutlinedText);
 
                 QString llabel = QString("%1").arg(p.label);
-                v->drawVisibleText(paint, 
+                PaintAssistant::drawVisibleText(v, paint, 
                                    x,
                                    y + h + 2 + paint.fontMetrics().descent(),
-                                   llabel, View::OutlinedText);
+                                   llabel, PaintAssistant::OutlinedText);
                 QString nlabel = QString("%1").arg(noteNumber);
-                v->drawVisibleText(paint, 
+                PaintAssistant::drawVisibleText(v, paint, 
                                    x + paint.fontMetrics().averageCharWidth() / 2,
                                    y + h/2 - paint.fontMetrics().descent(),
-                                   nlabel, View::OutlinedText);
+                                   nlabel, PaintAssistant::OutlinedText);
         }
     
         paint.drawRect(x, y - h/2, w, h);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/LayerGeometryProvider.h	Fri Aug 05 15:05:02 2016 +0100
@@ -0,0 +1,184 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    This program 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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef LAYER_GEOMETRY_PROVIDER_H
+#define LAYER_GEOMETRY_PROVIDER_H
+
+#include "base/BaseTypes.h"
+
+#include <QMutex>
+#include <QMutexLocker>
+#include <QPainter>
+
+class ViewManager;
+class View;
+class Layer;
+
+/**
+ * Interface for classes that provide geometry information (such as
+ * size, start frame, and a large number of other properties) about
+ * the disposition of a layer. The main implementor of this interface
+ * is the View class, but other implementations may be used in
+ * different circumstances, e.g. as a proxy to handle hi-dpi
+ * coordinate mapping.
+ *
+ * Note it is expected that some implementations of this may be
+ * disposable, created on-the-fly for a single use. Code that receives
+ * a LayerGeometryProvider pointer as an argument to something should
+ * not, in general, store that pointer as it may be invalidated before
+ * the next use. Use getId() to instead obtain a persistent identifier
+ * for a LayerGeometryProvider, for example to establish whether the
+ * same one is being provided in two separate calls.
+ */
+class LayerGeometryProvider
+{
+protected:
+    static int getNextId() {
+        static QMutex idMutex;
+        static int nextId = 1;
+        static int maxId = INT_MAX;
+        QMutexLocker locker(&idMutex);
+        int id = nextId;
+        if (nextId == maxId) {
+            // we don't expect this to happen in the lifetime of a
+            // process, but it would be undefined behaviour if it did
+            // since we're using a signed int, so we should really
+            // guard for it...
+            nextId = 1;
+        } else {
+            nextId++;
+        }
+        return id;
+    }            
+    
+public:
+    LayerGeometryProvider() { }
+    
+    /**
+     * Retrieve the id of this object.
+     */
+    virtual int getId() const = 0;
+
+    /**
+     * Retrieve the first visible sample frame on the widget.
+     * This is a calculated value based on the centre-frame, widget
+     * width and zoom level.  The result may be negative.
+     */
+    virtual sv_frame_t getStartFrame() const = 0;
+
+    /**
+     * Return the centre frame of the visible widget.  This is an
+     * exact value that does not depend on the zoom block size.  Other
+     * frame values (start, end) are calculated from this based on the
+     * zoom and other factors.
+     */
+    virtual sv_frame_t getCentreFrame() const = 0;
+
+    /**
+     * Retrieve the last visible sample frame on the widget.
+     * This is a calculated value based on the centre-frame, widget
+     * width and zoom level.
+     */
+    virtual sv_frame_t getEndFrame() const = 0;
+
+    /**
+     * Return the pixel x-coordinate corresponding to a given sample
+     * frame (which may be negative).
+     */
+    virtual int getXForFrame(sv_frame_t frame) const = 0;
+
+    /**
+     * Return the closest frame to the given pixel x-coordinate.
+     */
+    virtual sv_frame_t getFrameForX(int x) const = 0;
+
+    virtual sv_frame_t getModelsStartFrame() const = 0;
+    virtual sv_frame_t getModelsEndFrame() const = 0;
+
+    /**
+     * Return the closest pixel x-coordinate corresponding to a given
+     * view x-coordinate.
+     */
+    virtual int getXForViewX(int viewx) const = 0;
+    
+    /**
+     * Return the closest view x-coordinate corresponding to a given
+     * pixel x-coordinate.
+     */
+    virtual int getViewXForX(int x) const = 0;
+    
+    /**
+     * Return the (maybe fractional) pixel y-coordinate corresponding
+     * to a given frequency, if the frequency range is as specified.
+     * This does not imply any policy about layer frequency ranges,
+     * but it might be useful for layers to match theirs up if
+     * desired.
+     *
+     * Not thread-safe in logarithmic mode.  Call only from GUI thread.
+     */
+    virtual double getYForFrequency(double frequency,
+                                    double minFreq, double maxFreq, 
+                                    bool logarithmic) const = 0;
+
+    /**
+     * Return the closest frequency to the given (maybe fractional)
+     * pixel y-coordinate, if the frequency range is as specified.
+     *
+     * Not thread-safe in logarithmic mode.  Call only from GUI thread.
+     */
+    virtual double getFrequencyForY(double y,
+                                    double minFreq, double maxFreq,
+                                    bool logarithmic) const = 0;
+
+    virtual int getTextLabelHeight(const Layer *layer, QPainter &) const = 0;
+
+    virtual bool getValueExtents(QString unit, double &min, double &max,
+                                 bool &log) const = 0;
+
+    /**
+     * Return the zoom level, i.e. the number of frames per pixel
+     */
+    virtual int getZoomLevel() const = 0;
+
+    /**
+     * To be called from a layer, to obtain the extent of the surface
+     * that the layer is currently painting to. This may be the extent
+     * of the view (if 1x display scaling is in effect) or of a larger
+     * cached pixmap (if greater display scaling is in effect).
+     */
+    virtual QRect getPaintRect() const = 0;
+
+    virtual QSize getPaintSize() const { return getPaintRect().size(); }
+    virtual int getPaintWidth() const { return getPaintRect().width(); }
+    virtual int getPaintHeight() const { return getPaintRect().height(); }
+
+    virtual bool hasLightBackground() const = 0;
+    virtual QColor getForeground() const = 0;
+    virtual QColor getBackground() const = 0;
+
+    virtual ViewManager *getViewManager() const = 0;
+
+    virtual bool shouldIlluminateLocalFeatures(const Layer *, QPoint &) const = 0;
+    virtual bool shouldShowFeatureLabels() const = 0;
+
+    virtual void drawMeasurementRect(QPainter &p, const Layer *,
+                                     QRect rect, bool focus) const = 0;
+
+    virtual void updatePaintRect(QRect r) = 0;
+    
+    virtual View *getView() = 0;
+    virtual const View *getView() const = 0;
+};
+
+#endif
--- a/layer/LinearColourScale.cpp	Mon Jun 13 11:44:08 2016 +0100
+++ b/layer/LinearColourScale.cpp	Fri Aug 05 15:05:02 2016 +0100
@@ -20,7 +20,7 @@
 
 #include <cmath>
 
-#include "view/LayerGeometryProvider.h"
+#include "LayerGeometryProvider.h"
 
 int
 LinearColourScale::getWidth(LayerGeometryProvider *,
--- a/layer/LinearNumericalScale.cpp	Mon Jun 13 11:44:08 2016 +0100
+++ b/layer/LinearNumericalScale.cpp	Fri Aug 05 15:05:02 2016 +0100
@@ -20,7 +20,7 @@
 
 #include <cmath>
 
-#include "view/View.h"
+#include "LayerGeometryProvider.h"
 
 int
 LinearNumericalScale::getWidth(LayerGeometryProvider *,
--- a/layer/LogColourScale.cpp	Mon Jun 13 11:44:08 2016 +0100
+++ b/layer/LogColourScale.cpp	Fri Aug 05 15:05:02 2016 +0100
@@ -22,7 +22,7 @@
 
 #include <cmath>
 
-#include "view/View.h"
+#include "LayerGeometryProvider.h"
 
 int
 LogColourScale::getWidth(LayerGeometryProvider *,
--- a/layer/LogNumericalScale.cpp	Mon Jun 13 11:44:08 2016 +0100
+++ b/layer/LogNumericalScale.cpp	Fri Aug 05 15:05:02 2016 +0100
@@ -22,7 +22,7 @@
 
 #include <cmath>
 
-#include "view/View.h"
+#include "LayerGeometryProvider.h"
 
 //#define DEBUG_TIME_VALUE_LAYER 1
 
--- a/layer/NoteLayer.cpp	Mon Jun 13 11:44:08 2016 +0100
+++ b/layer/NoteLayer.cpp	Fri Aug 05 15:05:02 2016 +0100
@@ -21,12 +21,13 @@
 #include "base/Pitch.h"
 #include "base/LogRange.h"
 #include "base/RangeMapper.h"
-#include "ColourDatabase.h"
 #include "view/View.h"
 
+#include "ColourDatabase.h"
 #include "PianoScale.h"
 #include "LinearNumericalScale.h"
 #include "LogNumericalScale.h"
+#include "PaintAssistant.h"
 
 #include "data/model/NoteModel.h"
 
@@ -809,18 +810,18 @@
             paint.setBrush(v->getForeground());
 
             QString vlabel = QString("%1%2").arg(p.value).arg(getScaleUnits());
-            v->drawVisibleText(paint, 
+            PaintAssistant::drawVisibleText(v, paint, 
                                x - paint.fontMetrics().width(vlabel) - 2,
                                y + paint.fontMetrics().height()/2
                                  - paint.fontMetrics().descent(), 
-                               vlabel, View::OutlinedText);
+                               vlabel, PaintAssistant::OutlinedText);
 
             QString hlabel = RealTime::frame2RealTime
                 (p.frame, m_model->getSampleRate()).toText(true).c_str();
-            v->drawVisibleText(paint, 
+            PaintAssistant::drawVisibleText(v, paint, 
                                x,
                                y - h/2 - paint.fontMetrics().descent() - 2,
-                               hlabel, View::OutlinedText);
+                               hlabel, PaintAssistant::OutlinedText);
 	}
 	
 	paint.drawRect(x, y - h/2, w, h);
--- a/layer/PaintAssistant.cpp	Mon Jun 13 11:44:08 2016 +0100
+++ b/layer/PaintAssistant.cpp	Fri Aug 05 15:05:02 2016 +0100
@@ -15,6 +15,8 @@
 
 #include "PaintAssistant.h"
 
+#include "LayerGeometryProvider.h"
+
 #include "base/AudioLevel.h"
 
 #include <QPaintDevice>
@@ -207,3 +209,55 @@
 
     return vy;
 }
+
+void
+PaintAssistant::drawVisibleText(const LayerGeometryProvider *v,
+                                QPainter &paint, int x, int y,
+                                QString text, TextStyle style)
+{
+    if (style == OutlinedText || style == OutlinedItalicText) {
+
+        paint.save();
+
+        if (style == OutlinedItalicText) {
+            QFont f(paint.font());
+            f.setItalic(true);
+            paint.setFont(f);
+        }
+
+        QColor penColour, surroundColour, boxColour;
+
+        penColour = v->getForeground();
+        surroundColour = v->getBackground();
+        boxColour = surroundColour;
+        boxColour.setAlpha(127);
+
+        paint.setPen(Qt::NoPen);
+        paint.setBrush(boxColour);
+        
+        QRect r = paint.fontMetrics().boundingRect(text);
+        r.translate(QPoint(x, y));
+//        cerr << "drawVisibleText: r = " << r.x() << "," <<r.y() << " " << r.width() << "x" << r.height() << endl;
+        paint.drawRect(r);
+        paint.setBrush(Qt::NoBrush);
+
+	paint.setPen(surroundColour);
+
+	for (int dx = -1; dx <= 1; ++dx) {
+	    for (int dy = -1; dy <= 1; ++dy) {
+		if (!(dx || dy)) continue;
+		paint.drawText(x + dx, y + dy, text);
+	    }
+	}
+
+	paint.setPen(penColour);
+
+	paint.drawText(x, y, text);
+
+        paint.restore();
+
+    } else {
+
+        std::cerr << "ERROR: PaintAssistant::drawVisibleText: Boxed style not yet implemented!" << std::endl;
+    }
+}
--- a/layer/PaintAssistant.h	Mon Jun 13 11:44:08 2016 +0100
+++ b/layer/PaintAssistant.h	Fri Aug 05 15:05:02 2016 +0100
@@ -13,13 +13,15 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _PAINT_ASSISTANT_H_
-#define _PAINT_ASSISTANT_H_
+#ifndef PAINT_ASSISTANT_H
+#define PAINT_ASSISTANT_H
 
 #include <QRect>
 #include <vector>
 
 class QPainter;
+class Layer;
+class LayerGeometryProvider;
 
 class PaintAssistant
 {
@@ -34,6 +36,16 @@
     static int getYForValue(Scale scale, double value,
                             double minVal, double maxVal,
                             int minY, int height);
+
+    enum TextStyle {
+	BoxedText,
+	OutlinedText,
+        OutlinedItalicText
+    };
+
+    static void drawVisibleText(const LayerGeometryProvider *,
+                                QPainter &p, int x, int y,
+                                QString text, TextStyle style);
 };
 
 #endif
--- a/layer/PianoScale.cpp	Mon Jun 13 11:44:08 2016 +0100
+++ b/layer/PianoScale.cpp	Fri Aug 05 15:05:02 2016 +0100
@@ -21,7 +21,7 @@
 
 #include "base/Pitch.h"
 
-#include "view/LayerGeometryProvider.h"
+#include "LayerGeometryProvider.h"
 
 void
 PianoScale::paintPianoVertical(LayerGeometryProvider *v,
--- a/layer/RegionLayer.cpp	Mon Jun 13 11:44:08 2016 +0100
+++ b/layer/RegionLayer.cpp	Fri Aug 05 15:05:02 2016 +0100
@@ -19,13 +19,14 @@
 #include "base/RealTime.h"
 #include "base/Profiler.h"
 #include "base/LogRange.h"
+
 #include "ColourDatabase.h"
-
 #include "ColourMapper.h"
 #include "LinearNumericalScale.h"
 #include "LogNumericalScale.h"
 #include "LinearColourScale.h"
 #include "LogColourScale.h"
+#include "PaintAssistant.h"
 
 #include "view/View.h"
 
@@ -979,18 +980,18 @@
                 paint.setBrush(v->getForeground());
 
                 QString vlabel = QString("%1%2").arg(p.value).arg(getScaleUnits());
-                v->drawVisibleText(paint, 
+                PaintAssistant::drawVisibleText(v, paint, 
                                    x - paint.fontMetrics().width(vlabel) - 2,
                                    y + paint.fontMetrics().height()/2
                                    - paint.fontMetrics().descent(), 
-                                   vlabel, View::OutlinedText);
+                                   vlabel, PaintAssistant::OutlinedText);
                 
                 QString hlabel = RealTime::frame2RealTime
                     (p.frame, m_model->getSampleRate()).toText(true).c_str();
-                v->drawVisibleText(paint, 
+                PaintAssistant::drawVisibleText(v, paint, 
                                    x,
                                    y - h/2 - paint.fontMetrics().descent() - 2,
-                                   hlabel, View::OutlinedText);
+                                   hlabel, PaintAssistant::OutlinedText);
             }
             
             paint.drawLine(x, y-1, x + w, y-1);
@@ -1048,7 +1049,7 @@
                 nextLabelMinX = labelX + paint.fontMetrics().width(label);
             }
 
-            v->drawVisibleText(paint, labelX, labelY, label, View::OutlinedText);
+            PaintAssistant::drawVisibleText(v, paint, labelX, labelY, label, PaintAssistant::OutlinedText);
         }
     }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/RenderTimer.h	Fri Aug 05 15:05:02 2016 +0100
@@ -0,0 +1,102 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    This program 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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef RENDER_TIMER_H
+#define RENDER_TIMER_H
+
+#include <chrono>
+
+class RenderTimer
+{
+public:
+    enum Type {
+	/// A normal rendering operation with normal responsiveness demands
+	FastRender,
+
+	/// An operation that the user might accept being slower
+	SlowRender,
+
+	/// An operation that should always complete, i.e. as if there
+	/// were no RenderTimer in use, but without having to change
+	/// client code structurally
+	NoTimeout
+    };
+    
+    /**
+     * Create a new RenderTimer and start timing. Make one of these
+     * before rendering, and then call outOfTime() regularly during
+     * rendering. If outOfTime() returns true, abandon rendering!  and
+     * schedule the rest for after some user responsiveness has
+     * happened.
+     */
+    RenderTimer(Type t) :
+	m_start(std::chrono::steady_clock::now()),
+	m_haveLimits(true),
+	m_minFraction(0.1),
+	m_softLimit(0.1),
+	m_hardLimit(0.2),
+	m_softLimitOverridden(false) {
+
+	if (t == NoTimeout) {
+	    m_haveLimits = false;
+	} else if (t == SlowRender) {
+	    m_softLimit = 0.2;
+	    m_hardLimit = 0.4;
+	}
+    }
+
+
+    /**
+     * Return true if we have run out of time and should suspend
+     * rendering and handle user events instead. Call this regularly
+     * during rendering work: fractionComplete should be an estimate
+     * of how much of the work has been done as of this call, as a
+     * number between 0.0 (none of it) and 1.0 (all of it).
+     */
+    bool outOfTime(double fractionComplete) {
+
+	if (!m_haveLimits || fractionComplete < m_minFraction) {
+	    return false;
+	}
+	
+	auto t = std::chrono::steady_clock::now();
+	double elapsed = std::chrono::duration<double>(t - m_start).count();
+	
+	if (elapsed > m_hardLimit) {
+	    return true;
+	} else if (!m_softLimitOverridden && elapsed > m_softLimit) {
+	    if (fractionComplete > 0.6) {
+		// If we're significantly more than half way by the
+		// time we reach the soft limit, ignore it (though
+		// always respect the hard limit, above). Otherwise
+		// respect the soft limit and report out of time now.
+		m_softLimitOverridden = true;
+	    } else {
+		return true;
+	    }
+	}
+
+	return false;
+    }
+
+private:
+    std::chrono::time_point<std::chrono::steady_clock> m_start;
+    bool m_haveLimits;
+    double m_minFraction;
+    double m_softLimit;
+    double m_hardLimit;
+    bool m_softLimitOverridden;
+};
+
+#endif
--- a/layer/ScrollableImageCache.cpp	Mon Jun 13 11:44:08 2016 +0100
+++ b/layer/ScrollableImageCache.cpp	Fri Aug 05 15:05:02 2016 +0100
@@ -20,18 +20,22 @@
 //#define DEBUG_SCROLLABLE_IMAGE_CACHE 1
 
 void
-ScrollableImageCache::scrollTo(sv_frame_t newStartFrame)
+ScrollableImageCache::scrollTo(const LayerGeometryProvider *v,
+                               sv_frame_t newStartFrame)
 {
-    if (!m_v) throw std::logic_error("ScrollableImageCache: not associated with a LayerGeometryProvider");
-	
-    int dx = (m_v->getXForFrame(m_startFrame) -
-	      m_v->getXForFrame(newStartFrame));
-
+    int dx = (v->getXForFrame(m_startFrame) -
+	      v->getXForFrame(newStartFrame));
+    
 #ifdef DEBUG_SCROLLABLE_IMAGE_CACHE
     cerr << "ScrollableImageCache::scrollTo: start frame " << m_startFrame
 	 << " -> " << newStartFrame << ", dx = " << dx << endl;
 #endif
-    
+
+    if (m_startFrame == newStartFrame) {
+	// haven't moved
+        return;
+    }
+	
     m_startFrame = newStartFrame;
 	
     if (!isValid()) {
@@ -41,7 +45,7 @@
     int w = m_image.width();
 
     if (dx == 0) {
-	// haven't moved
+	// haven't moved visibly (even though start frame may have changed)
 	return;
     }
 
@@ -68,8 +72,8 @@
 	
     // update valid area
         
-    int px = m_left;
-    int pw = m_width;
+    int px = m_validLeft;
+    int pw = m_validWidth;
 	
     px += dx;
 	
@@ -92,8 +96,8 @@
 	}
     }
 
-    m_left = px;
-    m_width = pw;
+    m_validLeft = px;
+    m_validWidth = pw;
 }
 
 void
@@ -103,21 +107,21 @@
 #ifdef DEBUG_SCROLLABLE_IMAGE_CACHE
     cerr << "ScrollableImageCache::adjustToTouchValidArea: left " << left
          << ", width " << width << endl;
-    cerr << "ScrollableImageCache: my left " << m_left
-         << ", width " << m_width << " so right " << (m_left + m_width) << endl;
+    cerr << "ScrollableImageCache: my left " << m_validLeft
+         << ", width " << m_validWidth << " so right " << (m_validLeft + m_validWidth) << endl;
 #endif
-    if (left < m_left) {
+    if (left < m_validLeft) {
 	isLeftOfValidArea = true;
-	if (left + width <= m_left + m_width) {
-	    width = m_left - left;
+	if (left + width <= m_validLeft + m_validWidth) {
+	    width = m_validLeft - left;
 	}
 #ifdef DEBUG_SCROLLABLE_IMAGE_CACHE
         cerr << "ScrollableImageCache: we're left of valid area, adjusted width to " << width << endl;
 #endif
     } else {
 	isLeftOfValidArea = false;
-	width = left + width - (m_left + m_width);
-	left = m_left + m_width;
+	width = left + width - (m_validLeft + m_validWidth);
+	left = m_validLeft + m_validWidth;
 	if (width < 0) width = 0;
 #ifdef DEBUG_SCROLLABLE_IMAGE_CACHE
         cerr << "ScrollableImageCache: we're right of valid area, adjusted left to " << left << ", width to " << width << endl;
@@ -160,43 +164,43 @@
     painter.end();
 
     if (!isValid()) {
-	m_left = left;
-	m_width = width;
+	m_validLeft = left;
+	m_validWidth = width;
 	return;
     }
 	
-    if (left < m_left) {
-	if (left + width > m_left + m_width) {
+    if (left < m_validLeft) {
+	if (left + width > m_validLeft + m_validWidth) {
 	    // new image completely contains the old valid area --
 	    // use the new area as is
-	    m_left = left;
-	    m_width = width;
-	} else if (left + width < m_left) {
+	    m_validLeft = left;
+	    m_validWidth = width;
+	} else if (left + width < m_validLeft) {
 	    // new image completely off left of old valid area --
 	    // we can't extend the valid area because the bit in
 	    // between is not valid, so must use the new area only
-	    m_left = left;
-	    m_width = width;
+	    m_validLeft = left;
+	    m_validWidth = width;
 	} else {
 	    // new image overlaps old valid area on left side --
 	    // use new left edge, and extend width to existing
 	    // right edge
-	    m_width = (m_left + m_width) - left;
-	    m_left = left;
+	    m_validWidth = (m_validLeft + m_validWidth) - left;
+	    m_validLeft = left;
 	}
     } else {
-	if (left > m_left + m_width) {
+	if (left > m_validLeft + m_validWidth) {
 	    // new image completely off right of old valid area --
 	    // we can't extend the valid area because the bit in
 	    // between is not valid, so must use the new area only
-	    m_left = left;
-	    m_width = width;
-	} else if (left + width > m_left + m_width) {
+	    m_validLeft = left;
+	    m_validWidth = width;
+	} else if (left + width > m_validLeft + m_validWidth) {
 	    // new image overlaps old valid area on right side --
 	    // use existing left edge, and extend width to new
 	    // right edge
-	    m_width = (left + width) - m_left;
-	    // (m_left unchanged)
+	    m_validWidth = (left + width) - m_validLeft;
+	    // (m_validLeft unchanged)
 	} else {
 	    // new image completely contained within old valid
 	    // area -- leave the old area unchanged
--- a/layer/ScrollableImageCache.h	Mon Jun 13 11:44:08 2016 +0100
+++ b/layer/ScrollableImageCache.h	Fri Aug 05 15:05:02 2016 +0100
@@ -17,14 +17,14 @@
 
 #include "base/BaseTypes.h"
 
-#include "view/LayerGeometryProvider.h"
+#include "LayerGeometryProvider.h"
 
 #include <QImage>
 #include <QRect>
 #include <QPainter>
 
 /**
- * A cached image for a view that scrolls horizontally, primarily the
+ * A cached image for a view that scrolls horizontally, such as a
  * spectrogram. The cache object holds an image, reports the size of
  * the image (likely the same as the underlying view, but it's the
  * caller's responsibility to set the size appropriately), can scroll
@@ -37,54 +37,67 @@
 class ScrollableImageCache
 {
 public:
-    ScrollableImageCache(const LayerGeometryProvider *v = 0) :
-	m_v(v),
-	m_left(0),
-	m_width(0),
+    ScrollableImageCache() :
+	m_validLeft(0),
+	m_validWidth(0),
 	m_startFrame(0),
 	m_zoomLevel(0)
     {}
 
     void invalidate() {
-	m_width = 0;
+	m_validWidth = 0;
     }
     
     bool isValid() const {
-	return m_width > 0;
+	return m_validWidth > 0;
     }
 
     QSize getSize() const {
 	return m_image.size();
     }
-    
+
+    /**
+     * Set the size of the cache. If the new size differs from the
+     * current size, the cache is invalidated.
+     */
     void resize(QSize newSize) {
-	m_image = QImage(newSize, QImage::Format_ARGB32_Premultiplied);
-	invalidate();
+        if (getSize() != newSize) {
+            m_image = QImage(newSize, QImage::Format_ARGB32_Premultiplied);
+            invalidate();
+        }
     }
 	
     int getValidLeft() const {
-	return m_left;
+	return m_validLeft;
     }
     
     int getValidWidth() const {
-	return m_width;
+	return m_validWidth;
     }
 
     int getValidRight() const {
-	return m_left + m_width;
+	return m_validLeft + m_validWidth;
     }
 
     QRect getValidArea() const {
-	return QRect(m_left, 0, m_width, m_image.height());
+	return QRect(m_validLeft, 0, m_validWidth, m_image.height());
     }
     
     int getZoomLevel() const {
 	return m_zoomLevel;
     }
-    
+
+    /**
+     * Set the zoom level. If the new zoom level differs from the
+     * current one, the cache is invalidated. (Determining whether to
+     * invalidate the cache here is the only thing the zoom level is
+     * used for.)
+     */
     void setZoomLevel(int zoom) {
-	m_zoomLevel = zoom;
-	invalidate();
+        if (m_zoomLevel != zoom) {
+            m_zoomLevel = zoom;
+            invalidate();
+        }
     }
 
     sv_frame_t getStartFrame() const {
@@ -92,13 +105,16 @@
     }
 
     /**
-     * Set the start frame and invalidate the cache. To scroll,
-     * i.e. to set the start frame while retaining cache validity
-     * where possible, use scrollTo() instead.
+     * Set the start frame. If the new start frame differs from the
+     * current one, the cache is invalidated. To scroll, i.e. to set
+     * the start frame while retaining cache validity where possible,
+     * use scrollTo() instead.
      */
     void setStartFrame(sv_frame_t frame) {
-	m_startFrame = frame;
-	invalidate();
+        if (m_startFrame != frame) {
+            m_startFrame = frame;
+            invalidate();
+        }
     }
     
     const QImage &getImage() const {
@@ -106,20 +122,23 @@
     }
 
     /**
-     * Set the new start frame for the cache, if possible also moving
-     * along any existing valid data within the cache so that it
-     * continues to be valid for the new start frame.
+     * Set the new start frame for the cache, according to the
+     * geometry of the supplied LayerGeometryProvider, if possible
+     * also moving along any existing valid data within the cache so
+     * that it continues to be valid for the new start frame.
      */
-    void scrollTo(sv_frame_t newStartFrame);
+    void scrollTo(const LayerGeometryProvider *v, sv_frame_t newStartFrame);
 
     /**
      * Take a left coordinate and width describing a region, and
      * adjust them so that they are contiguous with the cache valid
      * region and so that the union of the adjusted region with the
-     * cache valid region contains the supplied region.
+     * cache valid region contains the supplied region.  Does not
+     * modify anything about the cache, only about the arguments.
      */
     void adjustToTouchValidArea(int &left, int &width,
 				bool &isLeftOfValidArea) const;
+    
     /**
      * Draw from an image onto the cache. The supplied image must have
      * the same height as the cache and the full height is always
@@ -134,10 +153,9 @@
 		   int imageWidth);
     
 private:
-    const LayerGeometryProvider *m_v;
     QImage m_image;
-    int m_left;  // of valid region
-    int m_width; // of valid region
+    int m_validLeft;
+    int m_validWidth;
     sv_frame_t m_startFrame;
     int m_zoomLevel;
 };
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/ScrollableMagRangeCache.cpp	Fri Aug 05 15:05:02 2016 +0100
@@ -0,0 +1,113 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    This program 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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "ScrollableMagRangeCache.h"
+
+#include <iostream>
+using namespace std;
+
+//#define DEBUG_SCROLLABLE_MAG_RANGE_CACHE 1
+
+void
+ScrollableMagRangeCache::scrollTo(const LayerGeometryProvider *v,
+				  sv_frame_t newStartFrame)
+{	
+    int dx = (v->getXForFrame(m_startFrame) -
+	      v->getXForFrame(newStartFrame));
+
+#ifdef DEBUG_SCROLLABLE_MAG_RANGE_CACHE
+    cerr << "ScrollableMagRangeCache::scrollTo: start frame " << m_startFrame
+	 << " -> " << newStartFrame << ", dx = " << dx << endl;
+#endif
+
+    if (m_startFrame == newStartFrame) {
+	// haven't moved
+        return;
+    }
+    
+    m_startFrame = newStartFrame;
+
+    if (dx == 0) {
+	// haven't moved visibly (even though start frame may have changed)
+	return;
+    }
+	
+    int w = int(m_ranges.size());
+
+    if (dx <= -w || dx >= w) {
+	// scrolled entirely off
+	invalidate();
+	return;
+    }
+	
+    // dx is in range, cache is scrollable
+
+    if (dx < 0) {
+	// The new start frame is to the left of the old start
+	// frame. We need to add some empty ranges at the left (start)
+	// end and clip the right end. Assemble -dx new values, then
+	// w+dx old values starting at index 0.
+
+	auto newRanges = vector<MagnitudeRange>(-dx);
+	newRanges.insert(newRanges.end(),
+			 m_ranges.begin(), m_ranges.begin() + (w + dx));
+	m_ranges = newRanges;
+	
+    } else {
+	// The new start frame is to the right of the old start
+	// frame. We want to clip the left (start) end and add some
+	// empty ranges at the right end. Assemble w-dx old values
+	// starting at index dx, then dx new values.
+
+	auto newRanges = vector<MagnitudeRange>(dx);
+	newRanges.insert(newRanges.begin(),
+			 m_ranges.begin() + dx, m_ranges.end());
+	m_ranges = newRanges;
+    }
+
+#ifdef DEBUG_SCROLLABLE_MAG_RANGE_CACHE
+    cerr << "maxes (" << m_ranges.size() << ") now: ";
+    for (int i = 0; in_range_for(m_ranges, i); ++i) {
+	cerr << m_ranges[i].getMax() << " ";
+    }
+    cerr << endl;
+#endif
+}
+
+MagnitudeRange
+ScrollableMagRangeCache::getRange(int x, int count) const
+{
+    MagnitudeRange r;
+#ifdef DEBUG_SCROLLABLE_MAG_RANGE_CACHE
+    cerr << "ScrollableMagRangeCache::getRange(" << x << ", " << count << ")" << endl;
+#endif
+    for (int i = 0; i < count; ++i) {
+	r.sample(m_ranges.at(x + i));
+    }
+    return r;
+}
+
+void
+ScrollableMagRangeCache::sampleColumn(int column, const MagnitudeRange &r)
+{
+    if (!in_range_for(m_ranges, column)) {
+	cerr << "ERROR: ScrollableMagRangeCache::sampleColumn: column " << column
+	     << " is out of range for cache of width " << m_ranges.size()
+	     << " (with start frame " << m_startFrame << ")" << endl;
+	throw logic_error("column out of range");
+    } else {
+	m_ranges[column].sample(r);
+    }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/ScrollableMagRangeCache.h	Fri Aug 05 15:05:02 2016 +0100
@@ -0,0 +1,139 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    This program 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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef SCROLLABLE_MAG_RANGE_CACHE_H
+#define SCROLLABLE_MAG_RANGE_CACHE_H
+
+#include "base/BaseTypes.h"
+#include "base/MagnitudeRange.h"
+
+#include "LayerGeometryProvider.h"
+
+/**
+ * A cached set of magnitude range records for a view that scrolls
+ * horizontally, such as a spectrogram. The cache object holds a
+ * magnitude range per column of the view, can report width (likely
+ * the same as the underlying view, but it's the caller's
+ * responsibility to set the size appropriately), can scroll the set
+ * of ranges, and can report and update which columns have had a range
+ * specified.
+ *
+ * The only way to *update* the valid area in a cache is to update the
+ * magnitude range for a column using the sampleColumn call.
+ */
+class ScrollableMagRangeCache
+{
+public:
+    ScrollableMagRangeCache() :
+	m_startFrame(0),
+	m_zoomLevel(0)
+    {}
+
+    void invalidate() {
+	m_ranges = std::vector<MagnitudeRange>(m_ranges.size());
+    }
+    
+    int getWidth() const {
+	return int(m_ranges.size());
+    }
+
+    /**
+     * Set the width of the cache in columns. If the new size differs
+     * from the current size, the cache is invalidated.
+     */
+    void resize(int newWidth) {
+        if (getWidth() != newWidth) {
+	    m_ranges = std::vector<MagnitudeRange>(newWidth);
+        }
+    }
+	
+    int getZoomLevel() const {
+	return m_zoomLevel;
+    }
+
+    /**
+     * Set the zoom level. If the new zoom level differs from the
+     * current one, the cache is invalidated. (Determining whether to
+     * invalidate the cache here is the only thing the zoom level is
+     * used for.)
+     */
+    void setZoomLevel(int zoom) {
+        if (m_zoomLevel != zoom) {
+            m_zoomLevel = zoom;
+            invalidate();
+        }
+    }
+
+    sv_frame_t getStartFrame() const {
+	return m_startFrame;
+    }
+
+    /**
+     * Set the start frame. If the new start frame differs from the
+     * current one, the cache is invalidated. To scroll, i.e. to set
+     * the start frame while retaining cache validity where possible,
+     * use scrollTo() instead.
+     */
+    void setStartFrame(sv_frame_t frame) {
+        if (m_startFrame != frame) {
+            m_startFrame = frame;
+            invalidate();
+        }
+    }
+
+    bool isColumnSet(int column) const {
+	return in_range_for(m_ranges, column) && m_ranges.at(column).isSet();
+    }
+
+    bool areColumnsSet(int x, int count) const {
+	for (int i = 0; i < count; ++i) {
+	    if (!isColumnSet(x + i)) return false;
+	}
+	return true;
+    }
+    
+    /**
+     * Get the magnitude range for a single column.
+     */
+    MagnitudeRange getRange(int column) const {
+	return m_ranges.at(column);
+    }
+
+    /**
+     * Get the magnitude range for a range of columns.
+     */
+    MagnitudeRange getRange(int x, int count) const;
+    
+    /**
+     * Set the new start frame for the cache, according to the
+     * geometry of the supplied LayerGeometryProvider, if possible
+     * also moving along any existing valid data within the cache so
+     * that it continues to be valid for the new start frame.
+     */
+    void scrollTo(const LayerGeometryProvider *v, sv_frame_t newStartFrame);
+    
+    /**
+     * Update a column in the cache, by column index. (Column zero is
+     * the first column in the cache, it has nothing to do with any
+     * underlying model that the cache may be used with.)
+     */
+    void sampleColumn(int column, const MagnitudeRange &r);
+    
+private:
+    std::vector<MagnitudeRange> m_ranges;
+    sv_frame_t m_startFrame;
+    int m_zoomLevel;
+};
+
+#endif
--- a/layer/SliceLayer.cpp	Mon Jun 13 11:44:08 2016 +0100
+++ b/layer/SliceLayer.cpp	Fri Aug 05 15:05:02 2016 +0100
@@ -485,17 +485,17 @@
         v->drawVisibleText
             (paint, xorigin + 5,
              paint.fontMetrics().ascent() + 5,
-             startText, View::OutlinedText);
+             startText, PaintAssistant::OutlinedText);
         
         v->drawVisibleText
             (paint, xorigin + 5,
              paint.fontMetrics().ascent() + paint.fontMetrics().height() + 10,
-             endText, View::OutlinedText);
+             endText, PaintAssistant::OutlinedText);
         
         v->drawVisibleText
             (paint, xorigin + 5,
              paint.fontMetrics().ascent() + 2*paint.fontMetrics().height() + 15,
-             durationText, View::OutlinedText);
+             durationText, PaintAssistant::OutlinedText);
     }
 */
 }
--- a/layer/SpectrogramLayer.cpp	Mon Jun 13 11:44:08 2016 +0100
+++ b/layer/SpectrogramLayer.cpp	Fri Aug 05 15:05:02 2016 +0100
@@ -23,11 +23,14 @@
 #include "base/Preferences.h"
 #include "base/RangeMapper.h"
 #include "base/LogRange.h"
+#include "base/ColumnOp.h"
 #include "widgets/CommandHistory.h"
+#include "data/model/Dense3DModelPeakCache.h"
+
 #include "ColourMapper.h"
-#include "ImageRegionFinder.h"
-#include "data/model/Dense3DModelPeakCache.h"
 #include "PianoScale.h"
+#include "PaintAssistant.h"
+#include "Colour3DPlotRenderer.h"
 
 #include <QPainter>
 #include <QImage>
@@ -48,7 +51,8 @@
 #include <alloca.h>
 #endif
 
-#define DEBUG_SPECTROGRAM_REPAINT 1
+//#define DEBUG_SPECTROGRAM 1
+//#define DEBUG_SPECTROGRAM_REPAINT 1
 
 using namespace std;
 
@@ -58,28 +62,29 @@
     m_windowSize(1024),
     m_windowType(HanningWindow),
     m_windowHopLevel(2),
-    m_zeroPadLevel(0),
-    m_fftSize(1024),
     m_gain(1.0),
     m_initialGain(1.0),
-    m_threshold(0.0),
-    m_initialThreshold(0.0),
+    m_threshold(1.0e-8f),
+    m_initialThreshold(1.0e-8f),
     m_colourRotation(0),
     m_initialRotation(0),
     m_minFrequency(10),
     m_maxFrequency(8000),
     m_initialMaxFrequency(8000),
-    m_colourScale(dBColourScale),
+    m_colourScale(ColourScaleType::Log),
+    m_colourScaleMultiple(1.0),
     m_colourMap(0),
-    m_frequencyScale(LinearFrequencyScale),
-    m_binDisplay(AllBins),
-    m_normalization(NoNormalization),
+    m_binScale(BinScale::Linear),
+    m_binDisplay(BinDisplay::AllBins),
+    m_normalization(ColumnNormalization::None),
+    m_normalizeVisibleArea(false),
     m_lastEmittedZoomStep(-1),
     m_synchronous(false),
     m_haveDetailedScale(false),
     m_exiting(false),
-    m_peakCacheDivisor(8),
-    m_sliceableModel(0)
+    m_fftModel(0),
+    m_peakCache(0),
+    m_peakCacheDivisor(8)
 {
     QString colourConfigName = "spectrogram-colour";
     int colourConfigDefault = int(ColourMapper::Green);
@@ -93,9 +98,9 @@
         m_initialMaxFrequency = 1500;
 	setMaxFrequency(1500);
         setMinFrequency(40);
-	setColourScale(LinearColourScale);
+	setColourScale(ColourScaleType::Linear);
         setColourMap(ColourMapper::Sunset);
-        setFrequencyScale(LogFrequencyScale);
+        setBinScale(BinScale::Log);
         colourConfigName = "spectrogram-melodic-colour";
         colourConfigDefault = int(ColourMapper::Sunset);
 //        setGain(20);
@@ -105,10 +110,10 @@
         m_initialMaxFrequency = 2000;
 	setMaxFrequency(2000);
 	setMinFrequency(40);
-	setFrequencyScale(LogFrequencyScale);
-	setColourScale(LinearColourScale);
-	setBinDisplay(PeakFrequencies);
-        setNormalization(NormalizeColumns);
+	setBinScale(BinScale::Log);
+	setColourScale(ColourScaleType::Linear);
+	setBinDisplay(BinDisplay::PeakFrequencies);
+        setNormalization(ColumnNormalization::Max1);
         colourConfigName = "spectrogram-melodic-colour";
         colourConfigDefault = int(ColourMapper::Sunset);
     }
@@ -122,13 +127,65 @@
     connect(prefs, SIGNAL(propertyChanged(PropertyContainer::PropertyName)),
             this, SLOT(preferenceChanged(PropertyContainer::PropertyName)));
     setWindowType(prefs->getWindowType());
-
-    initialisePalette();
 }
 
 SpectrogramLayer::~SpectrogramLayer()
 {
-    invalidateFFTModels();
+    invalidateRenderers();
+    invalidateFFTModel();
+}
+
+pair<ColourScaleType, double>
+SpectrogramLayer::convertToColourScale(int value)
+{
+    switch (value) {
+    case 0: return { ColourScaleType::Linear, 1.0 };
+    case 1: return { ColourScaleType::Meter, 1.0 };
+    case 2: return { ColourScaleType::Log, 2.0 }; // dB^2 (i.e. log of power)
+    case 3: return { ColourScaleType::Log, 1.0 }; // dB   (of magnitude)
+    case 4: return { ColourScaleType::Phase, 1.0 };
+    default: return { ColourScaleType::Linear, 1.0 };
+    }
+}
+
+int
+SpectrogramLayer::convertFromColourScale(ColourScaleType scale, double multiple)
+{
+    switch (scale) {
+    case ColourScaleType::Linear: return 0;
+    case ColourScaleType::Meter: return 1;
+    case ColourScaleType::Log: return (multiple > 1.5 ? 2 : 3);
+    case ColourScaleType::Phase: return 4;
+    case ColourScaleType::PlusMinusOne:
+    case ColourScaleType::Absolute:
+    default: return 0;
+    }
+}
+
+std::pair<ColumnNormalization, bool>
+SpectrogramLayer::convertToColumnNorm(int value)
+{
+    switch (value) {
+    default:
+    case 0: return { ColumnNormalization::None, false };
+    case 1: return { ColumnNormalization::Max1, false };
+    case 2: return { ColumnNormalization::None, true }; // visible area
+    case 3: return { ColumnNormalization::Hybrid, false };
+    }
+}
+
+int
+SpectrogramLayer::convertFromColumnNorm(ColumnNormalization norm, bool visible)
+{
+    if (visible) return 2;
+    switch (norm) {
+    case ColumnNormalization::None: return 0;
+    case ColumnNormalization::Max1: return 1;
+    case ColumnNormalization::Hybrid: return 3;
+
+    case ColumnNormalization::Sum1:
+    default: return 0;
+    }
 }
 
 void
@@ -139,7 +196,7 @@
     if (model == m_model) return;
 
     m_model = model;
-    invalidateFFTModels();
+    invalidateFFTModel();
 
     if (!m_model || !m_model->isOK()) return;
 
@@ -168,7 +225,6 @@
 //    list.push_back("Min Frequency");
 //    list.push_back("Max Frequency");
     list.push_back("Frequency Scale");
-////    list.push_back("Zero Padding");
     return list;
 }
 
@@ -187,7 +243,6 @@
     if (name == "Min Frequency") return tr("Min Frequency");
     if (name == "Max Frequency") return tr("Max Frequency");
     if (name == "Frequency Scale") return tr("Frequency Scale");
-    if (name == "Zero Padding") return tr("Smoothing");
     return "";
 }
 
@@ -203,7 +258,6 @@
     if (name == "Gain") return RangeProperty;
     if (name == "Colour Rotation") return RangeProperty;
     if (name == "Threshold") return RangeProperty;
-    if (name == "Zero Padding") return ToggleProperty;
     return ValueProperty;
 }
 
@@ -213,8 +267,7 @@
     if (name == "Bin Display" ||
         name == "Frequency Scale") return tr("Bins");
     if (name == "Window Size" ||
-	name == "Window Increment" ||
-        name == "Zero Padding") return tr("Window");
+	name == "Window Increment") return tr("Window");
     if (name == "Colour" ||
 	name == "Threshold" ||
 	name == "Colour Rotation") return tr("Colour");
@@ -250,8 +303,8 @@
 
     } else if (name == "Threshold") {
 
-	*min = -50;
-	*max = 0;
+	*min = -81;
+	*max = -1;
 
         *deflt = int(lrint(AudioLevel::multiplier_to_dB(m_initialThreshold)));
 	if (*deflt < *min) *deflt = *min;
@@ -271,11 +324,12 @@
 
     } else if (name == "Colour Scale") {
 
+        // linear, meter, db^2, db, phase
 	*min = 0;
 	*max = 4;
-        *deflt = int(dBColourScale);
-
-	val = (int)m_colourScale;
+        *deflt = 2;
+
+	val = convertFromColourScale(m_colourScale, m_colourScaleMultiple);
 
     } else if (name == "Colour") {
 
@@ -303,14 +357,6 @@
 
         val = m_windowHopLevel;
     
-    } else if (name == "Zero Padding") {
-	
-	*min = 0;
-	*max = 1;
-        *deflt = 0;
-	
-        val = m_zeroPadLevel > 0 ? 1 : 0;
-    
     } else if (name == "Min Frequency") {
 
 	*min = 0;
@@ -353,22 +399,23 @@
 
 	*min = 0;
 	*max = 1;
-        *deflt = int(LinearFrequencyScale);
-	val = (int)m_frequencyScale;
+        *deflt = int(BinScale::Linear);
+	val = (int)m_binScale;
 
     } else if (name == "Bin Display") {
 
 	*min = 0;
 	*max = 2;
-        *deflt = int(AllBins);
+        *deflt = int(BinDisplay::AllBins);
 	val = (int)m_binDisplay;
 
     } else if (name == "Normalization") {
 	
         *min = 0;
         *max = 3;
-        *deflt = int(NoNormalization);
-        val = (int)m_normalization;
+        *deflt = 0;
+        
+        val = convertFromColumnNorm(m_normalization, m_normalizeVisibleArea);
 
     } else {
 	val = Layer::getPropertyRangeAndValue(name, min, max, deflt);
@@ -411,10 +458,6 @@
 	case 5: return tr("93.75 %");
 	}
     }
-    if (name == "Zero Padding") {
-        if (value == 0) return tr("None");
-        return QString("%1x").arg(value + 1);
-    }
     if (name == "Min Frequency") {
 	switch (value) {
 	default:
@@ -486,7 +529,7 @@
         return new LinearRangeMapper(-50, 50, -25, 25, tr("dB"));
     }
     if (name == "Threshold") {
-        return new LinearRangeMapper(-50, 0, -50, 0, tr("dB"));
+        return new LinearRangeMapper(-81, -1, -81, -1, tr("dB"));
     }
     return 0;
 }
@@ -497,7 +540,7 @@
     if (name == "Gain") {
 	setGain(float(pow(10, float(value)/20.0)));
     } else if (name == "Threshold") {
-	if (value == -50) setThreshold(0.0);
+	if (value == -81) setThreshold(0.0);
 	else setThreshold(float(AudioLevel::dB_to_multiplier(value)));
     } else if (name == "Colour Rotation") {
 	setColourRotation(value);
@@ -507,8 +550,6 @@
 	setWindowSize(32 << value);
     } else if (name == "Window Increment") {
         setWindowHopLevel(value);
-    } else if (name == "Zero Padding") {
-        setZeroPadLevel(value > 0.1 ? 3 : 0);
     } else if (name == "Min Frequency") {
 	switch (value) {
 	default:
@@ -548,48 +589,50 @@
             m_lastEmittedZoomStep = vs;
         }
     } else if (name == "Colour Scale") {
+        setColourScaleMultiple(1.0);
 	switch (value) {
 	default:
-	case 0: setColourScale(LinearColourScale); break;
-	case 1: setColourScale(MeterColourScale); break;
-	case 2: setColourScale(dBSquaredColourScale); break;
-	case 3: setColourScale(dBColourScale); break;
-	case 4: setColourScale(PhaseColourScale); break;
+	case 0: setColourScale(ColourScaleType::Linear); break;
+	case 1: setColourScale(ColourScaleType::Meter); break;
+	case 2:
+            setColourScale(ColourScaleType::Log);
+            setColourScaleMultiple(2.0);
+            break;
+	case 3: setColourScale(ColourScaleType::Log); break;
+	case 4: setColourScale(ColourScaleType::Phase); break;
 	}
     } else if (name == "Frequency Scale") {
 	switch (value) {
 	default:
-	case 0: setFrequencyScale(LinearFrequencyScale); break;
-	case 1: setFrequencyScale(LogFrequencyScale); break;
+	case 0: setBinScale(BinScale::Linear); break;
+	case 1: setBinScale(BinScale::Log); break;
 	}
     } else if (name == "Bin Display") {
 	switch (value) {
 	default:
-	case 0: setBinDisplay(AllBins); break;
-	case 1: setBinDisplay(PeakBins); break;
-	case 2: setBinDisplay(PeakFrequencies); break;
+	case 0: setBinDisplay(BinDisplay::AllBins); break;
+	case 1: setBinDisplay(BinDisplay::PeakBins); break;
+	case 2: setBinDisplay(BinDisplay::PeakFrequencies); break;
 	}
     } else if (name == "Normalization") {
-        switch (value) {
-        default:
-        case 0: setNormalization(NoNormalization); break;
-        case 1: setNormalization(NormalizeColumns); break;
-        case 2: setNormalization(NormalizeVisibleArea); break;
-        case 3: setNormalization(NormalizeHybrid); break;
-        }
+        auto n = convertToColumnNorm(value);
+        setNormalization(n.first);
+        setNormalizeVisibleArea(n.second);
     }
 }
 
 void
-SpectrogramLayer::invalidateImageCaches()
+SpectrogramLayer::invalidateRenderers()
 {
 #ifdef DEBUG_SPECTROGRAM
-    cerr << "SpectrogramLayer::invalidateImageCaches called" << endl;
+    cerr << "SpectrogramLayer::invalidateRenderers called" << endl;
 #endif
-    for (ViewImageCache::iterator i = m_imageCaches.begin();
-         i != m_imageCaches.end(); ++i) {
-        i->second.invalidate();
+
+    for (ViewRendererMap::iterator i = m_renderers.begin();
+         i != m_renderers.end(); ++i) {
+        delete i->second;
     }
+    m_renderers.clear();
 }
 
 void
@@ -602,12 +645,13 @@
         return;
     }
     if (name == "Spectrogram Y Smoothing") {
-        invalidateImageCaches();
+        setWindowSize(m_windowSize);
+        invalidateRenderers();
         invalidateMagnitudes();
         emit layerParametersChanged();
     }
     if (name == "Spectrogram X Smoothing") {
-        invalidateImageCaches();
+        invalidateRenderers();
         invalidateMagnitudes();
         emit layerParametersChanged();
     }
@@ -621,9 +665,9 @@
 {
     if (m_channel == ch) return;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     m_channel = ch;
-    invalidateFFTModels();
+    invalidateFFTModel();
 
     emit layerParametersChanged();
 }
@@ -634,17 +678,40 @@
     return m_channel;
 }
 
+int
+SpectrogramLayer::getFFTOversampling() const
+{
+    if (m_binDisplay != BinDisplay::AllBins) {
+        return 1;
+    }
+
+    Preferences::SpectrogramSmoothing smoothing = 
+        Preferences::getInstance()->getSpectrogramSmoothing();
+    
+    if (smoothing == Preferences::NoSpectrogramSmoothing ||
+        smoothing == Preferences::SpectrogramInterpolated) {
+        return 1;
+    }
+
+    return 4;
+}
+
+int
+SpectrogramLayer::getFFTSize() const
+{
+    return m_windowSize * getFFTOversampling();
+}
+
 void
 SpectrogramLayer::setWindowSize(int ws)
 {
     if (m_windowSize == ws) return;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     
     m_windowSize = ws;
-    m_fftSize = ws * (m_zeroPadLevel + 1);
     
-    invalidateFFTModels();
+    invalidateFFTModel();
 
     emit layerParametersChanged();
 }
@@ -660,11 +727,11 @@
 {
     if (m_windowHopLevel == v) return;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     
     m_windowHopLevel = v;
     
-    invalidateFFTModels();
+    invalidateFFTModel();
 
     emit layerParametersChanged();
 
@@ -678,36 +745,15 @@
 }
 
 void
-SpectrogramLayer::setZeroPadLevel(int v)
-{
-    if (m_zeroPadLevel == v) return;
-
-    invalidateImageCaches();
-    
-    m_zeroPadLevel = v;
-    m_fftSize = m_windowSize * (v + 1);
-
-    invalidateFFTModels();
-
-    emit layerParametersChanged();
-}
-
-int
-SpectrogramLayer::getZeroPadLevel() const
-{
-    return m_zeroPadLevel;
-}
-
-void
 SpectrogramLayer::setWindowType(WindowType w)
 {
     if (m_windowType == w) return;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     
     m_windowType = w;
 
-    invalidateFFTModels();
+    invalidateFFTModel();
 
     emit layerParametersChanged();
 }
@@ -726,7 +772,7 @@
 
     if (m_gain == gain) return;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     
     m_gain = gain;
     
@@ -744,7 +790,7 @@
 {
     if (m_threshold == threshold) return;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     
     m_threshold = threshold;
 
@@ -764,7 +810,7 @@
 
 //    SVDEBUG << "SpectrogramLayer::setMinFrequency: " << mf << endl;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     invalidateMagnitudes();
     
     m_minFrequency = mf;
@@ -785,7 +831,7 @@
 
 //    SVDEBUG << "SpectrogramLayer::setMaxFrequency: " << mf << endl;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     invalidateMagnitudes();
     
     m_maxFrequency = mf;
@@ -802,47 +848,67 @@
 void
 SpectrogramLayer::setColourRotation(int r)
 {
-    invalidateImageCaches();
-
     if (r < 0) r = 0;
     if (r > 256) r = 256;
     int distance = r - m_colourRotation;
 
     if (distance != 0) {
-	rotatePalette(-distance);
 	m_colourRotation = r;
     }
+
+    // Initially the idea with colour rotation was that we would just
+    // rotate the palette of an already-generated cache. That's not
+    // really practical now that cacheing is handled in a separate
+    // class in which the main cache no longer has a palette.
+    invalidateRenderers();
     
     emit layerParametersChanged();
 }
 
 void
-SpectrogramLayer::setColourScale(ColourScale colourScale)
+SpectrogramLayer::setColourScale(ColourScaleType colourScale)
 {
     if (m_colourScale == colourScale) return;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     
     m_colourScale = colourScale;
     
     emit layerParametersChanged();
 }
 
-SpectrogramLayer::ColourScale
+ColourScaleType
 SpectrogramLayer::getColourScale() const
 {
     return m_colourScale;
 }
 
 void
+SpectrogramLayer::setColourScaleMultiple(double multiple)
+{
+    if (m_colourScaleMultiple == multiple) return;
+
+    invalidateRenderers();
+    
+    m_colourScaleMultiple = multiple;
+    
+    emit layerParametersChanged();
+}
+
+double
+SpectrogramLayer::getColourScaleMultiple() const
+{
+    return m_colourScaleMultiple;
+}
+
+void
 SpectrogramLayer::setColourMap(int map)
 {
     if (m_colourMap == map) return;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     
     m_colourMap = map;
-    initialisePalette();
 
     emit layerParametersChanged();
 }
@@ -854,20 +920,20 @@
 }
 
 void
-SpectrogramLayer::setFrequencyScale(FrequencyScale frequencyScale)
+SpectrogramLayer::setBinScale(BinScale binScale)
 {
-    if (m_frequencyScale == frequencyScale) return;
-
-    invalidateImageCaches();
-    m_frequencyScale = frequencyScale;
+    if (m_binScale == binScale) return;
+
+    invalidateRenderers();
+    m_binScale = binScale;
 
     emit layerParametersChanged();
 }
 
-SpectrogramLayer::FrequencyScale
-SpectrogramLayer::getFrequencyScale() const
+BinScale
+SpectrogramLayer::getBinScale() const
 {
-    return m_frequencyScale;
+    return m_binScale;
 }
 
 void
@@ -875,37 +941,55 @@
 {
     if (m_binDisplay == binDisplay) return;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     m_binDisplay = binDisplay;
 
     emit layerParametersChanged();
 }
 
-SpectrogramLayer::BinDisplay
+BinDisplay
 SpectrogramLayer::getBinDisplay() const
 {
     return m_binDisplay;
 }
 
 void
-SpectrogramLayer::setNormalization(Normalization n)
+SpectrogramLayer::setNormalization(ColumnNormalization n)
 {
     if (m_normalization == n) return;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     invalidateMagnitudes();
     m_normalization = n;
 
     emit layerParametersChanged();
 }
 
-SpectrogramLayer::Normalization
+ColumnNormalization
 SpectrogramLayer::getNormalization() const
 {
     return m_normalization;
 }
 
 void
+SpectrogramLayer::setNormalizeVisibleArea(bool n)
+{
+    if (m_normalizeVisibleArea == n) return;
+
+    invalidateRenderers();
+    invalidateMagnitudes();
+    m_normalizeVisibleArea = n;
+    
+    emit layerParametersChanged();
+}
+
+bool
+SpectrogramLayer::getNormalizeVisibleArea() const
+{
+    return m_normalizeVisibleArea;
+}
+
+void
 SpectrogramLayer::setLayerDormant(const LayerGeometryProvider *v, bool dormant)
 {
     if (dormant) {
@@ -921,33 +1005,7 @@
 
         Layer::setLayerDormant(v, true);
 
-        const View *view = v->getView();
-        
-	invalidateImageCaches();
-
-        m_imageCaches.erase(view->getId());
-
-        if (m_fftModels.find(view->getId()) != m_fftModels.end()) {
-
-            if (m_sliceableModel == m_fftModels[view->getId()]) {
-                bool replaced = false;
-                for (ViewFFTMap::iterator i = m_fftModels.begin();
-                     i != m_fftModels.end(); ++i) {
-                    if (i->second != m_sliceableModel) {
-                        emit sliceableModelReplaced(m_sliceableModel, i->second);
-                        replaced = true;
-                        break;
-                    }
-                }
-                if (!replaced) emit sliceableModelReplaced(m_sliceableModel, 0);
-            }
-
-            delete m_fftModels[view->getId()];
-            m_fftModels.erase(view->getId());
-
-            delete m_peakCaches[view->getId()];
-            m_peakCaches.erase(view->getId());
-        }
+	invalidateRenderers();
 	
     } else {
 
@@ -962,7 +1020,7 @@
     cerr << "SpectrogramLayer::cacheInvalid()" << endl;
 #endif
 
-    invalidateImageCaches();
+    invalidateRenderers();
     invalidateMagnitudes();
 }
 
@@ -985,7 +1043,7 @@
     // pulling out the image cache code, but it might not matter very
     // much, since the underlying models for spectrogram layers don't
     // change very often. Let's see.
-    invalidateImageCaches();
+    invalidateRenderers();
     invalidateMagnitudes();
 }
 
@@ -995,146 +1053,16 @@
     return ColourMapper(m_colourMap, 1.f, 255.f).hasLightBackground();
 }
 
-void
-SpectrogramLayer::initialisePalette()
-{
-    int formerRotation = m_colourRotation;
-
-    if (m_colourMap == (int)ColourMapper::BlackOnWhite) {
-	m_palette.setColour(NO_VALUE, Qt::white);
-    } else {
-	m_palette.setColour(NO_VALUE, Qt::black);
-    }
-
-    ColourMapper mapper(m_colourMap, 1.f, 255.f);
-    
-    for (int pixel = 1; pixel < 256; ++pixel) {
-        m_palette.setColour((unsigned char)pixel, mapper.map(pixel));
-    }
-
-    m_crosshairColour = mapper.getContrastingColour();
-
-    m_colourRotation = 0;
-    rotatePalette(m_colourRotation - formerRotation);
-    m_colourRotation = formerRotation;
-
-    m_drawBuffer = QImage();
-}
-
-void
-SpectrogramLayer::rotatePalette(int distance)
-{
-    QColor newPixels[256];
-
-    newPixels[NO_VALUE] = m_palette.getColour(NO_VALUE);
-
-    for (int pixel = 1; pixel < 256; ++pixel) {
-	int target = pixel + distance;
-	while (target < 1) target += 255;
-	while (target > 255) target -= 255;
-	newPixels[target] = m_palette.getColour((unsigned char)pixel);
-    }
-
-    for (int pixel = 0; pixel < 256; ++pixel) {
-	m_palette.setColour((unsigned char)pixel, newPixels[pixel]);
-    }
-
-    m_drawBuffer = QImage();
-}
-
-unsigned char
-SpectrogramLayer::getDisplayValue(LayerGeometryProvider *v, double input) const
-{
-    int value;
-
-    double min = 0.0;
-    double max = 1.0;
-
-    if (m_normalization == NormalizeVisibleArea) {
-        min = m_viewMags[v->getId()].getMin();
-        max = m_viewMags[v->getId()].getMax();
-    } else if (m_normalization != NormalizeColumns) {
-        if (m_colourScale == LinearColourScale //||
-//            m_colourScale == MeterColourScale) {
-            ) {
-            max = 0.1;
-        }
-    }
-
-    double thresh = -80.0;
-
-    if (max == 0.0) max = 1.0;
-    if (max == min) min = max - 0.0001;
-
-    switch (m_colourScale) {
-	
-    default:
-    case LinearColourScale:
-        value = int(((input - min) / (max - min)) * 255.0) + 1;
-	break;
-	
-    case MeterColourScale:
-        value = AudioLevel::multiplier_to_preview
-            ((input - min) / (max - min), 254) + 1;
-	break;
-
-    case dBSquaredColourScale:
-        input = ((input - min) * (input - min)) / ((max - min) * (max - min));
-        if (input > 0.0) {
-            input = 10.0 * log10(input);
-        } else {
-            input = thresh;
-        }
-        if (min > 0.0) {
-            thresh = 10.0 * log10(min * min);
-            if (thresh < -80.0) thresh = -80.0;
-        }
-	input = (input - thresh) / (-thresh);
-	if (input < 0.0) input = 0.0;
-	if (input > 1.0) input = 1.0;
-	value = int(input * 255.0) + 1;
-	break;
-	
-    case dBColourScale:
-        //!!! experiment with normalizing the visible area this way.
-        //In any case, we need to have some indication of what the dB
-        //scale is relative to.
-        input = (input - min) / (max - min);
-        if (input > 0.0) {
-            input = 10.0 * log10(input);
-        } else {
-            input = thresh;
-        }
-        if (min > 0.0) {
-            thresh = 10.0 * log10(min);
-            if (thresh < -80.0) thresh = -80.0;
-        }
-	input = (input - thresh) / (-thresh);
-	if (input < 0.0) input = 0.0;
-	if (input > 1.0) input = 1.0;
-	value = int(input * 255.0) + 1;
-	break;
-	
-    case PhaseColourScale:
-	value = int((input * 127.0 / M_PI) + 128);
-	break;
-    }
-
-    if (value > UCHAR_MAX) value = UCHAR_MAX;
-    if (value < 0) value = 0;
-    return (unsigned char)value;
-}
-
 double
 SpectrogramLayer::getEffectiveMinFrequency() const
 {
     sv_samplerate_t sr = m_model->getSampleRate();
-    double minf = double(sr) / m_fftSize;
+    double minf = double(sr) / getFFTSize();
 
     if (m_minFrequency > 0.0) {
-	int minbin = int((double(m_minFrequency) * m_fftSize) / sr + 0.01);
+	int minbin = int((double(m_minFrequency) * getFFTSize()) / sr + 0.01);
 	if (minbin < 1) minbin = 1;
-	minf = minbin * sr / m_fftSize;
+	minf = minbin * sr / getFFTSize();
     }
 
     return minf;
@@ -1147,9 +1075,9 @@
     double maxf = double(sr) / 2;
 
     if (m_maxFrequency > 0.0) {
-	int maxbin = int((double(m_maxFrequency) * m_fftSize) / sr + 0.1);
-	if (maxbin > m_fftSize / 2) maxbin = m_fftSize / 2;
-	maxf = maxbin * sr / m_fftSize;
+	int maxbin = int((double(m_maxFrequency) * getFFTSize()) / sr + 0.1);
+	if (maxbin > getFFTSize() / 2) maxbin = getFFTSize() / 2;
+	maxf = maxbin * sr / getFFTSize();
     }
 
     return maxf;
@@ -1159,55 +1087,46 @@
 SpectrogramLayer::getYBinRange(LayerGeometryProvider *v, int y, double &q0, double &q1) const
 {
     Profiler profiler("SpectrogramLayer::getYBinRange");
-    
     int h = v->getPaintHeight();
     if (y < 0 || y >= h) return false;
-
+    q0 = getBinForY(v, y);
+    q1 = getBinForY(v, y-1);
+    return true;
+}
+
+double
+SpectrogramLayer::getYForBin(const LayerGeometryProvider *v, double bin) const
+{
+    double minf = getEffectiveMinFrequency();
+    double maxf = getEffectiveMaxFrequency();
+    bool logarithmic = (m_binScale == BinScale::Log);
+    sv_samplerate_t sr = m_model->getSampleRate();
+
+    double freq = (bin * sr) / getFFTSize();
+    
+    double y = v->getYForFrequency(freq, minf, maxf, logarithmic);
+    
+    return y;
+}
+
+double
+SpectrogramLayer::getBinForY(const LayerGeometryProvider *v, double y) const
+{
     sv_samplerate_t sr = m_model->getSampleRate();
     double minf = getEffectiveMinFrequency();
     double maxf = getEffectiveMaxFrequency();
 
-    bool logarithmic = (m_frequencyScale == LogFrequencyScale);
-
-    q0 = v->getFrequencyForY(y, minf, maxf, logarithmic);
-    q1 = v->getFrequencyForY(y - 1, minf, maxf, logarithmic);
-
-    // Now map these on to ("proportions of") actual bins, using raw
-    // FFT size (unsmoothed)
-
-    q0 = (q0 * m_fftSize) / sr;
-    q1 = (q1 * m_fftSize) / sr;
-
-    return true;
+    bool logarithmic = (m_binScale == BinScale::Log);
+
+    double freq = v->getFrequencyForY(y, minf, maxf, logarithmic);
+
+    // Now map on to ("proportion of") actual bins
+    double bin = (freq * getFFTSize()) / sr;
+
+    return bin;
 }
 
 bool
-SpectrogramLayer::getSmoothedYBinRange(LayerGeometryProvider *v, int y, double &q0, double &q1) const
-{
-    Profiler profiler("SpectrogramLayer::getSmoothedYBinRange");
-
-    int h = v->getPaintHeight();
-    if (y < 0 || y >= h) return false;
-
-    sv_samplerate_t sr = m_model->getSampleRate();
-    double minf = getEffectiveMinFrequency();
-    double maxf = getEffectiveMaxFrequency();
-
-    bool logarithmic = (m_frequencyScale == LogFrequencyScale);
-
-    q0 = v->getFrequencyForY(y, minf, maxf, logarithmic);
-    q1 = v->getFrequencyForY(y - 1, minf, maxf, logarithmic);
-
-    // Now map these on to ("proportions of") actual bins, using raw
-    // FFT size (unsmoothed)
-
-    q0 = (q0 * getFFTSize(v)) / sr;
-    q1 = (q1 * getFFTSize(v)) / sr;
-
-    return true;
-}
-    
-bool
 SpectrogramLayer::getXBinRange(LayerGeometryProvider *v, int x, double &s0, double &s1) const
 {
     sv_frame_t modelStart = m_model->getStartFrame();
@@ -1263,8 +1182,8 @@
     sv_samplerate_t sr = m_model->getSampleRate();
 
     for (int q = q0i; q <= q1i; ++q) {
-	if (q == q0i) freqMin = (sr * q) / m_fftSize;
-	if (q == q1i) freqMax = (sr * (q+1)) / m_fftSize;
+	if (q == q0i) freqMin = (sr * q) / getFFTSize();
+	if (q == q1i) freqMax = (sr * (q+1)) / getFFTSize();
     }
     return true;
 }
@@ -1279,7 +1198,7 @@
 	return false;
     }
 
-    FFTModel *fft = getFFTModel(v);
+    FFTModel *fft = getFFTModel();
     if (!fft) return false;
 
     double s0 = 0, s1 = 0;
@@ -1298,8 +1217,8 @@
 
     bool haveAdj = false;
 
-    bool peaksOnly = (m_binDisplay == PeakBins ||
-		      m_binDisplay == PeakFrequencies);
+    bool peaksOnly = (m_binDisplay == BinDisplay::PeakBins ||
+		      m_binDisplay == BinDisplay::PeakFrequencies);
 
     for (int q = q0i; q <= q1i; ++q) {
 
@@ -1311,7 +1230,10 @@
 
 	    if (peaksOnly && !fft->isLocalPeak(s, q)) continue;
 
-	    if (!fft->isOverThreshold(s, q, float(m_threshold * double(m_fftSize)/2.0))) continue;
+	    if (!fft->isOverThreshold
+                (s, q, float(m_threshold * double(getFFTSize())/2.0))) {
+                continue;
+            }
 
             double freq = binfreq;
 	    
@@ -1357,11 +1279,7 @@
 
     bool rv = false;
 
-    int zp = getZeroPadLevel(v);
-    q0i *= zp + 1;
-    q1i *= zp + 1;
-
-    FFTModel *fft = getFFTModel(v);
+    FFTModel *fft = getFFTModel();
 
     if (fft) {
 
@@ -1384,7 +1302,7 @@
                     if (!have || value < phaseMin) { phaseMin = value; }
                     if (!have || value > phaseMax) { phaseMax = value; }
 
-                    value = fft->getMagnitudeAt(s, q) / (m_fftSize/2.0);
+                    value = fft->getMagnitudeAt(s, q) / (getFFTSize()/2.0);
                     if (!have || value < min) { min = value; }
                     if (!have || value > max) { max = value; }
                     
@@ -1400,166 +1318,83 @@
 
     return rv;
 }
-   
-int
-SpectrogramLayer::getZeroPadLevel(const LayerGeometryProvider *v) const
-{
-    //!!! tidy all this stuff
-
-    if (m_binDisplay != AllBins) return 0;
-
-    Preferences::SpectrogramSmoothing smoothing = 
-        Preferences::getInstance()->getSpectrogramSmoothing();
-    
-    if (smoothing == Preferences::NoSpectrogramSmoothing ||
-        smoothing == Preferences::SpectrogramInterpolated) return 0;
-
-    if (m_frequencyScale == LogFrequencyScale) return 3;
-
-    sv_samplerate_t sr = m_model->getSampleRate();
-    
-    int maxbin = m_fftSize / 2;
-    if (m_maxFrequency > 0) {
-	maxbin = int((double(m_maxFrequency) * m_fftSize) / sr + 0.1);
-	if (maxbin > m_fftSize / 2) maxbin = m_fftSize / 2;
-    }
-
-    int minbin = 1;
-    if (m_minFrequency > 0) {
-	minbin = int((double(m_minFrequency) * m_fftSize) / sr + 0.1);
-	if (minbin < 1) minbin = 1;
-	if (minbin >= maxbin) minbin = maxbin - 1;
-    }
-
-    double perPixel =
-        double(v->getPaintHeight()) /
-        double((maxbin - minbin) / (m_zeroPadLevel + 1));
-
-    if (perPixel > 2.8) {
-        return 3; // 4x oversampling
-    } else if (perPixel > 1.5) {
-        return 1; // 2x
-    } else {
-        return 0; // 1x
-    }
-}
-
-int
-SpectrogramLayer::getFFTSize(const LayerGeometryProvider *v) const
-{
-    return m_fftSize * (getZeroPadLevel(v) + 1);
-}
 	
 FFTModel *
-SpectrogramLayer::getFFTModel(const LayerGeometryProvider *v) const
+SpectrogramLayer::getFFTModel() const
 {
     if (!m_model) return 0;
 
-    int fftSize = getFFTSize(v);
-
-    const View *view = v->getView();
+    int fftSize = getFFTSize();
+
+    //!!! it is now surely slower to do this on every getFFTModel()
+    //!!! request than it would be to recreate the model immediately
+    //!!! when something changes instead of just invalidating it
     
-    if (m_fftModels.find(view->getId()) != m_fftModels.end()) {
-        if (m_fftModels[view->getId()] == 0) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-            cerr << "SpectrogramLayer::getFFTModel(" << v << "): Found null model" << endl;
-#endif
-            return 0;
-        }
-        if (m_fftModels[view->getId()]->getHeight() != fftSize / 2 + 1) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-            cerr << "SpectrogramLayer::getFFTModel(" << v << "): Found a model with the wrong height (" << m_fftModels[view->getId()]->getHeight() << ", wanted " << (fftSize / 2 + 1) << ")" << endl;
-#endif
-            delete m_fftModels[view->getId()];
-            m_fftModels.erase(view->getId());
-            delete m_peakCaches[view->getId()];
-            m_peakCaches.erase(view->getId());
-        } else {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-            cerr << "SpectrogramLayer::getFFTModel(" << v << "): Found a good model of height " << m_fftModels[view->getId()]->getHeight() << endl;
-#endif
-            return m_fftModels[view->getId()];
-        }
+    if (m_fftModel &&
+        m_fftModel->getHeight() == fftSize / 2 + 1 &&
+        m_fftModel->getWindowIncrement() == getWindowIncrement()) {
+        return m_fftModel;
     }
-
-    if (m_fftModels.find(view->getId()) == m_fftModels.end()) {
-
-        FFTModel *model = new FFTModel(m_model,
-                                       m_channel,
-                                       m_windowType,
-                                       m_windowSize,
-                                       getWindowIncrement(),
-                                       fftSize);
-
-        if (!model->isOK()) {
-            QMessageBox::critical
-                (0, tr("FFT cache failed"),
-                 tr("Failed to create the FFT model for this spectrogram.\n"
-                    "There may be insufficient memory or disc space to continue."));
-            delete model;
-            m_fftModels[view->getId()] = 0;
-            return 0;
-        }
-
-        if (!m_sliceableModel) {
-#ifdef DEBUG_SPECTROGRAM
-            cerr << "SpectrogramLayer: emitting sliceableModelReplaced(0, " << model << ")" << endl;
-#endif
-            ((SpectrogramLayer *)this)->sliceableModelReplaced(0, model);
-            m_sliceableModel = model;
-        }
-
-        m_fftModels[view->getId()] = model;
+    
+    delete m_peakCache;
+    m_peakCache = 0;
+
+    delete m_fftModel;
+    m_fftModel = new FFTModel(m_model,
+                              m_channel,
+                              m_windowType,
+                              m_windowSize,
+                              getWindowIncrement(),
+                              fftSize);
+
+    if (!m_fftModel->isOK()) {
+        QMessageBox::critical
+            (0, tr("FFT cache failed"),
+             tr("Failed to create the FFT model for this spectrogram.\n"
+                "There may be insufficient memory or disc space to continue."));
+        delete m_fftModel;
+        m_fftModel = 0;
+        return 0;
     }
 
-    return m_fftModels[view->getId()];
+    ((SpectrogramLayer *)this)->sliceableModelReplaced(0, m_fftModel);
+
+    return m_fftModel;
 }
 
 Dense3DModelPeakCache *
-SpectrogramLayer::getPeakCache(const LayerGeometryProvider *v) const
+SpectrogramLayer::getPeakCache() const
 {
-    const View *view = v->getView();
-    if (!m_peakCaches[view->getId()]) {
-        FFTModel *f = getFFTModel(v);
+    //!!! see comment in getFFTModel
+    
+    if (!m_peakCache) {
+        FFTModel *f = getFFTModel();
         if (!f) return 0;
-        m_peakCaches[view->getId()] =
-            new Dense3DModelPeakCache(f, m_peakCacheDivisor);
+        m_peakCache = new Dense3DModelPeakCache(f, m_peakCacheDivisor);
     }
-    return m_peakCaches[view->getId()];
+    return m_peakCache;
 }
 
 const Model *
 SpectrogramLayer::getSliceableModel() const
 {
-    if (m_sliceableModel) return m_sliceableModel;
-    if (m_fftModels.empty()) return 0;
-    m_sliceableModel = m_fftModels.begin()->second;
-    return m_sliceableModel;
+    return m_fftModel;
 }
 
 void
-SpectrogramLayer::invalidateFFTModels()
+SpectrogramLayer::invalidateFFTModel()
 {
 #ifdef DEBUG_SPECTROGRAM
-    cerr << "SpectrogramLayer::invalidateFFTModels called" << endl;
+    cerr << "SpectrogramLayer::invalidateFFTModel called" << endl;
 #endif
-    for (ViewFFTMap::iterator i = m_fftModels.begin();
-         i != m_fftModels.end(); ++i) {
-        delete i->second;
-    }
-    for (PeakCacheMap::iterator i = m_peakCaches.begin();
-         i != m_peakCaches.end(); ++i) {
-        delete i->second;
-    }
-    
-    m_fftModels.clear();
-    m_peakCaches.clear();
-
-    if (m_sliceableModel) {
-        cerr << "SpectrogramLayer: emitting sliceableModelReplaced(" << m_sliceableModel << ", 0)" << endl;
-        emit sliceableModelReplaced(m_sliceableModel, 0);
-        m_sliceableModel = 0;
-    }
+
+    emit sliceableModelReplaced(m_fftModel, 0);
+
+    delete m_fftModel;
+    delete m_peakCache;
+
+    m_fftModel = 0;
+    m_peakCache = 0;
 }
 
 void
@@ -1569,54 +1404,6 @@
     cerr << "SpectrogramLayer::invalidateMagnitudes called" << endl;
 #endif
     m_viewMags.clear();
-    for (vector<MagnitudeRange>::iterator i = m_columnMags.begin();
-         i != m_columnMags.end(); ++i) {
-        *i = MagnitudeRange();
-    }
-}
-
-bool
-SpectrogramLayer::updateViewMagnitudes(LayerGeometryProvider *v) const
-{
-    MagnitudeRange mag;
-
-    int x0 = 0, x1 = v->getPaintWidth();
-    double s00 = 0, s01 = 0, s10 = 0, s11 = 0;
-    
-    if (!getXBinRange(v, x0, s00, s01)) {
-        s00 = s01 = double(m_model->getStartFrame()) / getWindowIncrement();
-    }
-
-    if (!getXBinRange(v, x1, s10, s11)) {
-        s10 = s11 = double(m_model->getEndFrame()) / getWindowIncrement();
-    }
-
-    int s0 = int(min(s00, s10) + 0.0001);
-    int s1 = int(max(s01, s11) + 0.0001);
-
-//    SVDEBUG << "SpectrogramLayer::updateViewMagnitudes: x0 = " << x0 << ", x1 = " << x1 << ", s00 = " << s00 << ", s11 = " << s11 << " s0 = " << s0 << ", s1 = " << s1 << endl;
-
-    if (int(m_columnMags.size()) <= s1) {
-        m_columnMags.resize(s1 + 1);
-    }
-
-    for (int s = s0; s <= s1; ++s) {
-        if (m_columnMags[s].isSet()) {
-            mag.sample(m_columnMags[s]);
-        }
-    }
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    cerr << "SpectrogramLayer::updateViewMagnitudes returning from cols "
-         << s0 << " -> " << s1 << " inclusive" << endl;
-    cerr << "SpectrogramLayer::updateViewMagnitudes: for view id " << v->getId()
-         << ": min is " << mag.getMin() << ", max is " << mag.getMax() << endl;
-#endif
-
-    if (!mag.isSet()) return false;
-    if (mag == m_viewMags[v->getId()]) return false;
-    m_viewMags[v->getId()] = mag;
-    return true;
 }
 
 void
@@ -1625,13 +1412,136 @@
     m_synchronous = synchronous;
 }
 
-ScrollableImageCache &
-SpectrogramLayer::getImageCacheReference(const LayerGeometryProvider *view) const
+Colour3DPlotRenderer *
+SpectrogramLayer::getRenderer(LayerGeometryProvider *v) const
 {
-    if (m_imageCaches.find(view->getId()) == m_imageCaches.end()) {
-        m_imageCaches[view->getId()] = ScrollableImageCache(view);
+    int viewId = v->getId();
+    
+    if (m_renderers.find(viewId) == m_renderers.end()) {
+
+        Colour3DPlotRenderer::Sources sources;
+        sources.verticalBinLayer = this;
+        sources.fft = getFFTModel();
+        sources.source = sources.fft;
+        sources.peaks = getPeakCache();
+
+        ColourScale::Parameters cparams;
+        cparams.colourMap = m_colourMap;
+        cparams.scaleType = m_colourScale;
+        cparams.multiple = m_colourScaleMultiple;
+
+        if (m_colourScale != ColourScaleType::Phase) {
+            cparams.gain = m_gain;
+            cparams.threshold = m_threshold;
+        }
+
+        float minValue = 0.0f;
+        float maxValue = 1.0f;
+        
+        if (m_normalizeVisibleArea && m_viewMags[viewId].isSet()) {
+            minValue = m_viewMags[viewId].getMin();
+            maxValue = m_viewMags[viewId].getMax();
+        } else if (m_colourScale == ColourScaleType::Linear &&
+                   m_normalization == ColumnNormalization::None) {
+            maxValue = 0.1f;
+        }
+
+        if (maxValue <= minValue) {
+            maxValue = minValue + 0.1f;
+        }
+        if (maxValue <= m_threshold) {
+            maxValue = m_threshold + 0.1f;
+        }
+
+        cparams.minValue = minValue;
+        cparams.maxValue = maxValue;
+
+        m_lastRenderedMags[viewId] = MagnitudeRange(minValue, maxValue);
+
+        Colour3DPlotRenderer::Parameters params;
+        params.colourScale = ColourScale(cparams);
+        params.normalization = m_normalization;
+        params.binDisplay = m_binDisplay;
+        params.binScale = m_binScale;
+        params.alwaysOpaque = true;
+        params.invertVertical = false;
+        params.scaleFactor = 1.0;
+        params.colourRotation = m_colourRotation;
+
+        if (m_colourScale != ColourScaleType::Phase &&
+            m_normalization != ColumnNormalization::Hybrid) {
+            params.scaleFactor *= 2.f / float(getFFTSize());
+        }
+
+        Preferences::SpectrogramSmoothing smoothing = 
+            Preferences::getInstance()->getSpectrogramSmoothing();
+        params.interpolate = 
+            (smoothing == Preferences::SpectrogramInterpolated ||
+             smoothing == Preferences::SpectrogramZeroPaddedAndInterpolated);
+
+        m_renderers[v->getId()] = new Colour3DPlotRenderer(sources, params);
     }
-    return m_imageCaches.at(view->getId());
+
+    return m_renderers[v->getId()];
+}
+
+void
+SpectrogramLayer::paintWithRenderer(LayerGeometryProvider *v, QPainter &paint, QRect rect) const
+{
+    Colour3DPlotRenderer *renderer = getRenderer(v);
+
+    Colour3DPlotRenderer::RenderResult result;
+    MagnitudeRange magRange;
+    int viewId = v->getId();
+
+    bool continuingPaint = !renderer->geometryChanged(v);
+    
+    if (continuingPaint) {
+        magRange = m_viewMags[viewId];
+    }
+    
+    if (m_synchronous) {
+
+        result = renderer->render(v, paint, rect);
+
+    } else {
+
+        result = renderer->renderTimeConstrained(v, paint, rect);
+
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+        cerr << "rect width from this paint: " << result.rendered.width()
+             << ", mag range in this paint: " << result.range.getMin() << " -> "
+             << result.range.getMax() << endl;
+#endif
+        
+        QRect uncached = renderer->getLargestUncachedRect(v);
+        if (uncached.width() > 0) {
+            v->updatePaintRect(uncached);
+        }
+    }
+
+    magRange.sample(result.range);
+
+    if (magRange.isSet()) {
+        if (m_viewMags[viewId] != magRange) {
+            m_viewMags[viewId] = magRange;
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+            cerr << "mag range in this view has changed: "
+                 << magRange.getMin() << " -> " << magRange.getMax() << endl;
+#endif
+        }
+    }
+
+    if (!continuingPaint && m_normalizeVisibleArea &&
+        m_viewMags[viewId] != m_lastRenderedMags[viewId]) {
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+        cerr << "mag range has changed from last rendered range: re-rendering"
+             << endl;
+#endif
+        delete m_renderers[viewId];
+        m_renderers.erase(viewId);
+        v->updatePaintRect(v->getPaintRect());
+    }
 }
 
 void
@@ -1645,8 +1555,6 @@
     cerr << "SpectrogramLayer::paint(): rect is " << rect.x() << "," << rect.y() << " " << rect.width() << "x" << rect.height() << endl;
 #endif
 
-    sv_frame_t startFrame = v->getStartFrame();
-
     if (!m_model || !m_model->isOK() || !m_model->isReady()) {
 	return;
     }
@@ -1655,995 +1563,9 @@
 	SVDEBUG << "SpectrogramLayer::paint(): Layer is dormant, making it undormant again" << endl;
     }
 
-    // Need to do this even if !isLayerDormant, as that could mean v
-    // is not in the dormancy map at all -- we need it to be present
-    // and accountable for when determining whether we need the cache
-    // in the cache-fill thread above.
-    //!!! no inter use cache-fill thread
-    const_cast<SpectrogramLayer *>(this)->Layer::setLayerDormant(v, false);
-
-    int fftSize = getFFTSize(v);
-
-    const View *view = v->getView();
-    ScrollableImageCache &cache = getImageCacheReference(view);
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    cerr << "SpectrogramLayer::paint(): image cache valid area from " << cache.getValidLeft() << " width " << cache.getValidWidth() << ", height " << cache.getSize().height() << endl;
-    if (rect.x() + rect.width() + 1 < cache.getValidLeft() ||
-        rect.x() > cache.getValidRight()) {
-        cerr << "SpectrogramLayer: NOTE: requested rect is not contiguous with cache valid area" << endl;
-    }
-#endif
-
-    int zoomLevel = v->getZoomLevel();
-
-    int x0 = v->getXForViewX(rect.x());
-    int x1 = v->getXForViewX(rect.x() + rect.width());
-    if (x0 < 0) x0 = 0;
-    if (x1 > v->getPaintWidth()) x1 = v->getPaintWidth();
-
-    if (updateViewMagnitudes(v)) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "SpectrogramLayer: magnitude range changed to [" << m_viewMags[v->getId()].getMin() << "->" << m_viewMags[v->getId()].getMax() << "]" << endl;
-#endif
-        if (m_normalization == NormalizeVisibleArea) {
-            cache.invalidate();
-        }
-    }
-
-    if (cache.getZoomLevel() != zoomLevel ||
-        cache.getSize() != v->getPaintSize()) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "SpectrogramLayer: resizing image cache from "
-             << cache.getSize().width() << "x" << cache.getSize().height()
-             << " to "
-             << v->getPaintSize().width() << "x" << v->getPaintSize().height()
-             << " and updating zoom level from " << cache.getZoomLevel()
-             << " to " << zoomLevel
-             << endl;
-#endif
-        cache.resize(v->getPaintSize());
-        cache.setZoomLevel(zoomLevel);
-        cache.setStartFrame(startFrame);
-    }
-    
-    if (cache.isValid()) {
-        
-        if (v->getXForFrame(cache.getStartFrame()) ==
-            v->getXForFrame(startFrame) &&
-            cache.getValidLeft() <= x0 &&
-            cache.getValidRight() >= x1) {
-                
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-            cerr << "SpectrogramLayer: image cache hit!" << endl;
-#endif
-
-            paint.drawImage(rect, cache.getImage(), rect);
-
-            illuminateLocalFeatures(v, paint);
-            return;
-
-        } else {
-
-            // cache doesn't begin at the right frame or doesn't
-            // contain the complete view, but might be scrollable or
-            // partially usable
-                
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-            cerr << "SpectrogramLayer: scrolling the image cache if applicable" << endl;
-#endif
-
-            cache.scrollTo(startFrame);
-            
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-            cerr << "SpectrogramLayer: after scrolling, cache valid from "
-                 << cache.getValidLeft() << " width " << cache.getValidWidth()
-                 << endl;
-#endif
-        }
-    }
-
-    bool rightToLeft = false;
-    
-    if (!cache.isValid()) {
-        if (!m_synchronous) {
-            // When rendering the whole thing, start from somewhere near
-            // the middle so that the region of interest appears first
-
-            //!!! (perhaps we should have some cunning test to avoid
-            //!!! doing this if past repaints have appeared fast
-            //!!! enough to do the whole width in one shot)
-            if (x0 == 0 && x1 == v->getPaintWidth()) {
-                x0 = int(x1 * 0.3);
-            }
-        }
-    } else {
-        // When rendering only a part of the cache, we need to make
-        // sure that the part we're rendering is adjacent to (or
-        // overlapping) a valid area of cache, if we have one. The
-        // alternative is to ditch the valid area of cache and render
-        // only the requested area, but that's risky because this can
-        // happen when just waving the pointer over a small part of
-        // the view -- if we lose the partly-built cache every time
-        // the user does that, we'll never finish building it.
-        int left = x0;
-        int width = x1 - x0;
-        bool isLeftOfValidArea = false;
-        cache.adjustToTouchValidArea(left, width, isLeftOfValidArea);
-        x0 = left;
-        x1 = x0 + width;
-
-        // That call also told us whether we should be painting
-        // sub-regions of our target region in right-to-left order in
-        // order to ensure contiguity
-        rightToLeft = isLeftOfValidArea;
-    }
-    
-    // We always paint the full height when refreshing the cache.
-    // Smaller heights can be used when painting direct from cache
-    // (further up in this function), but we want to ensure the cache
-    // is coherent without having to worry about vertical matching of
-    // required and valid areas as well as horizontal.
-    int h = v->getPaintHeight();
-    
-    int repaintWidth = x1 - x0;
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    cerr << "SpectrogramLayer: x0 " << x0 << ", x1 " << x1
-         << ", repaintWidth " << repaintWidth << ", h " << h
-         << ", rightToLeft " << rightToLeft << endl;
-#endif
-
-    sv_samplerate_t sr = m_model->getSampleRate();
-
-    // Set minFreq and maxFreq to the frequency extents of the possibly
-    // zero-padded visible bin range, and displayMinFreq and displayMaxFreq
-    // to the actual scale frequency extents (presumably not zero padded).
-
-    // If we are zero padding, we want to use the zero-padded
-    // equivalents of the bins that we would be using if not zero
-    // padded, to avoid spaces at the top and bottom of the display.
-
-    // Note fftSize is the actual zero-padded fft size, m_fftSize the
-    // nominal fft size.
-    
-    int maxbin = m_fftSize / 2;
-    if (m_maxFrequency > 0) {
-	maxbin = int((double(m_maxFrequency) * m_fftSize) / sr + 0.001);
-	if (maxbin > m_fftSize / 2) maxbin = m_fftSize / 2;
-    }
-
-    int minbin = 1;
-    if (m_minFrequency > 0) {
-	minbin = int((double(m_minFrequency) * m_fftSize) / sr + 0.001);
-//        cerr << "m_minFrequency = " << m_minFrequency << " -> minbin = " << minbin << endl;
-	if (minbin < 1) minbin = 1;
-	if (minbin >= maxbin) minbin = maxbin - 1;
-    }
-
-    int zpl = getZeroPadLevel(v) + 1;
-    minbin = minbin * zpl;
-    maxbin = (maxbin + 1) * zpl - 1;
-
-    double minFreq = (double(minbin) * sr) / fftSize;
-    double maxFreq = (double(maxbin) * sr) / fftSize;
-
-    double displayMinFreq = minFreq;
-    double displayMaxFreq = maxFreq;
-
-    if (fftSize != m_fftSize) {
-        displayMinFreq = getEffectiveMinFrequency();
-        displayMaxFreq = getEffectiveMaxFrequency();
-    }
-
-//    cerr << "(giving actual minFreq " << minFreq << " and display minFreq " << displayMinFreq << ")" << endl;
-
-    int increment = getWindowIncrement();
-    
-    bool logarithmic = (m_frequencyScale == LogFrequencyScale);
-
-    MagnitudeRange overallMag = m_viewMags[v->getId()];
-    bool overallMagChanged = false;
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    cerr << "SpectrogramLayer: " << ((double(v->getFrameForX(1) - v->getFrameForX(0))) / increment) << " bin(s) per pixel" << endl;
-#endif
-
-    if (repaintWidth == 0) {
-        SVDEBUG << "*** NOTE: repaintWidth == 0" << endl;
-    }
-
-    Profiler outerprof("SpectrogramLayer::paint: all cols");
-
-    // The draw buffer contains a fragment at either our pixel
-    // resolution (if there is more than one time-bin per pixel) or
-    // time-bin resolution (if a time-bin spans more than one pixel).
-    // We need to ensure that it starts and ends at points where a
-    // time-bin boundary occurs at an exact pixel boundary, and with a
-    // certain amount of overlap across existing pixels so that we can
-    // scale and draw from it without smoothing errors at the edges.
-
-    // If (getFrameForX(x) / increment) * increment ==
-    // getFrameForX(x), then x is a time-bin boundary.  We want two
-    // such boundaries at either side of the draw buffer -- one which
-    // we draw up to, and one which we subsequently crop at.
-
-    bool bufferIsBinResolution = false;
-    if (increment > zoomLevel) bufferIsBinResolution = true;
-
-    sv_frame_t leftBoundaryFrame = -1, leftCropFrame = -1;
-    sv_frame_t rightBoundaryFrame = -1, rightCropFrame = -1;
-
-    int bufwid;
-
-    if (bufferIsBinResolution) {
-
-        for (int x = x0; ; --x) {
-            sv_frame_t f = v->getFrameForX(x);
-            if ((f / increment) * increment == f) {
-                if (leftCropFrame == -1) leftCropFrame = f;
-                else if (x < x0 - 2) {
-                    leftBoundaryFrame = f;
-                    break;
-                }
-            }
-        }
-        for (int x = x0 + repaintWidth; ; ++x) {
-            sv_frame_t f = v->getFrameForX(x);
-            if ((f / increment) * increment == f) {
-                if (rightCropFrame == -1) rightCropFrame = f;
-                else if (x > x0 + repaintWidth + 2) {
-                    rightBoundaryFrame = f;
-                    break;
-                }
-            }
-        }
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "Left: crop: " << leftCropFrame << " (bin " << leftCropFrame/increment << "); boundary: " << leftBoundaryFrame << " (bin " << leftBoundaryFrame/increment << ")" << endl;
-        cerr << "Right: crop: " << rightCropFrame << " (bin " << rightCropFrame/increment << "); boundary: " << rightBoundaryFrame << " (bin " << rightBoundaryFrame/increment << ")" << endl;
-#endif
-
-        bufwid = int((rightBoundaryFrame - leftBoundaryFrame) / increment);
-
-    } else {
-        
-        bufwid = repaintWidth;
-    }
-
-    vector<int> binforx(bufwid);
-    vector<double> binfory(h);
-    
-    bool usePeaksCache = false;
-
-    if (bufferIsBinResolution) {
-        for (int x = 0; x < bufwid; ++x) {
-            binforx[x] = int(leftBoundaryFrame / increment) + x;
-        }
-        m_drawBuffer = QImage(bufwid, h, QImage::Format_Indexed8);
-    } else {
-        for (int x = 0; x < bufwid; ++x) {
-            double s0 = 0, s1 = 0;
-            if (getXBinRange(v, x + x0, s0, s1)) {
-                binforx[x] = int(s0 + 0.0001);
-            } else {
-                binforx[x] = -1; //???
-            }
-        }
-        if (m_drawBuffer.width() < bufwid || m_drawBuffer.height() != h) {
-            m_drawBuffer = QImage(bufwid, h, QImage::Format_Indexed8);
-        }
-        usePeaksCache = (increment * m_peakCacheDivisor) < zoomLevel;
-        if (m_colourScale == PhaseColourScale) usePeaksCache = false;
-    }
-
-    for (int pixel = 0; pixel < 256; ++pixel) {
-        m_drawBuffer.setColor((unsigned char)pixel,
-                              m_palette.getColour((unsigned char)pixel).rgb());
-    }
-
-    m_drawBuffer.fill(0);
-    int attainedBufwid = bufwid;
-
-    double softTimeLimit;
-
-    if (m_synchronous) {
-
-        // must paint the whole thing for synchronous mode, so give
-        // "no timeout"
-        softTimeLimit = 0.0;
-        
-    } else if (bufferIsBinResolution) {
-        
-        // calculating boundaries later will be too fiddly for partial
-        // paints, and painting should be fast anyway when this is the
-        // case because it means we're well zoomed in
-        softTimeLimit = 0.0;
-
-    } else {
-
-        // neither limitation applies, so use a short soft limit
-
-        if (m_binDisplay == PeakFrequencies) {
-            softTimeLimit = 0.15;
-        } else {
-            softTimeLimit = 0.1;
-        }
-    }
-
-    if (m_binDisplay != PeakFrequencies) {
-
-        for (int y = 0; y < h; ++y) {
-            double q0 = 0, q1 = 0;
-            if (!getSmoothedYBinRange(v, h-y-1, q0, q1)) {
-                binfory[y] = -1;
-            } else {
-                binfory[y] = q0;
-            }
-        }
-
-        attainedBufwid = 
-            paintDrawBuffer(v, bufwid, h, binforx, binfory,
-                            usePeaksCache,
-                            overallMag, overallMagChanged,
-                            rightToLeft,
-                            softTimeLimit);
-
-    } else {
-
-        attainedBufwid = 
-            paintDrawBufferPeakFrequencies(v, bufwid, h, binforx,
-                                           minbin, maxbin,
-                                           displayMinFreq, displayMaxFreq,
-                                           logarithmic,
-                                           overallMag, overallMagChanged,
-                                           rightToLeft,
-                                           softTimeLimit);
-    }
-
-    int failedToRepaint = bufwid - attainedBufwid;
-
-    int paintedLeft = x0;
-    int paintedWidth = x1 - x0;
-    
-    if (failedToRepaint > 0) {
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "SpectrogramLayer::paint(): Failed to repaint " << failedToRepaint << " of " << bufwid
-             << " columns in time (so managed to repaint " << bufwid - failedToRepaint << ")" << endl;
-#endif
-
-        if (rightToLeft) {
-            paintedLeft += failedToRepaint;
-        }
-
-        paintedWidth -= failedToRepaint;
-
-        if (paintedWidth < 0) {
-            paintedWidth = 0;
-        }
-        
-    } else if (failedToRepaint < 0) {
-        cerr << "WARNING: failedToRepaint < 0 (= " << failedToRepaint << ")"
-             << endl;
-        failedToRepaint = 0;
-    }
-
-    if (overallMagChanged) {
-        m_viewMags[v->getId()] = overallMag;
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "SpectrogramLayer: Overall mag is now [" << m_viewMags[v->getId()].getMin() << "->" << m_viewMags[v->getId()].getMax() << "] - will be updating" << endl;
-#endif
-    }
-
-    outerprof.end();
-
-    Profiler profiler2("SpectrogramLayer::paint: draw image");
-
-    if (paintedWidth > 0) {
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "SpectrogramLayer: Copying " << paintedWidth << "x" << h
-                  << " from draw buffer at " << paintedLeft - x0 << "," << 0
-                  << " to " << paintedWidth << "x" << h << " on cache at "
-                  << x0 << "," << 0 << endl;
-#endif
-
-        if (bufferIsBinResolution) {
-
-            int scaledLeft = v->getXForFrame(leftBoundaryFrame);
-            int scaledRight = v->getXForFrame(rightBoundaryFrame);
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-            cerr << "SpectrogramLayer: Rescaling image from " << bufwid
-                 << "x" << h << " to "
-                 << scaledRight-scaledLeft << "x" << h << endl;
-#endif
-
-            Preferences::SpectrogramXSmoothing xsmoothing = 
-                Preferences::getInstance()->getSpectrogramXSmoothing();
-
-            QImage scaled = m_drawBuffer.scaled
-                (scaledRight - scaledLeft, h,
-                 Qt::IgnoreAspectRatio,
-                 ((xsmoothing == Preferences::SpectrogramXInterpolated) ?
-                  Qt::SmoothTransformation : Qt::FastTransformation));
-            
-            int scaledLeftCrop = v->getXForFrame(leftCropFrame);
-            int scaledRightCrop = v->getXForFrame(rightCropFrame);
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-            cerr << "SpectrogramLayer: Drawing image region of width " << scaledRightCrop - scaledLeftCrop << " to "
-                 << scaledLeftCrop << " from " << scaledLeftCrop - scaledLeft << endl;
-#endif
-
-            int targetLeft = scaledLeftCrop;
-            if (targetLeft < 0) {
-                targetLeft = 0;
-            }
-
-            int targetWidth = scaledRightCrop - targetLeft;
-            if (targetLeft + targetWidth > cache.getSize().width()) {
-                targetWidth = cache.getSize().width() - targetLeft;
-            }
-            
-            int sourceLeft = targetLeft - scaledLeft;
-            if (sourceLeft < 0) {
-                sourceLeft = 0;
-            }
-            
-            int sourceWidth = targetWidth;
-
-            if (targetWidth > 0) {
-                cache.drawImage
-                    (targetLeft,
-                     targetWidth,
-                     scaled,
-                     sourceLeft,
-                     sourceWidth);
-            }
-
-        } else {
-
-            cache.drawImage(paintedLeft, paintedWidth,
-                            m_drawBuffer,
-                            paintedLeft - x0, paintedWidth);
-        }
-    }
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    cerr << "SpectrogramLayer: Cache valid area now from " << cache.getValidLeft()
-         << " width " << cache.getValidWidth() << ", height "
-         << cache.getSize().height() << endl;
-#endif
-
-    QRect pr = rect & cache.getValidArea();
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    cerr << "SpectrogramLayer: Copying " << pr.width() << "x" << pr.height()
-              << " from cache at " << pr.x() << "," << pr.y()
-              << " to window" << endl;
-#endif
-
-    paint.drawImage(pr.x(), pr.y(), cache.getImage(),
-                    pr.x(), pr.y(), pr.width(), pr.height());
-
-    if (!m_synchronous) {
-
-        if ((m_normalization != NormalizeVisibleArea) || !overallMagChanged) {
-
-            QRect areaLeft(0, 0, cache.getValidLeft(), h);
-            QRect areaRight(cache.getValidRight(), 0,
-                            cache.getSize().width() - cache.getValidRight(), h);
-
-            bool haveSpaceLeft = (areaLeft.width() > 0);
-            bool haveSpaceRight = (areaRight.width() > 0);
-
-            bool updateLeft = haveSpaceLeft;
-            bool updateRight = haveSpaceRight;
-            
-            if (updateLeft && updateRight) {
-                if (rightToLeft) {
-                    // we just did something adjoining the cache on
-                    // its left side, so now do something on its right
-                    updateLeft = false;
-                } else {
-                    updateRight = false;
-                }
-            }
-            
-            if (updateLeft) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                cerr << "SpectrogramLayer::paint() updating left ("
-                     << areaLeft.x() << ", "
-                     << areaLeft.width() << ")" << endl;
-#endif
-                v->updatePaintRect(areaLeft);
-            }
-            
-            if (updateRight) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                cerr << "SpectrogramLayer::paint() updating right ("
-                     << areaRight.x() << ", "
-                     << areaRight.width() << ")" << endl;
-#endif
-                v->updatePaintRect(areaRight);
-            }
-            
-        } else {
-            // overallMagChanged
-            cerr << "\noverallMagChanged - updating all\n" << endl;
-            cache.invalidate();
-            v->updatePaintRect(v->getPaintRect());
-        }
-    }
+    paintWithRenderer(v, paint, rect);
 
     illuminateLocalFeatures(v, paint);
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    cerr << "SpectrogramLayer::paint() returning" << endl;
-#endif
-}
-
-int
-SpectrogramLayer::paintDrawBufferPeakFrequencies(LayerGeometryProvider *v,
-                                                 int w,
-                                                 int h,
-                                                 const vector<int> &binforx,
-                                                 int minbin,
-                                                 int maxbin,
-                                                 double displayMinFreq,
-                                                 double displayMaxFreq,
-                                                 bool logarithmic,
-                                                 MagnitudeRange &overallMag,
-                                                 bool &overallMagChanged,
-                                                 bool rightToLeft,
-                                                 double softTimeLimit) const
-{
-    Profiler profiler("SpectrogramLayer::paintDrawBufferPeakFrequencies");
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    cerr << "SpectrogramLayer::paintDrawBufferPeakFrequencies: minbin " << minbin << ", maxbin " << maxbin << "; w " << w << ", h " << h << endl;
-#endif
-    if (minbin < 0) minbin = 0;
-    if (maxbin < 0) maxbin = minbin+1;
-
-    FFTModel *fft = getFFTModel(v);
-    if (!fft) return 0;
-
-    FFTModel::PeakSet peakfreqs;
-
-    int psx = -1;
-
-#ifdef __GNUC__
-    float values[maxbin - minbin + 1];
-#else
-    float *values = (float *)alloca((maxbin - minbin + 1) * sizeof(float));
-#endif
-
-    int minColumns = 4;
-    bool haveTimeLimits = (softTimeLimit > 0.0);
-    double hardTimeLimit = softTimeLimit * 2.0;
-    bool overridingSoftLimit = false;
-    auto startTime = chrono::steady_clock::now();
-    
-    int start = 0;
-    int finish = w;
-    int step = 1;
-
-    if (rightToLeft) {
-        start = w-1;
-        finish = -1;
-        step = -1;
-    }
-    
-    int columnCount = 0;
-    
-    for (int x = start; x != finish; x += step) {
-        
-        ++columnCount;
-        
-        if (binforx[x] < 0) continue;
-
-        int sx0 = binforx[x];
-        int sx1 = sx0;
-        if (x+1 < w) sx1 = binforx[x+1];
-        if (sx0 < 0) sx0 = sx1 - 1;
-        if (sx0 < 0) continue;
-        if (sx1 <= sx0) sx1 = sx0 + 1;
-
-        for (int sx = sx0; sx < sx1; ++sx) {
-
-            if (sx < 0 || sx >= int(fft->getWidth())) continue;
-
-            MagnitudeRange mag;
-
-            if (sx != psx) {
-                peakfreqs = fft->getPeakFrequencies(FFTModel::AllPeaks, sx,
-                                                    minbin, maxbin - 1);
-                if (m_colourScale == PhaseColourScale) {
-                    fft->getPhasesAt(sx, values, minbin, maxbin - minbin + 1);
-                } else if (m_normalization == NormalizeColumns) {
-                    fft->getNormalizedMagnitudesAt(sx, values, minbin, maxbin - minbin + 1);
-                } else if (m_normalization == NormalizeHybrid) {
-                    float max = fft->getNormalizedMagnitudesAt
-                        (sx, values, minbin, maxbin - minbin + 1);
-                    float scale = log10f(max + 1.f);
-                    for (int i = minbin; i <= maxbin; ++i) {
-                        values[i - minbin] *= scale;
-                    }
-                } else {
-                    fft->getMagnitudesAt(sx, values, minbin, maxbin - minbin + 1);
-                }
-                psx = sx;
-            }
-
-            for (FFTModel::PeakSet::const_iterator pi = peakfreqs.begin();
-                 pi != peakfreqs.end(); ++pi) {
-
-                int bin = pi->first;
-                double freq = pi->second;
-
-                if (bin < minbin) continue;
-                if (bin > maxbin) break;
-
-                double value = values[bin - minbin];
-
-                if (m_colourScale != PhaseColourScale) {
-                    if (m_normalization != NormalizeColumns) {
-                        value /= (m_fftSize/2.0);
-                    }
-                    mag.sample(float(value));
-                    value *= m_gain;
-                }
-
-                double y = v->getYForFrequency
-                    (freq, displayMinFreq, displayMaxFreq, logarithmic);
-
-                int iy = int(y + 0.5);
-                if (iy < 0 || iy >= h) continue;
-
-                m_drawBuffer.setPixel(x, iy, getDisplayValue(v, value));
-            }
-
-            if (mag.isSet()) {
-                if (sx >= int(m_columnMags.size())) {
-#ifdef DEBUG_SPECTROGRAM
-                    cerr << "INTERNAL ERROR: " << sx << " >= "
-                              << m_columnMags.size()
-                              << " at SpectrogramLayer.cpp::paintDrawBuffer"
-                              << endl;
-#endif
-                } else {
-                    m_columnMags[sx].sample(mag);
-                    if (overallMag.sample(mag)) overallMagChanged = true;
-                }
-            }
-        }
-
-        if (haveTimeLimits) {
-            if (columnCount >= minColumns) {
-                auto t = chrono::steady_clock::now();
-                double diff = chrono::duration<double>(t - startTime).count();
-                if (diff > hardTimeLimit) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                    cerr << "SpectrogramLayer::paintDrawBufferPeakFrequencies: hard limit " << hardTimeLimit << " sec exceeded after "
-                         << columnCount << " columns with time " << diff << endl;
-#endif
-                    return columnCount;
-                } else if (diff > softTimeLimit && !overridingSoftLimit) {
-                    // If we're more than half way through by the time
-                    // we reach the soft limit, ignore it (though
-                    // still respect the hard limit, above). Otherwise
-                    // respect the soft limit and return now.
-                    if (columnCount > w/2) {
-                        overridingSoftLimit = true;
-                    } else {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                        cerr << "SpectrogramLayer::paintDrawBufferPeakFrequencies: soft limit " << softTimeLimit << " sec exceeded after "
-                             << columnCount << " columns with time " << diff << endl;
-#endif
-                        return columnCount;
-                    }
-                }                        
-            }
-        }
-    }
-
-    return columnCount;
-}
-
-int
-SpectrogramLayer::paintDrawBuffer(LayerGeometryProvider *v,
-                                  int w,
-                                  int h,
-                                  const vector<int> &binforx,
-                                  const vector<double> &binfory,
-                                  bool usePeaksCache,
-                                  MagnitudeRange &overallMag,
-                                  bool &overallMagChanged,
-                                  bool rightToLeft,
-                                  double softTimeLimit) const
-{
-    Profiler profiler("SpectrogramLayer::paintDrawBuffer");
-
-    int minbin = int(binfory[0] + 0.0001);
-    int maxbin = int(binfory[h-1]);
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    cerr << "SpectrogramLayer::paintDrawBuffer: minbin " << minbin << ", maxbin " << maxbin << "; w " << w << ", h " << h << endl;
-#endif
-    if (minbin < 0) minbin = 0;
-    if (maxbin < 0) maxbin = minbin+1;
-
-    DenseThreeDimensionalModel *sourceModel = 0;
-    FFTModel *fft = 0;
-    int divisor = 1;
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    cerr << "SpectrogramLayer::paintDrawBuffer: Note: bin display = " << m_binDisplay << ", w = " << w << ", binforx[" << w-1 << "] = " << binforx[w-1] << ", binforx[0] = " << binforx[0] << endl;
-#endif
-    if (usePeaksCache) {
-        sourceModel = getPeakCache(v);
-        divisor = m_peakCacheDivisor;
-        minbin = 0;
-        maxbin = sourceModel->getHeight();
-    } else {
-        sourceModel = fft = getFFTModel(v);
-    }
-
-    if (!sourceModel) return 0;
-
-    bool interpolate = false;
-    Preferences::SpectrogramSmoothing smoothing = 
-        Preferences::getInstance()->getSpectrogramSmoothing();
-    if (smoothing == Preferences::SpectrogramInterpolated ||
-        smoothing == Preferences::SpectrogramZeroPaddedAndInterpolated) {
-        if (m_binDisplay != PeakBins &&
-            m_binDisplay != PeakFrequencies) {
-            interpolate = true;
-        }
-    }
-
-    int psx = -1;
-
-#ifdef __GNUC__
-    float autoarray[maxbin - minbin + 1];
-    float peaks[h];
-#else
-    float *autoarray = (float *)alloca((maxbin - minbin + 1) * sizeof(float));
-    float *peaks = (float *)alloca(h * sizeof(float));
-#endif
-
-    const float *values = autoarray;
-    DenseThreeDimensionalModel::Column c;
-
-    int minColumns = 4;
-    bool haveTimeLimits = (softTimeLimit > 0.0);
-    double hardTimeLimit = softTimeLimit * 2.0;
-    bool overridingSoftLimit = false;
-    auto startTime = chrono::steady_clock::now();
-    
-    int start = 0;
-    int finish = w;
-    int step = 1;
-
-    if (rightToLeft) {
-        start = w-1;
-        finish = -1;
-        step = -1;
-    }
-
-    int columnCount = 0;
-    
-    for (int x = start; x != finish; x += step) {
-
-        // x is the on-canvas pixel coord; sx (later) will be the
-        // source column index
-        
-        ++columnCount;
-        
-        if (binforx[x] < 0) continue;
-
-        float columnMax = 0.f;
-
-        int sx0 = binforx[x] / divisor;
-        int sx1 = sx0;
-        if (x+1 < w) sx1 = binforx[x+1] / divisor;
-        if (sx0 < 0) sx0 = sx1 - 1;
-        if (sx0 < 0) continue;
-        if (sx1 <= sx0) sx1 = sx0 + 1;
-
-        for (int y = 0; y < h; ++y) peaks[y] = 0.f;
-            
-        for (int sx = sx0; sx < sx1; ++sx) {
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-//            cerr << "sx = " << sx << endl;
-#endif
-
-            if (sx < 0 || sx >= int(sourceModel->getWidth())) continue;
-
-            MagnitudeRange mag;
-
-            if (sx != psx) {
-                if (fft) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-//                    cerr << "Retrieving column " << sx << " from fft directly" << endl;
-#endif
-                    if (m_colourScale == PhaseColourScale) {
-                        fft->getPhasesAt(sx, autoarray, minbin, maxbin - minbin + 1);
-                    } else if (m_normalization == NormalizeColumns) {
-                        fft->getNormalizedMagnitudesAt(sx, autoarray, minbin, maxbin - minbin + 1);
-                    } else if (m_normalization == NormalizeHybrid) {
-                        float max = fft->getNormalizedMagnitudesAt
-                            (sx, autoarray, minbin, maxbin - minbin + 1);
-                        float scale = log10f(max + 1.f);
-                        for (int i = minbin; i <= maxbin; ++i) {
-                            autoarray[i - minbin] *= scale;
-                        }
-                    } else {
-                        fft->getMagnitudesAt(sx, autoarray, minbin, maxbin - minbin + 1);
-                    }
-                } else {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-//                    cerr << "Retrieving column " << sx << " from peaks cache" << endl;
-#endif
-                    c = sourceModel->getColumn(sx);
-                    if (m_normalization == NormalizeColumns ||
-                        m_normalization == NormalizeHybrid) {
-                        for (int y = 0; y < h; ++y) {
-                            if (c[y] > columnMax) columnMax = c[y];
-                        }
-                    }
-                    values = c.data() + minbin;
-                }
-                psx = sx;
-            }
-
-            for (int y = 0; y < h; ++y) {
-
-                double sy0 = binfory[y];
-                double sy1 = sy0 + 1;
-                if (y+1 < h) sy1 = binfory[y+1];
-
-                double value = 0.0;
-
-                if (interpolate && fabs(sy1 - sy0) < 1.0) {
-
-                    double centre = (sy0 + sy1) / 2;
-                    double dist = (centre - 0.5) - rint(centre - 0.5);
-                    int bin = int(centre);
-                    int other = (dist < 0 ? (bin-1) : (bin+1));
-                    if (bin < minbin) bin = minbin;
-                    if (bin > maxbin) bin = maxbin;
-                    if (other < minbin || other > maxbin) other = bin;
-                    double prop = 1.0 - fabs(dist);
-
-                    double v0 = values[bin - minbin];
-                    double v1 = values[other - minbin];
-                    if (m_binDisplay == PeakBins) {
-                        if (bin == minbin || bin == maxbin ||
-                            v0 < values[bin-minbin-1] ||
-                            v0 < values[bin-minbin+1]) v0 = 0.0;
-                        if (other == minbin || other == maxbin ||
-                            v1 < values[other-minbin-1] ||
-                            v1 < values[other-minbin+1]) v1 = 0.0;
-                    }
-                    if (v0 == 0.0 && v1 == 0.0) continue;
-                    value = prop * v0 + (1.0 - prop) * v1;
-
-                    if (m_colourScale != PhaseColourScale) {
-                        if (m_normalization != NormalizeColumns &&
-                            m_normalization != NormalizeHybrid) {
-                            value /= (m_fftSize/2.0);
-                        }
-                        mag.sample(float(value));
-                        value *= m_gain;
-                    }
-
-                    peaks[y] = float(value);
-
-                } else {                    
-
-                    int by0 = int(sy0 + 0.0001);
-                    int by1 = int(sy1 + 0.0001);
-                    if (by1 < by0 + 1) by1 = by0 + 1;
-
-                    for (int bin = by0; bin < by1; ++bin) {
-
-                        value = values[bin - minbin];
-                        if (m_binDisplay == PeakBins) {
-                            if (bin == minbin || bin == maxbin ||
-                                value < values[bin-minbin-1] ||
-                                value < values[bin-minbin+1]) continue;
-                        }
-
-                        if (m_colourScale != PhaseColourScale) {
-                            if (m_normalization != NormalizeColumns &&
-                                m_normalization != NormalizeHybrid) {
-                                value /= (m_fftSize/2.0);
-                            }
-                            mag.sample(float(value));
-                            value *= m_gain;
-                        }
-
-                        if (value > peaks[y]) {
-                            peaks[y] = float(value); //!!! not right for phase!
-                        }
-                    }
-                }
-            }
-
-            if (mag.isSet()) {
-                if (sx >= int(m_columnMags.size())) {
-#ifdef DEBUG_SPECTROGRAM
-                    cerr << "INTERNAL ERROR: " << sx << " >= "
-                              << m_columnMags.size()
-                              << " at SpectrogramLayer.cpp::paintDrawBuffer"
-                              << endl;
-#endif
-                } else {
-                    m_columnMags[sx].sample(mag);
-                    if (overallMag.sample(mag)) overallMagChanged = true;
-                }
-            }
-        }
-
-        // at this point we have updated m_columnMags and overallMag
-        // -- used elsewhere for calculating the overall view range
-        // for NormalizeVisibleArea mode -- and calculated "peaks"
-        // (the possibly scaled and interpolated value array from
-        // which we actually draw the column) and "columnMax" (maximum
-        // value used for normalisation)
-        
-        for (int y = 0; y < h; ++y) {
-
-            double peak = peaks[y];
-            
-            if (m_colourScale != PhaseColourScale &&
-                (m_normalization == NormalizeColumns ||
-                 m_normalization == NormalizeHybrid) &&
-                columnMax > 0.f) {
-                peak /= columnMax;
-                if (m_normalization == NormalizeHybrid) {
-                    peak *= log10(columnMax + 1.f);
-                }
-            }
-            
-            unsigned char peakpix = getDisplayValue(v, peak);
-
-            m_drawBuffer.setPixel(x, h-y-1, peakpix);
-        }
-
-        if (haveTimeLimits) {
-            if (columnCount >= minColumns) {
-                auto t = chrono::steady_clock::now();
-                double diff = chrono::duration<double>(t - startTime).count();
-                if (diff > hardTimeLimit) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                    cerr << "SpectrogramLayer::paintDrawBuffer: hard limit " << hardTimeLimit << " sec exceeded after "
-                         << columnCount << " columns with time " << diff << endl;
-#endif
-                    return columnCount;
-                } else if (diff > softTimeLimit && !overridingSoftLimit) {
-                    // If we're more than half way through by the time
-                    // we reach the soft limit, ignore it (though
-                    // still respect the hard limit, above). Otherwise
-                    // respect the soft limit and return now.
-                    if (columnCount > w/2) {
-                        overridingSoftLimit = true;
-                    } else {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                        cerr << "SpectrogramLayer::paintDrawBuffer: soft limit " << softTimeLimit << " sec exceeded after "
-                             << columnCount << " columns with time " << diff << endl;
-#endif
-                        return columnCount;
-                    }
-                }                        
-            }
-        }
-    }
-
-    return columnCount;
 }
 
 void
@@ -2656,8 +1578,10 @@
         return;
     }
 
-//    cerr << "SpectrogramLayer: illuminateLocalFeatures("
-//              << localPos.x() << "," << localPos.y() << ")" << endl;
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+    cerr << "SpectrogramLayer: illuminateLocalFeatures("
+              << localPos.x() << "," << localPos.y() << ")" << endl;
+#endif
 
     double s0, s1;
     double f0, f1;
@@ -2674,8 +1598,10 @@
         int y1 = int(getYForFrequency(v, f1));
         int y0 = int(getYForFrequency(v, f0));
         
-//        cerr << "SpectrogramLayer: illuminate "
-//                  << x0 << "," << y1 << " -> " << x1 << "," << y0 << endl;
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+        cerr << "SpectrogramLayer: illuminate "
+                  << x0 << "," << y1 << " -> " << x1 << "," << y0 << endl;
+#endif
         
         paint.setPen(v->getForeground());
 
@@ -2691,7 +1617,7 @@
     return v->getYForFrequency(frequency,
 			       getEffectiveMinFrequency(),
 			       getEffectiveMaxFrequency(),
-			       m_frequencyScale == LogFrequencyScale);
+			       m_binScale == BinScale::Log);
 }
 
 double
@@ -2700,17 +1626,14 @@
     return v->getFrequencyForY(y,
 			       getEffectiveMinFrequency(),
 			       getEffectiveMaxFrequency(),
-			       m_frequencyScale == LogFrequencyScale);
+			       m_binScale == BinScale::Log);
 }
 
 int
-SpectrogramLayer::getCompletion(LayerGeometryProvider *v) const
+SpectrogramLayer::getCompletion(LayerGeometryProvider *) const
 {
-    const View *view = v->getView();
-    
-    if (m_fftModels.find(view->getId()) == m_fftModels.end()) return 100;
-
-    int completion = m_fftModels[view->getId()]->getCompletion();
+    if (!m_fftModel) return 100;
+    int completion = m_fftModel->getCompletion();
 #ifdef DEBUG_SPECTROGRAM_REPAINT
     cerr << "SpectrogramLayer::getCompletion: completion = " << completion << endl;
 #endif
@@ -2718,11 +1641,10 @@
 }
 
 QString
-SpectrogramLayer::getError(LayerGeometryProvider *v) const
+SpectrogramLayer::getError(LayerGeometryProvider *) const
 {
-    const View *view = v->getView();
-    if (m_fftModels.find(view->getId()) == m_fftModels.end()) return "";
-    return m_fftModels[view->getId()]->getError();
+    if (!m_fftModel) return "";
+    return m_fftModel->getError();
 }
 
 bool
@@ -2732,10 +1654,10 @@
     if (!m_model) return false;
 
     sv_samplerate_t sr = m_model->getSampleRate();
-    min = double(sr) / m_fftSize;
+    min = double(sr) / getFFTSize();
     max = double(sr) / 2;
     
-    logarithmic = (m_frequencyScale == LogFrequencyScale);
+    logarithmic = (m_binScale == BinScale::Log);
     unit = "Hz";
     return true;
 }
@@ -2765,7 +1687,7 @@
 
     if (m_minFrequency == minf && m_maxFrequency == maxf) return true;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     invalidateMagnitudes();
 
     m_minFrequency = minf;
@@ -2817,16 +1739,10 @@
 void
 SpectrogramLayer::measureDoubleClick(LayerGeometryProvider *v, QMouseEvent *e)
 {
-    const View *view = v->getView();
-    ScrollableImageCache &cache = getImageCacheReference(view);
-
-    cerr << "cache width: " << cache.getSize().width() << ", height: "
-         << cache.getSize().height() << endl;
-
-    QImage image = cache.getImage();
-
-    ImageRegionFinder finder;
-    QRect rect = finder.findRegionExtents(&image, e->pos());
+    const Colour3DPlotRenderer *renderer = getRenderer(v);
+    if (!renderer) return;
+
+    QRect rect = renderer->findSimilarRegionExtents(e->pos());
     if (rect.isValid()) {
         MeasureRect mr;
         setMeasureRectFromPixrect(v, mr, rect);
@@ -2894,35 +1810,35 @@
     
     double fundamental = getFrequencyForY(v, cursorPos.y());
 
-    v->drawVisibleText(paint,
+    PaintAssistant::drawVisibleText(v, paint,
                        sw + 2,
                        cursorPos.y() - 2,
                        QString("%1 Hz").arg(fundamental),
-                       View::OutlinedText);
+                       PaintAssistant::OutlinedText);
 
     if (Pitch::isFrequencyInMidiRange(fundamental)) {
         QString pitchLabel = Pitch::getPitchLabelForFrequency(fundamental);
-        v->drawVisibleText(paint,
+        PaintAssistant::drawVisibleText(v, paint,
                            sw + 2,
                            cursorPos.y() + paint.fontMetrics().ascent() + 2,
                            pitchLabel,
-                           View::OutlinedText);
+                           PaintAssistant::OutlinedText);
     }
 
     sv_frame_t frame = v->getFrameForX(cursorPos.x());
     RealTime rt = RealTime::frame2RealTime(frame, m_model->getSampleRate());
     QString rtLabel = QString("%1 s").arg(rt.toText(true).c_str());
     QString frameLabel = QString("%1").arg(frame);
-    v->drawVisibleText(paint,
+    PaintAssistant::drawVisibleText(v, paint,
                        cursorPos.x() - paint.fontMetrics().width(frameLabel) - 2,
                        v->getPaintHeight() - 2,
                        frameLabel,
-                       View::OutlinedText);
-    v->drawVisibleText(paint,
+                       PaintAssistant::OutlinedText);
+    PaintAssistant::drawVisibleText(v, paint,
                        cursorPos.x() + 2,
                        v->getPaintHeight() - 2,
                        rtLabel,
-                       View::OutlinedText);
+                       PaintAssistant::OutlinedText);
 
     int harmonic = 2;
 
@@ -2978,7 +1894,7 @@
 
     QString adjFreqText = "", adjPitchText = "";
 
-    if (m_binDisplay == PeakFrequencies) {
+    if (m_binDisplay == BinDisplay::PeakFrequencies) {
 
 	if (!getAdjustedYBinSourceRange(v, x, y, freqMin, freqMax,
 					adjFreqMin, adjFreqMax)) {
@@ -3090,13 +2006,14 @@
     int fw = paint.fontMetrics().width(tr("43Hz"));
     if (tw < fw) tw = fw;
 
-    int tickw = (m_frequencyScale == LogFrequencyScale ? 10 : 4);
+    int tickw = (m_binScale == BinScale::Log ? 10 : 4);
     
     return cw + tickw + tw + 13;
 }
 
 void
-SpectrogramLayer::paintVerticalScale(LayerGeometryProvider *v, bool detailed, QPainter &paint, QRect rect) const
+SpectrogramLayer::paintVerticalScale(LayerGeometryProvider *v, bool detailed,
+                                     QPainter &paint, QRect rect) const
 {
     if (!m_model || !m_model->isOK()) {
 	return;
@@ -3105,113 +2022,32 @@
     Profiler profiler("SpectrogramLayer::paintVerticalScale");
 
     //!!! cache this?
-
+    
     int h = rect.height(), w = rect.width();
-
-    int tickw = (m_frequencyScale == LogFrequencyScale ? 10 : 4);
-    int pkw = (m_frequencyScale == LogFrequencyScale ? 10 : 0);
-
-    int bins = m_fftSize / 2;
+    int textHeight = paint.fontMetrics().height();
+
+    if (detailed && (h > textHeight * 3 + 10)) {
+        paintDetailedScale(v, paint, rect);
+    }
+    m_haveDetailedScale = detailed;
+
+    int tickw = (m_binScale == BinScale::Log ? 10 : 4);
+    int pkw = (m_binScale == BinScale::Log ? 10 : 0);
+
+    int bins = getFFTSize() / 2;
     sv_samplerate_t sr = m_model->getSampleRate();
 
     if (m_maxFrequency > 0) {
-	bins = int((double(m_maxFrequency) * m_fftSize) / sr + 0.1);
-	if (bins > m_fftSize / 2) bins = m_fftSize / 2;
+	bins = int((double(m_maxFrequency) * getFFTSize()) / sr + 0.1);
+	if (bins > getFFTSize() / 2) bins = getFFTSize() / 2;
     }
 
     int cw = 0;
-
     if (detailed) cw = getColourScaleWidth(paint);
-    int cbw = paint.fontMetrics().width("dB");
 
     int py = -1;
-    int textHeight = paint.fontMetrics().height();
     int toff = -textHeight + paint.fontMetrics().ascent() + 2;
 
-    if (detailed && (h > textHeight * 3 + 10)) {
-
-        int topLines = 2;
-        if (m_colourScale == PhaseColourScale) topLines = 1;
-
-	int ch = h - textHeight * (topLines + 1) - 8;
-//	paint.drawRect(4, textHeight + 4, cw - 1, ch + 1);
-	paint.drawRect(4 + cw - cbw, textHeight * topLines + 4, cbw - 1, ch + 1);
-
-	QString top, bottom;
-        double min = m_viewMags[v->getId()].getMin();
-        double max = m_viewMags[v->getId()].getMax();
-
-        double dBmin = AudioLevel::multiplier_to_dB(min);
-        double dBmax = AudioLevel::multiplier_to_dB(max);
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "paintVerticalScale: for view id " << v->getId()
-             << ": min = " << min << ", max = " << max
-             << ", dBmin = " << dBmin << ", dBmax = " << dBmax << endl;
-#endif
-        
-        if (dBmax < -60.f) dBmax = -60.f;
-        else top = QString("%1").arg(lrint(dBmax));
-
-        if (dBmin < dBmax - 60.f) dBmin = dBmax - 60.f;
-        bottom = QString("%1").arg(lrint(dBmin));
-
-        //!!! & phase etc
-
-        if (m_colourScale != PhaseColourScale) {
-            paint.drawText((cw + 6 - paint.fontMetrics().width("dBFS")) / 2,
-                           2 + textHeight + toff, "dBFS");
-        }
-
-//	paint.drawText((cw + 6 - paint.fontMetrics().width(top)) / 2,
-	paint.drawText(3 + cw - cbw - paint.fontMetrics().width(top),
-		       2 + textHeight * topLines + toff + textHeight/2, top);
-
-	paint.drawText(3 + cw - cbw - paint.fontMetrics().width(bottom),
-		       h + toff - 3 - textHeight/2, bottom);
-
-	paint.save();
-	paint.setBrush(Qt::NoBrush);
-
-        int lasty = 0;
-        int lastdb = 0;
-
-	for (int i = 0; i < ch; ++i) {
-
-            double dBval = dBmin + (((dBmax - dBmin) * i) / (ch - 1));
-            int idb = int(dBval);
-
-            double value = AudioLevel::dB_to_multiplier(dBval);
-            int colour = getDisplayValue(v, value * m_gain);
-
-	    paint.setPen(m_palette.getColour((unsigned char)colour));
-
-            int y = textHeight * topLines + 4 + ch - i;
-
-            paint.drawLine(5 + cw - cbw, y, cw + 2, y);
-
-            if (i == 0) {
-                lasty = y;
-                lastdb = idb;
-            } else if (i < ch - paint.fontMetrics().ascent() &&
-                       idb != lastdb &&
-                       ((abs(y - lasty) > textHeight && 
-                         idb % 10 == 0) ||
-                        (abs(y - lasty) > paint.fontMetrics().ascent() && 
-                         idb % 5 == 0))) {
-                paint.setPen(v->getBackground());
-                QString text = QString("%1").arg(idb);
-                paint.drawText(3 + cw - cbw - paint.fontMetrics().width(text),
-                               y + toff + textHeight/2, text);
-                paint.setPen(v->getForeground());
-                paint.drawLine(5 + cw - cbw, y, 8 + cw - cbw, y);
-                lasty = y;
-                lastdb = idb;
-            }
-	}
-	paint.restore();
-    }
-
     paint.drawLine(cw + 7, 0, cw + 7, h);
 
     int bin = -1;
@@ -3230,10 +2066,10 @@
 	    continue;
 	}
 
-	int freq = int((sr * bin) / m_fftSize);
+	int freq = int((sr * bin) / getFFTSize());
 
 	if (py >= 0 && (vy - py) < textHeight - 1) {
-	    if (m_frequencyScale == LinearFrequencyScale) {
+	    if (m_binScale == BinScale::Linear) {
 		paint.drawLine(w - tickw, h - vy, w, h - vy);
 	    }
 	    continue;
@@ -3251,7 +2087,7 @@
 	py = vy;
     }
 
-    if (m_frequencyScale == LogFrequencyScale) {
+    if (m_binScale == BinScale::Log) {
 
         // piano keyboard
 
@@ -3263,6 +2099,155 @@
     m_haveDetailedScale = detailed;
 }
 
+void
+SpectrogramLayer::paintDetailedScale(LayerGeometryProvider *v,
+                                     QPainter &paint, QRect rect) const
+{
+    // The colour scale
+
+    if (m_colourScale == ColourScaleType::Phase) {
+        paintDetailedScalePhase(v, paint, rect);
+        return;
+    }
+    
+    int h = rect.height();
+    int textHeight = paint.fontMetrics().height();
+    int toff = -textHeight + paint.fontMetrics().ascent() + 2;
+
+    int cw = getColourScaleWidth(paint);
+    int cbw = paint.fontMetrics().width("dB");
+
+    int topLines = 2;
+
+    int ch = h - textHeight * (topLines + 1) - 8;
+//	paint.drawRect(4, textHeight + 4, cw - 1, ch + 1);
+    paint.drawRect(4 + cw - cbw, textHeight * topLines + 4, cbw - 1, ch + 1);
+
+    QString top, bottom;
+    double min = m_viewMags[v->getId()].getMin();
+    double max = m_viewMags[v->getId()].getMax();
+
+    if (min < m_threshold) min = m_threshold;
+    if (max <= min) max = min + 0.1;
+        
+    double dBmin = AudioLevel::multiplier_to_dB(min);
+    double dBmax = AudioLevel::multiplier_to_dB(max);
+
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+    cerr << "paintVerticalScale: for view id " << v->getId()
+         << ": min = " << min << ", max = " << max
+         << ", dBmin = " << dBmin << ", dBmax = " << dBmax << endl;
+#endif
+        
+    if (dBmax < -60.f) dBmax = -60.f;
+    else top = QString("%1").arg(lrint(dBmax));
+
+    if (dBmin < dBmax - 60.f) dBmin = dBmax - 60.f;
+    bottom = QString("%1").arg(lrint(dBmin));
+
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+    cerr << "adjusted dB range to min = " << dBmin << ", max = " << dBmax
+         << endl;
+#endif
+        
+    paint.drawText((cw + 6 - paint.fontMetrics().width("dBFS")) / 2,
+                   2 + textHeight + toff, "dBFS");
+
+    paint.drawText(3 + cw - cbw - paint.fontMetrics().width(top),
+                   2 + textHeight * topLines + toff + textHeight/2, top);
+
+    paint.drawText(3 + cw - cbw - paint.fontMetrics().width(bottom),
+                   h + toff - 3 - textHeight/2, bottom);
+
+    paint.save();
+    paint.setBrush(Qt::NoBrush);
+
+    int lasty = 0;
+    int lastdb = 0;
+
+    for (int i = 0; i < ch; ++i) {
+
+        double dBval = dBmin + (((dBmax - dBmin) * i) / (ch - 1));
+        int idb = int(dBval);
+
+        double value = AudioLevel::dB_to_multiplier(dBval);
+        paint.setPen(getRenderer(v)->getColour(value));
+
+        int y = textHeight * topLines + 4 + ch - i;
+
+        paint.drawLine(5 + cw - cbw, y, cw + 2, y);
+        
+        if (i == 0) {
+            lasty = y;
+            lastdb = idb;
+        } else if (i < ch - paint.fontMetrics().ascent() &&
+                   idb != lastdb &&
+                   ((abs(y - lasty) > textHeight && 
+                     idb % 10 == 0) ||
+                    (abs(y - lasty) > paint.fontMetrics().ascent() && 
+                     idb % 5 == 0))) {
+            paint.setPen(v->getForeground());
+            QString text = QString("%1").arg(idb);
+            paint.drawText(3 + cw - cbw - paint.fontMetrics().width(text),
+                           y + toff + textHeight/2, text);
+            paint.drawLine(5 + cw - cbw, y, 8 + cw - cbw, y);
+            lasty = y;
+            lastdb = idb;
+        }
+    }
+    paint.restore();
+}
+
+void
+SpectrogramLayer::paintDetailedScalePhase(LayerGeometryProvider *v,
+                                          QPainter &paint, QRect rect) const
+{
+    // The colour scale in phase mode
+    
+    int h = rect.height();
+    int textHeight = paint.fontMetrics().height();
+    int toff = -textHeight + paint.fontMetrics().ascent() + 2;
+
+    int cw = getColourScaleWidth(paint);
+
+    // Phase is not measured in dB of course, but this places the
+    // scale at the same position as in the magnitude spectrogram
+    int cbw = paint.fontMetrics().width("dB");
+
+    int topLines = 1;
+
+    int ch = h - textHeight * (topLines + 1) - 8;
+    paint.drawRect(4 + cw - cbw, textHeight * topLines + 4, cbw - 1, ch + 1);
+
+    QString top, bottom, middle;
+    top = QString("%1").arg(QChar(0x3c0)); // pi
+    bottom = "-" + top;
+    middle = "0";
+    
+    double min = -M_PI;
+    double max =  M_PI;
+
+    paint.drawText(3 + cw - cbw - paint.fontMetrics().width(top),
+                   2 + textHeight * topLines + toff + textHeight/2, top);
+
+    paint.drawText(3 + cw - cbw - paint.fontMetrics().width(middle),
+                   2 + textHeight * topLines + ch/2 + toff + textHeight/2, middle);
+
+    paint.drawText(3 + cw - cbw - paint.fontMetrics().width(bottom),
+                   h + toff - 3 - textHeight/2, bottom);
+
+    paint.save();
+    paint.setBrush(Qt::NoBrush);
+
+    for (int i = 0; i < ch; ++i) {
+        double val = min + (((max - min) * i) / (ch - 1));
+        paint.setPen(getRenderer(v)->getColour(val));
+        int y = textHeight * topLines + 4 + ch - i;
+        paint.drawLine(5 + cw - cbw, y, cw + 2, y);
+    }
+    paint.restore();
+}
+
 class SpectrogramRangeMapper : public RangeMapper
 {
 public:
@@ -3327,9 +2312,9 @@
 
     sv_samplerate_t sr = m_model->getSampleRate();
 
-    SpectrogramRangeMapper mapper(sr, m_fftSize);
-
-//    int maxStep = mapper.getPositionForValue((double(sr) / m_fftSize) + 0.001);
+    SpectrogramRangeMapper mapper(sr, getFFTSize());
+
+//    int maxStep = mapper.getPositionForValue((double(sr) / getFFTSize()) + 0.001);
     int maxStep = mapper.getPositionForValue(0);
     int minStep = mapper.getPositionForValue(double(sr) / 2);
 
@@ -3351,7 +2336,7 @@
     double dmin, dmax;
     getDisplayExtents(dmin, dmax);
     
-    SpectrogramRangeMapper mapper(m_model->getSampleRate(), m_fftSize);
+    SpectrogramRangeMapper mapper(m_model->getSampleRate(), getFFTSize());
     int n = mapper.getPositionForValue(dmax - dmin);
 //    SVDEBUG << "SpectrogramLayer::getCurrentVerticalZoomStep: " << n << endl;
     return n;
@@ -3368,12 +2353,12 @@
 //    cerr << "current range " << dmin << " -> " << dmax << ", range " << dmax-dmin << ", mid " << (dmax + dmin)/2 << endl;
     
     sv_samplerate_t sr = m_model->getSampleRate();
-    SpectrogramRangeMapper mapper(sr, m_fftSize);
+    SpectrogramRangeMapper mapper(sr, getFFTSize());
     double newdist = mapper.getValueForPosition(step);
 
     double newmin, newmax;
 
-    if (m_frequencyScale == LogFrequencyScale) {
+    if (m_binScale == BinScale::Log) {
 
         // need to pick newmin and newmax such that
         //
@@ -3429,7 +2414,7 @@
 SpectrogramLayer::getNewVerticalZoomRangeMapper() const
 {
     if (!m_model) return 0;
-    return new SpectrogramRangeMapper(m_model->getSampleRate(), m_fftSize);
+    return new SpectrogramRangeMapper(m_model->getSampleRate(), getFFTSize());
 }
 
 void
@@ -3485,11 +2470,11 @@
 		 "binDisplay=\"%7\" ")
 	.arg(m_minFrequency)
 	.arg(m_maxFrequency)
-	.arg(m_colourScale)
+	.arg(convertFromColourScale(m_colourScale, m_colourScaleMultiple))
 	.arg(m_colourMap)
 	.arg(m_colourRotation)
-	.arg(m_frequencyScale)
-	.arg(m_binDisplay);
+	.arg(int(m_binScale))
+	.arg(int(m_binDisplay));
 
     // New-style normalization attributes, allowing for more types of
     // normalization in future: write out the column normalization
@@ -3497,8 +2482,8 @@
     // area as well afterwards
     
     s += QString("columnNormalization=\"%1\" ")
-        .arg(m_normalization == NormalizeColumns ? "peak" :
-             m_normalization == NormalizeHybrid ? "hybrid" : "none");
+        .arg(m_normalization == ColumnNormalization::Max1 ? "peak" :
+             m_normalization == ColumnNormalization::Hybrid ? "hybrid" : "none");
 
     // Old-style normalization attribute. We *don't* write out
     // normalizeHybrid here because the only release that would accept
@@ -3507,12 +2492,12 @@
     // v2.0+ will look odd in Tony v1.0
     
     s += QString("normalizeColumns=\"%1\" ")
-	.arg(m_normalization == NormalizeColumns ? "true" : "false");
+	.arg(m_normalization == ColumnNormalization::Max1 ? "true" : "false");
 
     // And this applies to both old- and new-style attributes
     
     s += QString("normalizeVisibleArea=\"%1\" ")
-        .arg(m_normalization == NormalizeVisibleArea ? "true" : "false");
+        .arg(m_normalizeVisibleArea ? "true" : "false");
     
     Layer::toXml(stream, indent, extraAttributes + " " + s);
 }
@@ -3560,9 +2545,12 @@
         setMaxFrequency(maxFrequency);
     }
 
-    ColourScale colourScale = (ColourScale)
-	attributes.value("colourScale").toInt(&ok);
-    if (ok) setColourScale(colourScale);
+    auto colourScale = convertToColourScale
+        (attributes.value("colourScale").toInt(&ok));
+    if (ok) {
+        setColourScale(colourScale.first);
+        setColourScaleMultiple(colourScale.second);
+    }
 
     int colourMap = attributes.value("colourScheme").toInt(&ok);
     if (ok) setColourMap(colourMap);
@@ -3570,9 +2558,9 @@
     int colourRotation = attributes.value("colourRotation").toInt(&ok);
     if (ok) setColourRotation(colourRotation);
 
-    FrequencyScale frequencyScale = (FrequencyScale)
+    BinScale binScale = (BinScale)
 	attributes.value("frequencyScale").toInt(&ok);
-    if (ok) setFrequencyScale(frequencyScale);
+    if (ok) setBinScale(binScale);
 
     BinDisplay binDisplay = (BinDisplay)
 	attributes.value("binDisplay").toInt(&ok);
@@ -3587,11 +2575,11 @@
         haveNewStyleNormalization = true;
 
         if (columnNormalization == "peak") {
-            setNormalization(NormalizeColumns);
+            setNormalization(ColumnNormalization::Max1);
         } else if (columnNormalization == "hybrid") {
-            setNormalization(NormalizeHybrid);
+            setNormalization(ColumnNormalization::Hybrid);
         } else if (columnNormalization == "none") {
-            // do nothing
+            setNormalization(ColumnNormalization::None);
         } else {
             cerr << "NOTE: Unknown or unsupported columnNormalization attribute \""
                  << columnNormalization << "\"" << endl;
@@ -3603,29 +2591,27 @@
         bool normalizeColumns =
             (attributes.value("normalizeColumns").trimmed() == "true");
         if (normalizeColumns) {
-            setNormalization(NormalizeColumns);
+            setNormalization(ColumnNormalization::Max1);
         }
 
         bool normalizeHybrid =
             (attributes.value("normalizeHybrid").trimmed() == "true");
         if (normalizeHybrid) {
-            setNormalization(NormalizeHybrid);
+            setNormalization(ColumnNormalization::Hybrid);
         }
     }
 
     bool normalizeVisibleArea =
-	(attributes.value("normalizeVisibleArea").trimmed() == "true");
-    if (normalizeVisibleArea) {
-        setNormalization(NormalizeVisibleArea);
-    }
-
-    if (!haveNewStyleNormalization && m_normalization == NormalizeHybrid) {
+        (attributes.value("normalizeVisibleArea").trimmed() == "true");
+    setNormalizeVisibleArea(normalizeVisibleArea);
+
+    if (!haveNewStyleNormalization && m_normalization == ColumnNormalization::Hybrid) {
         // Tony v1.0 is (and hopefully will remain!) the only released
         // SV-a-like to use old-style attributes when saving sessions
         // that ask for hybrid normalization. It saves them with the
         // wrong gain factor, so hack in a fix for that here -- this
         // gives us backward but not forward compatibility.
-        setGain(m_gain / float(m_fftSize / 2));
+        setGain(m_gain / float(getFFTSize() / 2));
     }
 }
     
--- a/layer/SpectrogramLayer.h	Mon Jun 13 11:44:08 2016 +0100
+++ b/layer/SpectrogramLayer.h	Fri Aug 05 15:05:02 2016 +0100
@@ -18,6 +18,7 @@
 
 #include "SliceableLayer.h"
 #include "base/Window.h"
+#include "base/MagnitudeRange.h"
 #include "base/RealTime.h"
 #include "base/Thread.h"
 #include "base/PropertyContainer.h"
@@ -25,7 +26,9 @@
 #include "data/model/DenseTimeValueModel.h"
 #include "data/model/FFTModel.h"
 
-#include "ScrollableImageCache.h"
+#include "VerticalBinLayer.h"
+#include "ColourScale.h"
+#include "Colour3DPlotRenderer.h"
 
 #include <QMutex>
 #include <QWaitCondition>
@@ -40,13 +43,12 @@
 class FFTModel;
 class Dense3DModelPeakCache;
 
-
 /**
  * SpectrogramLayer represents waveform data (obtained from a
  * DenseTimeValueModel) in spectrogram form.
  */
 
-class SpectrogramLayer : public SliceableLayer,
+class SpectrogramLayer : public VerticalBinLayer,
 			 public PowerOfSqrtTwoZoomConstraint
 {
     Q_OBJECT
@@ -112,9 +114,6 @@
     void setWindowType(WindowType type);
     WindowType getWindowType() const;
 
-    void setZeroPadLevel(int level);
-    int getZeroPadLevel() const;
-
     /**
      * Set the gain multiplier for sample values in this view.
      * The default is 1.0.
@@ -126,7 +125,7 @@
      * Set the threshold for sample values to qualify for being shown
      * in the FFT, in voltage units.
      *
-     * The default is 0.0.
+     * The default is 10^-8 (-80dB).
      */
     void setThreshold(float threshold);
     float getThreshold() const;
@@ -137,56 +136,44 @@
     void setMaxFrequency(int); // 0 -> no maximum
     int getMaxFrequency() const;
 
-    enum ColourScale {
-	LinearColourScale,
-	MeterColourScale,
-        dBSquaredColourScale,
-	dBColourScale,
-	PhaseColourScale
-    };
+    /**
+     * Specify the scale for sample levels.  See ColourScale and
+     * WaveformLayer for comparison and details of meter and dB
+     * scaling.  The default is LogColourScale.
+     */
+    void setColourScale(ColourScaleType);
+    ColourScaleType getColourScale() const;
 
     /**
-     * Specify the scale for sample levels.  See WaveformLayer for
-     * details of meter and dB scaling.  The default is dBColourScale.
+     * Specify multiple factor for colour scale. This is 2.0 for
+     * log-power spectrogram and 1.0 otherwise.
      */
-    void setColourScale(ColourScale);
-    ColourScale getColourScale() const;
-
-    enum FrequencyScale {
-	LinearFrequencyScale,
-	LogFrequencyScale
-    };
+    void setColourScaleMultiple(double);
+    double getColourScaleMultiple() const;
     
     /**
      * Specify the scale for the y axis.
      */
-    void setFrequencyScale(FrequencyScale);
-    FrequencyScale getFrequencyScale() const;
+    void setBinScale(BinScale);
+    BinScale getBinScale() const;
 
-    enum BinDisplay {
-	AllBins,
-	PeakBins,
-	PeakFrequencies
-    };
-    
     /**
      * Specify the processing of frequency bins for the y axis.
      */
     void setBinDisplay(BinDisplay);
     BinDisplay getBinDisplay() const;
 
-    enum Normalization {
-        NoNormalization,
-        NormalizeColumns,
-        NormalizeVisibleArea,
-        NormalizeHybrid
-    };
+    /**
+     * Specify the normalization mode for individual columns.
+     */
+    void setNormalization(ColumnNormalization);
+    ColumnNormalization getNormalization() const;
 
     /**
-     * Specify the normalization mode for bin values.
+     * Specify whether to normalize the visible area.
      */
-    void setNormalization(Normalization);
-    Normalization getNormalization() const;
+    void setNormalizeVisibleArea(bool);
+    bool getNormalizeVisibleArea() const;
 
     /**
      * Specify the colour map. See ColourMapper for the colour map
@@ -214,6 +201,10 @@
     double getYForFrequency(const LayerGeometryProvider *v, double frequency) const;
     double getFrequencyForY(const LayerGeometryProvider *v, int y) const;
 
+    //!!! VerticalBinLayer methods. Note overlap with get*BinRange()
+    double getYForBin(const LayerGeometryProvider *, double bin) const;
+    double getBinForY(const LayerGeometryProvider *, double y) const;
+    
     virtual int getCompletion(LayerGeometryProvider *v) const;
     virtual QString getError(LayerGeometryProvider *v) const;
 
@@ -255,8 +246,6 @@
     int                 m_windowSize;
     WindowType          m_windowType;
     int                 m_windowHopLevel;
-    int                 m_zeroPadLevel;
-    int                 m_fftSize;
     float               m_gain;
     float               m_initialGain;
     float               m_threshold;
@@ -266,56 +255,26 @@
     int                 m_minFrequency;
     int                 m_maxFrequency;
     int                 m_initialMaxFrequency;
-    ColourScale         m_colourScale;
+    ColourScaleType     m_colourScale;
+    double              m_colourScaleMultiple;
     int                 m_colourMap;
     QColor              m_crosshairColour;
-    FrequencyScale      m_frequencyScale;
+    BinScale            m_binScale;
     BinDisplay          m_binDisplay;
-    Normalization       m_normalization;
+    ColumnNormalization m_normalization; // of individual columns
+    bool                m_normalizeVisibleArea;
     int                 m_lastEmittedZoomStep;
     bool                m_synchronous;
 
     mutable bool        m_haveDetailedScale;
 
-    enum { NO_VALUE = 0 }; // colour index for unused pixels
-
-    class Palette
-    {
-    public:
-        QColor getColour(unsigned char index) const {
-            return m_colours[index];
-        }
-    
-        void setColour(unsigned char index, QColor colour) {
-            m_colours[index] = colour;
-        }
-
-    private:
-        QColor m_colours[256];
-    };
-
-    Palette m_palette;
-
-    typedef std::map<int, ScrollableImageCache> ViewImageCache; // key is view id
-    void invalidateImageCaches();
-    mutable ViewImageCache m_imageCaches;
-    ScrollableImageCache &getImageCacheReference(const LayerGeometryProvider *) const;
-
-    /**
-     * When painting, we draw directly onto the draw buffer and then
-     * copy this to the part of the image cache that needed refreshing
-     * before copying the image cache onto the window.  (Remind me why
-     * we don't draw directly onto the cache?)
-     */
-    mutable QImage m_drawBuffer;
+    static std::pair<ColourScaleType, double> convertToColourScale(int value);
+    static int convertFromColourScale(ColourScaleType type, double multiple);
+    static std::pair<ColumnNormalization, bool> convertToColumnNorm(int value);
+    static int convertFromColumnNorm(ColumnNormalization norm, bool visible);
 
     bool m_exiting;
 
-    void initialisePalette();
-    void rotatePalette(int distance);
-
-    unsigned char getDisplayValue(LayerGeometryProvider *v, double input) const;
-
     int getColourScaleWidth(QPainter &) const;
 
     void illuminateLocalFeatures(LayerGeometryProvider *v, QPainter &painter) const;
@@ -323,15 +282,8 @@
     double getEffectiveMinFrequency() const;
     double getEffectiveMaxFrequency() const;
 
-    // Note that the getYBin... methods return the nominal bin in the
-    // un-smoothed spectrogram.  This is not necessarily the same bin
-    // as is pulled from the spectrogram and drawn at the given
-    // position, if the spectrogram has oversampling smoothing.  Use
-    // getSmoothedYBinRange to obtain that.
-
     bool getXBinRange(LayerGeometryProvider *v, int x, double &windowMin, double &windowMax) const;
     bool getYBinRange(LayerGeometryProvider *v, int y, double &freqBinMin, double &freqBinMax) const;
-    bool getSmoothedYBinRange(LayerGeometryProvider *v, int y, double &freqBinMin, double &freqBinMax) const;
 
     bool getYBinSourceRange(LayerGeometryProvider *v, int y, double &freqMin, double &freqMax) const;
     bool getAdjustedYBinSourceRange(LayerGeometryProvider *v, int x, int y,
@@ -347,88 +299,38 @@
         else return m_windowSize / (1 << (m_windowHopLevel - 1));
     }
 
-    int getZeroPadLevel(const LayerGeometryProvider *v) const;
-    int getFFTSize(const LayerGeometryProvider *v) const;
-    FFTModel *getFFTModel(const LayerGeometryProvider *v) const;
-    Dense3DModelPeakCache *getPeakCache(const LayerGeometryProvider *v) const;
-    void invalidateFFTModels();
+    int getFFTOversampling() const;
+    int getFFTSize() const; // m_windowSize * getFFTOversampling()
 
-    typedef std::map<int, FFTModel *> ViewFFTMap; // key is view id
-    typedef std::map<int, Dense3DModelPeakCache *> PeakCacheMap; // key is view id
-    mutable ViewFFTMap m_fftModels;
-    mutable PeakCacheMap m_peakCaches;
+    mutable FFTModel *m_fftModel; //!!! should not be mutable, see getFFTModel()?
+    mutable Dense3DModelPeakCache *m_peakCache;
     const int m_peakCacheDivisor;
-    mutable Model *m_sliceableModel;
-
-    class MagnitudeRange {
-    public:
-        MagnitudeRange() : m_min(0), m_max(0) { }
-        bool operator==(const MagnitudeRange &r) {
-            return r.m_min == m_min && r.m_max == m_max;
-        }
-        bool isSet() const { return (m_min != 0.f || m_max != 0.f); }
-        void set(float min, float max) {
-            m_min = min;
-            m_max = max;
-            if (m_max < m_min) m_max = m_min;
-        }
-        bool sample(float f) {
-            bool changed = false;
-            if (isSet()) {
-                if (f < m_min) { m_min = f; changed = true; }
-                if (f > m_max) { m_max = f; changed = true; }
-            } else {
-                m_max = m_min = f;
-                changed = true;
-            }
-            return changed;
-        }            
-        bool sample(const MagnitudeRange &r) {
-            bool changed = false;
-            if (isSet()) {
-                if (r.m_min < m_min) { m_min = r.m_min; changed = true; }
-                if (r.m_max > m_max) { m_max = r.m_max; changed = true; }
-            } else {
-                m_min = r.m_min;
-                m_max = r.m_max;
-                changed = true;
-            }
-            return changed;
-        }            
-        float getMin() const { return m_min; }
-        float getMax() const { return m_max; }
-    private:
-        float m_min;
-        float m_max;
-    };
 
     typedef std::map<int, MagnitudeRange> ViewMagMap; // key is view id
     mutable ViewMagMap m_viewMags;
-    mutable std::vector<MagnitudeRange> m_columnMags;
+    mutable ViewMagMap m_lastRenderedMags; // when in normalizeVisibleArea mode
     void invalidateMagnitudes();
-    bool updateViewMagnitudes(LayerGeometryProvider *v) const;
-    int paintDrawBuffer(LayerGeometryProvider *v, int w, int h,
-                        const std::vector<int> &binforx,
-                        const std::vector<double> &binfory,
-                        bool usePeaksCache,
-                        MagnitudeRange &overallMag,
-                        bool &overallMagChanged,
-                        bool rightToLeft,
-                        double softTimeLimit) const;
-    int paintDrawBufferPeakFrequencies(LayerGeometryProvider *v, int w, int h,
-                                       const std::vector<int> &binforx,
-                                       int minbin,
-                                       int maxbin,
-                                       double displayMinFreq,
-                                       double displayMaxFreq,
-                                       bool logarithmic,
-                                       MagnitudeRange &overallMag,
-                                       bool &overallMagChanged,
-                                       bool rightToLeft,
-                                       double softTimeLimit) const;
 
-    virtual void updateMeasureRectYCoords(LayerGeometryProvider *v, const MeasureRect &r) const;
-    virtual void setMeasureRectYCoord(LayerGeometryProvider *v, MeasureRect &r, bool start, int y) const;
+    typedef std::map<int, Colour3DPlotRenderer *> ViewRendererMap; // key is view id
+    mutable ViewRendererMap m_renderers;
+    Colour3DPlotRenderer *getRenderer(LayerGeometryProvider *) const;
+    void invalidateRenderers();
+    
+    FFTModel *getFFTModel() const;
+    Dense3DModelPeakCache *getPeakCache() const;
+    void invalidateFFTModel();
+
+    void paintWithRenderer(LayerGeometryProvider *v, QPainter &paint, QRect rect) const;
+
+    void paintDetailedScale(LayerGeometryProvider *v,
+                            QPainter &paint, QRect rect) const;
+    void paintDetailedScalePhase(LayerGeometryProvider *v,
+                                 QPainter &paint, QRect rect) const;
+    
+    virtual void updateMeasureRectYCoords(LayerGeometryProvider *v,
+                                          const MeasureRect &r) const;
+    virtual void setMeasureRectYCoord(LayerGeometryProvider *v,
+                                      MeasureRect &r, bool start, int y) const;
 };
 
 #endif
--- a/layer/SpectrumLayer.cpp	Mon Jun 13 11:44:08 2016 +0100
+++ b/layer/SpectrumLayer.cpp	Fri Aug 05 15:05:02 2016 +0100
@@ -21,7 +21,9 @@
 #include "base/Preferences.h"
 #include "base/RangeMapper.h"
 #include "base/Pitch.h"
+
 #include "ColourMapper.h"
+#include "PaintAssistant.h"
 
 #include <QPainter>
 #include <QTextStream>
@@ -491,19 +493,19 @@
     int hoffset = 2;
     if (m_binScale == LogBins) hoffset = 13;
 
-    v->drawVisibleText(paint,
+    PaintAssistant::drawVisibleText(v, paint,
                        cursorPos.x() + 2,
                        v->getPaintHeight() - 2 - hoffset,
                        QString("%1 Hz").arg(fundamental),
-                       View::OutlinedText);
+                       PaintAssistant::OutlinedText);
 
     if (Pitch::isFrequencyInMidiRange(fundamental)) {
         QString pitchLabel = Pitch::getPitchLabelForFrequency(fundamental);
-        v->drawVisibleText(paint,
+        PaintAssistant::drawVisibleText(v, paint,
                            cursorPos.x() - paint.fontMetrics().width(pitchLabel) - 2,
                            v->getPaintHeight() - 2 - hoffset,
                            pitchLabel,
-                           View::OutlinedText);
+                           PaintAssistant::OutlinedText);
     }
 
     double value = getValueForY(cursorPos.y(), v);
@@ -512,17 +514,17 @@
     if (value > 0.0) db = 10.0 * log10(value);
     if (db < thresh) db = thresh;
 
-    v->drawVisibleText(paint,
+    PaintAssistant::drawVisibleText(v, paint,
                        xorigin + 2,
                        cursorPos.y() - 2,
                        QString("%1 V").arg(value),
-                       View::OutlinedText);
+                       PaintAssistant::OutlinedText);
 
-    v->drawVisibleText(paint,
+    PaintAssistant::drawVisibleText(v, paint,
                        xorigin + 2,
                        cursorPos.y() + 2 + paint.fontMetrics().ascent(),
                        QString("%1 dBV").arg(db),
-                       View::OutlinedText);
+                       PaintAssistant::OutlinedText);
     
     int harmonic = 2;
 
--- a/layer/TimeInstantLayer.cpp	Mon Jun 13 11:44:08 2016 +0100
+++ b/layer/TimeInstantLayer.cpp	Fri Aug 05 15:05:02 2016 +0100
@@ -20,7 +20,9 @@
 #include "view/View.h"
 #include "base/Profiler.h"
 #include "base/Clipboard.h"
+
 #include "ColourDatabase.h"
+#include "PaintAssistant.h"
 
 #include "data/model/SparseOneDimensionalModel.h"
 
@@ -456,7 +458,7 @@
 	    }
 
 	    if (good) {
-                v->drawVisibleText(paint, x + iw + 2, textY, p.label, View::OutlinedText);
+                PaintAssistant::drawVisibleText(v, paint, x + iw + 2, textY, p.label, PaintAssistant::OutlinedText);
 //		paint.drawText(x + iw + 2, textY, p.label);
 	    }
 	}
--- a/layer/TimeRulerLayer.cpp	Mon Jun 13 11:44:08 2016 +0100
+++ b/layer/TimeRulerLayer.cpp	Fri Aug 05 15:05:02 2016 +0100
@@ -19,8 +19,10 @@
 
 #include "data/model/Model.h"
 #include "base/RealTime.h"
+#include "view/View.h"
+
 #include "ColourDatabase.h"
-#include "view/View.h"
+#include "PaintAssistant.h"
 
 #include <QPainter>
 
@@ -324,7 +326,7 @@
                     // backmost layer, don't worry about outlining the text
                     paint.drawText(x+2 - tw/2, y, text);
                 } else {
-                    v->drawVisibleText(paint, x+2 - tw/2, y, text, View::OutlinedText);
+                    PaintAssistant::drawVisibleText(v, paint, x+2 - tw/2, y, text, PaintAssistant::OutlinedText);
                 }
             }
         }
--- a/layer/TimeValueLayer.cpp	Mon Jun 13 11:44:08 2016 +0100
+++ b/layer/TimeValueLayer.cpp	Fri Aug 05 15:05:02 2016 +0100
@@ -21,7 +21,6 @@
 #include "base/LogRange.h"
 #include "base/RangeMapper.h"
 #include "base/Pitch.h"
-#include "ColourDatabase.h"
 #include "view/View.h"
 
 #include "data/model/SparseTimeValueModel.h"
@@ -31,12 +30,14 @@
 #include "widgets/ListInputDialog.h"
 #include "widgets/TextAbbrev.h"
 
+#include "ColourDatabase.h"
 #include "ColourMapper.h"
 #include "PianoScale.h"
 #include "LinearNumericalScale.h"
 #include "LogNumericalScale.h"
 #include "LinearColourScale.h"
 #include "LogColourScale.h"
+#include "PaintAssistant.h"
 
 #include <QPainter>
 #include <QPainterPath>
@@ -1214,10 +1215,10 @@
                 if (haveRoom ||
                     (!haveNext &&
                      (pointCount == 0 || !italic))) {
-                    v->drawVisibleText(paint, x + 5, textY, label,
+                    PaintAssistant::drawVisibleText(v, paint, x + 5, textY, label,
                                        italic ?
-                                       View::OutlinedItalicText :
-                                       View::OutlinedText);
+                                       PaintAssistant::OutlinedItalicText :
+                                       PaintAssistant::OutlinedText);
                 }
             }
         }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/VerticalBinLayer.h	Fri Aug 05 15:05:02 2016 +0100
@@ -0,0 +1,62 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006-2016 Chris Cannam and QMUL.
+    
+    This program 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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef VERTICAL_BIN_LAYER_H
+#define VERTICAL_BIN_LAYER_H
+
+#include "SliceableLayer.h"
+
+/**
+ * Interface for layers in which the Y axis corresponds to bin number
+ * rather than scale value. Colour3DPlotLayer is the obvious example.
+ * Conceptually these are always SliceableLayers as well, and this
+ * subclasses from SliceableLayer to avoid a big inheritance mess.
+ */
+class VerticalBinLayer : public SliceableLayer
+{
+public:
+    /**
+     * Return the y coordinate at which the given bin "starts"
+     * (i.e. at the bottom of the bin, if the given bin is an integer
+     * and the vertical scale is the usual way up). Bin number may be
+     * fractional, to obtain a position part-way through a bin.
+     */
+    virtual double getYForBin(const LayerGeometryProvider *, double bin) const = 0;
+
+    /**
+     * As getYForBin, but rounding to integer values.
+     */
+    virtual int getIYForBin(const LayerGeometryProvider *v, int bin) const {
+        return int(round(getYForBin(v, bin)));
+    }
+    
+    /**
+     * Return the bin number, possibly fractional, at the given y
+     * coordinate. Note that the whole numbers occur at the positions
+     * at which the bins "start" (i.e. the bottom of the visible bin,
+     * if the vertical scale is the usual way up).
+     */
+    virtual double getBinForY(const LayerGeometryProvider *, double y) const = 0;
+
+    /**
+     * As getBinForY, but rounding to integer values.
+     */
+    virtual int getIBinForY(const LayerGeometryProvider *v, int y) const {
+        return int(floor(getBinForY(v, y)));
+    }
+};
+
+#endif
+
--- a/layer/VerticalScaleLayer.h	Mon Jun 13 11:44:08 2016 +0100
+++ b/layer/VerticalScaleLayer.h	Fri Aug 05 15:05:02 2016 +0100
@@ -16,6 +16,12 @@
 #ifndef VERTICAL_SCALE_LAYER_H
 #define VERTICAL_SCALE_LAYER_H
 
+/**
+ * Interface for layers in which the Y axis represents (or can
+ * sometimes represent, depending on the display mode) the sample
+ * value. For example, TimeValueLayer uses vertical scale when in
+ * point mode and so provides this interface.
+ */
 class VerticalScaleLayer
 {
 public:
--- a/layer/WaveformLayer.cpp	Mon Jun 13 11:44:08 2016 +0100
+++ b/layer/WaveformLayer.cpp	Fri Aug 05 15:05:02 2016 +0100
@@ -19,7 +19,9 @@
 #include "view/View.h"
 #include "base/Profiler.h"
 #include "base/RangeMapper.h"
+
 #include "ColourDatabase.h"
+#include "PaintAssistant.h"
 
 #include <QPainter>
 #include <QPixmap>
--- a/svgui.pro	Mon Jun 13 11:44:08 2016 +0100
+++ b/svgui.pro	Fri Aug 05 15:05:02 2016 +0100
@@ -39,14 +39,17 @@
 MOC_DIR = o
 
 HEADERS += layer/Colour3DPlotLayer.h \
+	   layer/Colour3DPlotRenderer.h \
 	   layer/ColourDatabase.h \
 	   layer/ColourMapper.h \
+           layer/ColourScale.h \
            layer/ColourScaleLayer.h \
            layer/FlexiNoteLayer.h \
            layer/ImageLayer.h \
            layer/ImageRegionFinder.h \
            layer/Layer.h \
            layer/LayerFactory.h \
+           layer/LayerGeometryProvider.h \
            layer/LinearNumericalScale.h \
            layer/LogNumericalScale.h \
            layer/LinearColourScale.h \
@@ -55,7 +58,9 @@
            layer/PaintAssistant.h \
            layer/PianoScale.h \
            layer/RegionLayer.h \
+           layer/RenderTimer.h \
            layer/ScrollableImageCache.h \
+           layer/ScrollableMagRangeCache.h \
            layer/SingleColourLayer.h \
            layer/SliceableLayer.h \
            layer/SliceLayer.h \
@@ -68,8 +73,10 @@
            layer/VerticalScaleLayer.h \
            layer/WaveformLayer.h
 SOURCES += layer/Colour3DPlotLayer.cpp \
+	   layer/Colour3DPlotRenderer.cpp \
 	   layer/ColourDatabase.cpp \
 	   layer/ColourMapper.cpp \
+	   layer/ColourScale.cpp \
            layer/FlexiNoteLayer.cpp \
            layer/ImageLayer.cpp \
            layer/ImageRegionFinder.cpp \
@@ -84,6 +91,7 @@
            layer/PianoScale.cpp \
            layer/RegionLayer.cpp \
            layer/ScrollableImageCache.cpp \
+           layer/ScrollableMagRangeCache.cpp \
            layer/SingleColourLayer.cpp \
            layer/SliceLayer.cpp \
            layer/SpectrogramLayer.cpp \
--- a/view/LayerGeometryProvider.h	Mon Jun 13 11:44:08 2016 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,174 +0,0 @@
-/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
-
-/*
-    Sonic Visualiser
-    An audio file viewer and annotation editor.
-    Centre for Digital Music, Queen Mary, University of London.
-    
-    This program 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.  See the file
-    COPYING included with this distribution for more information.
-*/
-
-#ifndef LAYER_GEOMETRY_PROVIDER_H
-#define LAYER_GEOMETRY_PROVIDER_H
-
-#include "base/BaseTypes.h"
-
-#include <QMutex>
-#include <QMutexLocker>
-#include <QPainter>
-
-class ViewManager;
-class View;
-class Layer;
-
-class LayerGeometryProvider
-{
-protected:
-    static int getNextId() {
-        static QMutex idMutex;
-        static int nextId = 1;
-        static int maxId = INT_MAX;
-        QMutexLocker locker(&idMutex);
-        int id = nextId;
-        if (nextId == maxId) {
-            // we don't expect this to happen in the lifetime of a
-            // process, but it would be undefined behaviour if it did
-            // since we're using a signed int, so we should really
-            // guard for it...
-            nextId = 1;
-        } else {
-            nextId++;
-        }
-        return id;
-    }            
-    
-public:
-    LayerGeometryProvider() { }
-    
-    /**
-     * Retrieve the id of this object.
-     */
-    virtual int getId() const = 0;
-
-    /**
-     * Retrieve the first visible sample frame on the widget.
-     * This is a calculated value based on the centre-frame, widget
-     * width and zoom level.  The result may be negative.
-     */
-    virtual sv_frame_t getStartFrame() const = 0;
-
-    /**
-     * Return the centre frame of the visible widget.  This is an
-     * exact value that does not depend on the zoom block size.  Other
-     * frame values (start, end) are calculated from this based on the
-     * zoom and other factors.
-     */
-    virtual sv_frame_t getCentreFrame() const = 0;
-
-    /**
-     * Retrieve the last visible sample frame on the widget.
-     * This is a calculated value based on the centre-frame, widget
-     * width and zoom level.
-     */
-    virtual sv_frame_t getEndFrame() const = 0;
-
-    /**
-     * Return the pixel x-coordinate corresponding to a given sample
-     * frame (which may be negative).
-     */
-    virtual int getXForFrame(sv_frame_t frame) const = 0;
-
-    /**
-     * Return the closest frame to the given pixel x-coordinate.
-     */
-    virtual sv_frame_t getFrameForX(int x) const = 0;
-
-    virtual sv_frame_t getModelsStartFrame() const = 0;
-    virtual sv_frame_t getModelsEndFrame() const = 0;
-
-    /**
-     * Return the closest pixel x-coordinate corresponding to a given
-     * view x-coordinate.
-     */
-    virtual int getXForViewX(int viewx) const = 0;
-    
-    /**
-     * Return the closest view x-coordinate corresponding to a given
-     * pixel x-coordinate.
-     */
-    virtual int getViewXForX(int x) const = 0;
-    
-    /**
-     * Return the pixel y-coordinate corresponding to a given
-     * frequency, if the frequency range is as specified.  This does
-     * not imply any policy about layer frequency ranges, but it might
-     * be useful for layers to match theirs up if desired.
-     *
-     * Not thread-safe in logarithmic mode.  Call only from GUI thread.
-     */
-    virtual double getYForFrequency(double frequency, double minFreq, double maxFreq, 
-                                    bool logarithmic) const = 0;
-
-    /**
-     * Return the closest frequency to the given pixel y-coordinate,
-     * if the frequency range is as specified.
-     *
-     * Not thread-safe in logarithmic mode.  Call only from GUI thread.
-     */
-    virtual double getFrequencyForY(int y, double minFreq, double maxFreq,
-			   bool logarithmic) const = 0;
-
-    virtual int getTextLabelHeight(const Layer *layer, QPainter &) const = 0;
-
-    virtual bool getValueExtents(QString unit, double &min, double &max,
-                                 bool &log) const = 0;
-
-    /**
-     * Return the zoom level, i.e. the number of frames per pixel
-     */
-    virtual int getZoomLevel() const = 0;
-
-    /**
-     * To be called from a layer, to obtain the extent of the surface
-     * that the layer is currently painting to. This may be the extent
-     * of the view (if 1x display scaling is in effect) or of a larger
-     * cached pixmap (if greater display scaling is in effect).
-     */
-    virtual QRect getPaintRect() const = 0;
-
-    virtual QSize getPaintSize() const { return getPaintRect().size(); }
-    virtual int getPaintWidth() const { return getPaintRect().width(); }
-    virtual int getPaintHeight() const { return getPaintRect().height(); }
-
-    virtual bool hasLightBackground() const = 0;
-    virtual QColor getForeground() const = 0;
-    virtual QColor getBackground() const = 0;
-
-    virtual ViewManager *getViewManager() const = 0;
-
-    virtual bool shouldIlluminateLocalFeatures(const Layer *, QPoint &) const = 0;
-    virtual bool shouldShowFeatureLabels() const = 0;
-
-    enum TextStyle {
-	BoxedText,
-	OutlinedText,
-        OutlinedItalicText
-    };
-
-    virtual void drawVisibleText(QPainter &p, int x, int y,
-				 QString text, TextStyle style) const = 0;
-
-    virtual void drawMeasurementRect(QPainter &p, const Layer *,
-                                     QRect rect, bool focus) const = 0;
-
-    virtual void updatePaintRect(QRect r) = 0;
-    
-    virtual View *getView() = 0;
-    virtual const View *getView() const = 0;
-};
-
-#endif
--- a/view/Pane.cpp	Mon Jun 13 11:44:08 2016 +0100
+++ b/view/Pane.cpp	Fri Aug 05 15:05:02 2016 +0100
@@ -25,6 +25,7 @@
 #include "base/Preferences.h"
 #include "layer/WaveformLayer.h"
 #include "layer/TimeRulerLayer.h"
+#include "layer/PaintAssistant.h"
 
 // GF: added so we can propagate the mouse move event to the note layer for context handling.
 #include "layer/LayerFactory.h"
@@ -777,14 +778,14 @@
             int tw = paint.fontMetrics().width(text);
             int x = width()/2 - 4 - tw;
             
-            drawVisibleText(paint, x, y, text, OutlinedText);
+            PaintAssistant::drawVisibleText(this, paint, x, y, text, PaintAssistant::OutlinedText);
         }
         
         QString text = QString("%1").arg(m_centreFrame);
         
         int x = width()/2 + 4;
         
-        drawVisibleText(paint, x, y, text, OutlinedText);
+        PaintAssistant::drawVisibleText(this, paint, x, y, text, PaintAssistant::OutlinedText);
     }
 }
 
@@ -866,8 +867,8 @@
         return;
     }
     
-    drawVisibleText(paint, m_scaleWidth + 5,
-                    paint.fontMetrics().ascent() + y, text, OutlinedText);
+    PaintAssistant::drawVisibleText(this, paint, m_scaleWidth + 5,
+                    paint.fontMetrics().ascent() + y, text, PaintAssistant::OutlinedText);
 
     paint.restore();
 }
@@ -905,8 +906,8 @@
         return;
     }
     
-    drawVisibleText(paint, m_scaleWidth + 5,
-                    paint.fontMetrics().ascent() + y, text, OutlinedText);
+    PaintAssistant::drawVisibleText(this, paint, m_scaleWidth + 5,
+                    paint.fontMetrics().ascent() + y, text, PaintAssistant::OutlinedText);
 
     paint.restore();
 }
@@ -954,9 +955,9 @@
                 paint.setPen(getForeground());
             }
             
-            drawVisibleText(paint, llx,
+            PaintAssistant::drawVisibleText(this, paint, llx,
                             lly - fontHeight + fontAscent,
-                            texts[i], OutlinedText);
+                            texts[i], PaintAssistant::OutlinedText);
 
             if (!pixmaps[i].isNull()) {
                 paint.drawPixmap(llx - fontAscent - 3,
@@ -1018,10 +1019,10 @@
             offsetText = tr("+%1").arg(offsetText);
         }
     }
-    drawVisibleText(paint, p0 + 2, fontAscent + fontHeight + 4, startText, OutlinedText);
-    drawVisibleText(paint, p1 + 2, fontAscent + fontHeight + 4, endText, OutlinedText);
-    drawVisibleText(paint, p0 + 2, fontAscent + fontHeight*2 + 4, offsetText, OutlinedText);
-    drawVisibleText(paint, p1 + 2, fontAscent + fontHeight*2 + 4, offsetText, OutlinedText);
+    PaintAssistant::drawVisibleText(this, paint, p0 + 2, fontAscent + fontHeight + 4, startText, PaintAssistant::OutlinedText);
+    PaintAssistant::drawVisibleText(this, paint, p1 + 2, fontAscent + fontHeight + 4, endText, PaintAssistant::OutlinedText);
+    PaintAssistant::drawVisibleText(this, paint, p0 + 2, fontAscent + fontHeight*2 + 4, offsetText, PaintAssistant::OutlinedText);
+    PaintAssistant::drawVisibleText(this, paint, p1 + 2, fontAscent + fontHeight*2 + 4, offsetText, PaintAssistant::OutlinedText);
     
     //!!! duplicating display policy with View::drawSelections
     
@@ -1084,9 +1085,9 @@
     if (x < pbw + 5) x = pbw + 5;
 
     if (r.x() < x + paint.fontMetrics().width(desc)) {
-        drawVisibleText(paint, x,
+        PaintAssistant::drawVisibleText(this, paint, x,
                         height() - fontHeight + fontAscent - 6,
-                        desc, OutlinedText);
+                        desc, PaintAssistant::OutlinedText);
     }
 }
 
--- a/view/View.cpp	Mon Jun 13 11:44:08 2016 +0100
+++ b/view/View.cpp	Fri Aug 05 15:05:02 2016 +0100
@@ -24,6 +24,8 @@
 
 #include "layer/TimeRulerLayer.h"
 #include "layer/SingleColourLayer.h"
+#include "layer/PaintAssistant.h"
+
 #include "data/model/PowerOfSqrtTwoZoomConstraint.h"
 #include "data/model/RangeSummarisableTimeValueModel.h"
 
@@ -413,12 +415,12 @@
 }
 
 double
-View::getFrequencyForY(int y,
+View::getFrequencyForY(double y,
 		       double minf,
 		       double maxf,
 		       bool logarithmic) const
 {
-    int h = height();
+    double h = height();
 
     if (logarithmic) {
 
@@ -810,56 +812,6 @@
 }
 
 void
-View::drawVisibleText(QPainter &paint, int x, int y, QString text, TextStyle style) const
-{
-    if (style == OutlinedText || style == OutlinedItalicText) {
-
-        paint.save();
-
-        if (style == OutlinedItalicText) {
-            QFont f(paint.font());
-            f.setItalic(true);
-            paint.setFont(f);
-        }
-
-        QColor penColour, surroundColour, boxColour;
-
-        penColour = getForeground();
-        surroundColour = getBackground();
-        boxColour = surroundColour;
-        boxColour.setAlpha(127);
-
-        paint.setPen(Qt::NoPen);
-        paint.setBrush(boxColour);
-        
-        QRect r = paint.fontMetrics().boundingRect(text);
-        r.translate(QPoint(x, y));
-//        cerr << "drawVisibleText: r = " << r.x() << "," <<r.y() << " " << r.width() << "x" << r.height() << endl;
-        paint.drawRect(r);
-        paint.setBrush(Qt::NoBrush);
-
-	paint.setPen(surroundColour);
-
-	for (int dx = -1; dx <= 1; ++dx) {
-	    for (int dy = -1; dy <= 1; ++dy) {
-		if (!(dx || dy)) continue;
-		paint.drawText(x + dx, y + dy, text);
-	    }
-	}
-
-	paint.setPen(penColour);
-
-	paint.drawText(x, y, text);
-
-        paint.restore();
-
-    } else {
-
-	cerr << "ERROR: View::drawVisibleText: Boxed style not yet implemented!" << endl;
-    }
-}
-
-void
 View::setPlaybackFollow(PlaybackFollowMode m)
 {
     m_followPlay = m;
@@ -2369,32 +2321,32 @@
     }
     
     if (axs != "") {
-        drawVisibleText(paint, axx, axy, axs, OutlinedText);
+        PaintAssistant::drawVisibleText(this, paint, axx, axy, axs, PaintAssistant::OutlinedText);
         axy += fontHeight;
     }
     
     if (ays != "") {
-        drawVisibleText(paint, axx, axy, ays, OutlinedText);
+        PaintAssistant::drawVisibleText(this, paint, axx, axy, ays, PaintAssistant::OutlinedText);
         axy += fontHeight;
     }
 
     if (bxs != "") {
-        drawVisibleText(paint, bxx, bxy, bxs, OutlinedText);
+        PaintAssistant::drawVisibleText(this, paint, bxx, bxy, bxs, PaintAssistant::OutlinedText);
         bxy += fontHeight;
     }
 
     if (bys != "") {
-        drawVisibleText(paint, bxx, bxy, bys, OutlinedText);
+        PaintAssistant::drawVisibleText(this, paint, bxx, bxy, bys, PaintAssistant::OutlinedText);
         bxy += fontHeight;
     }
 
     if (dxs != "") {
-        drawVisibleText(paint, dxx, dxy, dxs, OutlinedText);
+        PaintAssistant::drawVisibleText(this, paint, dxx, dxy, dxs, PaintAssistant::OutlinedText);
         dxy += fontHeight;
     }
 
     if (dys != "") {
-        drawVisibleText(paint, dxx, dxy, dys, OutlinedText);
+        PaintAssistant::drawVisibleText(this, paint, dxx, dxy, dys, PaintAssistant::OutlinedText);
         dxy += fontHeight;
     }
 
--- a/view/View.h	Mon Jun 13 11:44:08 2016 +0100
+++ b/view/View.h	Fri Aug 05 15:05:02 2016 +0100
@@ -19,7 +19,7 @@
 #include <QFrame>
 #include <QProgressBar>
 
-#include "LayerGeometryProvider.h"
+#include "layer/LayerGeometryProvider.h"
 
 #include "base/ZoomConstraint.h"
 #include "base/PropertyContainer.h"
@@ -144,8 +144,8 @@
      *
      * Not thread-safe in logarithmic mode.  Call only from GUI thread.
      */
-    double getFrequencyForY(int y, double minFreq, double maxFreq,
-			   bool logarithmic) const;
+    double getFrequencyForY(double y, double minFreq, double maxFreq,
+                            bool logarithmic) const;
 
     /**
      * Return the zoom level, i.e. the number of frames per pixel
@@ -266,9 +266,6 @@
     virtual QColor getForeground() const;
     virtual QColor getBackground() const;
 
-    virtual void drawVisibleText(QPainter &p, int x, int y,
-				 QString text, TextStyle style) const;
-
     virtual void drawMeasurementRect(QPainter &p, const Layer *,
                                      QRect rect, bool focus) const;
 
--- a/view/ViewProxy.h	Mon Jun 13 11:44:08 2016 +0100
+++ b/view/ViewProxy.h	Fri Aug 05 15:05:02 2016 +0100
@@ -15,7 +15,7 @@
 #ifndef VIEW_PROXY_H
 #define VIEW_PROXY_H
 
-#include "LayerGeometryProvider.h"
+#include "layer/LayerGeometryProvider.h"
 
 class ViewProxy : public LayerGeometryProvider
 {
@@ -63,14 +63,10 @@
 	return m_scaleFactor *
 	    m_view->getYForFrequency(frequency, minFreq, maxFreq, logarithmic);
     }
-    virtual double getFrequencyForY(int y, double minFreq, double maxFreq,
+    virtual double getFrequencyForY(double y, double minFreq, double maxFreq,
 				    bool logarithmic) const {
-        double f0 = m_view->getFrequencyForY
+        return m_view->getFrequencyForY
             (y / m_scaleFactor, minFreq, maxFreq, logarithmic);
-        if (m_scaleFactor == 1) return f0;
-        double f1 = m_view->getFrequencyForY
-            ((y / m_scaleFactor) + 1, minFreq, maxFreq, logarithmic);
-        return f0 + ((f1 - f0) * (y % m_scaleFactor)) / m_scaleFactor;
     }
     virtual int getTextLabelHeight(const Layer *layer, QPainter &paint) const {
 	return m_scaleFactor * m_view->getTextLabelHeight(layer, paint);
@@ -128,11 +124,6 @@
 	return m_view->shouldShowFeatureLabels();
     }
 
-    virtual void drawVisibleText(QPainter &p, int x, int y,
-				 QString text, TextStyle style) const {
-	m_view->drawVisibleText(p, x, y, text, style);
-    }
-
     virtual void drawMeasurementRect(QPainter &p, const Layer *layer,
                                      QRect rect, bool focus) const {
 	m_view->drawMeasurementRect(p, layer, rect, focus);
--- a/widgets/PropertyBox.cpp	Mon Jun 13 11:44:08 2016 +0100
+++ b/widgets/PropertyBox.cpp	Fri Aug 05 15:05:02 2016 +0100
@@ -212,10 +212,10 @@
 	gainDial->setFixedWidth(24);
 	gainDial->setFixedHeight(24);
 	gainDial->setNotchesVisible(false);
-	gainDial->setDefaultValue(0);
         gainDial->setObjectName(tr("Playback Gain"));
         gainDial->setRangeMapper(new LinearRangeMapper
                                  (-50, 50, -25, 25, tr("dB")));
+	gainDial->setDefaultValue(0);
         gainDial->setShowToolTip(true);
 	connect(gainDial, SIGNAL(valueChanged(int)),
 		this, SLOT(playGainDialChanged(int)));
@@ -384,8 +384,10 @@
 	    dial->setMaximum(max);
 	    dial->setPageStep(1);
 	    dial->setNotchesVisible((max - min) <= 12);
+            // important to set the range mapper before the default,
+            // because the range mapper is used to map the default
+            dial->setRangeMapper(m_container->getNewPropertyRangeMapper(name));
 	    dial->setDefaultValue(deflt);
-            dial->setRangeMapper(m_container->getNewPropertyRangeMapper(name));
             dial->setShowToolTip(true);
 	    connect(dial, SIGNAL(valueChanged(int)),
 		    this, SLOT(propertyControllerChanged(int)));