I'm writing an utility audio plugin that specializes in "fullscreen" (the window is freely resizable) visualization of audio, like spectrograms, oscilloscopes and in this instance, a vectorscope. For those who doesn't know, a vectorscope is a coordinate system where each sample in an audiostream maps the coordinates (left is x, while right channel is y). Here's an image of the visualization the program performs:
I am however experiencing troublesome performance, dialogs like 1280x1024 can consume 100% of one core depending on circumstances.
I'm currently hoping my rendering code is simply bad and that the performance can be increased, therefore I'm asking for a review of the following code. This is the first time I'm writing graphics code, so bear with me. So far I've steered clear of most bad premature optimizations (I can also measure most didn't do much), and tried to retain nice modularization of the code.
I've included the relevant parts of the code and I hope it's clear what's going on. Here's the painting function:
void CVectorScope::paint(Graphics & g)
{
auto clockStart = cpl::Misc::ClockCounter();
// erase previous content
waveFormGraphics->fillAll(Colours::black);
// .. draw graph here
// render waveform of all audio samples in the buffer, interconnecting each new pair with a line to the previous
renderGeneric(numSamples,
// this is the 'fetch' lambda, that returns a sample from the audio buffer
[&](std::size_t channel, std::size_t sample)
{
return audioData[channel].directAccess(sample);
},
// this is the line-drawing lambda
[](Image::BitmapData & data, int x, int y, int ox, int oy, float sampleFade)
{
bDrawLine<float>(x, y, ox, oy,
// the line drawing lambda uses following lambda to color pixels
[&](int xx, int yy)
{
// get the pixel pointer to the second channel (green)
auto p1 = (data.getPixelPointer(xx, yy) + 1);
// color the pixel up to half of the remaining bits
*p1 = *p1 + ((0xFF - *p1) >> 1) * sampleFade;
}
);
}
);
auto renderStop = cpl::Misc::ClockCounter();
// 'transferring' software image to screen buffer?
g.drawImageAt(waveForm, 0, 0);
auto cyclesNow = cpl::Misc::ClockCounter();
// .. print diagnostics below
}
And here's the bDrawLine function:
/*
http://en.wikipedia.org/wiki/Bresenham's_line_algorithm
plots lines fast, but unaliased
*/
template <typename Ty, class Plot>
void inline bDrawLine(Ty x0, Ty y0, Ty x1, Ty y1, Plot plot)
{
Ty dx = fastabs(x1 - x0);
Ty dy = fastabs(y1 - y0);
Ty sx, sy, err, e2;
if (x0 < x1)
sx = 1;
else
sx = -1;
if (y0 < y1)
sy = 1;
else
sy = -1;
err = dx - dy;
while (true)
{
plot(x0, y0);
if (x0 == x1 && y0 == y1)
break;
e2 = err * 2;
if (e2 > -dy)
{
err = err - dy;
x0 = x0 + sx;
}
if (e2 < dx)
{
err = err + dx;
y0 = y0 + sy;
}
}
}
Render generic:
template<class Fetcher, class Plot>
void CVectorScope::renderGeneric(std::size_t numSamples, Fetcher fetch, Plot plot)
{
float xn, yn, sleft, sright;
int x, y;
auto middleHeight = displaySize.getY() / 2 + displaySize.getHeight() / 2 - 2;
auto middleWidth = displaySize.getX() / 2 + displaySize.getWidth() / 2 - 2;
// stack reference to the software buffer image we draw on
Image::BitmapData data(waveForm, Image::BitmapData::ReadWriteMode::writeOnly);
float sampleFade = 1.0 / numSamples;
// main loop, iterate over each sample and plot them
for (int i = 0; i < numSamples; ++i)
{
// fetch sample
sleft = fetch(0, i);
sright = fetch(1, i);
// rotate matrix
xn = sleft * cosrol - sright * sinrol;
yn = sleft * sinrol + sright * cosrol;
sleft = xn;
sright = yn;
// hardclip
if (fastabs(sright) > 1)
continue;
if (fastabs(sleft) > 1)
continue;
x = sleft * middleWidth + middleWidth;
y = sright * middleHeight + middleHeight;
plot(data, x, y, ox, oy, sampleFade * i);
ox = x; oy = y;
}
}
To give an overview over what happens and what amount of data we are talking about, here's the chain of events:
paint()
gets called for each frame, which is around 60fps (variable)renderGeneric()
loops over the entire audiobuffer and draws it. This could be up to 1 second long, which for a 44100 samplerate means it has to draw 44100 lines of arbitrary length 60 times / second.
Until now I only tested it on OSX, but on Windows the performance is even worse. On OSX, I compile it with Xcode + LLVM, and MSVC++ 12 on Windows.
Looking over the Assembly, MSVC does NOT inline the bDrawLine()
call (all lambdas are inlined, though). Also MSVC doesn't emit any SIMD instructions (or well it does, but nothing that isn't single-scalar). LLVM inlines everything.
I measure time spent using rdtsc (wrapped inside ClockCounter()
), which gives an overview of the time spent both in total and between rendering and writing.
Here's some measurements for a certain sound on a 1100x700 window:
- Windows, software context x64:
- cpu time: 113% (render 73%, transfer 27%)
- Windows, openGL context (using JUCE) x64:
- cpu time: 200% (render 42%, transfer 58%)
- OSX, software context x64:
- cpu time: 77% (render 40%, transfer 60%)
- OSX, openGL context (using JUCE), x64:
- cpu time: 51% (render 73%, transfer 27%)
Where the cpu time is calculated as follows:
cputime = (totalClocksSpent * (1000.0 / refreshRate)) / (processorSpeedInGHZ * 1000 * 1000) * 100;
This is obviously not a precise measurement, but it gives an overview of how much time the painting function uses (on one core). Note that the openGL context runs in a seperate thread. Interestingly, openGL on windows yields much worse performance for transferring.
I currently use JUCE for the graphics, but I quickly realized I wouldn't get much performance out of writing using the API functions on software contexts, so I rewrote the code to render on a software bitmap (waveForm
, where waveformGraphics
is a context writing to the waveForm
image) which at the end of paint()
gets written to the graphics context of the window.
I'm really interested in answers/advice on the following questions:
- Am I reaching the upper limits of the performance of my computer? (note: it's a completely new top-of-the-line Macbook Pro) -- what kind of performance should I expect? I was really not expecting it to eat more than a few percentages of CPU, after all it is just a couple of lines on a black background.
- How can I reduce the time spent on just transferring the image I rendered? Can it be true it takes so much time to copy an image? Although looking at it, transferring 1100x700 pixels 60 times / sec yields 46 megabytes / sec... That is a lot.
- How would you tackle this situation? How does fullscreen applications transfer this amount of data without eating half of the cpu? Note, 1100x700 isn't even that high of an resolution.
- How about my method of drawing on a bitmap? Is it an okay way of doing this? Or should i look into completely rendering using openGL or something (which I also have no experience in!)
- How can I convince MSVC to inline the calls? (note,
__forceinline
had no effect onbDrawLine()
) - I suspect this is where a huge part of the performance went. - How about the translation of Bresenham's line algorithm, is it optimal? And any general notes on the speed and quality of the code?
- Would I achieve anything trying to use SIMD intrinsics / parallel features / loop unrolling?