Skip to content

Commit e6ac8fa

Browse files
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
1 parent 560c995 commit e6ac8fa

3 files changed

Lines changed: 73 additions & 4 deletions

File tree

‎Plugins/LfpViewer/LfpChannelDisplay.cpp‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,15 @@ void LfpChannelDisplay::pxPaint()
154154
jto_wholechannel = display->lfpChannelBitmap.getHeight() - 1;
155155
};
156156

157+
// Each clamp above only moves one end of the span, so a channel lying
158+
// above or below the bitmap window ends up with jto < jfrom. The bitmap
159+
// only covers the visible viewport plus a margin, and LfpDisplay paints
160+
// every channel that overlaps that window at all, so this is the normal
161+
// state of the channels straddling its top and bottom edges.
162+
const bool channelSpanIsVisible = jfrom_wholechannel <= jto_wholechannel;
163+
157164
// draw most recent drawn sample position
158-
if (ito_local < display->lfpChannelBitmap.getWidth() - 1)
165+
if (channelSpanIsVisible && ito_local < display->lfpChannelBitmap.getWidth() - 1)
159166
{
160167
overlayGraphics.setColour (Colours::yellow);
161168
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
433440
jto_wholechannel = display->lfpChannelBitmap.getHeight() - 1;
434441
};
435442

436-
if (playhead < rightEdge - 1)
443+
// See pxPaint(): the one-sided clamps above can leave jto < jfrom for a
444+
// channel that sits outside the bitmap window.
445+
const bool channelSpanIsVisible = jfrom_wholechannel <= jto_wholechannel;
446+
447+
if (channelSpanIsVisible && playhead < rightEdge - 1)
437448
{
438449
overlayGraphics.setColour (Colours::yellow);
439450
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
698709

699710
void LfpChannelDisplay::drawEventOverlay (const int rawEventState, int x, int yfrom, int yto, Graphics& g)
700711
{
712+
if (yto < yfrom)
713+
return; // channel span lies outside the channel bitmap
714+
701715
float alpha = channelHeight > 5 ? 0.3f : 0.5f;
702716
for (int ev_ch = 0; ev_ch < 8; ev_ch++)
703717
{

‎Plugins/LfpViewer/LfpDisplay.h‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,9 @@ namespace LfpViewer
4141
Holds and draws all of the LfpDisplayChannel and lfpDisplayChannelInfo
4242
instances.
4343
44-
All of the channels and channelInfos are drawn here to a "master" bitmap
45-
lfpChannelBitmap with height equal to the sum of all channel heights. This
44+
All of the channels and channelInfos are drawn here to a bitmap
45+
lfpChannelBitmap covering the visible viewport plus a small vertical
46+
margin, positioned at channelBitmapYOrigin in component coordinates. This
4647
bitmap is drawn by the LfpViewport using Viewport::setViewedComponent.
4748
4849
*/

‎Plugins/LfpViewer/Tests/LfpDisplayNodeTests.cpp‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323

2424
#include <stdio.h>
2525

26+
#include <iostream>
27+
#include <string>
28+
2629
#include "gtest/gtest.h"
2730

2831
#include "../LfpDisplayCanvas.h"
@@ -602,3 +605,54 @@ TEST_F (LfpDisplayNodeLowRateTests, LowRateTraceIsConnected)
602605

603606
processor->stopAcquisition();
604607
}
608+
609+
/*
610+
The channel bitmap only covers the visible viewport plus a small margin, so
611+
a tall channel stack leaves some channels hanging off its top or bottom
612+
edge. Those channels are still painted, and their vertical extent has to be
613+
clipped to the bitmap before it is handed to Graphics::fillRect. Passing a
614+
negative height there trips a jassertquiet in juce_GraphicsContext.cpp,
615+
which floods the console on every refresh of a Debug build.
616+
617+
Logger::outputDebugString writes to stderr, so capturing stderr around the
618+
refresh is enough to detect the assertion.
619+
*/
620+
TEST_F (LfpDisplayNodeTests, OffscreenChannelsDoNotAssertOnNegativeOverlaySpan)
621+
{
622+
const int canvasX = 600;
623+
const int canvasY = 400;
624+
const int tallChannelHeight = 120; // 16 channels -> 1920px stack, far taller than the viewport
625+
626+
std::unique_ptr<LfpViewer::LfpDisplayCanvas> canvas =
627+
std::make_unique<LfpViewer::LfpDisplayCanvas> (processor, LfpViewer::SplitLayouts::SINGLE, false);
628+
canvas->updateSettings();
629+
canvas->setSize (canvasX, canvasY);
630+
canvas->resized();
631+
canvas->setVisible (true);
632+
canvas->setChannelHeight (0, tallChannelHeight);
633+
canvas->refreshState();
634+
635+
processor->startAcquisition();
636+
canvas->beginAnimation();
637+
638+
testing::internal::CaptureStderr();
639+
640+
for (int block = 0; block < 4; block++)
641+
{
642+
auto inputBuffer = createBufferSinusoidal (5, numChannels, 500, 125);
643+
writeBlock (inputBuffer);
644+
canvas->refreshState();
645+
}
646+
647+
const std::string captured = testing::internal::GetCapturedStderr();
648+
649+
// Echo whatever was captured so a failure is still visible in the test log.
650+
std::cerr << captured;
651+
652+
EXPECT_EQ (captured.find ("juce_GraphicsContext.cpp"), std::string::npos)
653+
<< "LFP Viewer passed an out-of-range rectangle to a Graphics call while "
654+
"painting channels that hang off the edge of the channel bitmap:\n"
655+
<< captured;
656+
657+
processor->stopAcquisition();
658+
}

0 commit comments

Comments
 (0)