I'm currently developing a web client (HTML5 and JavaScript) for my city building game project. The client interacts through SignalR with a web server written in C#/.NET, on which all the game logic resides.
The implementation requires quite a large map which is implemented by a set of canvas elements that represent different layers. The actual drawing of the map consists of drawing 25x25px cells, some of which are animated. That means that there's a lot of small 'drawImage'-invocations taking place on the '2D contexts'.
The current implementation works fine and smooth in Mozilla Firefox, Internet Explorer and Edge. It is however tremendously slow on Google Chrome, most likely due to the fact that the implementation does not play nice with its hardware accelerated rendering.
Acquisition of the tile cell .PNG-images are done through downloading them from the web server and storing them in in-memory 'Image'-objects. From there, I draw them directly to the canvas when necessary. If my current research is done correctly this is where the bottleneck resides; the source 'Image'-objects reside in CPU-memory whereas the target Canvas-element is optimized for GPU-memory access, resulting in a lot of swapping.
I have tried moving the 'Image'-objects to a large off-screen 'buffer' canvas (large enough so that the hardware acceleration is supposed to kick in on Chrome) but this does not produce a noticable difference:
I have also tried implementing deferred invocations of the 'drawImage'-function through requestAnimationFrame but this too did not produce noticeable differences.
I have the following questions:
- Do I understand the problem correctly?
- What can I do to improve the performance of the web client?
Some links to questions I have researched but so far with no result:
- HTML5 Canvas slow on Chrome, but fast on FireFox
- Google Chrome hardware acceleration making game run slow
I'm currently developing a web client (HTML5 and JavaScript) for my city building game project. The client interacts through SignalR with a web server written in C#/.NET, on which all the game logic resides.
The implementation requires quite a large map which is implemented by a set of canvas elements that represent different layers. The actual drawing of the map consists of drawing 25x25px cells, some of which are animated. That means that there's a lot of small 'drawImage'-invocations taking place on the '2D contexts'.
The current implementation works fine and smooth in Mozilla Firefox, Internet Explorer and Edge. It is however tremendously slow on Google Chrome, most likely due to the fact that the implementation does not play nice with its hardware accelerated rendering.
Acquisition of the tile cell .PNG-images are done through downloading them from the web server and storing them in in-memory 'Image'-objects. From there, I draw them directly to the canvas when necessary. If my current research is done correctly this is where the bottleneck resides; the source 'Image'-objects reside in CPU-memory whereas the target Canvas-element is optimized for GPU-memory access, resulting in a lot of swapping.
I have tried moving the 'Image'-objects to a large off-screen 'buffer' canvas (large enough so that the hardware acceleration is supposed to kick in on Chrome) but this does not produce a noticable difference: https://github./Miragecoder/Urbanization/mit/86ac62a785b233eea28c53b8a7d474ef92ffc283
I have also tried implementing deferred invocations of the 'drawImage'-function through requestAnimationFrame but this too did not produce noticeable differences.
I have the following questions:
- Do I understand the problem correctly?
- What can I do to improve the performance of the web client?
Some links to questions I have researched but so far with no result:
- HTML5 Canvas slow on Chrome, but fast on FireFox
- https://gamedev.stackexchange./questions/32221/huge-performance-difference-when-using-drawimage-with-img-vs-canvas
- https://code.google./p/chromium/issues/detail?id=170021
- Google Chrome hardware acceleration making game run slow
- 2 Looking at the code you would not be forcing anything to GPU memory. Only rendering will swap an image into GPU memory if there is no room. If you are carefull with rendering order you will get some benefit. ie 'ctx.drawImage(one,0,0); ctx.drawImage(two,0,0); ctx.drawImage(one,0,0); ' will incur 3 swaps while 'ctx.drawImage(one,0,0); ctx.drawImage(one,0,0); ctx.drawImage(two,0,0); ` will incur only 2. Smaller images (parts of the whole) will also help. – Blindman67 Commented Dec 12, 2015 at 15:20
- Hi, Blindman67. What do you exactly mean with rendering order? As in, start rendering the bottom layer first and then move your way up? Also, the 'drawImage'-invocations are as small as 25x25 pixels so there's little to no optimization to be acquired there I think. Thanks for your thoughts! – Rob Wijkstra Commented Dec 12, 2015 at 17:35
- 1 i do not think that having the images in another canvas will help with gpu. i do no think it is so optimized to say "those images are already on gpu memory no need to reupload them". optimizing drawing order seems best. you get same slowness on all gpus? ati nvidia intel? – AndreaBogazzi Commented Dec 14, 2015 at 8:40
- The following mit: github./Miragecoder/Urbanization/mit/… Does the following: - Extract 25x25 tile images from a large buffer canvas (void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);); - Group the 'drawImage'-invocations by tile image and execute them in ordered fashion. I believe this is the approach both you and Blindman67 are proposing. I think it indeed produces some noticable improvement but overall performance is still poor. Thanks for your suggestions! Do you have any more? – Rob Wijkstra Commented Dec 14, 2015 at 9:37
1 Answer
Reset to default 5Your main problem seems to be the number of different Image-objects you use to draw to the canvas. You absolutely should use a Textureatlas, putting as many graphics as possible in a single image.
You should then render your sprites from as few main-images as possible by specifying the relevant rectangle:
void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
instead of the whole image:
void ctx.drawImage(image, dx, dy);
see CanvasRenderingContext2D.drawImage() for details. This way, you avoid too many context switches. As Blindman67 mentioned, you should be careful to switch between texture atlases as few times as possible - for example, you may want to use a single texture atlas for all your rendering to canvas1, another one for all your images for canvas2 etc.