Could use some tips on maximizing canvas performance

Hi Guys,

I have a Canvas subclass that will draw a grid with up to 512 “Cells” and will do various things like changing the cell color if a “Channel” (Name of each Cell) has changed. It works very well (it draws in 60ms in fullscreen). But… I need to call the invalidate(false) function using a timer at least every 200ms to have a relatively fluid view of the incoming “Channel Values” - which will be input via UDP at approx every 40ms… Now If I invalidate it every 200ms, it will actually take 30% of my MacBook Pros CPU, according to “Activity Manager”.

I am always refreshing the whole thing, since if I pass an area to only refresh this part, it is even slower… Any ideas welcome… Heres my code in the paint event:

[code] 'DrawGrid(g)

Dim i,j,k,l As Integer

//Now calculate the size of a box
//do it here in case canvas is resized

CellWidth = Self.Width \ ColumnCount
CellHeight = Self.Height \ rowCount

for k = 1 to ColumnCount

for l = 1 to RowCount
  
  // Calc ChannelNumber depending on row and column
  dim chNum as integer
  chNum= (l-1)*mColumnCount+k
  
  if chNum <= channelCount then
    
    if displayHighlightChangedValues = false Then
      
      
      // Set Background Rectangle Color
      g.ForeColor=&c999999
      
    else
      
      if ChannelChanged(chNum-1) = true then //Gets this Value off of a Array of Bools with 511 length, so Channel 1 = index 0
        
        // Set Background Rectangle Color
        g.ForeColor=&c33FF33
        
      else
        
        
        // Set Background Rectangle Color
        g.ForeColor=&c999999
        
      end
      
    end
    
    
    // Draw Background Rectangle
    g.FillRect (k*CellWidth -CellWidth, _
    l*CellHeight-CellHeight, _
    CellWidth-1, _
    CellHeight-1)
    
    
    // Draw ChannelNumber
    g.ForeColor=&cCCCCCC
    g.TextSize=CellHeight/4
    g.DrawString(str(chNum), (k*CellWidth+2)-CellWidth ,l*CellHeight-CellHeight + g.TextSize+2,CellWidth)
    
    /Get ChannelValue
    dim chVal as string
    chVal = str(ChannelValue(chNum-1))
    
    If displayZeroIfZero = true then
      
      // Draw Channel Value
      g.ForeColor=&c000000
      g.TextSize=CellHeight/3
      g.DrawString(chVal, (k*CellWidth)-CellWidth/2 -Graphics.StringWidth(chVal)/2  ,l*CellHeight-CellHeight + CellHeight/2 + g.TextSize-CellHeight/25,CellWidth)
      
    else
      
      if ChannelValue(chNum-1) > 0 then
        
        // Draw Channel Value
        g.ForeColor=&c000000
        g.TextSize=CellHeight/3
        g.DrawString(chVal, (k*CellWidth)-CellWidth/2 -Graphics.StringWidth(chVal)/2  ,l*CellHeight-CellHeight + CellHeight/2 + g.TextSize-CellHeight/25,CellWidth)
        
      end
      
    end
    
  end
  
next

next
[/code]

some speed ups…
don’t ‘dim’ inside a loop
precompute values, i.e. calc CellHeight/25 only once (if thats constant)
use OpenGL

Not sure how much affect the first 2 will have, maybe negligible. OpenGL I think has the most potential. You’ll need a text drawer, I have a class for that but haven’t tested it’s speed.

also, maybe you shouldn’t be doing this…

Graphics.StringWidth(chVal)/2

Use g instead.

I thought that use of Graphics was removed. I never use it (only the g parameter or a ‘g’ retrieved from a picture) and know it can cause problems.

Actually, now it takes 68ms (average) to draw it at the same size, after changing it back it draws in 60ms average again :wink:

I already thought about OpenGL, but I am a bit hesitant as I publish my app for Mac, Windows and Linux and dont want to make it too dependant on other Libs / Hardware

Something you could easily try is #pragma BackgroundTasks False. Not sure if it will make much of a difference, but it sometimes does when loops are involved.

