# HG changeset patch # User Chris Cannam # Date 1539266374 -3600 # Node ID 93eaff6f206db93197389c71b8a6c1432a4f94f6 # Parent dddfd28e4f020b2e7c3c42c531ae4c4d32f9062b Rework cacheing logic to reduce the number of reallocations and be more correct about the repaint areas. I don't expect the difference to be really noticeable but in theory performance should be a little better... diff -r dddfd28e4f02 -r 93eaff6f206d view/Pane.cpp --- a/view/Pane.cpp Thu Oct 11 10:15:45 2018 +0100 +++ b/view/Pane.cpp Thu Oct 11 14:59:34 2018 +0100 @@ -2373,12 +2373,7 @@ void Pane::horizontalThumbwheelMoved(int value) { - cerr << "horizontalThumbwheelMoved(" << value << ")" << endl; - - ZoomLevel level = getZoomLevelByIndex - (m_hthumb->getMaximumValue() - value); - - cerr << "new level is " << level << endl; + ZoomLevel level = getZoomLevelByIndex(m_hthumb->getMaximumValue() - value); setZoomLevel(level); } diff -r dddfd28e4f02 -r 93eaff6f206d view/View.cpp --- a/view/View.cpp Thu Oct 11 10:15:45 2018 +0100 +++ b/view/View.cpp Thu Oct 11 14:59:34 2018 +0100 @@ -20,6 +20,7 @@ #include "base/Profiler.h" #include "base/Pitch.h" #include "base/Preferences.h" +#include "base/HitCount.h" #include "ViewProxy.h" #include "layer/TimeRulerLayer.h" @@ -63,6 +64,7 @@ m_showProgress(showProgress), m_cache(0), m_buffer(0), + m_cacheValid(false), m_cacheCentreFrame(0), m_cacheZoomLevel(ZoomLevel::FramesPerPixel, 1024), m_selectionCached(false), @@ -260,8 +262,7 @@ return; } - delete m_cache; - m_cache = 0; + m_cacheValid = false; Layer *selectedLayer = 0; @@ -293,8 +294,7 @@ void View::overlayModeChanged() { - delete m_cache; - m_cache = 0; + m_cacheValid = false; update(); } @@ -617,8 +617,7 @@ void View::addLayer(Layer *layer) { - delete m_cache; - m_cache = 0; + m_cacheValid = false; SingleColourLayer *scl = dynamic_cast(layer); if (scl) scl->setDefaultColourFor(this); @@ -689,8 +688,7 @@ return; } - delete m_cache; - m_cache = 0; + m_cacheValid = false; for (LayerList::iterator i = m_fixedOrderLayers.begin(); i != m_fixedOrderLayers.end(); @@ -907,8 +905,7 @@ } if (recreate) { - delete m_cache; - m_cache = 0; + m_cacheValid = false; } emit layerModelChanged(); @@ -955,8 +952,7 @@ } if (recreate) { - delete m_cache; - m_cache = 0; + m_cacheValid = false; } if (startFrame < myStartFrame) startFrame = myStartFrame; @@ -991,9 +987,7 @@ #ifdef DEBUG_VIEW_WIDGET_PAINT cerr << "View(" << this << ")::modelReplaced()" << endl; #endif - delete m_cache; - m_cache = 0; - + m_cacheValid = false; update(); } @@ -1006,8 +1000,7 @@ SVDEBUG << "View::layerParametersChanged()" << endl; #endif - delete m_cache; - m_cache = 0; + m_cacheValid = false; update(); if (layer) { @@ -1203,8 +1196,7 @@ View::selectionChanged() { if (m_selectionCached) { - delete m_cache; - m_cache = 0; + m_cacheValid = false; m_selectionCached = false; } update(); @@ -1803,90 +1795,90 @@ m_zoomLevel = getZoomConstraintLevel (m_zoomLevel, ZoomConstraint::RoundNearest); - QPainter paint; - bool repaintCache = false; - bool paintedCacheRect = false; - - QRect cacheRect(rect()); - + // We have a cache, which retains the state of scrollable (back) + // layers from one paint to the next, and a buffer, which we paint + // onto before copying directly to the widget. Both are at scaled + // resolution (e.g. 2x on a pixel-doubled display), whereas the + // paint event always comes in at formal (1x) resolution. + + // If we touch the cache, we always leave it in a valid state + // across its whole extent. When another method invalidates the + // cache, it does so by setting m_cacheValid false, so if that + // flag is true on entry, then the cache is valid across its whole + // extent - although it may be valid for a different centre frame, + // zoom level, or view size from those now in effect. + + // Our process goes: + // + // 1. Check whether we have any scrollable (cacheable) layers. If + // we don't, then invalidate and ignore the cache and go to + // step 5. Otherwise: + // + // 2. Check the cache, scroll as necessary, identify any area that + // needs to be refreshed (this might be the whole cache). + // + // 3. Paint to cache the area that needs to be refreshed, from the + // stack of scrollable layers. + // + // 4. Paint to buffer from cache: if there are no non-cached areas + // or selections and the cache has not scrolled, then paint the + // union of the area of cache that has changed and the area + // that the paint event reported as exposed; otherwise paint + // the whole. + // + // 5. Paint the exposed area to the buffer from the cache plus all + // the layers that haven't been cached, plus selections etc. + // + // 6. Paint the exposed rect from the buffer. + // + // Note that all rects except the target for the final step are at + // cache (scaled, 2x as applicable) resolution. + + int dpratio = effectiveDevicePixelRatio(); + + QRect requestedPaintArea(scaledRect(rect(), dpratio)); if (e) { - cacheRect &= e->rect(); -#ifdef DEBUG_VIEW_WIDGET_PAINT - cerr << "paint rect " << cacheRect.width() << "x" << cacheRect.height() - << ", my rect " << width() << "x" << height() << endl; -#endif + // cut down to only the area actually exposed + requestedPaintArea &= scaledRect(e->rect(), dpratio); } - QRect nonCacheRect(cacheRect); - - int dpratio = effectiveDevicePixelRatio(); - // If not all layers are scrollable, but some of the back layers // are, we should store only those in the cache. bool layersChanged = false; LayerList scrollables = getScrollableBackLayers(true, layersChanged); LayerList nonScrollables = getNonScrollableFrontLayers(true, layersChanged); - bool selectionCacheable = nonScrollables.empty(); - bool haveSelections = m_manager && !m_manager->getSelections().empty(); - - // If all the non-scrollable layers are non-opaque, then we draw - // the selection rectangle behind them and cache it. If any are - // opaque, however, or if our device-pixel ratio is not 1 (so we - // need to paint direct to the widget), then we can't cache. - // - if (dpratio == 1) { - - if (!selectionCacheable) { - selectionCacheable = true; - for (LayerList::const_iterator i = nonScrollables.begin(); - i != nonScrollables.end(); ++i) { - if ((*i)->isLayerOpaque()) { - selectionCacheable = false; - break; - } - } - } - - if (selectionCacheable) { - QPoint localPos; - bool closeToLeft, closeToRight; - if (shouldIlluminateLocalSelection - (localPos, closeToLeft, closeToRight)) { - selectionCacheable = false; - } - } - - } else { - - selectionCacheable = false; - } #ifdef DEBUG_VIEW_WIDGET_PAINT cerr << "View(" << this << ")::paintEvent: have " << scrollables.size() << " scrollable back layers and " << nonScrollables.size() << " non-scrollable front layers" << endl; - cerr << "haveSelections " << haveSelections << ", selectionCacheable " - << selectionCacheable << ", m_selectionCached " << m_selectionCached << endl; #endif - if (layersChanged || scrollables.empty() || - (haveSelections && (selectionCacheable != m_selectionCached))) { - delete m_cache; - m_cache = 0; - m_selectionCached = false; + if (layersChanged || scrollables.empty()) { + m_cacheValid = false; } - QSize scaledCacheSize(scaledSize(size(), dpratio)); - QRect scaledCacheRect(scaledRect(cacheRect, dpratio)); - - if (!m_buffer || scaledCacheSize != m_buffer->size()) { + QRect wholeArea(scaledRect(rect(), dpratio)); + QSize wholeSize(scaledSize(size(), dpratio)); + + if (!m_buffer || wholeSize != m_buffer->size()) { delete m_buffer; - m_buffer = new QPixmap(scaledCacheSize); + m_buffer = new QPixmap(wholeSize); } + + bool shouldUseCache = false; + bool shouldRepaintCache = false; + QRect cacheAreaToRepaint; + static HitCount count("View cache"); + if (!scrollables.empty()) { + shouldUseCache = true; + shouldRepaintCache = true; + cacheAreaToRepaint = wholeArea; + #ifdef DEBUG_VIEW_WIDGET_PAINT cerr << "View(" << this << "): cache " << m_cache << ", cache zoom " << m_cacheZoomLevel << ", zoom " << m_zoomLevel << endl; @@ -1894,209 +1886,185 @@ using namespace std::rel_ops; - if (!m_cache || + if (!m_cacheValid || + !m_cache || m_cacheZoomLevel != m_zoomLevel || - scaledCacheSize != m_cache->size()) { - - // cache is not valid - - if (cacheRect.width() < width()/10) { - delete m_cache; - m_cache = 0; + m_cache->size() != wholeSize) { + + // cache is not valid at all + + if (requestedPaintArea.width() < wholeSize.width() / 10) { + + m_cacheValid = false; + shouldUseCache = false; + shouldRepaintCache = false; + #ifdef DEBUG_VIEW_WIDGET_PAINT - cerr << "View(" << this << ")::paintEvent: small repaint, not bothering to recreate cache" << endl; + cerr << "View(" << this << ")::paintEvent: cache is invalid but only small area requested, will repaint directly instead" << endl; #endif } else { - delete m_cache; - m_cache = new QPixmap(scaledCacheSize); + + if (!m_cache || + m_cache->size() != wholeSize) { + delete m_cache; + m_cache = new QPixmap(wholeSize); + } + #ifdef DEBUG_VIEW_WIDGET_PAINT - cerr << "View(" << this << ")::paintEvent: recreated cache" << endl; + cerr << "View(" << this << ")::paintEvent: cache is invalid, will repaint whole" << endl; #endif - cacheRect = rect(); - repaintCache = true; } + count.miss(); + } else if (m_cacheCentreFrame != m_centreFrame) { - int dx = - getXForFrame(m_cacheCentreFrame) - - getXForFrame(m_centreFrame); - - if (dx > -width() && dx < width()) { - static QPixmap *tmpPixmap = 0; - if (!tmpPixmap || tmpPixmap->size() != scaledCacheSize) { - delete tmpPixmap; - tmpPixmap = new QPixmap(scaledCacheSize); + int dx = dpratio * (getXForFrame(m_cacheCentreFrame) - + getXForFrame(m_centreFrame)); + + if (dx > -m_cache->width() && dx < m_cache->width()) { + + m_cache->scroll(dx, 0, m_cache->rect(), 0); + + if (dx < 0) { + cacheAreaToRepaint = + QRect(m_cache->width() + dx, 0, -dx, m_cache->height()); + } else { + cacheAreaToRepaint = + QRect(0, 0, dx, m_cache->height()); } - paint.begin(tmpPixmap); - paint.drawPixmap(0, 0, *m_cache); - paint.end(); - paint.begin(m_cache); - paint.drawPixmap(dx, 0, *tmpPixmap); - paint.end(); - if (dx < 0) { - cacheRect = QRect(width() + dx, 0, -dx, height()); - } else { - cacheRect = QRect(0, 0, dx, height()); - } + + count.partial(); + #ifdef DEBUG_VIEW_WIDGET_PAINT cerr << "View(" << this << ")::paintEvent: scrolled cache by " << dx << endl; #endif } else { - cacheRect = rect(); + count.miss(); #ifdef DEBUG_VIEW_WIDGET_PAINT cerr << "View(" << this << ")::paintEvent: scrolling too far" << endl; #endif } - repaintCache = true; } else { #ifdef DEBUG_VIEW_WIDGET_PAINT cerr << "View(" << this << ")::paintEvent: cache is good" << endl; #endif - paint.begin(m_buffer); - paint.drawPixmap(scaledCacheRect, *m_cache, scaledCacheRect); - paint.end(); - QFrame::paintEvent(e); - paintedCacheRect = true; + count.hit(); + shouldRepaintCache = false; } - + } + +#ifdef DEBUG_VIEW_WIDGET_PAINT + cerr << "View(" << this << ")::paintEvent: m_cacheValid = " << m_cacheValid << ", shouldUseCache = " << shouldUseCache << ", shouldRepaintCache = " << shouldRepaintCache << ", cacheAreaToRepaint = " << cacheAreaToRepaint.x() << "," << cacheAreaToRepaint.y() << " " << cacheAreaToRepaint.width() << "x" << cacheAreaToRepaint.height() << endl; +#endif + + if (shouldRepaintCache && !shouldUseCache) { + // If we are repainting the cache, then we paint the + // scrollables only to the cache, not to the buffer. So if + // shouldUseCache is also false, then the scrollables can't + // appear because they will only be on the cache + throw std::logic_error("ERROR: shouldRepaintCache is true, but shouldUseCache is false: this can't lead to the correct result"); + } + + // Scrollable (cacheable) items first. If we are repainting the + // cache, then we paint these to the cache; otherwise straight to + // the buffer. + + ViewProxy proxy(this, dpratio); + QRect areaToPaint; + QPainter paint; + + if (shouldRepaintCache) { + paint.begin(m_cache); + areaToPaint = cacheAreaToRepaint; + } else { + paint.begin(m_buffer); + areaToPaint = requestedPaintArea; + } + + setPaintFont(paint); + paint.setClipRect(areaToPaint); + + paint.setPen(getBackground()); + paint.setBrush(getBackground()); + paint.drawRect(areaToPaint); + + paint.setPen(getForeground()); + paint.setBrush(Qt::NoBrush); + + for (LayerList::iterator i = scrollables.begin(); + i != scrollables.end(); ++i) { + + paint.setRenderHint(QPainter::Antialiasing, false); + paint.save(); + +#ifdef DEBUG_VIEW_WIDGET_PAINT + cerr << "Painting scrollable layer " << *i << " using proxy with shouldRepaintCache = " << shouldRepaintCache << ", dpratio = " << dpratio << ", areaToPaint = " << areaToPaint.x() << "," << areaToPaint.y() << " " << areaToPaint.width() << "x" << areaToPaint.height() << endl; +#endif + + (*i)->paint(&proxy, paint, areaToPaint); + + paint.restore(); + } + + paint.end(); + + if (shouldRepaintCache) { + // and now we have + m_cacheValid = true; m_cacheCentreFrame = m_centreFrame; m_cacheZoomLevel = m_zoomLevel; } -#ifdef DEBUG_VIEW_WIDGET_PAINT -// cerr << "View(" << this << ")::paintEvent: cacheRect " << cacheRect << ", nonCacheRect " << (nonCacheRect | cacheRect) << ", repaintCache " << repaintCache << ", paintedCacheRect " << paintedCacheRect << endl; -#endif - - // Scrollable (cacheable) items first - - ViewProxy proxy(this, dpratio); - - if (!paintedCacheRect) { - - QRect rectToPaint; - - if (repaintCache) { - paint.begin(m_cache); - rectToPaint = scaledCacheRect; - } else { - paint.begin(m_buffer); - rectToPaint = scaledCacheRect; - } - - setPaintFont(paint); - paint.setClipRect(rectToPaint); - - paint.setPen(getBackground()); - paint.setBrush(getBackground()); - paint.drawRect(rectToPaint); - - paint.setPen(getForeground()); - paint.setBrush(Qt::NoBrush); - - for (LayerList::iterator i = scrollables.begin(); i != scrollables.end(); ++i) { - paint.setRenderHint(QPainter::Antialiasing, false); - paint.save(); -#ifdef DEBUG_VIEW_WIDGET_PAINT - cerr << "Painting scrollable layer " << *i << " using proxy with repaintCache = " << repaintCache << ", dpratio = " << dpratio << ", rectToPaint = " << rectToPaint.x() << "," << rectToPaint.y() << " " << rectToPaint.width() << "x" << rectToPaint.height() << endl; -#endif - (*i)->paint(&proxy, paint, rectToPaint); - paint.restore(); - } - - if (haveSelections && selectionCacheable) { - drawSelections(paint); - m_selectionCached = repaintCache; - } - + if (shouldUseCache) { + paint.begin(m_buffer); + paint.drawPixmap(requestedPaintArea, *m_cache, requestedPaintArea); paint.end(); - - if (repaintCache) { - cacheRect |= (e ? e->rect() : rect()); - scaledCacheRect = scaledRect(cacheRect, dpratio); - paint.begin(m_buffer); - paint.drawPixmap(scaledCacheRect, *m_cache, scaledCacheRect); - paint.end(); - } } - // Now non-cacheable items. We always need to redraw the - // non-cacheable items across at least the area we drew of the - // cacheable items. - - nonCacheRect |= cacheRect; - - QRect scaledNonCacheRect = scaledRect(nonCacheRect, dpratio); - + // Now non-cacheable items. + paint.begin(m_buffer); - paint.setClipRect(scaledNonCacheRect); + paint.setClipRect(requestedPaintArea); setPaintFont(paint); if (scrollables.empty()) { paint.setPen(getBackground()); paint.setBrush(getBackground()); - paint.drawRect(scaledNonCacheRect); + paint.drawRect(requestedPaintArea); } paint.setPen(getForeground()); paint.setBrush(Qt::NoBrush); - for (LayerList::iterator i = nonScrollables.begin(); i != nonScrollables.end(); ++i) { + for (LayerList::iterator i = nonScrollables.begin(); + i != nonScrollables.end(); ++i) { + // Profiler profiler2("View::paintEvent non-cacheable"); #ifdef DEBUG_VIEW_WIDGET_PAINT - cerr << "Painting non-scrollable layer " << *i << " without proxy with repaintCache = " << repaintCache << ", dpratio = " << dpratio << ", rectToPaint = " << nonCacheRect.x() << "," << nonCacheRect.y() << " " << nonCacheRect.width() << "x" << nonCacheRect.height() << endl; + cerr << "Painting non-scrollable layer " << *i << " without proxy with shouldRepaintCache = " << shouldRepaintCache << ", dpratio = " << dpratio << ", requestedPaintArea = " << requestedPaintArea.x() << "," << requestedPaintArea.y() << " " << requestedPaintArea.width() << "x" << requestedPaintArea.height() << endl; #endif - (*i)->paint(&proxy, paint, scaledNonCacheRect); + (*i)->paint(&proxy, paint, requestedPaintArea); } paint.end(); - - paint.begin(this); - QRect finalPaintRect = e ? e->rect() : rect(); - paint.drawPixmap(finalPaintRect, *m_buffer, scaledRect(finalPaintRect, dpratio)); - paint.end(); + + // Now paint to widget from buffer: target rects from here on, + // unlike all the preceding, are at formal (1x) resolution paint.begin(this); setPaintFont(paint); if (e) paint.setClipRect(e->rect()); - if (!m_selectionCached) { - drawSelections(paint); - } + + QRect finalPaintRect = e ? e->rect() : rect(); + paint.drawPixmap(finalPaintRect, *m_buffer, + scaledRect(finalPaintRect, dpratio)); + + drawSelections(paint); + drawPlayPointer(paint); + paint.end(); - bool showPlayPointer = true; - if (m_followPlay == PlaybackScrollContinuous) { - showPlayPointer = false; - } else if (m_playPointerFrame <= getStartFrame() || - m_playPointerFrame >= getEndFrame()) { - showPlayPointer = false; - } else if (m_manager && !m_manager->isPlaying()) { - if (m_playPointerFrame == getCentreFrame() && - m_manager->shouldShowCentreLine() && - m_followPlay != PlaybackIgnore) { - // Don't show the play pointer when it is redundant with - // the centre line - showPlayPointer = false; - } - } - - if (showPlayPointer) { - - paint.begin(this); - - int playx = getXForFrame(m_playPointerFrame); - - paint.setPen(getForeground()); - paint.drawLine(playx - 1, 0, playx - 1, height() - 1); - paint.drawLine(playx + 1, 0, playx + 1, height() - 1); - paint.drawPoint(playx, 0); - paint.drawPoint(playx, height() - 1); - paint.setPen(getBackground()); - paint.drawLine(playx, 1, playx, height() - 2); - - paint.end(); - } - QFrame::paintEvent(e); } @@ -2258,6 +2226,40 @@ } void +View::drawPlayPointer(QPainter &paint) +{ + bool showPlayPointer = true; + + if (m_followPlay == PlaybackScrollContinuous) { + showPlayPointer = false; + } else if (m_playPointerFrame <= getStartFrame() || + m_playPointerFrame >= getEndFrame()) { + showPlayPointer = false; + } else if (m_manager && !m_manager->isPlaying()) { + if (m_playPointerFrame == getCentreFrame() && + m_manager->shouldShowCentreLine() && + m_followPlay != PlaybackIgnore) { + // Don't show the play pointer when it is redundant with + // the centre line + showPlayPointer = false; + } + } + + if (showPlayPointer) { + + int playx = getXForFrame(m_playPointerFrame); + + paint.setPen(getForeground()); + paint.drawLine(playx - 1, 0, playx - 1, height() - 1); + paint.drawLine(playx + 1, 0, playx + 1, height() - 1); + paint.drawPoint(playx, 0); + paint.drawPoint(playx, height() - 1); + paint.setPen(getBackground()); + paint.drawLine(playx, 1, playx, height() - 2); + } +} + +void View::drawMeasurementRect(QPainter &paint, const Layer *topLayer, QRect r, bool focus) const { diff -r dddfd28e4f02 -r 93eaff6f206d view/View.h --- a/view/View.h Thu Oct 11 10:15:45 2018 +0100 +++ b/view/View.h Thu Oct 11 14:59:34 2018 +0100 @@ -440,6 +440,7 @@ virtual void paintEvent(QPaintEvent *e); virtual void drawSelections(QPainter &); virtual bool shouldLabelSelections() const { return true; } + virtual void drawPlayPointer(QPainter &); virtual bool render(QPainter &paint, int x0, sv_frame_t f0, sv_frame_t f1); virtual void setPaintFont(QPainter &paint); @@ -499,6 +500,7 @@ QPixmap *m_cache; // I own this QPixmap *m_buffer; // I own this + bool m_cacheValid; sv_frame_t m_cacheCentreFrame; ZoomLevel m_cacheZoomLevel; bool m_selectionCached;