changeset 1030:0be17aafa935 spectrogram-minor-refactor

Start refactoring out the spectrogram image cache
author Chris Cannam
date Fri, 29 Jan 2016 15:08:01 +0000
parents fdfd84b022df
children 55ac6ac1982e
files layer/ScrollableImageCache.h layer/SpectrogramLayer.cpp layer/SpectrogramLayer.h svgui.pro view/LayerGeometryProvider.h view/View.h view/ViewProxy.h
diffstat 7 files changed, 502 insertions(+), 398 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/ScrollableImageCache.h	Fri Jan 29 15:08:01 2016 +0000
@@ -0,0 +1,262 @@
+/* -*- 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_IMAGE_CACHE_H
+#define SCROLLABLE_IMAGE_CACHE_H
+
+#include "base/BaseTypes.h"
+
+#include "view/LayerGeometryProvider.h"
+
+#include <QImage>
+#include <QRect>
+#include <QPainter>
+
+/**
+ * A cached image for a view that scrolls horizontally, primarily the
+ * 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
+ * the image, and can report and update which contiguous horizontal
+ * range of the image is valid.
+ *
+ * The only way to *update* the valid area in a cache is to draw to it
+ * using the drawImage call.
+ */
+class ScrollableImageCache
+{
+public:
+    ScrollableImageCache(const LayerGeometryProvider *v = 0) :
+	m_v(v),
+	m_left(0),
+	m_width(0),
+	m_startFrame(0),
+	m_zoomLevel(0)
+    {}
+
+    void invalidate() {
+	m_width = 0;
+    }
+    
+    bool isValid() const {
+	return m_width > 0;
+    }
+
+    bool spans(int left, int right) const {
+	return (getValidLeft() <= left &&
+		getValidRight() >= right);
+    }
+    
+    QSize getSize() const {
+	return m_image.size();
+    }
+    
+    void resize(QSize newSize) {
+	m_image = QImage(newSize, QImage::Format_ARGB32_Premultiplied);
+	invalidate();
+    }
+	
+    int getValidLeft() const {
+	return m_left;
+    }
+    
+    int getValidWidth() const {
+	return m_width;
+    }
+
+    int getValidRight() const {
+	return m_left + m_width;
+    }
+
+    QRect getValidArea() const {
+	return QRect(m_left, 0, m_width, m_image.height());
+    }
+    
+    int getZoomLevel() const {
+	return m_zoomLevel;
+    }
+
+    sv_frame_t getStartFrame() const {
+	return m_startFrame;
+    }
+    
+    void setZoomLevel(int zoom) {
+	m_zoomLevel = zoom;
+	invalidate();
+    }
+    
+    const QImage &getImage() const {
+	return m_image;
+    }
+    
+    void scrollTo(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));
+
+	m_startFrame = newStartFrame;
+	
+	if (!isValid()) {
+	    return;
+	}
+
+	int w = m_image.width();
+
+	if (dx == 0) {
+	    // haven't moved
+	    return;
+	}
+
+	if (dx <= -w || dx >= w) {
+	    // scrolled entirely off
+	    invalidate();
+	    return;
+	}
+	
+	// dx is in range, cache is scrollable
+
+	int dxp = dx;
+	if (dxp < 0) dxp = -dxp;
+
+	int copylen = (w - dxp) * int(sizeof(QRgb));
+	for (int y = 0; y < m_image.height(); ++y) {
+	    QRgb *line = (QRgb *)m_image.scanLine(y);
+	    if (dx < 0) {
+		memmove(line, line + dxp, copylen);
+	    } else {
+		memmove(line + dxp, line, copylen);
+	    }
+	}
+	
+	// update valid area
+        
+	int px = m_left;
+	int pw = m_width;
+	
+	px += dx;
+	
+	if (dx < 0) {
+	    // we scrolled left
+	    if (px < 0) {
+		pw += px;
+		px = 0;
+		if (pw < 0) {
+		    pw = 0;
+		}
+	    }
+	} else {
+	    // we scrolled right
+	    if (px + pw > w) {
+		pw = w - px;
+		if (pw < 0) {
+		    pw = 0;
+		}
+	    }
+	}
+
+	m_left = px;
+	m_width = pw;
+    }
+
+    void resizeToTouchValidArea(int &left, int &width,
+				bool &isLeftOfValidArea) const {
+	if (left < m_left) {
+	    isLeftOfValidArea = true;
+	    if (left + width < m_left + m_width) {
+		width = m_left - left;
+	    }
+	} else {
+	    isLeftOfValidArea = false;
+	    width = left + width - (m_left + m_width);
+	    left = m_left + m_width;
+	    if (width < 0) width = 0;
+	}
+    }
+    
+    void drawImage(int left,
+		   int width,
+		   QImage image,
+		   int imageLeft,
+		   int imageWidth) {
+
+	if (image.height() != m_image.height()) {
+	    throw std::logic_error("Image height must match cache height in ScrollableImageCache::drawImage");
+	}
+	if (left < 0 || left + width > m_image.width()) {
+	    throw std::logic_error("Drawing area out of bounds in ScrollableImageCache::drawImage");
+	}
+	
+	QPainter painter(&m_image);
+	painter.drawImage(QRect(left, 0, width, m_image.height()),
+			  image,
+			  QRect(imageLeft, 0, imageWidth, image.height()));
+	painter.end();
+
+	if (!isValid()) {
+	    m_left = left;
+	    m_width = width;
+	    return;
+	}
+	
+	if (left < m_left) {
+	    if (left + width > m_left + m_width) {
+		// 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) {
+		// 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;
+	    } 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;
+	    }
+	} else {
+	    if (left > m_left + m_width) {
+		// 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) {
+		// 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)
+	    } else {
+		// new image completely contained within old valid
+		// area -- leave the old area unchanged
+	    }
+	}
+    }
+    
+private:
+    const LayerGeometryProvider *m_v;
+    QImage m_image;
+    int m_left;  // of valid region
+    int m_width; // of valid region
+    sv_frame_t m_startFrame;
+    int m_zoomLevel;
+};
+
+#endif
--- a/layer/SpectrogramLayer.cpp	Wed Jan 27 11:10:48 2016 +0000
+++ b/layer/SpectrogramLayer.cpp	Fri Jan 29 15:08:01 2016 +0000
@@ -584,74 +584,7 @@
 {
     for (ViewImageCache::iterator i = m_imageCaches.begin();
          i != m_imageCaches.end(); ++i) {
-        i->second.validArea = QRect();
-    }
-}
-
-void
-SpectrogramLayer::invalidateImageCaches(sv_frame_t startFrame, sv_frame_t endFrame)
-{
-    for (ViewImageCache::iterator i = m_imageCaches.begin();
-         i != m_imageCaches.end(); ++i) {
-
-        //!!! when are views removed from the map? on setLayerDormant?
-        const LayerGeometryProvider *v = i->first;
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "SpectrogramLayer::invalidateImageCaches(" 
-                  << startFrame << ", " << endFrame << "): view range is "
-                  << v->getStartFrame() << ", " << v->getEndFrame()
-                  << endl;
-
-        cerr << "Valid area was: " << i->second.validArea.x() << ", "
-                  << i->second.validArea.y() << " "
-                  << i->second.validArea.width() << "x"
-                  << i->second.validArea.height() << endl;
-#endif
-
-        if (int(startFrame) > v->getStartFrame()) {
-            if (startFrame >= v->getEndFrame()) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                cerr << "Modified start frame is off right of view" << endl;
-#endif
-                return;
-            }
-            int x = v->getXForFrame(startFrame);
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-            cerr << "clipping from 0 to " << x-1 << endl;
-#endif
-            if (x > 1) {
-                i->second.validArea &=
-                    QRect(0, 0, x-1, v->getPaintHeight());
-            } else {
-                i->second.validArea = QRect();
-            }
-        } else {
-            if (int(endFrame) < v->getStartFrame()) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                cerr << "Modified end frame is off left of view" << endl;
-#endif
-                return;
-            }
-            int x = v->getXForFrame(endFrame);
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-            cerr << "clipping from " << x+1 << " to " << v->getPaintWidth()
-                      << endl;
-#endif
-            if (x < v->getPaintWidth()) {
-                i->second.validArea &=
-                    QRect(x+1, 0, v->getPaintWidth()-(x+1), v->getPaintHeight());
-            } else {
-                i->second.validArea = QRect();
-            }
-        }
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "Valid area is now: " << i->second.validArea.x() << ", "
-                  << i->second.validArea.y() << " "
-                  << i->second.validArea.width() << "x"
-                  << i->second.validArea.height() << endl;
-#endif
+        i->second.invalidate();
     }
 }
 
