Best practice for 2D graph scrolling


#1

I've been using JUCE for a while now to draw a range of 2D scrolling graphs, with X marking time and various values plotted on Y.  One example attached.

So far I've done this in a real numnuts way, redrawing the entire graph area on each paint().  Obviously this is fairly CPU intensive!  I do at least use/check clip boundaries and only redraw the text on the axes as and when required.

How can I improve this?  I have very little experience of graphics coding so I'm sure I'm missing concepts as well as not knowing which bits of JUCE are relevant.  What I really need is a few high level pointers - some bullet points on the basic process to follow and a few key JUCE classes to dive in to.

I presume I'll need to cache the history as a bitmap and move that left while adding new data to the right, somehow keeping the grid separate.  Beyond that I'm a bit lost and haven't had much luck finding anything in the forums.

Thanks in advance!

 


#2

TBH using bitmaps isn't necessarily the best plan. Bitmaps involve a lot of memory copying, which is the main thing that slows down modern CPUs, whereas drawing paths is pretty fast. For example in tracktion we didn't bother trying to cache our waveforms as bitmaps, it was quicker to just render the paths each time, even when scrolling.

And building a scrollable panel using bitmaps is also pretty complicated - to do it optimally you'd need to use tiles and manage them as they go off the edge - really non-trivial stuff.

My advice would be to run a profiler on a release build, and actually measure where the bottlenecks are before you jump to any conclusions about what's going on or how to improve it. Often this can produce surprising results.


#3

OK, thanks, I'll profile it and see.

I'm currently using g.drawLine(x1, y1, x2, y2) to draw between each data point of the graph.  Is this what you mean by render the paths each time?

 


#4

Just a side note: On your implementation you need to assign x1 = x2, y1 = y2 after each line you draw.

This is how you could do it using a Path:

Path yourPath;
...
yourPath.startNewSubPath(x, y);
    
for (int i = 1; i < numberOfPoints; ++i)
{
    ... // calculate or optain the coordinates x and y of the current point

    yourPath.lineTo(x, y);
}

float lineThickness = 0.75f;
g.strokePath (yourPath, PathStrokeType(lineThickness));

Btw you might have read on this forum that using drawVerticalLine is much more efficient than drawing arbitrary angled lines and paths.

I did some profiling yesterday and in my project - a spectrum analyser -  the (overall) additional CPU load of stroking and filling a path vs. using the drawVerticalLine was only around 5%. -> I double Jules suggestion, use a Profiler.


#5

No! Don't use drawLine! That's probably the least efficient way you could possibly draw it! It'll vastly quicker to build an entire path and then draw it in one hit.


#6

Ah ha!  Great, that's what I was looking for smiley  I knew from the way my fan was spinning up there must be a more efficient way!

Thanks guys.


#7

Re: drawVerticalLine: If you're drawing many vertical lines, Graphics::fillRectList will be much faster than multiple calls to drawVerticalLine, especially on some renderers like CoreGraphics.


#8

I switched to using strokePath() instead of drawLine() (coded pretty much exactly as suggested by samuel) but it doesn't seem to have made much difference to the CPU load (maybe x2 improvement, but not, say, x10).

I'm drawing 11 paths, each of which grows to ~2000 path segments (as external data arrives), so > 20,000 path segments for each paint().  Once this much data is available, it takes ~4 seconds to paint the whole graph.  The time taken to draw is pretty much linearly related to the number of path segments rather than any of the more constant bits like the gridlines and text.

(Drawing that many segments may sound excessive but I'm already sub-sampling the data so that I don't draw sub-pixel lines and I need to represent a min/max Y value at each X pixel with a vertical line.)

I'm running Kubuntu 12.04 on an i5 Sony Vaio laptop using JUCE 3.0.8.

Maybe that sort of paint time is to be expected?  I don't have the experience to know, but it does feel like I should be able to do better!

gprof gives me:

  %   cumulative   self              self     total           

 time   seconds   seconds    calls  Ts/call  Ts/call  name    
 23.17      3.07     3.07                             juce::EdgeTable::copyEdgeTableData(int*, int, int const*, int, int)
 11.62      4.61     1.54                             juce::RenderingHelpers::EdgeTableFillers::SolidColour<juce::PixelARGB, false>::replaceLine(juce::PixelARGB*, juce::PixelARGB, int) const
  6.57      5.48     0.87                             juce_xy_graph_t::x_to_px(double) const
  5.74      6.24     0.76                             juce::PixelARGB::getARGB() const
  5.21      6.93     0.69                             juce_scrolling_time_graph_t::paint_data(juce::Graphics&, juce_scrolling_time_graph_t::y_data_fifo_t*, unsigned int, unsigned int, bool)
  3.77      7.43     0.50                             juce::PixelARGB* juce::addBytesToPointer<juce::PixelARGB, int>(juce::PixelARGB*, int)
  3.47      7.89     0.46                             juce::EdgeTable::EdgeTable(juce::Rectangle<int> const&, juce::Path const&, juce::AffineTransform const&)
  3.40      8.34     0.45                             juce::EdgeTable::LineItem::operator<(juce::EdgeTable::LineItem const&) const
  2.75      8.71     0.36                             void juce::PixelARGB::set<juce::PixelARGB>(juce::PixelARGB const&)
  2.72      9.06     0.36                             void std::__adjust_heap<juce::EdgeTable::LineItem*, int, juce::EdgeTable::LineItem>(juce::EdgeTable::LineItem*, int, int, juce::EdgeTable::LineItem)
  2.04      9.34     0.27                             juce::EdgeTable::LineItem* std::__unguarded_partition<juce::EdgeTable::LineItem*, juce::EdgeTable::LineItem>(juce::EdgeTable::LineItem*, juce::EdgeTable::LineItem*, juce::EdgeTable::LineItem const&)
  2.00      9.60     0.27                             juce_xy_graph_t::y1_to_px(float) const
  1.43      9.79     0.19                             void juce::EdgeTable::iterate<juce::RenderingHelpers::EdgeTableFillers::SolidColour<juce::PixelARGB, false> >(juce::RenderingHelpers::EdgeTableFillers::SolidColour<juce::PixelARGB, false>&) const
  1.36      9.97     0.18                             juce::PathFlatteningIterator::next()
  1.36     10.15     0.18                             int const& std::max<int>(int const&, int const&)
  1.17     10.30     0.15                             void juce::PixelRGB::set<juce::PixelARGB>(juce::PixelARGB const&)
  1.09     10.45     0.14                             juce::EdgeTable::sanitiseLevels(bool)

Any further pointers gratefully received.  Thanks!

 


#9

4 seconds!? That's crazy, in tracktion we can scroll a screenful of audio waveforms with thousands of lines in a fraction of that time.

Sounds like you've hit some kind of pathological case - probably because you're drawing paths that contain many vertical lines next to each other - all that time spent in copyEdgeTableData means that it's having to keep resizing the edge table because the path has too much complexity along the horizontal axis. You might actually get a much better result if you split your path up into narrower sections and render each one separately.