From 64053342d8cae2d38c49cfb6c5b24fe4234ffdef Mon Sep 17 00:00:00 2001 From: Cas Pascal <81458575+khoidauminh@users.noreply.github.com> Date: Wed, 16 Apr 2025 10:40:16 +0700 Subject: [PATCH] [Follow up] Improve performance when rendering sample waveforms (#7695) A follow up to 786088baec64bec452cbf7fc736582c0a4ac9cb2 to fix rendering issues and crashes. --------- Co-authored-by: Sotonye Atemie --- include/SampleClipView.h | 1 + include/SampleThumbnail.h | 13 +++-- src/gui/SampleThumbnail.cpp | 74 ++++++++++++++++++---------- src/gui/clips/ClipView.cpp | 2 + src/gui/clips/SampleClipView.cpp | 34 +++++++++---- src/gui/editors/AutomationEditor.cpp | 1 + 6 files changed, 82 insertions(+), 43 deletions(-) diff --git a/include/SampleClipView.h b/include/SampleClipView.h index 10ce5b2f3..14f9a8235 100644 --- a/include/SampleClipView.h +++ b/include/SampleClipView.h @@ -67,6 +67,7 @@ private: SampleClip * m_clip; SampleThumbnail m_sampleThumbnail; QPixmap m_paintPixmap; + long m_paintPixmapXPosition; bool splitClip( const TimePos pos ) override; } ; diff --git a/include/SampleThumbnail.h b/include/SampleThumbnail.h index b8e1e85af..7d1082437 100644 --- a/include/SampleThumbnail.h +++ b/include/SampleThumbnail.h @@ -54,10 +54,8 @@ public: { QRect sampleRect; //!< A rectangle that covers the entire range of samples. - QRect drawRect; //!< Specifies the location in `sampleRect` where the waveform will be drawn. Equals - //!< `sampleRect` when null. - - QRect viewportRect; //!< Clips `drawRect`. Equals `drawRect` when null. + QRect viewportRect; //!< Specifies the location in `sampleRect` where the waveform will be drawn. Equals + //!< `sampleRect` when null. float amplification = 1.0f; //!< The amount of amplification to apply to the waveform. @@ -95,8 +93,8 @@ private: Peak operator+(const Peak& other) const { return Peak(std::min(min, other.min), std::max(max, other.max)); } Peak operator+(const SampleFrame& frame) const { return *this + Peak{frame}; } - float min = std::numeric_limits::max(); - float max = std::numeric_limits::min(); + float min = std::numeric_limits::infinity(); + float max = -std::numeric_limits::infinity(); }; Thumbnail() = default; @@ -105,6 +103,7 @@ private: Thumbnail zoomOut(float factor) const; + Peak* data() { return m_peaks.data(); } Peak& operator[](size_t index) { return m_peaks[index]; } const Peak& operator[](size_t index) const { return m_peaks[index]; } @@ -134,7 +133,7 @@ private: using ThumbnailCache = std::vector; std::shared_ptr m_thumbnailCache = std::make_shared(); - + std::shared_ptr m_buffer = SampleBuffer::emptyBuffer(); inline static std::unordered_map, Hash> s_sampleThumbnailCacheMap; }; diff --git a/src/gui/SampleThumbnail.cpp b/src/gui/SampleThumbnail.cpp index c31c0d93e..8ec18b5cb 100644 --- a/src/gui/SampleThumbnail.cpp +++ b/src/gui/SampleThumbnail.cpp @@ -70,6 +70,7 @@ SampleThumbnail::Thumbnail SampleThumbnail::Thumbnail::zoomOut(float factor) con } SampleThumbnail::SampleThumbnail(const Sample& sample) + : m_buffer(sample.buffer()) { auto entry = SampleThumbnailEntry{sample.sampleFile(), QFileInfo{sample.sampleFile()}.lastModified()}; if (!entry.filePath.isEmpty()) @@ -91,11 +92,9 @@ SampleThumbnail::SampleThumbnail(const Sample& sample) s_sampleThumbnailCacheMap[std::move(entry)] = m_thumbnailCache; } - if (!sample.buffer()) { throw std::runtime_error{"Cannot create a sample thumbnail with no buffer"}; } - if (sample.sampleSize() == 0) { return; } - - const auto fullResolutionWidth = sample.sampleSize() * DEFAULT_CHANNELS; - m_thumbnailCache->emplace_back(&sample.buffer()->data()->left(), fullResolutionWidth, fullResolutionWidth); + const auto flatBuffer = m_buffer->data()->data(); + const auto flatBufferSize = m_buffer->size() * DEFAULT_CHANNELS; + m_thumbnailCache->emplace_back(flatBuffer, flatBufferSize, flatBufferSize / AggregationPerZoomStep); while (m_thumbnailCache->back().width() >= AggregationPerZoomStep) { @@ -107,48 +106,71 @@ SampleThumbnail::SampleThumbnail(const Sample& sample) void SampleThumbnail::visualize(VisualizeParameters parameters, QPainter& painter) const { const auto& sampleRect = parameters.sampleRect; - const auto& drawRect = parameters.drawRect.isNull() ? sampleRect : parameters.drawRect; - const auto& viewportRect = parameters.viewportRect.isNull() ? drawRect : parameters.viewportRect; + const auto& viewportRect = parameters.viewportRect.isNull() ? sampleRect : parameters.viewportRect; - const auto renderRect = sampleRect.intersected(drawRect).intersected(viewportRect); + const auto renderRect = sampleRect.intersected(viewportRect); if (renderRect.isNull()) { return; } const auto sampleRange = parameters.sampleEnd - parameters.sampleStart; - if (sampleRange <= 0 || sampleRange > 1) { return; } + if (sampleRange <= 0.0f || sampleRange > 1.0f) { return; } - const auto targetThumbnailWidth = static_cast(static_cast(sampleRect.width()) / sampleRange); + const auto targetThumbnailWidth = static_cast(sampleRect.width() / sampleRange); const auto finerThumbnail = std::find_if(m_thumbnailCache->rbegin(), m_thumbnailCache->rend(), [&](const auto& thumbnail) { return thumbnail.width() >= targetThumbnailWidth; }); - if (finerThumbnail == m_thumbnailCache->rend()) - { - qDebug() << "Could not find closest finer thumbnail for a target width of" << targetThumbnailWidth; - return; - } + const auto useOriginalBuffer = finerThumbnail == m_thumbnailCache->rend(); + const auto drawOriginalBuffer = static_cast(targetThumbnailWidth) == m_buffer->size(); painter.save(); painter.setRenderHint(QPainter::Antialiasing, true); - const auto thumbnailBeginForward = std::max(renderRect.x() - sampleRect.x(), static_cast(parameters.sampleStart * targetThumbnailWidth)); - const auto thumbnailEndForward = std::max(renderRect.x() + renderRect.width() - sampleRect.x(), static_cast(parameters.sampleEnd * targetThumbnailWidth)); + const auto thumbnailBeginForward = std::max(renderRect.x() - sampleRect.x(), parameters.sampleStart * targetThumbnailWidth); + const auto thumbnailEndForward = std::max(renderRect.x() + renderRect.width() - sampleRect.x(), parameters.sampleEnd * targetThumbnailWidth); const auto thumbnailBegin = parameters.reversed ? targetThumbnailWidth - thumbnailBeginForward - 1 : thumbnailBeginForward; const auto thumbnailEnd = parameters.reversed ? targetThumbnailWidth - thumbnailEndForward : thumbnailEndForward; const auto advanceThumbnailBy = parameters.reversed ? -1 : 1; - const auto finerThumbnailScaleFactor = static_cast(finerThumbnail->width()) / targetThumbnailWidth; - const auto yScale = drawRect.height() / 2 * parameters.amplification; + const auto finerThumbnailWidth = useOriginalBuffer ? m_buffer->size() : finerThumbnail->width(); + const auto finerThumbnailScaleFactor = static_cast(finerThumbnailWidth) / targetThumbnailWidth; + const auto yScale = renderRect.height() / 2 * parameters.amplification; for (auto x = renderRect.x(), i = thumbnailBegin; x < renderRect.x() + renderRect.width() && i != thumbnailEnd; - ++x, i += advanceThumbnailBy) + ++x, i += advanceThumbnailBy) { - const auto beginAggregationAt = &(*finerThumbnail)[static_cast(std::floor(i * finerThumbnailScaleFactor))]; - const auto endAggregationAt = &(*finerThumbnail)[static_cast(std::ceil((i + 1) * finerThumbnailScaleFactor))]; - const auto peak = std::accumulate(beginAggregationAt, endAggregationAt, Thumbnail::Peak{}); + if (useOriginalBuffer && drawOriginalBuffer) + { + const auto value = m_buffer->data()->data()[i]; + painter.drawPoint(x, renderRect.center().y() - value * yScale); + continue; + } + else + { + const auto beginIndex = std::clamp(std::floor(i * finerThumbnailScaleFactor), 0, finerThumbnail->width() - 1); + const auto endIndex = std::clamp(std::ceil((i + 1) * finerThumbnailScaleFactor), 0, finerThumbnail->width() - 1); - const auto yMin = drawRect.center().y() - peak.min * yScale; - const auto yMax = drawRect.center().y() - peak.max * yScale; + auto minPeak = 0.f; + auto maxPeak = 0.f; - painter.drawLine(x, yMin, x, yMax); + if (useOriginalBuffer) + { + const auto flatBuffer = m_buffer->data()->data(); + const auto [min, max] = std::minmax_element(flatBuffer + beginIndex, flatBuffer + endIndex); + minPeak = *min; + maxPeak = *max; + } + else + { + const auto beginAggregationAt = finerThumbnail->data() + beginIndex; + const auto endAggregationAt = finerThumbnail->data() + endIndex; + const auto peak = std::accumulate(beginAggregationAt, endAggregationAt, Thumbnail::Peak{}); + minPeak = peak.min; + maxPeak = peak.max; + } + + const auto yMin = renderRect.center().y() - minPeak * yScale; + const auto yMax = renderRect.center().y() - maxPeak * yScale; + painter.drawLine(x, yMin, x, yMax); + } } painter.restore(); diff --git a/src/gui/clips/ClipView.cpp b/src/gui/clips/ClipView.cpp index f98351f37..6c3953cf5 100644 --- a/src/gui/clips/ClipView.cpp +++ b/src/gui/clips/ClipView.cpp @@ -306,6 +306,8 @@ void ClipView::remove() // as actually deleting the Clip with the deleteLater function. That being said, it shouldn't // be possible to make a Clip without a Track (i.e., Clip::getTrack is never nullptr). m_clip->deleteLater(); + + m_trackView->update(); } diff --git a/src/gui/clips/SampleClipView.cpp b/src/gui/clips/SampleClipView.cpp index d5cfb211e..a420d271a 100644 --- a/src/gui/clips/SampleClipView.cpp +++ b/src/gui/clips/SampleClipView.cpp @@ -37,6 +37,7 @@ #include "SampleThumbnail.h" #include "Song.h" #include "StringPairDrag.h" +#include "TrackView.h" namespace lmms::gui { @@ -45,7 +46,8 @@ namespace lmms::gui SampleClipView::SampleClipView( SampleClip * _clip, TrackView * _tv ) : ClipView( _clip, _tv ), m_clip( _clip ), - m_paintPixmap() + m_paintPixmap(), + m_paintPixmapXPosition(0) { // update UI and tooltip updateSample(); @@ -210,15 +212,22 @@ void SampleClipView::paintEvent( QPaintEvent * pe ) if( !needsUpdate() ) { - painter.drawPixmap( 0, 0, m_paintPixmap ); + painter.drawPixmap(m_paintPixmapXPosition, 0, m_paintPixmap); return; } setNeedsUpdate( false ); - if (m_paintPixmap.isNull() || m_paintPixmap.size() != size()) + const auto trackViewWidth = getTrackView()->rect().width(); + + // Use the clip's height to avoid artifacts when rendering while something else is overlaying the clip. + const auto viewPortRect = QRect(0, 0, trackViewWidth * 2, rect().height()); + + m_paintPixmapXPosition = std::max(0, pe->rect().x() - trackViewWidth); + + if (m_paintPixmap.isNull() || m_paintPixmap.size() != viewPortRect.size()) { - m_paintPixmap = QPixmap(size()); + m_paintPixmap = QPixmap(viewPortRect.size()); } QPainter p( &m_paintPixmap ); @@ -274,12 +283,14 @@ void SampleClipView::paintEvent( QPaintEvent * pe ) float sampleLength = m_clip->sampleLength() * ppb / ticksPerBar; const auto& sample = m_clip->m_sample; + + const auto sampleRextX = static_cast(offsetStart) - m_paintPixmapXPosition; + if (sample.sampleSize() > 0) { const auto param = SampleThumbnail::VisualizeParameters{ - .sampleRect = QRect(offsetStart, spacing, sampleLength, height() - spacing), - .drawRect = QRect(0, spacing, width(), height() - spacing), - .viewportRect = pe->rect(), + .sampleRect = QRect(sampleRextX, spacing, sampleLength, height() - spacing), + .viewportRect = viewPortRect, .amplification = sample.amplification(), .reversed = sample.reversed() }; @@ -295,12 +306,15 @@ void SampleClipView::paintEvent( QPaintEvent * pe ) // inner border p.setPen( c.lighter( 135 ) ); - p.drawRect( 1, 1, rect().right() - BORDER_WIDTH, + p.drawRect( + -m_paintPixmapXPosition + 1, + 1, + rect().right() - BORDER_WIDTH, rect().bottom() - BORDER_WIDTH ); // outer border p.setPen( c.darker( 200 ) ); - p.drawRect( 0, 0, rect().right(), rect().bottom() ); + p.drawRect(-m_paintPixmapXPosition, 0, rect().right(), rect().bottom()); // draw the 'muted' pixmap only if the clip was manualy muted if( m_clip->isMuted() ) @@ -332,7 +346,7 @@ void SampleClipView::paintEvent( QPaintEvent * pe ) p.end(); - painter.drawPixmap( 0, 0, m_paintPixmap ); + painter.drawPixmap(m_paintPixmapXPosition, 0, m_paintPixmap); } diff --git a/src/gui/editors/AutomationEditor.cpp b/src/gui/editors/AutomationEditor.cpp index e1805f727..1239da55e 100644 --- a/src/gui/editors/AutomationEditor.cpp +++ b/src/gui/editors/AutomationEditor.cpp @@ -1216,6 +1216,7 @@ void AutomationEditor::paintEvent(QPaintEvent * pe ) const auto param = SampleThumbnail::VisualizeParameters{ .sampleRect = QRect(startPos, yOffset, sampleWidth, sampleHeight), + .viewportRect = rect(), .amplification = sample.amplification(), .sampleStart = static_cast(sample.startFrame()) / sample.sampleSize(), .sampleEnd = static_cast(sample.endFrame()) / sample.sampleSize(),