Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Very poor performance. Engine needs optimization. #1021

Open
T3rm1 opened this issue Dec 9, 2020 · 10 comments
Open

Very poor performance. Engine needs optimization. #1021

T3rm1 opened this issue Dec 9, 2020 · 10 comments

Comments

@T3rm1
Copy link

T3rm1 commented Dec 9, 2020

Please have a look at the attached screenshot. As you can see the game only runs at 42 fps. The game has very easy graphics and any video card from the last 5 years should run this little amount of graphics with several hundreds of fps.

performance-shapez

Although the load of CPU and GPU is way too high for the game, it still shows that something is bottlenecking.
image
image

It would be nice if you could optimize the engine more.

@tobspr
Copy link
Member

tobspr commented Dec 9, 2020

The engine is already super optimized, how many buildings do you have? (you can see that in esc)

@T3rm1
Copy link
Author

T3rm1 commented Dec 9, 2020

5700 buildings. Please note, that when I zoom in I get a lot more fps, so I assume that the total number of buildings is not that important for the fps. Also when I scroll to an area with no buildings (same zoom level as in screenshot) I'm back to 144 fps (manually limited this with graphics driver).

@Bob620
Copy link

Bob620 commented Dec 18, 2020

I would like to add on to this issue. As I have played and tested various aspects for #1028 I have made assumptions that lend to issues with how the game is rendered on the screen (draw functions) being performance issues and not specifically the game tick itself.

I have a very basic production line to create 4 separate corner parts from 1 extraction location. The game is currently drawing 981 buildings (As calculated by the delete function) and it impacts my fps, bringing it down from 60fps to 40fps, and 20fps when selected using ctrl+drag.
image

This issue only occurs when looking at the production line, thus I believe it is a draw efficiency issue.

I have not inspected the code thus far (may in the future to be more specific with issues and fixes) but I think its a problem specifically relating to over-using JS for things html/css can render, or if canvas is being used it may be caused by re-rendering the entire scene rather than delta-moving existing objects, only rendering objects that have moved, and other optimizations.

@Bob620
Copy link

Bob620 commented Dec 19, 2020

After reviewing the draw code and attempting a few changes to try to improve performance, I don't think there is much that can be pulled out of how it is now, its really well optimized for the most part. The several strategies I tried to help with drawing didn't improve performance more than ~2fps, though I wasn't able to test it in electron to see if it lowered cpu or gpu usage.

I have yet to inspect anything dealing with rendering shapes on belts.

It may very well be that the game is hitting the upper end of javascript's single threadedness for what its doing. Optimally you would spawn several threads, each to handle a subset of chunks on screen and draw all to the same context, though I don't think context can move between threads, so I'm not sure that would help with drawing speed, though it would help with other possible bottlenecks.

By making the game logic all be handled in multiple, separate threads and only ever asking the rendering/main thread to draw or emit user events it would be able to utilize many more resources for the game logic, though rarely does the game hit tick problems, just fps/drawing problems it seems.

@jbeaudoin11
Copy link

jbeaudoin11 commented Dec 27, 2020

I'm really curious on how to implement multithreading in this game and with JS. We have access to workers, I don't know enough of the game code to understand how things are intertwined... I haven't work with workers but i'm really interested by them, especially in computation heavy systems.

I see a lot of for loops in the code, with a little bit of brain power i'm sure it could be possible to split these loops in batches. But obviously the loop code must not do fancy stuff and should either be a pure function (doesn't modify the input/globals) or changes must be isolated to the item it process. This could be done in a short period of time, using something like ParallelJS (not sure if you want external packages in your project, at least it could be a nice PoC). That said, maybe the current loops aren't built for that and would need some major changes.

One could also split the simulation in multiple chunks, but now you need to think about how communication works between chunks ? I'm not sure i like that idea since it's basically the same game code with a communication layer...

I could also see something like a bunch of independent systems, ie belts, rendering, buildings. They all communicate with "pipes", like a belt connected to the input of a building could send a simple message to the worker responsible for that building to notify it that it has a new input. Every main loop iteration you simply send a tick message to update all systems. That said, maybe things need to happen in a specific order, maybe the main thread could be responsible to control that. This opens the door to WebAssembly.

Maybe it would be possible to implement something like ECS like Unity does, at least some part of the game. With SOA, i don't know if JS is cool with that, this could help with cache misses since most of the time you have a lot of similar behavior like objects moving.

In the end, i think the first option is a good starting point and the easiest to test out. Maybe it's good enough and there is no need to over engineer it.

@Bob620
Copy link

Bob620 commented Dec 27, 2020

@jbeaudoin11

#818 provides many improvements and has a branch doing some basic multithreading (I haven't looked into that branch but it is abandoned for being too complex).

A majority of the code is already split into chunks (16x16 blocks) and I have some work in my own fork that implements better structures such as using Maps and Sets to improve searching and interaction of logic with specific blocks rather than using the already implemented 2d arrays. This cuts down on memory but doesn't improve the fps, though a lot more can be overhauled to improve it further, I'm sure.

I have no clue how inter-chunk things work, haven't inspected that code and I assume it just works like any other single-block item but exists in multiple chunks. There are also belt paths that allow for speeding up logic of long paths of belts.

web assembly is silly and unneeded in most cases and I think that includes this since it's not the logic that is slowing down the game but seemingly drawing issues.

