5
\$\begingroup\$

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:

enter image description here

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:

  1. Windows, software context x64:
    • cpu time: 113% (render 73%, transfer 27%)
  2. Windows, openGL context (using JUCE) x64:
    • cpu time: 200% (render 42%, transfer 58%)
  3. OSX, software context x64:
    • cpu time: 77% (render 40%, transfer 60%)
  4. 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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!)
  5. How can I convince MSVC to inline the calls? (note, __forceinline had no effect on bDrawLine()) - I suspect this is where a huge part of the performance went.
  6. How about the translation of Bresenham's line algorithm, is it optimal? And any general notes on the speed and quality of the code?
  7. Would I achieve anything trying to use SIMD intrinsics / parallel features / loop unrolling?
\$\endgroup\$
  • 1
    \$\begingroup\$ Why are you plotting pixels yourself instead of just specifying the line and letting the graphics card do it for you? \$\endgroup\$ – Snowbody Jul 1 '14 at 16:27
  • \$\begingroup\$ Because i have no idea what I'm doing! But that sounds terrific, how would i go on about doing that.. ? \$\endgroup\$ – Shaggi Jul 1 '14 at 16:35
  • \$\begingroup\$ read a tutorial about openGL or whatever graphics package you are using. OpenGL is intended as a graphics abstraction layer; you tell it "put a rectangle here / put a line here" and it dose it. \$\endgroup\$ – Snowbody Jul 1 '14 at 18:50
  • \$\begingroup\$ In MSVC did you set the configuration to inline functions? And to use SSE? \$\endgroup\$ – akaltar Mar 20 '15 at 21:52
  • \$\begingroup\$ I since switched to OpenGL, having no problems now. \$\endgroup\$ – Shaggi Mar 21 '15 at 1:22
4
\$\begingroup\$

Here are some suggestions:

Double Buffering
Draw into one buffer (bitmap, etc) while the graphics processor is processing the other. When the graphics processor is finished, switch buffers. Use more buffers as necessary to accommodate speed differences.

Use threads
You could use 3 threads. Thread 1 reads the data and stores into a data buffer. Thread 2 processes data from the data buffer and writes into an image buffer. Thread 3 gives the image buffer to the graphics processor.

Don't Paint Often Painting is an expensive operation. Consider some of the operations such as clipping that need to be performed. Instead, draw many times into a buffer, then paint to the buffer. For example, perform 4 draw operations into a buffer then paint the buffer. You've just saved 3 expensive paint operations.

Use a library
Use a graphics library that will detect your graphics hardware and exploit as much hardware functionality. Some GPUs have the ability to BitBlit, which is to copy rectangular regions in memory to the display without using the CPU. (Taking time away from the CPU).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.