Announcing ComputeSharp 2.0 — run C# on the GPU with ease through DirectX 12 and D2D1!
After over 2 years of work, I’m happy to announce that ComputeSharp 2.0 is finally available! This is a complete rewrite of the library, featuring support for both .NET Standard 2.0 and .NET 6, new source generators to fully replace runtime code generation, a lot of new APIs, brand new support for D2D1 pixel shaders too, XAML controls for UWP and WinUI 3, and so much more! 🎉
A lot of effort has gone into this release, and the scope of the whole project has grown so much over time, way more than I had originally anticipated. In this blog post I want to introduce the library to those not familiar with it, highlight some of the new features, and show a few examples of what can be done with it.
For those that prefer a video form, here is a link to my talk about ComputeSharp at .NET Conf 2022, and for those that just want to jump straight into the code, here is a link to the GitHub repo as well. Let’s jump into ComputeSharp 2.0! 🚀
Let’s start with an elevator pitch for the library: ComputeSharp is a .NET library to run C# code in parallel on the GPU through DX12, D2D1, and dynamically generated HLSL compute shaders, with the goal of making GPU computing easy to use for all .NET developers! It lets you write shaders (ie. code that can run on the GPU) using C#, and then it does all the heavy lifting to transform that code and actually dispatch it on the GPU in use on your machine. This can be used to hardware accelerate all sorts of workloads — image processing, parallel computations, generating fancy animations, you name it! And all without you needing to ever dive deep into the platform specific APIs necessary to make any of this work, nor to learn how to actually write HLSL, which is the programming language DirectX uses for shaders.
For a practical example, here’s the ComputeSharp Sample App in action:
Here you can see one of the XAML controls that ComputeSharp offers for UWP and WinUI 3 being used to render several animated compute shaders, in this case running into a WinUI 3 application. The entire source code is available on GitHub, and all the shaders you see have been ported from GLSL shaders from shadertoy to just C#, and are all powered by ComputeSharp and running using DirectX 12 compute shaders.
And here’s another example of ComputeSharp in action:
I’ve spent the past few months adding Direct2D support to ComputeSharp as well, which is now a fundamental component of the upcoming Paint.NET 5.0 release. ComputeSharp.D2D1 (the version of the library for D2D1 pixel shaders) is being used to implement dozens of image effects in the app, which are entirely written in C# and GPU accelerated through the power of D2D. This allowed Rick Brewster, the developer of Paint.NET, to greatly increase the number of effects he could run on the GPU, while at the same time significantly reducing the complexity of the application with respect to D2D GPU effects, as there was no longer the need to manually write effects in HLSL and compile them with a custom build step — ComputeSharp is doing all of this work behind the scenes!
Fun fact — the bokeh blur showcased in the Paint.NET screenshot above is Rick’s port of my ComputeSharp.D2D1 bokeh blur sample, which is itself a port I did of my ComputeSharp bokeh blur sample, which is a port from the C# version I wrote for ImageSharp, which is a port from the Python version that professor Mike Pound wrote for his Computerphile video on bokeh blur effects. Go check it out!
And this is also available for plugin authors, meaning that anyone can now also use ComputeSharp.D2D1 to write fully customized and GPU accelerated effects to integrate into Paint.NET. What used to require knowledge of HLSL and D2D internals is now easily accessible to all C# developers, with just a few lines of code and friendly, high-level APIs.
If you’re curious to see what a ComputeSharp shader looks like, here’s a very simple one: the hello world shader, which just displays an animated color gradient that changes over time. Here’s the C# code for it:
You can see how the shader is simply a C# struct type, that captures some values (in this case a float to indicate the time passed), and then performs some computation to calculate the color to display in the output. This is the entire shader, which ComputeSharp will then take care of processing at build time through a source generator, to convert it to HLSL and also generate additional code to support its execution.
Here is what the resulting HLSL code looks like for this shader:
It’s easy to see some of the transformations that ComputeSharp has done behind the scenes: the shader now has a constant buffer, which is used to marshal captured values to the GPU, it has thread size annotations to control the shader dispatching, automatic bounds checks have been added, and the shader is also implicitly capturing the output texture and assigning the resulting pixel to it. As a C# developer using ComputeSharp, you will never have to worry about any of this: the source generator bundled with ComputeSharp will take care of all this heavy lifting while you can write C# and benefit from IntelliSense and all the great tooling that C# offers.
I also want to show a couple of full end to end examples of what running code on the GPU using ComputeSharp looks like. We’ve seen how you can write a shader, but you might we wondering if dispatching it is actually difficult or not. It isn’t! And there’s also several ways to do that, so you can pick the one that best suits your needs as well.
ComputeSharp offers an eager execution mode, and a computational graph mode. The former is particularly useful if you’re just getting started with the library, want to try things out or just need to run some simple script and don’t want to have to worry about setting up a more complex execution pipeline. Here is how you can run a very simple test shader:
That’s all you need to do to run C# code on the GPU! You can see how we have a very simple shader, which is just multiplying all values in a texture by 2, and then we’re dispatching it over a writeable buffer we allocated on the GPU with some random values in it. After dispatching the shader, we can also copy that GPU buffer back on the CPU so we can read its contents. ComputeSharp exposes dozens of very easy to use APIs to perform common operations like these, which can adapt to all scenarios from simple minimal scripts to more complicated architectures.
Let’s also look at an example using the computational graph mode:
This mode is perfect if you want to create a fully customized execution pipeline which can also squeeze the most performance out of the GPU. In this mode, we’re first creating a ComputeContext, which is an object that allows us to queue operations on the GPU. Then we’re creating our pipeline (including operations such as dispatching shaders, controlling transitions to synchronize accesses to shared resources, and more), and finally we’re dispatching that on the GPU when going outside the scope of the context. You can also see how here we’re leveraging the await using syntax to execute all this work asynchronously on the GPU, so we don’t block the current thread until the GPU finishes processing our request.
What about XAML support? ComputeSharp offers two controls (ComputeShaderPanel and AnimatedComputeShaderPanel), which make it super easy to integrate a ComputeSharp-powered effect into a UWP or WinUI 3 application: simply plop the XAML control into your view, assign a shader runner objec to it, and you’re good to go!
Here’s how a ComputeShaderPanel can easily be declared within a view. Then, it just needs an IShaderRunner instance to know how to render each frame. For simple cases, for instance when we just want to display a shader animation, the built-in ShaderRunner<T> type is more than enough:
And that’s it! The panel will use the shader runner to draw each frame, and will take care of the entire setup needed to actually display each generated frame. For more advanced scenarios, you can also implement the shader runner yourself, which gives you full control over the frame timing, the computation graph used to produce each image to display, and more!
Finally, I also want to show an example of a D2D1 pixel shader. These are the shaders that Paint.NET uses, and they’re also part of the infrastructure I’m currently working on to enable using these into UWP and WinUI 3 applications through the Win2D library (you can see my proposal for that here as well). They’re vastly different than DirectX 12 compute shaders, but thanks to ComputeSharp’s infrastructure they’re just as easy to implement, and can also be run without too much effort by leveraging the built-in pixel shader effect that ComputeSharp.D2D1 includes.
Here’s an example of a pixelate effect from Paint.NET:
You’ll notice there are some differences between this shader and the DX12 shader showed above, but the developer experience is mostly the same: you just need to author a C# type, use the available HLSL projections and APIs that ComputeSharp includes, let IntelliSense guide you, and then let ComputeSharp do all the rest of the work for you at compile time.
Let’s also look at the transpiled code for this shader:
Once again, the C# type has been transformed as needed to follow the necessary HLSL conventions, in this case specifically for D2D1 pixel shaders. We have declared fields to map to the various captured values in C#, all intrinsics have been forwarded to their HLSL equivalents, the shader syntax has been updated, and much more. And all of these shaders, just like the DX12 ones, can also be precompiled at build time, meaning your applications can have blazing fast startup performance and no need to invoke or bundle the shader compiler at all once deployed!
There is much more that is available in ComputeSharp, and this is also just the very first stable release after the rewrite — there’s plenty of new features and improvements I’m already working on and that I plan to include in future releases! You’re welcome to check out the repository on GitHub, try out the library (you can find it on NuGet!), and leave feedbacks and ideas!
And if you do use it and build something — please share it! I absolutely love to see all the cool things that people can build using ComputeSharp! 🙌