Skip to content

Commit 0a0d18b

Browse files
committed
Optimize LFP Viewer screen buffer processring
Use SIMD operations for min. max, and mean calulcation
1 parent de69124 commit 0a0d18b

1 file changed

Lines changed: 151 additions & 68 deletions

File tree

Plugins/LfpViewer/LfpDisplayCanvas.cpp

Lines changed: 151 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -850,7 +850,13 @@ void LfpDisplaySplitter::beginAnimation()
850850
eventState = 0;
851851
}
852852

853-
startTimer (20);
853+
// Adaptive timer: slower refresh for high channel counts to reduce CPU load
854+
// Base: 50Hz (20ms), >384 channels: ~30Hz (33ms)
855+
int refreshIntervalMs = 20;
856+
if (nChans > 384)
857+
refreshIntervalMs = 33; // ~30Hz for 256+ channels
858+
859+
startTimer (refreshIntervalMs);
854860

855861
reachedEnd = true;
856862
}
@@ -1162,7 +1168,7 @@ void LfpDisplaySplitter::updateScreenBuffer()
11621168
int triggerTime = hasTrigger ? int (*triggerTimeOpt) : -1;
11631169

11641170
int maxSamples = screenBufferWidth;
1165-
int displayWidth = lfpDisplay->lfpChannelBitmap.getWidth();
1171+
const int displayWidth = lfpDisplay->lfpChannelBitmap.getWidth();
11661172

11671173
if (triggerChannel >= 0)
11681174
{
@@ -1175,6 +1181,14 @@ void LfpDisplaySplitter::updateScreenBuffer()
11751181
processor->acknowledgeTrigger (splitID);
11761182
}
11771183

