2024-02-10

Project Raygine #21 - Performance

I worked this week on the physics integration and managed to get to a point where I could run the asteroids test with physics. During testing it in browser, I noticed disappointingly low performance. After spawning like 100 entities, the performance dropped below 30 fps, which is not acceptable.

Here is the test project running in browser:

I looked into this and benchmarked a little. The C++ part implementation code takes roughly 3x more execution time in browser compared to desktop. That was expected and is acceptable. The Lua part however runs 40x slower in browser compared to desktop. The reason here is that the browser runs Lua 5.1 instead of LuaJIT. The combined effect of slower general code execution and the interpreted mode of Lua 5.1 makes the performance drop so much.

What now?

Continuing like this is not an option - browser portability is a key feature of the project. I implemented most things until now in Lua because it's the simpler and more stable way of getting results. I gained a few insights how to structure the asset database, which is valuable. What I don't want to miss out is the ability to write game logic and the editor in Lua. The workflow works well and is very productive.

One way could now be to port the Lua code parts into C++ one by one, starting with the scene graph. I am sure that this would improve scene rendering already by a lot. What I don't like here however is, that the current approach isn't very good for performance either. I am quite sure that an ECS approach would be much more efficient. I already thought a little how to handle this before stumbling over this Lua ECS library - and it looks quite alike to what I had in mind. But the dependence on Lua is now a concern to me. The only runtime code that should be executed should be the game's logic itself provided by the project. Everything around that is optimally running in C++ land.

So far I also used Lua as a way to handle memory management. The garbage collector would take care of freeing memory when it's not needed anymore. The benefit here is stability. When working on the editor I sometimes have the editor instance running for hours while changing Lua scripts, reloading the editor repeatedly in the process hundreds of times, each reload taking only a few hundred milliseconds. All without crashing the editor. This is very valuable in itself. On the contrary, when working in the C++ environment, I had regularly problems with memory segmentation faults and memory leaks that I had to adress. It's a slow and painful process compared to working on the Lua scripting side.

I am seriously considering to restart from scratch in a new project.

Rethinking

Asset management and loading is still the biggest concern for me. I would start with that. A system that allows loading assets and serializing objects using unique id identifiers. The first thing here is to figure out how to map structures on a serializable format. It needs the flexibility to allow adding new fields to objects without breaking the serialization. It also needs to allow reflection so that an editor can allow manipulating the objects. This in return means that the mapping needs a type system that allows writing editor code that can handle the types in an inspector view. On top of that, it needs to be possible to have hooks that serve no other purpose than allowing the inspector to draw custom UI elements. As for example, a color value could be configured to be displayed as a HSV value. Or a vector2 value could be defined to be a unit vector and the UI displays a unit circle for the direction. Or a button should be displayed to trigger custom action. This however is all stuff that I want to leave to the scripting side of the editor, so this is Lua code.

Also, Lua code needs to be able to establish own types and register them in the type system. This is important when making a game and storing for instance values for items in an asset. The asset system should be able to handle this and the editor should be able to display and edit these values.

Another thing is now resource management: If Lua is no longer in charge of handling resources, this needs to be done on C++ side and it needs to play well with the asset system: If an asset has a hard reference to another asset, the other asset needs to be loaded and kept in memory. If now the asset is not needed anymore, it needs to be unloaded together with the asset it references. Now it gets hairy: Assets can have bidirectional references - which makes rules out using reference counting alone. It needs to be a graph that is traversed and checked for references. My experience with Unity is, that this isn't implemented well there; unloading unused references is an expensive and slow operation. I know that code can avoid triggering this operation by managing the assets on its own, handling the loading and unloading responsibly in the project's codebase - but it's not a fun process and is often neglected.

The solution here is to apply a garbage collector approach and check if a severed reference means that both assets (the referred and the referring one) are still in use. If not, they can be unloaded. This is a complex subject. Unloading a reference means that it can trigger an unload operation, which would mean a potential jump from C++ to Lua and back. In Java, a finalizer (gc callback) can even revive an object by linking it to another object. Object revival could be unintentionally done when an asset is accessing another asset in the process of being unloaded. This is a complex subject and I am not sure if I want to go down this road.

Much to continue thinking here...

🍪