@@ -988,11 +921,11 @@
         
 	invalidateImageCaches();
 
-        m_imageCaches.erase(view);
-
-        if (m_fftModels.find(view) != m_fftModels.end()) {
-
-            if (m_sliceableModel == m_fftModels[view]) {
+        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) {
@@ -1005,11 +938,11 @@
                 if (!replaced) emit sliceableModelReplaced(m_sliceableModel, 0);
             }
 
-            delete m_fftModels[view];
-            m_fftModels.erase(view);
-
-            delete m_peakCaches[view];
-            m_peakCaches.erase(view);
+            delete m_fftModels[view->getId()];
+            m_fftModels.erase(view->getId());
+
+            delete m_peakCaches[view->getId()];
+            m_peakCaches.erase(view->getId());
         }
 	
     } else {
@@ -1036,7 +969,13 @@
     cerr << "SpectrogramLayer::cacheInvalid(" << from << ", " << to << ")" << endl;
 #endif
 
-    invalidateImageCaches(from, to);
+    // We used to call invalidateMagnitudes(from, to) to invalidate
+    // only those caches whose views contained some of the (from, to)
+    // range. That's the right thing to do; it has been lost in
+    // 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();
     invalidateMagnitudes();
 }
 
@@ -1102,8 +1041,8 @@
     double max = 1.0;
 
     if (m_normalization == NormalizeVisibleArea) {
-        min = m_viewMags[v].getMin();
-        max = m_viewMags[v].getMax();
+        min = m_viewMags[v->getId()].getMin();
+        max = m_viewMags[v->getId()].getMax();
     } else if (m_normalization != NormalizeColumns) {
         if (m_colourScale == LinearColourScale //||
 //            m_colourScale == MeterColourScale) {
@@ -1510,30 +1449,30 @@
 
     const View *view = v->getView();
     
-    if (m_fftModels.find(view) != m_fftModels.end()) {
-        if (m_fftModels[view] == 0) {
+    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]->getHeight() != fftSize / 2 + 1) {
+        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]->getHeight() << ", wanted " << (fftSize / 2 + 1) << ")" << endl;
+            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];
-            m_fftModels.erase(view);
-            delete m_peakCaches[view];
-            m_peakCaches.erase(view);
+            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]->getHeight() << endl;
+            cerr << "SpectrogramLayer::getFFTModel(" << v << "): Found a good model of height " << m_fftModels[view->getId()]->getHeight() << endl;
 #endif
