Skip to main content

Velaptor Release v1.0.0-preview.31

· 12 min read
Calvin Wilkinson
Creator and Maintainer of Velaptor (and other projects)

New Velaptor Release

Welcome to the new exciting and latest update on our 2D game development framework Velaptor! We're thrilled to announce that our newest release is now live which consists of some bug fixes, technical debt cleanup, updates to the CICD system, deprecation of the UI controls API, and some other developer experience items.

But the most exciting part of this release is the performance improvements due to updating to dotnet 8 as well as vastly improving the performance of the keyboard input system.

We invite you to check out the Velaptor Release Notes to inform yourself of the changes.

A quick thanks to the following contributors for their contributions to this release:

We appreciate your help and support in making Velaptor better!

Introduction

Imagine you're playing a game. The graphics are stunning, the storyline is captivating, and the gameplay is immersive. But every few seconds, the game stutters. The frame rate drops, the controls become unresponsive, and the whole experience is ruined. Frustrating, isn't it?

This is why performance is so important in game development. It's the invisible thread that weaves the entire gaming experience together. And when it comes to game development libraries, performance isn't just important - it's critical.

Why, you ask? Game libraries are the tools that developers use to build games. They're the foundation upon which the entire game is built. If the foundation is shaky, the whole structure suffers. The more performant the library, the more "room" a game developer has to create a smooth and responsive game.

At Velaptor, we understand this. We don't just write performant code - we live and breathe it. We're constantly looking for ways to improve, to squeeze out every last drop of performance. Because we know that every millisecond counts. And we're committed to giving you, the game developer, the best tools possible to create the best games possible.

So let's dive into the performance improvements in this release of Velaptor!

Performance improvements

The performance improvements for this release are for the most part related to the keyboard input system. When a game is built, you never know when the user will press a key or move the mouse. Because of this, you have to poll for the state of the keyboard and mouse for every frame. Anything that ends up being executed as part of the game loop can be a candidate for performance improvement or degradation.

Specifically, this release focuses on improvements in getting the state of the keyboard using the Keyboard.GetState() method as well as the KeyboardState itself. The KeyboardState returned by the GetState() method mentioned above also has been improved. The improvements involve not only how the state is collected but also reducing the memory allocations that were occurring during the collection of the state.

When developing games with garbage-collected languages such as C#, you have to take memory allocations into account. The more memory you allocate on the heap during the game loop, the more often your game could trigger garbage collection.

A NOTE ABOUT GARBAGE COLLECTION

In game development, especially in languages like C# that use garbage collection, managing memory allocations is crucial. Allocating too much memory during the game loop can trigger frequent garbage collections, which can negatively impact game performance.

The garbage collector cleans up memory that is no longer being used, but this process can be expensive in terms of time, which is a critical resource in game development. Therefore, reducing memory allocations and managing them efficiently is an important aspect of game development in C# or any other garbage-collected language.

This is not to say that garbage collection is bad in video games, it is just that too many collections that affect the performance of YOUR game can be problematic.

C# is a great choice for game development and has a lot of successful 2D and 3D game titles that have been developed with it.

Performance tools used

When it comes to collecting and measuring the performance of C# code, the best tool in my opinion that exists for this is BenchmarkDotNet. This tool is used by the .NET team to measure the performance of the .NET runtime and the .NET libraries. It is also used by many other open-source projects to measure the performance of their code.

Improvement results

The performance gains that were achieved were impressive. Though these gains might not be noticeable for some games, indeed they would be for others. Remember, not all games are created equal and some games require more performance than others. As mentioned before, these efforts matter because our goal is to not be the bottleneck for your game. The gains here are always about and will always be about giving you more "bandwidth" to work with as well as reducing allocations.

It was important to improve the classes and structs related to keyboard input because they are used in the game loop of 99% of games. These types were great candidates and an easy win regarding performance improvements.

The types that were improved were KeyboardState, and Keyboard. When measuring performance, it is important to get a good baseline before making any changes. So we created a performance project to accomplish this.

Baseline Performance Results
Open To View

These are the baseline results for the Keyboard and KeyboardState types.