If I understand the problem correctly I probably would have tried the following design.

  • Array of instances of my own custom class for tracking channel state. I would try and store as many computed values as possible, i.e. location, size, etc, rather than compute them every time through.

  • UDP updates modify these instances. Modified instances have a flag property set so you know which have been modified and which have not.

  • Drawing code would have one loop, and would only draw those instances which have been modified, and would do so to an offscreen picture buffer.

  • Canvas.Paint event would simply copy the offscreen buffer to g.

I would use my own offscreen buffer because Canvas.Paint is for anything and everything, so you don’t know in Paint if you only have to draw modified channels or draw the entire thing due to an OS event. With the buffer it doesn’t matter.

If only a few channels are modified each time through then this would save a lot of calls to FillRect and DrawString. It has been a while since I’ve worked on high performance Canvas code. But last time I did DrawString was expensive. And I used an offscreen buffer to minimize calls to DrawString. Things may have changed since then though.

If you use invalidateRect, the Paint event will pass an array of rectangles that need to be repainted. This should significantly speed up your drawing time.

And yes, you should use “g”. Using Graphics is deprecated and will disappear from the language at some time soon.

By the way, another way to “speed up” your drawing code is to use some compiler directives at the top of your drawing code:

#pragma BackgroundTasks False #if not DebugBuild #pragma StackOverflowChecking False #pragma BoundsChecking False #pragma NilObjectChecking False #endif

Keep in mind that the last 3 are only enabled for non debug builds because your app will die a horrible death if a Stack Overflow, Bounds error or NilObjectException occurs with these turned off. You may need to add more explicit array bounds and Nil object checking to avoid these, but the first two should help quite a bit.

Will try!

That makes sense!

ChannelChanged() already does this, so should be easy to update it to an own class

[quote=64181:@Daniel Taylor]* Drawing code would have one loop, and would only draw those instances which have been modified, and would do so to an offscreen picture buffer.

  • Canvas.Paint event would simply copy the offscreen buffer to g.

I would use my own offscreen buffer because Canvas.Paint is for anything and everything, so you don’t know in Paint if you only have to draw modified channels or draw the entire thing due to an OS event. With the buffer it doesn’t matter.

If only a few channels are modified each time through then this would save a lot of calls to FillRect and DrawString. It has been a while since I’ve worked on high performance Canvas code. But last time I did DrawString was expensive. And I used an offscreen buffer to minimize calls to DrawString. Things may have changed since then though.[/quote]

Will give those a shot! Thanks for all suggestions so far! Keep 'em coming :slight_smile:

Already changed it to use g. Thanks! good catch :slight_smile:

I got DrawGrid running, of sorts. Used column and row counts of 23 to show 512 cells, each drawn with its index and value and possible highlighting. Using the profiler and fiddling around it looks like 95% of the time is in DrawString (averaged 60ms with DrawString, 3ms without).

So you want to DrawString as little as possible and that’s what Daniels suggestion will do. Only those few cells that have changed will need strings redrawn. Note that when the canvas dimensions change or you otherwise need to redraw the whole thing it’ll take the full 60ms.

OpenGL is built into Xojo and works on those platforms. When its not overkill its really fast:)

Have you considered using 512 small Canvases instead of one big one? You can create one, then duplicate and lay it out in code rather than duplicating it yourself. You can then refresh only the one(s) you need at the time you need it.

[quote=64501:@Will Shank]
OpenGL is built into Xojo and works on those platforms. When its not overkill its really fast:)[/quote]
I just gave the ezgl classes a try (found them in another thread) and the draw string function in there, is quite slow. not sure if I am doing it the right way, though…

No, that’s right. I tested it out a couple days ago and was quite disappointed. I’ve only used it for a handful of strings at a time and just assumed because it’s OpenGL it’d be fast, but 1024 strings just brings it down. Some optimizations could be made to the class but I’m not sure if that’s the bottleneck. Another way to draw text is bitmaps which may be faster but bitmaps are aliased. Because of the text heaviness of your drawing I’d stay with Canvas/Picture.

The best approach is going to be to limit your redrawing to only those areas that need to be drawn. Since you’re drawing an opaque background, you shouldn’t have any trouble with leaving artifacts behind.

turn the profiler on and see what it reports

If the drawstring method is too slow, I have two suggestions?
#1 What encoding are you using and does it contain non-ASCII characters?
#2 What about caching it to a picture object, then updating the picture when the text changes?