-            return m_fftModels[view];
+            return m_fftModels[view->getId()];
         }
     }
 
-    if (m_fftModels.find(view) == m_fftModels.end()) {
+    if (m_fftModels.find(view->getId()) == m_fftModels.end()) {
 
         FFTModel *model = new FFTModel(m_model,
                                        m_channel,
@@ -1548,7 +1487,7 @@
                  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] = 0;
+            m_fftModels[view->getId()] = 0;
             return 0;
         }
 
@@ -1560,22 +1499,22 @@
             m_sliceableModel = model;
         }
 
-        m_fftModels[view] = model;
+        m_fftModels[view->getId()] = model;
     }
 
-    return m_fftModels[view];
+    return m_fftModels[view->getId()];
 }
 
 Dense3DModelPeakCache *
 SpectrogramLayer::getPeakCache(const LayerGeometryProvider *v) const
 {
     const View *view = v->getView();
-    if (!m_peakCaches[view]) {
+    if (!m_peakCaches[view->getId()]) {
         FFTModel *f = getFFTModel(v);
         if (!f) return 0;
-        m_peakCaches[view] = new Dense3DModelPeakCache(f, 8);
+        m_peakCaches[view->getId()] = new Dense3DModelPeakCache(f, 8);
     }
-    return m_peakCaches[view];
+    return m_peakCaches[view->getId()];
 }
 
 const Model *
@@ -1656,8 +1595,8 @@
 #endif
 
     if (!mag.isSet()) return false;
-    if (mag == m_viewMags[v]) return false;
-    m_viewMags[v] = mag;
+    if (mag == m_viewMags[v->getId()]) return false;
+    m_viewMags[v->getId()] = mag;
     return true;
 }
 
@@ -1667,12 +1606,18 @@
     m_synchronous = synchronous;
 }
 
+ScrollableImageCache &
+SpectrogramLayer::getImageCacheReference(const LayerGeometryProvider *view) const
+{
+    if (m_imageCaches.find(view->getId()) == m_imageCaches.end()) {
+        m_imageCaches[view->getId()] = ScrollableImageCache(view);
+    }
+    return m_imageCaches.at(view->getId());
+}
+
 void
 SpectrogramLayer::paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const
 {
-    // What a lovely, old-fashioned function this is.
-    // It's practically FORTRAN 77 in its clarity and linearity.
-
     Profiler profiler("SpectrogramLayer::paint", false);
 
 #ifdef DEBUG_SPECTROGRAM_REPAINT
@@ -1701,230 +1646,118 @@
     int fftSize = getFFTSize(v);
 
     const View *view = v->getView();
-    
-    ImageCache &cache = m_imageCaches[view];
+    ScrollableImageCache &cache = getImageCacheReference(view);
 
 #ifdef DEBUG_SPECTROGRAM_REPAINT
-    cerr << "SpectrogramLayer::paint(): image cache valid area from " << cache.validArea.x() << "," << cache.validArea.y() << ", " << cache.validArea.width() << "x" << cache.validArea.height() << endl;
+    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 = 0;
-    int x1 = v->getPaintWidth();
-
-    bool recreateWholeImageCache = true;
-
-    x0 = rect.left();
-    x1 = rect.right() + 1;
+    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].getMin() << "->" << m_viewMags[v].getMax() << "]" << endl;
+        cerr << "SpectrogramLayer: magnitude range changed to [" << m_viewMags[v->getId()].getMin() << "->" << m_viewMags[v->getId()].getMax() << "]" << endl;
 #endif
         if (m_normalization == NormalizeVisibleArea) {
-            cache.validArea = QRect();
+            cache.invalidate();
         }
     }
