Cross-Platform Graphics in .NET Core
A couple weeks ago I was browsing /r/programming and was reading about the new road-map for .NET Core and .NET Standard. I came upon a comment where someone mentioned it was in fact possible to make games in .NET Core. I had no clue it was possible to do graphics at all, so I decided to do some investigation of my own. For those who want to skip to the final source, you can do so here.
.NET Core and Framework provide the ability to access structs, callbacks, and functions in unmanaged libraries (such as .dll, .dylib, .so) using something called Platform Invoke. This meant that I could call functions from the OpenGL library in C# and create graphics that way. I had never done any OpenGL before this though, so it was time to do some learning.
OpenGL and GLFW
When learning OpenGL, I came across two sites which helped me learn everything I needed just to get this working, these were:
From reading these, it became clear to me that I can’t create a graphics application using just OpenGL alone, I ended up deciding on using GLFW because the first link mentioned that it only came with the absolute necessities to create a context and that was what I needed.
Another nice thing about GLFW is the permissive license that allowed me to redistribute the library freely.
Getting it working on Windows
I mainly develop on Windows, so I firstly just wanted to get it working on that, then worry about cross-platform later. I ran into a couple issues.
The first issue was that some OpenGL functions are not fixed in the dll, but instead are retrieved as function pointers through another function such as
wglGetProcAddress for Windows or
glXGetProcAddress for Linux. Thankfully, GLFW has a function which meant I didn’t need to worry about the cross-platform issue here called
The next issue was about how to store function pointers in C#. C# has a feature called delegates which are essentially function pointers. As well, thanks to the project Pencil.Gaming, I was able to look at their source and see how they did things as they had written OpenGL Bindings in C# (except this was for Mono). This meant that the following code would let me use the function
glGenBuffers from OpenGL by calling
GenBuffers(1, ref vbo).
After overcoming these two issues, I got it working on Windows! I created a simple white triangle on a black background.
How to reference the right GLFW library
Unfortunately I don’t own a Mac and was a bit too lazy to set up a Linux VM, so I did a lot of this development blind. The first hurdle I knew I had to figure out was to somehow reference different GLFW libraries depending on the platform it was running on as I had a
glfw.dll for Windows, a
libglfw.dylib for OSX, and a
libglfw.so for Linux. As well, the name of the library had to be specified at compile time, and could not be decided at runtime.
After looking around, I found out that Mono had a great feature known as dllmap which allowed one to map library names to different ones depending on the platform being compiled for. Unfortunately, no such feature is available in .NET Core.
This then lead me onto an issue on the CoreCLR Github where there was plenty of discussion about how to get this working. One of the comments on the issue mentioned that CoreCLR actually will attempt to append platform specific suffixes and prefixes depending on the platform it is running. Because of this, it turned out that instead of using
"glfw.dll" in my code, I simply just had to use
"glfw" and it would work for every platform.
Outputs for each platform
After searching through many documents, I came across this section of an article about how to include native dependencies in libraries and thought this was just what I wanted. It said that I could use multiple runtime identifiers and then use includeFiles to say which files I wanted included.
Well, that would have been great, if I were creating a library. Unfortunately I was not creating a library and couldn’t use
packOptions. I then turned to try using
publishOptions, but the problem with
dotnet publish is that I can only publish one runtime at a time, and I couldn’t make use of the inbuilt way to include files automatically. It was getting a bit too complicated, so I just resorted back to a simple shell/batch script that published each runtime individually and then copied the GLFW library into the right directory afterwards. This worked great, except the fact that when I tried to get my friends to try and run it on Mac and Linux, it didn’t work at all.
Referencing the right OpenGL library
So, it turns out that the OpenGL library is not called
libopengl32.so on Linux, it is
libGL.so.1. This meant that I couldn’t take advantage of the automatic prefixing that worked for GLFW. Not only that, but the OpenGL library for OSX was not in a place that was automatically searched for, so I had to reference the fully qualified path for it.
I asked my friend who had a Mac first to test out changing the name of the OpenGL dll that I referenced to his version of the library and it worked!
I had an idea to instead make use of pre-processor directives to conditionally compile the part of my code that says the name of the OpenGL library. After all, that is how I would typically do this kind of thing in C or .NET Framework.
Although, this may have been the right approach, I came into another issue.
dotnet publish does not let me say what I want defined when I run it. So, again I resorted to a very shameful shell/batch script in a commit seen here. The script essentially renamed Program.cs to a temporary file, then added the #define I wanted at the top of the file, published it, did that for each platform, and then restored the backup.
I then sent this on to my Linux friends to try out, but it just didn’t seem to work still.
Why Linux wasn’t working.
Whenever people tried to run this on Linux, they were always getting the following error:
Failed to initialise CoreCLR, HRESULT: 0x80131500
Googling this error will lead to this issue here. I got all the people who I tried it out with to make sure that they could use
dotnet --version and that they had icu and libunwind installed. The issue was still occurring.
To be completely honest, I am not 100% sure exactly what caused it. But I have a hunch that it is related to this unresolved issue here.
Also, when creating this I chose the Ubuntu runtime, I couldn’t just select a generic Linux runtime to publish against because apparently you have to be very specific when compiling for a Linux runtime and specify the exact Ubuntu version. This was very inflexible and it made me ask myself why I was even trying to use the standalone apps in the first place. I then thought maybe it would be better just to go back to using a platform app instead of creating a standalone app.
Creating a platform app instead
I later realised I had a bug in my previous version where it was still including the
WINDOWS define because it was inside the
buildOptions part of the project.json. I then thought, that maybe instead of modifying the source file to add the
#define PLATFORM I wanted, I could just have multiple project.jsons, and even better I could use
includeFiles and not have to copy the GLFW library myself.
So I modified the project to instead be a platform app, then took advantage of the
-o option to specify the output directory on
dotnet publish so I could have an output for each platform. I then created a project.json for each platform, each with the
#define that I wanted, and the file I wanted included. I then modified the shell/batch script to hot-swap the project.json for each publish. You can see that commit here. In the end it worked for all platforms!
You can have a look at the source here.
My results from this investigation have turned up the following issues that I had to make workarounds for. If any readers know of a better solution or have found a mistake in my post, let me know.
- There is no clean way to conditionally compile for different operating systems with the project.json structure (this may be fixed in Preview 3 as it doesn’t use a project.json, however Preview 3 tools are only available on VS 2017 on Windows which defeats the whole multi-platform purpose)
- You aren’t able to create a standalone app that supports all Linux distributions because it wants you to be very specific about the runtime when publishing the app. As well, there seems to be some issue trying to get standalone apps running on Linux at all.
- There is no way to specify the DllImport path at runtime, it must be compile time. There is a potential alternative which is to instead use LoadLibrary, however I only found this out quite recently. It may be a better solution to all this but it seemed a little bit messier and requires much more code.
However, I can safely say that it is possible to make use of cross-platform graphics in .NET Core, and thus in turn games.
Edit 1: I thought I would add in that there is some awesome work being done by someone on GitHub named mellinoe. He has a game engine and other projects which are bindings for other GUI tool-kits. If you wanted to make a game or GUI application in .NET Core, I’d probably take a look at the stuff he is working on.