1184+
// Pre-compute loop invariants
1185+
const float ratio = sampleRate * timebase / float (displayWidth); // samples / pixel
1186+
const float invRatio = 1.0f / ratio;
1187+
const int maxSamplesMinusOne = maxSamples - 1;
1188+
const bool isPaused = lfpDisplay->isPaused();
1189+
const bool isTriggeredMode = triggerChannel >= 0;
1190+
const bool shouldAverage = isTriggeredMode && trialAveraging;
1191+
11781192
for (int channel = 0; channel <= nChans; channel++) // pull one extra channel for event display
11791193
{
11801194
int dbi = displayBufferIndex[channel]; // display buffer index from the last round of drawing
@@ -1196,10 +1210,8 @@ void LfpDisplaySplitter::updateScreenBuffer()
11961210
//if (channel == 0)
11971211
// std::cout << newSamples << " new samples." << std::endl;
11981212

1199-
// this number is crucial -- converting from samples to values (in px) for the screen buffer:
1200-
float ratio = sampleRate * timebase / float (displayWidth); // samples / pixel
1201-
1202-
float pixelsToFill = float (newSamples) / ratio; // M pixels to update
1213+
// ratio is pre-computed above the channel loop
1214+
float pixelsToFill = float (newSamples) * invRatio; // M pixels to update
12031215

12041216
int sbi = screenBufferIndex[channel];
12051217

@@ -1235,7 +1247,7 @@ void LfpDisplaySplitter::updateScreenBuffer()
12351247
if (newSamples < 0)
12361248
newSamples += displayBufferSize;
12371249

1238-
pixelsToFill = newSamples / ratio;
1250+
pixelsToFill = newSamples * invRatio;
12391251
subSampleOffset = 0;
12401252

12411253
// rewind screen buffer to the far left
@@ -1306,57 +1318,70 @@ void LfpDisplaySplitter::updateScreenBuffer()
13061318
{
13071319
float i;
13081320

1321+
// Get raw pointers for this channel to avoid repeated getSample() calls
1322+
const float* displayData = displayBuffer->getReadPointer (channel);
1323+
float* meanWritePtr = (channel < nChans) ? screenBufferMean->getWritePointer (channel) : nullptr;
1324+
float* minWritePtr = (channel < nChans) ? screenBufferMin->getWritePointer (channel) : nullptr;
1325+
float* maxWritePtr = (channel < nChans) ? screenBufferMax->getWritePointer (channel) : nullptr;
1326+
const float* meanReadPtr = meanWritePtr;
1327+
const float* minReadPtr = minWritePtr;
1328+
const float* maxReadPtr = maxWritePtr;
1329+
float* eventWritePtr = eventDisplayBuffer->getWritePointer (0);
1330+
1331+
const bool isEventChannel = (channel == nChans);
1332+
const bool shouldClearBuffers = ! isTriggeredMode || numTrials == 0 || ! trialAveraging;
1333+
const float trialGain = (numTrials > 0) ? static_cast<float> (numTrials) : 1.0f;
1334+
13091335
for (i = 0; i < pixelsToFill; i++)
13101336
{
1311-
if (! lfpDisplay->isPaused())
1337+
if (! isPaused)
13121338
{
1313-
if (channel == nChans)
1339+
if (isEventChannel)
13141340
{
1315-
eventDisplayBuffer->clear (0, sbi, 1);
1341+
eventWritePtr[sbi] = 0.0f;
13161342
}
13171343
else
13181344
{
1319-
if (triggerChannel < 0 || numTrials == 0 || trialAveraging == false)
1345+
if (shouldClearBuffers)
13201346
{
1321-
screenBufferMean->clear (channel, sbi, 1);
1322-
screenBufferMin->clear (channel, sbi, 1);
1323-
screenBufferMax->clear (channel, sbi, 1);
1347+
meanWritePtr[sbi] = 0.0f;
1348+
minWritePtr[sbi] = 0.0f;
1349+
maxWritePtr[sbi] = 0.0f;
13241350
}
13251351
else
13261352
{
1327-
screenBufferMean->applyGain (channel, sbi, 1, numTrials);
1328-
screenBufferMin->applyGain (channel, sbi, 1, numTrials);
1329-
screenBufferMax->applyGain (channel, sbi, 1, numTrials);
1353+
meanWritePtr[sbi] *= trialGain;
1354+
minWritePtr[sbi] *= trialGain;
1355+
maxWritePtr[sbi] *= trialGain;
13301356
}
13311357
}
13321358

1333-
if (ratio < 1.0) // less than one sample per pixel
1359+
if (ratio < 1.0f) // less than one sample per pixel
13341360
{
1335-
if (channel == nChans)
1361+
if (isEventChannel)
13361362
{
1337-
eventDisplayBuffer->setSample (0, sbi, displayBuffer->getSample (channel, dbi));
1363+
eventWritePtr[sbi] = displayData[dbi];
13381364
}
13391365
else
13401366
{
1341-
float alpha = subSampleOffset;
1342-
float invAlpha = 1.0f - alpha;
1367+
const float alpha = subSampleOffset;
1368+
const float invAlpha = 1.0f - alpha;
13431369

13441370
int lastIndex = dbi - 1;
13451371

13461372
if (lastIndex < 0)
13471373
{
1348-
lastIndex = displayBufferSize;
1374+
lastIndex = displayBufferSize - 1;
13491375
continue;
13501376
}
13511377

1352-
float val0 = displayBuffer->getSample (channel, lastIndex);
1353-
float val1 = displayBuffer->getSample (channel, dbi);
1354-
1355-
float val = invAlpha * val0 + alpha * val1;
1378+
const float val0 = displayData[lastIndex];
1379+
const float val1 = displayData[dbi];
1380+
const float val = invAlpha * val0 + alpha * val1;
13561381

1357-
screenBufferMean->addSample (channel, sbi, val);
1358-
screenBufferMin->addSample (channel, sbi, val);
1359-
screenBufferMax->addSample (channel, sbi, val);
1382+
meanWritePtr[sbi] += val;
1383+
minWritePtr[sbi] += val;
1384+
maxWritePtr[sbi] += val;
13601385
}
13611386

13621387
subSampleOffset += ratio;
@@ -1371,85 +1396,143 @@ void LfpDisplaySplitter::updateScreenBuffer()
13711396
else
13721397
{ // more than one sample per pixel
13731398

1374-
float sample_min = 10000000;
1375-
float sample_max = -10000000;
1376-
float sample_sum = 0;
1377-
float sampleCount = 0;
1399+
float sample_min = 1e10f;
1400+
float sample_max = -1e10f;
1401+
float sample_sum = 0.0f;
13781402

13791403
subSampleOffset += ratio;
13801404

1381-
if (subSampleOffset <= 1.0f)
1405+
// Calculate how many samples we need to process for this pixel
1406+
int samplesToProcess = static_cast<int> (subSampleOffset);
1407+
if (sampleNumber + samplesToProcess > newSamples)
1408+
samplesToProcess = newSamples - sampleNumber;
1409+
1410+
if (samplesToProcess <= 0)
13821411
{
1383-
sample_sum = displayBuffer->getSample (channel, dbi);
1384-
sample_min = sample_sum;
1385-
sample_max = sample_sum;
1386-
sampleCount = 1.0f;
1412+
subSampleOffset -= 1.0f;
1413+
dbi = (dbi + 1) % displayBufferSize;
1414+
sampleNumber++;
1415+
continue;
13871416
}
13881417

1389-
bool foundIt = false;
1418+
// Check if data is contiguous (doesn't wrap around buffer)
1419+
const int samplesUntilWrap = displayBufferSize - dbi;
13901420

1391-
while (subSampleOffset > 1.0f && sampleNumber < newSamples)
1421+
if (samplesToProcess >= 4 && samplesToProcess <= samplesUntilWrap)
13921422
{
1393-
sampleNumber++;
1423+
// SIMD path: contiguous data, use vectorized operations
1424+
const float* dataPtr = displayData + dbi;
13941425

1395-
float sample_current = displayBuffer->getSample (channel, dbi);
1426+
// Use JUCE's SIMD-accelerated min/max finder
1427+
juce::Range<float> minMax = juce::FloatVectorOperations::findMinAndMax (dataPtr, samplesToProcess);
1428+
sample_min = minMax.getStart();
1429+
sample_max = minMax.getEnd();
13961430

1397-
sample_sum = sample_sum + sample_current;
1431+
// SIMD sum - process 4 floats at a time
1432+
int idx = 0;
1433+
const int simdWidth = 4;
1434+
const int simdIterations = samplesToProcess / simdWidth;
13981435

1399-
if (sample_min >= sample_current)
1436+
if (simdIterations > 0)
14001437
{
1401-
sample_min = sample_current;
1438+
float sumAccum[4] = { 0.0f, 0.0f, 0.0f, 0.0f };
1439+
1440+
for (int s = 0; s < simdIterations; s++)
1441+
{
1442+
sumAccum[0] += dataPtr[idx];
1443+
sumAccum[1] += dataPtr[idx + 1];
1444+
sumAccum[2] += dataPtr[idx + 2];
1445+
sumAccum[3] += dataPtr[idx + 3];
1446+
idx += simdWidth;
1447+
}
1448+
1449+
sample_sum = sumAccum[0] + sumAccum[1] + sumAccum[2] + sumAccum[3];
14021450
}
14031451

1404-
if (sample_max <= sample_current)
1452+
// Handle remaining samples
1453+
for (; idx < samplesToProcess; idx++)
14051454
{
1406-
sample_max = sample_current;
1455+
sample_sum += dataPtr[idx];
14071456
}
14081457

1409-
subSampleOffset -= 1.0f;
1458+
dbi = (dbi + samplesToProcess) % displayBufferSize;
1459+
sampleNumber += samplesToProcess;
1460+
subSampleOffset -= static_cast<float> (samplesToProcess);
1461+
}
1462+
else
1463+
{
1464+
// Scalar path: handle wrap-around or small sample counts
1465+
// Process first chunk (up to buffer end)
1466+
const int firstChunk = std::min (samplesToProcess, samplesUntilWrap);
14101467

1411-
dbi += 1;
1412-
dbi %= displayBufferSize;
1468+
for (int s = 0; s < firstChunk; s++)
1469+
{
1470+
const float sample_current = displayData[dbi + s];
1471+
sample_sum += sample_current;
1472+
1473+
if (sample_current < sample_min)
1474+
sample_min = sample_current;
1475+
if (sample_current > sample_max)
1476+
sample_max = sample_current;
1477+
}
1478+
1479+
// Process second chunk (from buffer start) if wrapped
1480+
const int secondChunk = samplesToProcess - firstChunk;
1481+
for (int s = 0; s < secondChunk; s++)
1482+
{
1483+
const float sample_current = displayData[s];
1484+
sample_sum += sample_current;
1485+
1486+
if (sample_current < sample_min)
1487+
sample_min = sample_current;
1488+
if (sample_current > sample_max)
1489+
sample_max = sample_current;
1490+
}
14131491

1414-
sampleCount += 1.0f;
1492+
dbi = (dbi + samplesToProcess) % displayBufferSize;
1493+
sampleNumber += samplesToProcess;
1494+
subSampleOffset -= static_cast<float> (samplesToProcess);
14151495
}
14161496

1417-
float sample_mean = sample_sum / sampleCount;
1497+
const float sampleCount = static_cast<float> (samplesToProcess);
1498+
const float sample_mean = sample_sum / sampleCount;
14181499

14191500
// update event channel
1420-
if (channel == nChans)
1501+
if (isEventChannel)
14211502
{
1422-
eventDisplayBuffer->setSample (0, sbi, sample_max);
1503+
eventWritePtr[sbi] = sample_max;
14231504
}
14241505
else
14251506
{
14261507
if (sbi > 0)
14271508
{
1428-
if (sample_max < screenBufferMin->getSample (channel, sbi - 1))
1429-
sample_max = screenBufferMin->getSample (channel, sbi - 1);
1509+
const int prevSbi = sbi - 1;
1510+
if (sample_max < minReadPtr[prevSbi])
1511+
sample_max = minReadPtr[prevSbi];
14301512

1431-
if (sample_min > screenBufferMax->getSample (channel, sbi - 1))
1432-
sample_min = screenBufferMax->getSample (channel, sbi - 1);
1513+
if (sample_min > maxReadPtr[prevSbi])
1514+
sample_min = maxReadPtr[prevSbi];
14331515
}
14341516

1435-
screenBufferMean->addSample (channel, sbi, sample_mean);
1436-
screenBufferMin->addSample (channel, sbi, sample_min);
1437-
screenBufferMax->addSample (channel, sbi, sample_max);
1517+
meanWritePtr[sbi] += sample_mean;
1518+
minWritePtr[sbi] += sample_min;
1519+
maxWritePtr[sbi] += sample_max;
14381520
}
14391521
}
14401522

1441-
if (triggerChannel >= 0 && trialAveraging == true && channel != nChans)
1523+
if (shouldAverage && ! isEventChannel)
14421524
{
1443-
screenBufferMean->applyGain (channel, sbi, 1, 1 / (numTrials + 1));
1444-
screenBufferMin->applyGain (channel, sbi, 1, 1 / (numTrials + 1));
1445-
screenBufferMax->applyGain (channel, sbi, 1, 1 / (numTrials + 1));
1525+
const float avgGain = 1.0f / (numTrials + 1.0f);
1526+
meanWritePtr[sbi] *= avgGain;
1527+
minWritePtr[sbi] *= avgGain;
1528+
maxWritePtr[sbi] *= avgGain;
14461529
}
14471530

14481531
sbi++;
14491532

1450-
if (triggerChannel >= 0)
1533+
if (isTriggeredMode)
14511534
{
1452-
if (sbi == maxSamples - 1)
1535+
if (sbi == maxSamplesMinusOne)
14531536
{
14541537
//std::cout << "CH " << channel << " reached end: " << maxSamples << " samples " << std::endl;
14551538

0 commit comments

Comments
 (0)