+
+    if (cache.getZoomLevel() != zoomLevel ||
+        cache.getSize() != v->getPaintSize()) {
+        cache.resize(v->getPaintSize());
+    }
     
-    if (cache.validArea.width() > 0) {
-
-        int cw = cache.image.width();
-        int ch = cache.image.height();
+    if (cache.isValid()) {
         
-	if (int(cache.zoomLevel) == zoomLevel &&
-	    cw == v->getPaintWidth() &&
-	    ch == v->getPaintHeight()) {
-
-            // cache size and zoom level exactly match the view
-            
-	    if (v->getXForFrame(cache.startFrame) ==
-		v->getXForFrame(startFrame) &&
-                cache.validArea.x() <= x0 &&
-                cache.validArea.x() + cache.validArea.width() >= x1) {
-
-                // and cache begins at the right frame, so use it whole
+        if (v->getXForFrame(cache.getStartFrame()) ==
+            v->getXForFrame(startFrame) &&
+            cache.getValidLeft() <= x0 &&
+            cache.getValidRight() >= x1) {
                 
 #ifdef DEBUG_SPECTROGRAM_REPAINT
-		cerr << "SpectrogramLayer: image cache good" << endl;
+            cerr << "SpectrogramLayer: image cache hit!" << endl;
 #endif
 
-		paint.drawImage(rect, cache.image, 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
+            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: image cache partially OK" << endl;
+            cerr << "SpectrogramLayer: scrolling the image cache if applicable" << endl;
 #endif
 
-		recreateWholeImageCache = false;
-
-		int dx = v->getXForFrame(cache.startFrame) -
-		         v->getXForFrame(startFrame);
-
+            cache.scrollTo(startFrame);
+            
 #ifdef DEBUG_SPECTROGRAM_REPAINT
-		cerr << "SpectrogramLayer: dx = " << dx << " (image cache " << cw << "x" << ch << ")" << endl;
+            cerr << "SpectrogramLayer: cache valid now from "
+                 << cache.getValidLeft() << " width " << cache.getValidWidth()
+                 << endl;
 #endif
-
-		if (dx != 0 &&
-                    dx > -cw &&
-                    dx <  cw) {
-
-                    // cache is scrollable, scroll it
-                    
-                    int dxp = dx;
-                    if (dxp < 0) dxp = -dxp;
-                    size_t copy = (cw - dxp) * sizeof(QRgb);
-                    for (int y = 0; y < ch; ++y) {
-                        QRgb *line = (QRgb *)cache.image.scanLine(y);
-                        if (dx < 0) {
-                            memmove(line, line + dxp, copy);
-                        } else {
-                            memmove(line + dxp, line, copy);
-                        }
-                    }
-
-                    // and calculate its new valid area
-                    
-                    int px = cache.validArea.x();
-                    int pw = cache.validArea.width();
-
-                    // so px and pw will be updated to the new x and
-                    // width of the valid area of cache;
-
-                    // x0 and x1 will be the left and right extents of
-                    // the area needing repainted
-
-                    px += dx;
-
-		    if (dx < 0) {
-                        // we scrolled left
-                        if (px < 0) {
-                            pw += px;
-                            px = 0;
-                            if (pw < 0) {
-                                pw = 0;
-                            }
-                        }
-                        x0 = px + pw;
-                        x1 = cw;
-                    } else {
-                        // we scrolled right
-                        if (px + pw > cw) {
-                            pw = cw - px;
-                            if (pw < 0) {
-                                pw = 0;
-                            }
-                        }
-                        x0 = 0;
-                        x1 = px;
-                    }
-
-                    cache.validArea =
-                        QRect(px, cache.validArea.y(),
-                              pw, cache.validArea.height());
-
-                    if (cache.validArea.width() == 0) {
-                        recreateWholeImageCache = true;
-                    }
-                    
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                    cerr << "SpectrogramLayer: valid area now "
-                              << px << "," << cache.validArea.y()
-                              << " " << pw << "x" << cache.validArea.height()
-                              << endl;
-#endif
-
-                } else if (dx != 0) {
-
-                    // we've moved too far from the cached area for it
-                    // to be of use
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                    cerr << "SpectrogramLayer: dx == " << dx << ": scrolled too far for cache to be useful" << endl;
-#endif
-
-                    cache.validArea = QRect();
-                    recreateWholeImageCache = true;
-
-                } else {
-
-                    // dx == 0, we haven't scrolled but the cache is
-                    // only partly valid
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                    cerr << "SpectrogramLayer: haven't scrolled, but cache is not complete" << endl;
-#endif
-                    if (cache.validArea.x() == 0) {
-                        x0 = cache.validArea.width();
-                    } else {
-                        x1 = cache.validArea.x();
-                    }
-                }
-	    }
-	} else {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-	    cerr << "SpectrogramLayer: image cache useless" << endl;
-            if (int(cache.zoomLevel) != zoomLevel) {
-                cerr << "(cache zoomLevel " << cache.zoomLevel
-                          << " != " << zoomLevel << ")" << endl;
-            }
-            if (cw != v->getPaintWidth()) {
-                cerr << "(cache width " << cw
-                          << " != " << v->getPaintWidth();
-            }
-            if (ch != v->getPaintHeight()) {
-                cerr << "(cache height " << ch
-                          << " != " << v->getPaintHeight();
-            }
-#endif
-            cache.validArea = QRect();
-	}
+        }
     }
 
-    if (recreateWholeImageCache) {
+    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
-            x0 = int(v->getPaintWidth() * 0.4);
-        } else {
-            x0 = 0;
+            if (x0 == 0 && x1 == v->getPaintWidth()) {
+                x0 = int(x1 * 0.4);
+            }
         }
-        x1 = v->getPaintWidth();
+    } 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.resizeToTouchValidArea(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;
 
-    // If we are painting a section to the left of a valid area of
-    // cache, then we must paint it "backwards", right-to-left. This
-    // is because painting may be interrupted by a timeout, leaving us
-    // with a partially painted area, and we have no way to record
-    // that the union of the existing cached area and this new
-    // partially painted bit is a valid cache area unless they are
-    // adjacent (because our valid extent is a single x,width range).
-    
-    bool rightToLeft = (x0 == 0 && x1 < v->getPaintWidth());
-    
 #ifdef DEBUG_SPECTROGRAM_REPAINT
     cerr << "SpectrogramLayer: x0 " << x0 << ", x1 " << x1
          << ", repaintWidth " << repaintWidth << ", h " << h
-         << ", rightToLeft " << rightToLeft
-         << ", recreateWholeImageCache " << recreateWholeImageCache << endl;
+         << ", rightToLeft " << rightToLeft << endl;
 #endif
 
     sv_samplerate_t sr = m_model->getSampleRate();
@@ -1975,7 +1808,7 @@
     
     bool logarithmic = (m_frequencyScale == LogFrequencyScale);
 
-    MagnitudeRange overallMag = m_viewMags[v];
+    MagnitudeRange overallMag = m_viewMags[v->getId()];
     bool overallMagChanged = false;
 
 #ifdef DEBUG_SPECTROGRAM_REPAINT
@@ -2118,9 +1951,9 @@
     }
     
     if (overallMagChanged) {
-        m_viewMags[v] = overallMag;
+        m_viewMags[v->getId()] = overallMag;
 #ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "SpectrogramLayer: Overall mag is now [" << m_viewMags[v].getMin() << "->" << m_viewMags[v].getMax() << "] - will be updating" << endl;
+        cerr << "SpectrogramLayer: Overall mag is now [" << m_viewMags[v->getId()].getMin() << "->" << m_viewMags[v->getId()].getMax() << "] - will be updating" << endl;
 #endif
     }
 
@@ -2128,14 +1961,6 @@
 
     Profiler profiler2("SpectrogramLayer::paint: draw image");
 
-    if (recreateWholeImageCache) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "SpectrogramLayer: Recreating image cache: width = " << v->getPaintWidth()
-                  << ", height = " << h << endl;
-#endif
-	cache.image = QImage(v->getPaintWidth(), h, QImage::Format_ARGB32_Premultiplied);
-    }
-
     if (repaintWidth > 0) {
 
 #ifdef DEBUG_SPECTROGRAM_REPAINT
@@ -2145,8 +1970,6 @@
                   << x0 << "," << 0 << endl;
 #endif
 
-        QPainter cachePainter(&cache.image);
-
         if (bufferBinResolution) {
             int scaledLeft = v->getXForFrame(leftBoundaryFrame);
             int scaledRight = v->getXForFrame(rightBoundaryFrame);
@@ -2170,62 +1993,29 @@
             cerr << "SpectrogramLayer: Drawing image region of width " << scaledRightCrop - scaledLeftCrop << " to "
                  << scaledLeftCrop << " from " << scaledLeftCrop - scaledLeft << endl;
 #endif
-            cachePainter.drawImage
-                (QRect(scaledLeftCrop, 0,
-                       scaledRightCrop - scaledLeftCrop, h),
+
+            cache.drawImage
+                (scaledLeftCrop,
+                 scaledRightCrop - scaledLeftCrop,
                  scaled,
-                 QRect(scaledLeftCrop - scaledLeft, 0,
-                       scaledRightCrop - scaledLeftCrop, h));
+                 scaledLeftCrop - scaledLeft,
+                 scaledRightCrop - scaledLeftCrop);
 
         } else {
 
-            cachePainter.drawImage(QRect(x0, 0, repaintWidth, h),
-                                   m_drawBuffer,
-                                   QRect(0, 0, repaintWidth, h));
+            cache.drawImage(x0, repaintWidth,
+                            m_drawBuffer,
+                            0, repaintWidth);
         }
-
-        cachePainter.end();
     }
 
-    // update cache valid area based on painted area
-        
-    int left = x0;
-    int wid = x1 - x0;
-    
-    if (failedToRepaint > 0) {
 #ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "SpectrogramLayer: Reduced painted extent from "
-             << left << "," << wid;
+    cerr << "SpectrogramLayer: Cache valid area now from " << cache.getValidLeft()
+         << " width " << cache.getValidWidth() << ", height "
+         << cache.getSize().height() << endl;
 #endif
-        if (rightToLeft) {
-            left += failedToRepaint;
-        }
-        wid -= failedToRepaint;
-
-        if (wid < 0) wid = 0;
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << " to " << left << "," << wid << endl;
-#endif
-    }
-    
-    if (cache.validArea.width() > 0) {
-        left = min(left, cache.validArea.x());
-        wid = cache.validArea.width() + wid;
-    }
-
-    cache.validArea = QRect(left, 0, wid, h);
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    cerr << "SpectrogramLayer: Cache valid area becomes " << cache.validArea.x()
-         << ", " << cache.validArea.y() << ", "
-         << cache.validArea.width() << "x"
-         << cache.validArea.height() << " (size = "
-         << cache.image.width() << "x" << cache.image.height() << ")"
-         << endl;
-#endif
-
-    QRect pr = rect & cache.validArea;
+
+    QRect pr = rect & cache.getValidArea();
 
 #ifdef DEBUG_SPECTROGRAM_REPAINT
     cerr << "SpectrogramLayer: Copying " << pr.width() << "x" << pr.height()
