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(); +}