While developing my own tile/sprite drawing engine, I stumble upon several problems related to the Canvas element: I thought it would be a good idea to share my experiences.
Since not all canvas area is re-painted at each frame, I needed a way to clear the canvas before drawing onto it. The canvas API provides the context.clearRect() method for that.
I first decided to use it but when trying my engine on an old MacMini with a simple core 2 duo and integrated graphics chip, I noticed the game seemed to be really slow. Removing the canvas clearing significantly increased the drawing time and the game was running at only 5-6 fps.
After digging the internet, I learnt that the canvas could be cleared by setting the canvas element’s width/height. While this sounded absurd, this method turned out to be a lot faster on MacOSX: I reached 12-15fps which is more that what I expected, especially knowning that the whole screen is being redrawn at each frame.
Canvas clearing performance may vary a lot depending on the browser and the OS and may change upon time: browser & OS makers are contstantly making changes so a fast method can become slow and the other way around.
Right now, Windows seems to be efficient at using clearRect while MacOSX definitely needed the resize hack.
First canvas draw
After a few days, I had a nice engine that could draw most elements at 60fps but I noticed sometimes there appeared to be a delay before drawing some elements. I first thought this had something to do with the objects being created so I implemented pooling but the delay was still present.
The game I’m implementing has enemy waves that are triggered once you reach certain part of the level. I noticed that the delay happened when the very first wave was triggered, and did not happen again.
After some time trying to narrow down the code that was causing the slow down, I found out that context.drawImage() seemed to be the problem so I decided to measure the duration of the call.
The results speak for themselves:
The first call takes 124 ms (!) while the next one takes only 1 ms. To put that into perspective, it means that drawing 10 sprites would take more than one second. And this is for a single frame!
Some kind of conversion (texture ? color format ?) must happen when first using the enemy sprites canvas (that was converted from an image first).
I decided to use the enemy canvas once to draw random data before triggering the enemy wave. If was right, the first draw call would then be as fast as the next ones.
Again, the results confirm what I thought:
I have accessed the sprite canvas and wrote it onto the game’s canvas before triggering the enemy wave: as you can see, the first call now takes 0 ms!
Canvas on mobile devices: performance varies!
I’m currently focused on optimizing performance for the desktop but from time to time, I test the engine on mobile devices: this helps me fixing bottlenecks and optimizing the engine where it is the slower.
Since I do not own Apple iOS device, I first tested the engine on Android devices: on my 2-year old Nexus 4, the performance seemed to be acceptable but I noticed one thing: performance varies a lot across the different browsers.
Canvas rendering is a lot faster on Firefox than it is on Chrome: I reach about 30 fps on Firefox where Chrome barely reaches 20 fps: Firefox is 50% faster than Chrome in this particular case.
I then borrowed a friend’s iPad and Safari simply crashed when starting the game: I really wasn’t expecting this!
Since I do not own a Mac, debugging was a pain in the ass and I felt like I was back 10 years in the past when calling alert() was the only way to debug. I first fixed some bugs: my engine was drawing more tiles than needed so the target position was out of the canvas. This shouldn’t crash, but we never know. And… nothing: the crash was still there
I then tried to draw less tiles than needed so I only drew a single line of tiles: it seemed to fix the problem, but after testing on another iOS device, the crash was back, so it had nothing to do with the number of tiles and the crash seemed to appear randomly, depending on the device.
I really didn’t know what to do and was about to give up when I recalled I was caching the images I was using for the sprites into a canvas, and then was using that canvas to draw onto the engine’s canvas. There was no apparent reason why this should be the problem, but well… Desperate times call for desperate mesures 😉
So I removed the cache and drew directly from the sprites Image element onto the engine’s canvas and… Voila! It worked!
For some obscure reason, drawing from the cached canvas doesn’t work and crashes on Safari: maybe the image is too big (it’s about 2000 pixel wide), I haven’t tracked the exact problem. I also wanted to submit a bug to Apple but didn’t find a way to do that: submiting a bug seems to require a developer account.
Anyway, this was worth the trouble: the performance on iOS is excellent and 60 fps are reached on a rather old iPad Mini!
So it seems Apple really did a good work at optimising canvas rendering when compared to Android. But at the expense of some bugs 😉
Update: the crash has been fixed in iOS9.
@warpdesign_ just checked this and it works perfectly in iOS 9 beta 1's Safari
— Chris White (@CuriouslyChris) June 9, 2015