Efficient Canvas Drawing

As mentioned elsewhere, I’m working on a game in my spare time. I’ve started with what I was hoping would be a simple part - a square grid map.

The easiest way to illustrate the problem is for you to download the project as it stands: https://github.com/gkjpettet/GameKit.

There are two windows in the project (version 1 and version 2). Each window contains a canvas (either V1 or V2). Each version is a different attempt to improve the drawing speed.

You can scroll the map with the arrow keys.

Version 1 attempts only to draw the visible tiles and does so to an offscreen picture buffer (mBuffer) within the TileMap property. Every time the Canvas Paint event fires, I call TileMap.Render which essentially copies mBuffer to the Graphics object. mBuffer is only updated when the tile map detects a change (e.g: the camera moves, the viewport changes, etc).

Version 2 first draws the entire map to a giant offscreen Picture (also called mBuffer). Every time the Canvas Paint event fires, I use Graphics.DrawPicture to copy a portion of the giant pre-drawn map to the onscreen Canvas. I thought this would be faster as I basically never update the giant offscreen picture (obviously that would not be the case in the future but for the moment it is). However, there is virtually no difference in the speed of scrolling with this approach.

I’m stumped. I thought that copying a small portion of a larger picture would be pretty fast but it’s basically no faster than recalculating and redrawing only the visible tiles.

I don’t want to use OpenGL (as has been suggested in the past during previous forum searches) because it is deprecated on the Mac. Surely a simple grid-based tile map is possible with Xojo?

Would anyone take a look and give me some pointers?

I tried both versions in your project and I found the second one (full, fixed map) is faster. But then I realized this is because one keypress moves twice the distance in the second version. After adjusting that, they both seemed equal in performance. But the real question is, what measure of performance are you referring to? The drawing is very fast, almost immediate, in either version. I have found in my projects that the drawing speed is always very fast and it’s never been a performance bottleneck.

If you are trying to get the “map” to scroll faster, then I think the bottleneck is the key repeat rate. I don’t know what options there are for increasing the repeat rate within Xojo, but I do know that gaming keyboards have options for very fast repeat rates.

Just a couple of other observations. In your KeyDown event you are testing for each arrow key every time. You should change that to either a If…ElseIf… structure or a Case statement. Also, you aren’t returning True from KeyDown, which then causes the window to try to process the keypress, resulting in a beep (at least on Mac). I don’t think you want the beep, nor do you want the window doing extra processing that’s not necessary. Finally, you have the Key value from the KeyDown event which you can test, instead of calling Keyboard.AsyncKeyDown. This doesn’t make a measurable performance difference in this project, since the key repeat rate is really the limiting factor, but to me this is cleaner code:

Select Case Key
Case Encodings.ASCII.Chr(28) //Right arrow
  Return True
Case Encodings.ASCII.Chr(29) //Left arrow
  Return True
Case Encodings.ASCII.Chr(30) //Up arrow
  Return True
Case Encodings.ASCII.Chr(31) //Down arrow
  Return True
End Select

Thanks for the pointers @jmadren. You’re completely correct about the key checking and neglecting to return True. I’ve made that adjustment. For completion, you’ve got the left and right arrows backwards. It should be:

Select Case Key
Case Encodings.ASCII.Chr(28)
  // Left arrow.
  Return True
Case Encodings.ASCII.Chr(29)
  // Right arrow.
  Return True
Case Encodings.ASCII.Chr(30)
  // Up arrow.
  Return True
Case Encodings.ASCII.Chr(31)
  // Down arrow.
  Return True
End Select

I’ve made the change and pushed that to the repo.

Perhaps the key repeat rate is the issue. I’m finding that if I scroll the map with the window at the size it originally starts at then performance is OK. If I resize the window to fill my 5K screen however the performance takes a nose dive.

Maybe I should try to implement mouse scrolling and see if that fixes the issue…

I made a fork of your project (think I did it correctly) where I added a timer to “move” the map a larger distance, but in quick incremental steps. This is fired by the PgUp/PgDn keys.

See if the performance is better on your large screen. If so, then it points to the key repeat rate definitely being the issue.