As far as the unity stuff goes, this is a specific engine for 1 game/type of game, adding anything that insane would be overkill and out of scope. Might as well make a new project implementing all of it and make the game on top of that which is a whole new thing.

Since the game runs via electron, it's a better idea to make nodejs backend threads, render multiple frames ahead of time and move them to the front end or interlace in some method, would not only be easier to manage but more efficient than web workers since they are not tied to the browser itself.

Speaking of loops though, a good possible solution would be to async/await as many functions as possible and make the logic, specifically drawing logic, care less about order so long as layers are correctly drawn in order which would be a simple change.

@jbeaudoin11
Copy link

jbeaudoin11 commented Dec 27, 2020

#818 provides many improvements and has a branch doing some basic multithreading (I haven't looked into that branch but it is abandoned for being too complex).

Damn, i already had this in mind by reading GameSystemWithFilter. Feels like there is a lot of searching and code that could be handle better.

Maybe i'm missing something but i don't see how web workers are used in that branch 🤔.

Web assembly is silly and unneeded in most cases and I think that includes this since it's not the logic that is slowing down the game but seemingly drawing issues.

Yes i do agree, but it opens the door to it if systems are isolated properly.

As far as the unity stuff goes, this is a specific engine for 1 game/type of game, adding anything that insane would be overkill and out of scope. Might as well make a new project implementing all of it and make the game on top of that which is a whole new thing.

I'm not talking about a full fledged implementation of Unity ECS, obviously Unity is a generic game engine, not like the one used here. But maybe something that could let us do SOA would be nice. I've look a little more at the code and it's already something that looks like ECS, but it's more OOP so it's not really what i have in mind.

If you want to build some kind of job system out of web workers, your data would be better stored in ArrayBuffers and not in arrays of object... But maybe it's way too big or just not something that the dev want to implement.

Since the game runs via electron, it's a better idea to make nodejs backend threads, render multiple frames ahead of time and move them to the front end or interlace in some method, would not only be easier to manage but more efficient than web workers since they are not tied to the browser itself.

Speaking of loops though, a good possible solution would be to async/await as many functions as possible and make the logic, specifically drawing logic, care less about order so long as layers are correctly drawn in order which would be a simple change.

Since the game is also portable for the browser i don't think it's possible to have nodejs backend threads 🤔 ? Anyways, it's possible to use something like ThreadJs which takes care of the compatibility, but again idk if external deps are accepted.

But the rendering part should be in a separate thread, idk if it's possible to do it from a worker thread tho, maybe the api doesn't allow it like DOM manipulation. so imo the game logic should be in a separate thread and let the main thread take care of the rendering. The game thread needs to emit the game state to the main thread and gg. Maybe the data transfer is going to be bottleneck but once that is in place, there is multiple ways to optimize that.

@tobspr
Copy link
Member

tobspr commented Dec 29, 2020

So, first of all, the whole game is using an ECS :P There are entities, there are components, there are game systems. The only "modification" to the classical ECS I made was adding helper methods to the components. However, components are flat, dumb structures of data. It's very similar to unity already :)

Second, I think the draw code could actually get optimized by a lot. The current issue is that every game system loops over the whole list of entities in the chunk and then checks if the entity matches.

See: https://github.com/tobspr/shapez.io/blob/master/src/js/game/systems/lever.js#L37

This effectively iterates over all entities on screen, checking if they have a Lever component, and if so, renders them.

This made sense when performance wasn't an issue, because it allows for much more modular systems, however by now I think is a big performance issue.

My suggestion would be to instead:

  1. Have a main draw method, which once loops over all entities on screen
  2. Add a new enum DrawLayer:
export enum DrawLayer {
  BACKGROUND,
  BELTS,
  BELT_ITEMS,
  LEVERS
 ....
}
  1. In the main draw method, create a draw call structure like this:
const drawcallsByLayer : Record<DrawLayer, ((DrawParameters) => void)[]> = {};
  1. The GameSystem then doesn't have a drawChunk method anymore, instead it has a drawEntity(entity, drawcallsByLayer).
  2. When it decides it wants to draw the entity, it simply does (this is what makes it probably easy to transfer to this method):
drawcallsByLayer[DrawLayer.BUILDINGS].push(parameters => {
    const sprite = leverComp.toggled ? this.spriteOn : this.spriteOff;
    entity.components.StaticMapEntity.drawSpriteOnBoundsClipped(parameters, sprite);
})
  1. And at the end of the global draw method, it simply does:
  for (const drawers of Object.values(drawCallsByLayer)) {
            for (const drawer of drawers) {
                drawer(parameters);
            }
        }

This should significantly improve the rendering performance. For the HUD, the same applies, so the draw() method on the hud also needs to get the drawcallsByLayers instead of the DrawParameters

@tecnocat
Copy link

May be Shapez.io is a single thread game? That FPS on this Z8 G4 machine is very sadness....

image

@MaxDZ8
Copy link

MaxDZ8 commented Apr 24, 2022

As I run a pretty old system I think I am privileged in finding where/why CPU bottlenecks happen.
I made a very minimal example:

20220424184559_1

Other patterns were considered. As long as the number of belts is the similar, performance is similar.
If I can massage my system into building this I might try something related to belt rendering alone. It seems the belt itself has enough of an impact, even before shape rendering.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants