From e6ac8fac98e4167407052385c29ff2bcf2fae9df Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Wed, 5 Aug 2026 10:06:02 -0700 Subject: [PATCH] Fix LFP Viewer assertion when channels hang off the channel bitmap Since the channel bitmap became a viewport-sized window, pxPaint() and pxPaintHistory() measure each channel from channelBitmapYOrigin rather than from the top of the whole channel stack. The two clamps that bound the channel span were left as they were, and each of them only moves one end of that span: jfrom is raised to 0 and jto is lowered to the last row of the bitmap. A channel sitting above or below the bitmap window therefore ends up with jto < jfrom. LfpDisplay::refresh paints every channel that overlaps the bitmap window at all, so the channels straddling its top and bottom edges are painted in exactly that state on every refresh. The yellow playhead fillRect and drawEventOverlay then receive a negative height, which trips the jassertquiet in juce_GraphicsContext.cpp and floods the console of a Debug build. A negative-height rectangle is already a no-op in JUCE, so skipping the draw when the clipped span is empty leaves rendering unchanged and only removes the assertion. Also corrects the LfpDisplay doc comment, which still described the bitmap as spanning the sum of all channel heights. Adds a regression test that scrolls a tall channel stack past the viewport and asserts that no JUCE assertion reaches stderr. Fixes #705 --- Plugins/LfpViewer/LfpChannelDisplay.cpp | 18 ++++++- Plugins/LfpViewer/LfpDisplay.h | 5 +- .../LfpViewer/Tests/LfpDisplayNodeTests.cpp | 54 +++++++++++++++++++ 3 files changed, 73 insertions(+), 4 deletions(-) diff --git a/Plugins/LfpViewer/LfpChannelDisplay.cpp b/Plugins/LfpViewer/LfpChannelDisplay.cpp index fea8e33cd..9cefd4737 100644 --- a/Plugins/LfpViewer/LfpChannelDisplay.cpp +++ b/Plugins/LfpViewer/LfpChannelDisplay.cpp @@ -154,8 +154,15 @@ void LfpChannelDisplay::pxPaint() jto_wholechannel = display->lfpChannelBitmap.getHeight() - 1; }; + // Each clamp above only moves one end of the span, so a channel lying + // above or below the bitmap window ends up with jto < jfrom. The bitmap + // only covers the visible viewport plus a margin, and LfpDisplay paints + // every channel that overlaps that window at all, so this is the normal + // state of the channels straddling its top and bottom edges. + const bool channelSpanIsVisible = jfrom_wholechannel <= jto_wholechannel; + // draw most recent drawn sample position - if (ito_local < display->lfpChannelBitmap.getWidth() - 1) + if (channelSpanIsVisible && ito_local < display->lfpChannelBitmap.getWidth() - 1) { overlayGraphics.setColour (Colours::yellow); overlayGraphics.fillRect (ito_local + 1, jfrom_wholechannel, 1, jto_wholechannel - jfrom_wholechannel + 1); // draw yellow line @@ -433,7 +440,11 @@ void LfpChannelDisplay::pxPaintHistory (int playhead, int rightEdge, int maxScre jto_wholechannel = display->lfpChannelBitmap.getHeight() - 1; }; - if (playhead < rightEdge - 1) + // See pxPaint(): the one-sided clamps above can leave jto < jfrom for a + // channel that sits outside the bitmap window. + const bool channelSpanIsVisible = jfrom_wholechannel <= jto_wholechannel; + + if (channelSpanIsVisible && playhead < rightEdge - 1) { overlayGraphics.setColour (Colours::yellow); overlayGraphics.fillRect (playhead + 1, jfrom_wholechannel, 1, jto_wholechannel - jfrom_wholechannel + 1); // draw yellow line @@ -698,6 +709,9 @@ void LfpChannelDisplay::pxPaintHistory (int playhead, int rightEdge, int maxScre void LfpChannelDisplay::drawEventOverlay (const int rawEventState, int x, int yfrom, int yto, Graphics& g) { + if (yto < yfrom) + return; // channel span lies outside the channel bitmap + float alpha = channelHeight > 5 ? 0.3f : 0.5f; for (int ev_ch = 0; ev_ch < 8; ev_ch++) { diff --git a/Plugins/LfpViewer/LfpDisplay.h b/Plugins/LfpViewer/LfpDisplay.h index 0fc7f3ee1..450c3595a 100644 --- a/Plugins/LfpViewer/LfpDisplay.h +++ b/Plugins/LfpViewer/LfpDisplay.h @@ -41,8 +41,9 @@ namespace LfpViewer Holds and draws all of the LfpDisplayChannel and lfpDisplayChannelInfo instances. - All of the channels and channelInfos are drawn here to a "master" bitmap - lfpChannelBitmap with height equal to the sum of all channel heights. This + All of the channels and channelInfos are drawn here to a bitmap + lfpChannelBitmap covering the visible viewport plus a small vertical + margin, positioned at channelBitmapYOrigin in component coordinates. This bitmap is drawn by the LfpViewport using Viewport::setViewedComponent. */ diff --git a/Plugins/LfpViewer/Tests/LfpDisplayNodeTests.cpp b/Plugins/LfpViewer/Tests/LfpDisplayNodeTests.cpp index 63640fd99..3c4bba4c2 100644 --- a/Plugins/LfpViewer/Tests/LfpDisplayNodeTests.cpp +++ b/Plugins/LfpViewer/Tests/LfpDisplayNodeTests.cpp @@ -23,6 +23,9 @@ #include +#include +#include + #include "gtest/gtest.h" #include "../LfpDisplayCanvas.h" @@ -602,3 +605,54 @@ TEST_F (LfpDisplayNodeLowRateTests, LowRateTraceIsConnected) processor->stopAcquisition(); } + +/* + The channel bitmap only covers the visible viewport plus a small margin, so + a tall channel stack leaves some channels hanging off its top or bottom + edge. Those channels are still painted, and their vertical extent has to be + clipped to the bitmap before it is handed to Graphics::fillRect. Passing a + negative height there trips a jassertquiet in juce_GraphicsContext.cpp, + which floods the console on every refresh of a Debug build. + + Logger::outputDebugString writes to stderr, so capturing stderr around the + refresh is enough to detect the assertion. +*/ +TEST_F (LfpDisplayNodeTests, OffscreenChannelsDoNotAssertOnNegativeOverlaySpan) +{ + const int canvasX = 600; + const int canvasY = 400; + const int tallChannelHeight = 120; // 16 channels -> 1920px stack, far taller than the viewport + + std::unique_ptr canvas = + std::make_unique (processor, LfpViewer::SplitLayouts::SINGLE, false); + canvas->updateSettings(); + canvas->setSize (canvasX, canvasY); + canvas->resized(); + canvas->setVisible (true); + canvas->setChannelHeight (0, tallChannelHeight); + canvas->refreshState(); + + processor->startAcquisition(); + canvas->beginAnimation(); + + testing::internal::CaptureStderr(); + + for (int block = 0; block < 4; block++) + { + auto inputBuffer = createBufferSinusoidal (5, numChannels, 500, 125); + writeBlock (inputBuffer); + canvas->refreshState(); + } + + const std::string captured = testing::internal::GetCapturedStderr(); + + // Echo whatever was captured so a failure is still visible in the test log. + std::cerr << captured; + + EXPECT_EQ (captured.find ("juce_GraphicsContext.cpp"), std::string::npos) + << "LFP Viewer passed an out-of-range rectangle to a Graphics call while " + "painting channels that hang off the edge of the channel bitmap:\n" + << captured; + + processor->stopAcquisition(); +}