@@ -2233,46 +2023,41 @@
               << " to window" << endl;
 #endif
 
-    paint.drawImage(pr.x(), pr.y(), cache.image,
+    paint.drawImage(pr.x(), pr.y(), cache.getImage(),
                     pr.x(), pr.y(), pr.width(), pr.height());
 
-    cache.startFrame = startFrame;
-    cache.zoomLevel = zoomLevel;
-
     if (!m_synchronous) {
 
         if ((m_normalization != NormalizeVisibleArea) || !overallMagChanged) {
     
-            if (cache.validArea.x() > 0) {
+            if (cache.getValidLeft() > 0) {
 #ifdef DEBUG_SPECTROGRAM_REPAINT
                 cerr << "SpectrogramLayer::paint() updating left (0, "
-                          << cache.validArea.x() << ")" << endl;
+                          << cache.getValidLeft() << ")" << endl;
 #endif
-                v->getView()->update(0, 0, cache.validArea.x(), h);
+                v->updatePaintRect(QRect(0, 0, cache.getValidLeft(), h));
             }
             
-            if (cache.validArea.x() + cache.validArea.width() <
-                cache.image.width()) {
+            if (cache.getValidRight() <
+                cache.getSize().width()) {
 #ifdef DEBUG_SPECTROGRAM_REPAINT
                 cerr << "SpectrogramLayer::paint() updating right ("
-                          << cache.validArea.x() + cache.validArea.width()
-                          << ", "
-                          << cache.image.width() - (cache.validArea.x() +
-                                                     cache.validArea.width())
-                          << ")" << endl;
+                     << cache.getValidRight()
+                     << ", "
+                     << cache.getSize().width() - cache.getValidRight()
+                     << ")" << endl;
 #endif
-                v->getView()->update
-                    (cache.validArea.x() + cache.validArea.width(),
-                     0,
-                     cache.image.width() - (cache.validArea.x() +
-                                            cache.validArea.width()),
-                     h);
+                v->updatePaintRect
+                    (QRect(cache.getValidRight(),
+                           0,
+                           cache.getSize().width() - cache.getValidRight(),
+                           h));
             }
         } else {
             // overallMagChanged
             cerr << "\noverallMagChanged - updating all\n" << endl;
-            cache.validArea = QRect();
-            v->getView()->update();
+            cache.invalidate();
+            v->updatePaintRect(v->getPaintRect());
         }
     }
 
@@ -2766,9 +2551,9 @@
 {
     const View *view = v->getView();
     
-    if (m_fftModels.find(view) == m_fftModels.end()) return 100;
-
-    int completion = m_fftModels[view]->getCompletion();
+    if (m_fftModels.find(view->getId()) == m_fftModels.end()) return 100;
+
+    int completion = m_fftModels[view->getId()]->getCompletion();
 #ifdef DEBUG_SPECTROGRAM_REPAINT
     cerr << "SpectrogramLayer::getCompletion: completion = " << completion << endl;
 #endif
@@ -2779,8 +2564,8 @@
 SpectrogramLayer::getError(LayerGeometryProvider *v) const
 {
     const View *view = v->getView();
-    if (m_fftModels.find(view) == m_fftModels.end()) return "";
-    return m_fftModels[view]->getError();
+    if (m_fftModels.find(view->getId()) == m_fftModels.end()) return "";
+    return m_fftModels[view->getId()]->getError();
 }
 
 bool
@@ -2876,12 +2661,12 @@
 SpectrogramLayer::measureDoubleClick(LayerGeometryProvider *v, QMouseEvent *e)
 {
     const View *view = v->getView();
-    ImageCache &cache = m_imageCaches[view];
-
-    cerr << "cache width: " << cache.image.width() << ", height: "
-         << cache.image.height() << endl;
-
-    QImage image = cache.image;
+    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());
@@ -3196,8 +2981,8 @@
 	paint.drawRect(4 + cw - cbw, textHeight * topLines + 4, cbw - 1, ch + 1);
 
 	QString top, bottom;
-        double min = m_viewMags[v].getMin();
-        double max = m_viewMags[v].getMax();
+        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);
--- a/layer/SpectrogramLayer.h	Wed Jan 27 11:10:48 2016 +0000
+++ b/layer/SpectrogramLayer.h	Fri Jan 29 15:08:01 2016 +0000
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _SPECTROGRAM_LAYER_H_
-#define _SPECTROGRAM_LAYER_H_
+#ifndef SPECTROGRAM_LAYER_H
+#define SPECTROGRAM_LAYER_H
 
 #include "SliceableLayer.h"
 #include "base/Window.h"
@@ -25,6 +25,8 @@
 #include "data/model/DenseTimeValueModel.h"
 #include "data/model/FFTModel.h"
 
+#include "ScrollableImageCache.h"
+
 #include <QMutex>
 #include <QWaitCondition>
 #include <QImage>
@@ -294,22 +296,10 @@
 
     Palette m_palette;
 
-    /**
-     * ImageCache covers the area of the view, at view resolution.
-     * Not all of it is necessarily valid at once (it is refreshed
-     * in parts when scrolling, for example).
-     */
-    struct ImageCache
-    {
-        QImage image;
-        QRect validArea;
-        sv_frame_t startFrame;
-        int zoomLevel;
-    };
-    typedef std::map<const View *, ImageCache> ViewImageCache;
+    typedef std::map<int, ScrollableImageCache> ViewImageCache; // key is view id
     void invalidateImageCaches();
-    void invalidateImageCaches(sv_frame_t startFrame, sv_frame_t endFrame);
     mutable ViewImageCache m_imageCaches;
+    ScrollableImageCache &getImageCacheReference(const LayerGeometryProvider *) const;
 
     /**
      * When painting, we draw directly onto the draw buffer and then
@@ -363,8 +353,8 @@
     Dense3DModelPeakCache *getPeakCache(const LayerGeometryProvider *v) const;
     void invalidateFFTModels();
 
-    typedef std::map<const View *, FFTModel *> ViewFFTMap;
-    typedef std::map<const View *, Dense3DModelPeakCache *> PeakCacheMap;
+    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 Model *m_sliceableModel;
@@ -411,7 +401,7 @@
         float m_max;
     };
 
-    typedef std::map<const LayerGeometryProvider *, MagnitudeRange> ViewMagMap;
+    typedef std::map<int, MagnitudeRange> ViewMagMap; // key is view id
     mutable ViewMagMap m_viewMags;
     mutable std::vector<MagnitudeRange> m_columnMags;
     void invalidateMagnitudes();
--- a/svgui.pro	Wed Jan 27 11:10:48 2016 +0000
+++ b/svgui.pro	Fri Jan 29 15:08:01 2016 +0000
@@ -52,6 +52,7 @@
            layer/PaintAssistant.h \
            layer/PianoScale.h \
            layer/RegionLayer.h \
+           layer/ScrollableImageCache.h \
            layer/SingleColourLayer.h \
            layer/SliceableLayer.h \
            layer/SliceLayer.h \
--- a/view/LayerGeometryProvider.h	Wed Jan 27 11:10:48 2016 +0000
+++ b/view/LayerGeometryProvider.h	Fri Jan 29 15:08:01 2016 +0000
@@ -17,6 +17,9 @@
 
 #include "base/BaseTypes.h"
 
+#include <QMutex>
+#include <QMutexLocker>
+
 class ViewManager;
 class View;
 class Layer;
@@ -24,6 +27,20 @@
 class LayerGeometryProvider
 {
 public:
+    LayerGeometryProvider() {
+        static QMutex idMutex;
+        static int nextId = 1;
+        QMutexLocker locker(&idMutex);
+        m_id = nextId;
+        nextId++;
+    }
+    
+    /**
+     * Retrieve the id of this object. Each LayerGeometryProvider has
+     * a separate id.
+     */
+    int getId() const { return m_id; }
+
     /**
      * Retrieve the first visible sample frame on the widget.
      * This is a calculated value based on the centre-frame, widget
@@ -61,6 +78,18 @@
     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
@@ -123,8 +152,13 @@
     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;
+
+private:
+    int m_id;
 };
 
 #endif
--- a/view/View.h	Wed Jan 27 11:10:48 2016 +0000
+++ b/view/View.h	Fri Jan 29 15:08:01 2016 +0000
@@ -62,7 +62,7 @@
      * be managed elsewhere (e.g. by the Document).
      */
     virtual ~View();
-
+    
     /**
      * Retrieve the first visible sample frame on the widget.
      * This is a calculated value based on the centre-frame, widget
@@ -108,6 +108,20 @@
     sv_frame_t getFrameForX(int x) const;
 
     /**
+     * Return the closest pixel x-coordinate corresponding to a given
+     * view x-coordinate. Default is no scaling, ViewProxy handles
+     * scaling case.
+     */
+    int getXForViewX(int viewx) const { return viewx; }
+
+    /**
+     * Return the closest view x-coordinate corresponding to a given
+     * pixel x-coordinate. Default is no scaling, ViewProxy handles
+     * scaling case.
+     */
+    int getViewXForX(int x) const { return x; }
+
+    /**
      * 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
@@ -333,6 +347,8 @@
     sv_frame_t alignToReference(sv_frame_t) const;
     sv_frame_t getAlignedPlaybackFrame() const;
 
+    void updatePaintRect(QRect r) { update(r); }
+    
     View *getView() { return this; } 
     const View *getView() const { return this; } 
     
@@ -384,6 +400,9 @@
 
 protected:
     View(QWidget *, bool showProgress);
+
+    int m_id;
+    
     virtual void paintEvent(QPaintEvent *e);
     virtual void drawSelections(QPainter &);
     virtual bool shouldLabelSelections() const { return true; }
--- a/view/ViewProxy.h	Wed Jan 27 11:10:48 2016 +0000
+++ b/view/ViewProxy.h	Fri Jan 29 15:08:01 2016 +0000
@@ -42,6 +42,12 @@
         sv_frame_t f1 = m_view->getFrameForX((x / m_scaleFactor) + 1);
         return f0 + ((f1 - f0) * (x % m_scaleFactor)) / m_scaleFactor;
     }
+    virtual int getXForViewX(int viewx) const {
+        return viewx * m_scaleFactor;
+    }
+    virtual int getViewXForX(int x) const {
+        return x / m_scaleFactor;
+    }
     virtual sv_frame_t getModelsStartFrame() const {
 	return m_view->getModelsStartFrame();
     }
@@ -129,6 +135,13 @@
 	m_view->drawMeasurementRect(p, layer, rect, focus);
     }
 
+    virtual void updatePaintRect(QRect r) {
+        m_view->update(r.x() / m_scaleFactor,
+                       r.y() / m_scaleFactor,
+                       r.width() / m_scaleFactor,
+                       r.height() / m_scaleFactor);
+    }
+    
     virtual View *getView() { return m_view; }
     virtual const View *getView() const { return m_view; }