If I may offer some suggestions.

  1. Draw directly to the canvas graphics, the macOS now uses triple buffering, one for the screen, one for the window and one for the view. Buffering yourself is offers little improvement.
  2. Disable anti-aliasing.
  3. Make sure that your “tiles” are pixel perfect and don’t need to be scaled.
  4. 15 years ago we used CGLayers for this as they’re editable pictures on the graphics card, drawing a CGLayer took < 100 microseconds, sadly they got broke with 10.10 and finally deprecated. Supposedly they can be replicated with CALayers, but I’ve never tried.
  5. On macOS there are two ways how you can tile a single image across an entire view, if a majority of your map is the same, consider these methods and then drawing t’other tiles.
  6. Use the Nintendo way of scrolling the map, i.e. change page when the character moves from one page to next, but while they’re on the page, only update the tiles that are affected by the character.
  7. Don’t start a project using OpenGL, it’s deprecated on macOS and is broken in some places with 10.14. I wouldn’t suggest using Metal as it still has teething issues.
  8. If you’re looking at Mac Only, consider sprite kit.
  9. Older versions of the macOS had an option to disable buffering, so you could render directly to the screen, this would improve performance, but I don’t think that it’s available any more.
1 Like

You correctly forked the project :slight_smile:. I tried your code - I’m still seeing really slow scrolling when I enlarge the window to fill my 27" monitor. Performance (like with the arrow keys) is fine when the window is smaller. I could understand this if it was because I was drawing more tiles to the screen in a loop when the window is bigger but that’s not the case. The only difference between what is happening when the window is small and when it is large is that a larger picture is being drawn using Graphics.DrawPicture when the window is larger. There is no extra logic and there are no extra draw calls.

Can it really be that copying a larger picture takes 4 - 5 times longer?

@samRowlands: Welcome to the forum! I’m planning on sitting down this evening and working on this again. I have no doubt that you’re correct that using some of the underlying macOS technologies will be faster but I’m really trying to make this cross platform. I think you and I had a public discussion on the Xojo Inc forums a few years ago about this very problem and I am clearly too stubborn or too stupid to give up on this idea. I just find it unbelievable that the Canvas can’t handle what I’m trying to do.

I’d really appreciate if you take a look at the demo project in the repo @samRowlands. Can you see any obvious mistakes? The code is deliberately simple and well annotated. After all, I’m hoping to open source the mapping canvas when it’s done.

It’s taken me a lot longer to be able to sit down and look at your code.

I’m using 2018r3 for production (as there’s some issues with newer versions of Xojo that I haven’t found the time to resolve yet).

The immediate things I see, that I would do differently.

  1. Remove everything that isn’t absolutely necessary to drawing, including the event that show when rendering is started / stopped.
  2. Don’t cache the image, draw directly to the canvas. Ensure that the tiles are pixel perfect for the screen. i.e. Don’t get the macOS to scale the images, it looks like if I run this on a Retina machine, the OS has to scale the images to fit the pixel areas.
  3. Reduce the number of steps in your paint event, get it as close to a single function as possible, each time it has to call another function or method that adds an overhead.
  4. Do all tiles calculations elsewhere, I see there’s a function “computeVisibleTileIndexes” that gets called when the cache is refreshed, do this somewhen else, like setting up the map or when an event happens.
  5. Loops have an overhead, when you recalculate which tiles should be onscreen, stuff them into an array, along with their positions, when drawing, loop through a single array and extract the pre-computed locations.
  6. Separate the graphics from the tiles, so that tiles which use the same picture/image can use the same picture/image. The OS does some caching, so if you have a 100 tiles that are the same pixels, but different image instances, it has to draw 100 different images. Whereas using the same image, it should be able to gain a bit of performance (if Apple still care about performance). This will also keep your memory down.

I understand that you want to make this as x-plat as possible, but in-order to gain best performance, I would recommend considering ding some platform specific hacks.

Hope that this helps in some way. Oh and turn off anti-alias.

1 Like

Thanks ever so much for this input @samRowlands . Some of which I have already implemented in recent branches but many I will now incorporate. I’m trying to put together a better demo app to showcase what it can currently do.

Btw, how do you turn off anti-aliasing? I’m wondering if this is also causing blurring text when I use Graphics.DrawText on the map too…

Very exciting things you write there @samRowlands .
Does this tip only refer to this special case or do you really suggest to never use a buffer picture in Xojo with graphics programs? Doesn’t that lead to flickering?

I have made the experience that if you cache as many calculated values as possible, this can mean enormous speed increases in rendering.

on macOS because the OS already buffers things using an additional buffer picture isnt required or recommended

on Windows it may be since you want to draw as infrequently as possible and then only draw your buffer - doing otherwise can lead to flicker on Windows

so you may need slightly different code that has #if targetWindows and #if targetMacOS paths to have windows use a buffer and macOS not

1 Like

What about Linux?

g.antialias = false

I believe the reason why the text may look blurry is because one of several things.

  1. Text rendering on 1x displays took a dive in quality on macOS 10.14.
  2. I didn’t notice any handling of Retina assets in your code (doesn’t mean that there isn’t any), and so I would assume that running the application on a Retina display it’s drawing 1x tiles instead of 2x tiles, which also causes a performance hit as the tiles are not “pixel perfect”, causing the OS to perform scaling.