MethodMeanMemory Allocations
KeyboardState.IsKeyDown1.965 us8.2 KB
KeyboardState.IsKeyUp1.971 us8.2 KB
KeyboardState.SetKeyState2.042 us8.2 KB
KeyboardState.KeyToChar1.980 us8.25 KB
KeyboardState.GetDownKeys2.136 us8.23 KB
KeyboardState.AnyAltKeysDown2.054 us8.2 KB
KeyboardState.AnyCtrlKeysDown2.100 us8.2 KB
KeyboardState.AnyShiftKeysDown2.571 us8.2 KB
KeyboardState.AnyNumpadNumberKeysDown2.061 us8.23 KB
KeyboardState.AnyStandardNumberKeysDown1.985 us8.23 KB
KeyboardState.IsLeftAltKeyDown1.684 us8.2 KB
KeyboardState.IsLeftCtrlKeyDown1.969 us8.2 KB
KeyboardState.IsLeftShiftKeyDown1.940 us8.2 KB
KeyboardState.IsRightAltKeyDown1.894 us8.2 KB
KeyboardState.IsRightCtrlKeyDown1.966 us8.2 KB
KeyboardState.IsRightShiftKeyDown1.933 us8.2 KB
Keyboard.GetState3.230 us8.2 KB

Now, let's put these numbers into perspective. Imagine each frame as a "bandwidth" that you're working with. This analogy can help us understand how different parts of your game might impact the overall performance.

Consider a game running at 60 frames per second. This gives you a budget of 16.67 milliseconds for each frame. When you convert that into microseconds, it's only 16,660 microseconds. That might seem like a lot, but it can add up quickly.

Suddenly, the time budget doesn't seem so generous, does it? Especially when you consider that your game might have many other processes that need a slice of that frame bandwidth.

But what if we could reduce the performance down to the nanosecond range? Then you'd have a whopping 16,660,000 nanoseconds to work with per frame. Now that's a number we can work with!

This is why every microsecond counts.

Performance Results After Improvements
Open To View

Here are the results after all of the improvements were made.

MethodMeanAllocated
KeyboardState.IsKeyDown2.926 ns-
KeyboardState.IsKeyUp2.570 ns-
KeyboardState.SetKeyState3.417 ns-
KeyboardState.KeyToChar7.785 ns-
KeyboardState.GetDownKeys128.989 ns104 B
KeyboardState.AnyAltKeysDown4.244 ns-
KeyboardState.AnyCtrlKeysDown4.240 ns-
KeyboardState.AnyShiftKeysDown4.302 ns-
KeyboardState.AnyNumpadNumberKeysDown17.443 ns-
KeyboardState.AnyStandardNumberKeysDown21.220 ns-
KeyboardState.IsLeftAltKeyDown2.119 ns-
KeyboardState.IsLeftCtrlKeyDown2.161 ns-
KeyboardState.IsLeftShiftKeyDown2.128 ns-
KeyboardState.IsRightAltKeyDown2.140 ns-
KeyboardState.IsRightCtrlKeyDown2.210 ns-
KeyboardState.IsRightShiftKeyDown2.188 ns-
Keyboard.GetState591.791 ns2752 B

The results are impressive. Notice that the timescale has changed from 'us' to 'ns'. The acronym 'ns' stands for nanoseconds which are 1 billionth of a second. This is a very small amount of time. This is huge!! Also, notice the huge amount of reduction in memory allocations.

To help put this improvement into perspective, refer to the comparison table below with the time scale of the baseline converted into nanoseconds.

Performance Comparison (Time)
Open To View

Processing time comparison before and after the improvements.

MethodTime BeforeTime AfterPerf Improvement
KeyboardState.IsKeyDown2042 ns2.926 ns99.83%
KeyboardState.IsKeyUp2164 ns2.570 ns97.63%
KeyboardState.SetKeyState2136 ns3.417 ns93.96%
KeyboardState.KeyToChar2061 ns7.785 ns99.15%
KeyboardState.GetDownKeys1965 ns128.989 ns99.85%
KeyboardState.AnyAltKeysDown1971 ns4.244 ns99.87%
KeyboardState.AnyCtrlKeysDown1980 ns4.240 ns99.61%
KeyboardState.AnyShiftKeysDown2054 ns4.302 ns99.79%
KeyboardState.AnyNumpadNumberKeysDown2100 ns17.443 ns99.80%
KeyboardState.AnyStandardNumberKeysDown2571 ns21.220 ns99.83%
KeyboardState.IsLeftAltKeyDown1684 ns2.119 ns99.87%
KeyboardState.IsLeftCtrlKeyDown1969 ns2.161 ns99.89%
KeyboardState.IsLeftShiftKeyDown1940 ns2.128 ns99.89%
KeyboardState.IsRightAltKeyDown1894 ns2.140 ns99.89%
KeyboardState.IsRightCtrlKeyDown1966 ns2.210 ns99.89%
KeyboardState.IsRightShiftKeyDown1933 ns2.188 ns99.89%
Keyboard.GetState3230 ns591.791 ns81.68%