Regarding the second option, I personally would start by using Xojo picture object (as that’s a quick way to test this), if it works you may then want to consider moving some of the drawing specifically to Core Graphics on the Mac OS, to take advantage of CGLayers as these are stupidly fast (I used them to get 246 fps on a 2.6Ghz Retina Mac Book Pro - with Full Screen Retina graphics). It is quite a bit of work, but the payoff is ultimately a much faster and smoother application.

Another possibility: make a transparent image that contains the channel numbers so you can draw them all at once. Recreate the image when the canvas is resized, but otherwise, it’s static.

FillRect the entire canvas in “normal” color (&c999999)
FillRect the just the highlighted cells (&c33FF33)
DrawPicture the channel number template
DrawString the channel values

Nope, its only numbers. I’m not changing the default encoding, so I would assume its UTF8

I just implemented a little different approach…, It is kinda like a mixture of different suggestions in here and seems to work great, altough it still got some potential. Overall time needed to draw went down to 9ms at 1760 * 804px canvas size; initial draw (after opening or resizing takes longer than previously, but I guess thats OK).

The rectangles are drawn in the same manner as before.

What I am doing differently right now: I am drawing an Overlay Picture containing the channel numbers at the right places as to Tim’s suggestion, as soon as the grid is opened or resized, when it is resizing I am not redrawing this picture, neither am I drawing it onto the canvas.

The ChannelValues (0 to 255) are preDrawn in the same manner, but only with the appropriate width and height for one single cell. So I basically have an array now that is “ChannelValuePictures(255)”. These are also only redrawn when the canvas is opened, or resized - not while resizing.

In my Canvas.Paint event I am now only doing this:

[code] 'DrawGrid(g)

Dim i,j,k,l As Integer

for k = 1 to ColumnCount

for l = 1 to RowCount
  
  // Calc ChannelNumber depending on row and column
  dim chNum as integer
  chNum= (l-1)*mColumnCount+k
  
  if chNum <= channelCount then
    
    if displayHighlightChangedValues = false Then
      
      
      // Set Background Rectangle Color
      g.ForeColor=&c999999
      
    else
      
      if ChannelChanged(chNum-1) = true then
        
        // Set Background Rectangle Color
        g.ForeColor=&c33FF33
        
      else
        
        // Set Background Rectangle Color
        g.ForeColor=&c999999
        
      end
      
    end
    
    
    // Draw Background Rectangle
    g.FillRect (k*CellWidth -CellWidth, _
    l*CellHeight-CellHeight, _
    CellWidth-1, _
    CellHeight-1)
    
    
    if mResized = true then
      
      dim chVal as string
      chVal = str(ChannelValue(chNum-1))
      
      If displayZeroIfZero = true then
        
        g.DrawPicture(ChannelValuePictures(ChannelValue(chNum-1)),(k*CellWidth)-CellWidth,l*CellHeight-CellHeight)
        
      else
        
        if ChannelValue(chNum-1) > 0 then
          
          g.DrawPicture(ChannelValuePictures(ChannelValue(chNum-1)),(k*CellWidth)-CellWidth,l*CellHeight-CellHeight)
          
        end
        
      end
      
    end
    
  end
  
next

next

if mResized = true then
// If not resizing right now, draw the ChannelNumbers
g.DrawPicture(InfoStringsPicture,0,0)
end
[/code]

Still, I can save some performance by getting rid of the “double loop” - and might also be able to save some timing by only redrawing channelValue pictures that actually changed…

To get real speed don’t redraw everything, every time. You can use the rects passed in the Paint event to only draw the things you need.

Make sure you’re not using Invalidate and Refresh since the erase background defaults to true which forces everything to redraw. At least do Canvas.Refresh(False), Canvas.Invalidate(False) so it doesn’t erase the background.

Another approach that might be worth pursuing is to create one big picture of things that don’t change very often (the grid) and even perhaps the cells that haven’t changed since the last update. We’ve used this on a couple of projects and it works well and you don’t have to keep track of too many items.

There are drawbacks to this approach (naturally). One, if you’re concerned about Retina display you’ll have to create pictures that are double sized to make the look retina aware. Two, if you’re changing a lot of things rapidly this still might not be fast enough.