Introducing Catnip, a C# runtime for games
March 22, 2019
Whilst I’m not quite in a position to share lots more details and/or code yet, I thought I’d write a little bit about a project I’ve been working on for some time now.
It’s called “Catnip” (sorry, cat-related project names seem to happen a lot around me!), and it’s a new implementation of the Common Language Runtime - the foundation used by C# and other languages in what is often referred to as the “.net family”.
But why write a new CLR implementation when several very good ones already exist in the form of the “vanilla” CLR shipped with Windows, CoreCLR and of course Mono?
To put it simply, I was interested in the idea of building a runtime that was ground-up optimised for use in games (and specifically as a language for game code or scripting tasks), as the aforementioned runtimes all have a focus towards desktop/server workloads - which can have significantly different requirements and desired performance characteristics. In particular, there were several specific problems I wanted to try and solve:
1. Garbage collection performance
Existing runtimes tend to use garbage collection implementations that “stop the world” - halting all execution until the garbage collection has passed - and are optimised for maximum performance as measured over relatively large time periods. For games, however, we need to maintain a stable framerate, with a typical budget of either 16.6ms or 33.3ms for each frame. Thus, stopping execution completely for even a relatively small (by normal terms) period of time is problematic - doubly so if this happens unpredictably, as is often the case with garbage collection.
Games developers, thus, actually tend to care more about consistency than raw speed (up to a point!). A constant 10% performance hit across the whole game, for example, or a fixed 2ms taken from every frame would certainly be undesirable, but is something that can be managed without too much difficulty in most cases. An intermittent 10ms overhead, on the other hand, is exceptionally hard to deal with in an manner that doesn’t impact the user experience.
2. JIT not being possible on all platforms
JIT compilation is a keystone of the CLR - the whole system is designed around the assumption that the runtime will be turning IL into native code for efficient execution. However, there are many popular platforms for game development on which JIT is impossible due to system security measures that prevent the generation of executable code at runtime.
This can be worked around by using ahead-of-time compilation, but during development this is slow, unwieldy, and prevents the use of techniques such as hot code reloading to improve iteration times.
3. Portability
All of the existing runtime implementations are large, complex projects that contain a lot of architecture-specific and OS-specific code, making porting them to new platforms difficult. For a lot of use-cases this is the right design decision - general applications care about things like deep integration with OS functionality and broad support for calling into native libraries, which naturally tend to generate lots of platform-specific code.
However, for games portability to new platforms and OSes (or variants of existing ones) is a significant consideration, whilst at the same time game code (as opposed to engine code, which is not something I am considering as a target here) tends to interact very little with OS functionality directly, as the majority of the time it is concerned with APIs provided by the engine itself.
4. Ease of integration
This is a similar issue to point 3 - whilst more traditional scripting languages such as Lua are relatively easy to integrate into a game engine, the hurdle for adding a CLR implementation is fairly high, even when working on a supported platform. In particular, games often want to wrap OS functionality - for example redirecting all file I/O so that packed asset files can be used, or altering clock values so that debug builds can run slowly whilst keeping the game logic believing it is running at a normal fixed rate. These are difficult tasks to do in existing runtime implementations.
It was these four points that motivated me to start thinking about alternative ways of implementing a CLR, and ultimately lead to me starting work on Catnip to try and prove out the ideas I had for solving them.
You can see some of the results from this project here.