This results in an average improvement of 98.25% across the board for keyboard input.

Memory Allocation Comparison

Memory allocation comparison before and after the improvements.

MethodBeforeAfter
KeyboardState.IsKeyDown8.2 KB0 B
KeyboardState.IsKeyUp8.2 KB0 B
KeyboardState.SetKeyState8.2 KB0 B
KeyboardState.KeyToChar8.25 KB0 B
KeyboardState.GetDownKeys8.23 KB104 B
KeyboardState.AnyAltKeysDown8.2 KB0 B
KeyboardState.AnyCtrlKeysDown8.2 KB0 B
KeyboardState.AnyShiftKeysDown8.2 KB0 B
KeyboardState.AnyNumpadNumberKeysDown8.23 KB0 B
KeyboardState.AnyStandardNumberKeysDown8.23 KB0 B
KeyboardState.IsLeftAltKeyDown8.2 KB0 B
KeyboardState.IsLeftCtrlKeyDown8.2 KB0 B
KeyboardState.IsLeftShiftKeyDown8.2 KB0 B
KeyboardState.IsRightAltKeyDown8.2 KB0 B
KeyboardState.IsRightCtrlKeyDown8.2 KB0 B
KeyboardState.IsRightShiftKeyDown8.2 KB0 B
Keyboard.GetState8.2 KB2752 B

For the methods that still cause allocations, the average improvement is an 82.60% decrease in memory allocations. This would arguably be a bigger win vs the processing time improvements.

Performance Comparison Charts
Open To View

These charts show the values in number form above each bar. The performance improvement is so great that the majority of the green bars do not visibly register on the char.

perf-chart-1 perf-chart-2 perf-chart-3

How did we do it?

You might be wondering, how did we achieve such significant performance gains? Let's dive into the details. We began with the KeyboardState struct and Keyboard class. We noticed some unnecessary allocations happening every frame when collecting the keyboard state. A new instance of the KeyboardState struct was being created for each frame, and within this struct, the keyboard state was represented by a Dictionary of keys and the down state. This dictionary was lazily created whenever any of the struct methods were invoked.

The original intention was to create the dictionary only once, but we overlooked the fact that the KeyboardState struct was being constructed in every frame. This meant that the Dictionary was also being constructed every frame, leading to constant reallocations. To tackle this, we introduced a singleton service that continually updates the keyboard state every frame.

Since the total number of keyboard keys is known ahead of time, we pre-allocated the dictionary once and reused it every frame. We also optimized and introduced static keyboard state data within the public API. These represent groups of related keys such as letters, numbers, modifiers, and so on. Previously, allocations of an array were used to "collect" all of the requested keys are for processing.

Moreover, the KeyboardState struct dictionary is now allocated with a known capacity upfront. This means the dictionary doesn't need to be resized as more keys are added when setting the keyboard state in the struct. These strategies, among others, not only improved performance but also significantly reduced memory allocations.

Code changes

Curious about the nitty-gritty details? Feel free to explore the pull request here to see exactly how we improved the performance of the keyboard input system.

Join Our Community

We believe that everyone has something unique to offer, and we invite you to contribute to Velaptor and the KinsonDigital organization.

Interested?
Open for more information
  1. Contribute Your Skills: Whether you're a seasoned developer or just starting, your unique skills can truly make a difference. Your power to contribute is immense, and you can do so by:

    • Picking up issues and submitting pull requests with code improvements, bug fixes, or new features.
    • Providing feedback, reporting issues, or suggesting enhancements through GitHub issues.
  2. Support Us Financially: Consider supporting us financially if you can. Your contributions can help us cover expenses like hosting and infrastructure costs, and support the ongoing development and maintenance of all KinsonDigital projects. You can donate or sponsor us on:

Remember, every contribution, no matter how small, is valuable and appreciated. By contributing, you're not just helping us; you're helping the entire community by making game development better and more efficient. Join us on this and be involved in making something amazing and unique together!