There’s also another trick you can could try, and that’s to change the blending mode (done via declares or plugin) to a basic Copy, this way you avoid any transparency calculations during the draw. I can create the needed declares for this. I’ve not tested it (yet), so I don’t know how much difference it will make (if any), but in theory it should save some math.

it depends on what you’re doing, drawing a picture to a canvas graphics is one of the slowest things (in my experience), not to mention if you’re drawing to that picture and then to the canvas in the same run loop.
I find it’s often quicker to use the drawing primitives in a canvas, than creating a cached image, and drawing that. I’ve started to create some of my icons using native Xojo code, the other advantage is that you don’t need to worry as screen scales as native drawing primitives are vector in nature.
As for flickering, I only work on Mac, which has 3 levels of buffering in the OS, so I haven’t seen flickering for a while, except when I crash the graphics card.

This is something that Apple have been saying for decades, which is to ONLY draw in the paint event, do calculations elsewhere.

Li what ??? :stuck_out_tongue:
Linux should be more like Windows as its drawing engine doesnt inherently double buffer as far as I know

I did a small test, simply dragging an image around a canvas. It was a 1x image being displayed on a Retina screen (not pixel perfect).

Anti Alias ON (default): 20,000 Microseconds.
Anti Alias OFF: 6,000~7,000 Microsecond, so about 3x performance gain.

Drawing it at pixel perfect, so each pixel on the image matches each pixel on the screen.
AA ON: 3,000 Microseconds.
AA OFF: 1,500~ 2,000 Microseconds, so upto 13x performance gain (not 100% fair).

Pixel Perfect x 4 (to cover as much area as a 1x image not being drawn pixel perfect:
AA ON: 10,000 Microseconds.
AA OFF: 6,000 - 8,000 Microseconds.

I also experimented with altering various other options, including the compositing mode, but none made such a significant difference as disabling anti-alias.

In summary, it appears that doing pixel perfect graphics and disabling anti alias is the most optimal route I know, if you’re not going to bother with pixel perfect images, at least disable anti-aliasing.

Would you have the example project for download?

@samRowlands: Thank you so much for taking the time to look at this. Everything you have said has been incredibly useful.

I have just pushed new commits to the repo where I’m experimenting with gaming with Xojo. Feel free to download the project and run the desktop test harness. The code contains many fixes that @samRowlands has suggested.

I found by far the biggest performance increase was in drawing directly to the Graphics object that is derived from the Canvas.Paint event. This really surprised me. I had been doing a “big draw” of the map to a Picture and then drawing that into the visible Graphics object. I thought this would be faster as I could simply redraw a single tile to the buffered Picture if it changed and then copy the whole buffer back to the Graphics object but it turns out that it’s actually faster to redraw every single visible tile at 30 FPS than do a single copy of a portion of the larger buffer to the Graphics object. Very surprising.

I already only draw tiles that are visible which obviously hugely helps with performance.

In the demo app, I’m updating the Canvas with a timer 30 times a second. The first time the Canvas is painted, the entire visible map is drawn directly to the Graphics object. Simultaneously I also draw every visible tile to a Picture object that is the same size as the Canvas.Graphics object. I cache this picture and then the next time the Canvas.Paint event fires I check to see if any tiles have changed. If they haven’t I simply draw the cached Picture to the Graphics object. Speed wise this doesn’t change much but it hugely reduces the CPU load. The issue is though that Retina / /HiDPI is broken.

I say this because (in the demo at least) I’m only using Graphics primitives for the tiles (Graphics.FillRectangle). If I draw some text to the Canvas in the Paint event then it looks lovely. However, when the cached Picture is used, it’s blurry. Is there a way to fix this? Perhaps I’m not converting the HiDPI Graphics object to a Picture correctly? The code I’m using to cache it can be found in GameKit.SquareTileMap.RedrawViewport under the comment “// Render this tile to the cache.”.

Here’s a couple of screenshots. The first is the demonstration of GameKit’s customisable route finding using the A* algorithm:

The second screenshot demonstrates GameKit’s abstraction of maps. You can use a single data source for the tiles in a map but display two different size maps on a Canvas at once. The screenshot shows a large scrollable map and a smaller mini map. Both can be scrolled with the mouse and clicking a tile on either map colours it randomly and updates both maps simultaneously.

Is there a way to fix this? Perhaps I’m not converting the HiDPI Graphics object to a Picture correctly?

make sure your buffering picture is also set to the same dpi as the graphics object when you create it
I suspect your backing objec might be 72 dpi where the retina screen is 144 or higher

Ah, possibly. How does one achieve that? Graphics.ScaleX?

simply set the horizontal and vertical resolutions