Starting with C
I have written a small sample application with C that renders a colour to the screen using DirectX 12. In this article I will cover my motivations for using C, how to compile C with Zig, and a few interesting and difficult things I’ve encountered.
Why C
I am going back to basics, to learn the fundamentals of working with C and system libraries. In the past 10 years I’ve programmed many websites with JavaScript, TypeScript, and PHP; developed games with C++, C#, GDScript, and Rust; and explored toy programs with Elixir, Go, Zig, and Odin. While I look forward to using a few of those languages more, C was the first programming language I used at university, and almost every system I want to interface with has a C header of some sorts. So for simplicity’s sake, I’m starting with C.
I already have the Zig compiler installed, which can compile C, and I found the DirectX 12 samples repo on GitHub, so my task is to rewrite the D3D12HelloWindow sample in plain ol’ C99.
Full source code for reference.
Why not C++
C++ is a superset of C and I could just write C-style C++. That is what I was told and what I believed for a while. It isn’t quite true.
C++ is not “C”++
It is based on C, but does not contain it entirely. There are particular updates in C99, like Designated Initializers, that do not work in C++ due to language design incompatibilities. They are antithetical to idiomatic C++, if there is such a thing. I probably do not need the majority of what C++ provides, it’s filled with exotic constructs, like classes and templates, making code complicated and hard to grok; I do not want my code to be smart, I want it to be straightforward.
Designated Initializers
It’s not the most complex task to make a window and render a colour, so writing C was the easiest part of creating this sample application. I did have a few difficult and time-consuming tasks I had to go through first (build systems, reading C++), but let’s just dive right into the C.
To open a window in Microsoft Windows, register the window class with RegisterClassExW
, then call CreateWindowExW
and a blank window should appear. I will now point out how modern C makes initialising data beautiful while reducing cognitive overhead.
Below is a snippet from registering the window class. Before C99, I would have to declare the struct variable first, zero its memory using memset
for example, and then set each of the struct members individually, like so.
|
|
C99 introduced Designated Initializers which are terse and composable syntax for creating structs and arrays, and I can now write the code below.
|
|
This feels familiar and expressive, especially coming from the JavaScript world. I can specify the struct member variables I want to set in any order, and what is not explicitly set is initialised to zero. Useful, if I wish to reject RAII, and embrace ZII. Newer versions of C++ (and some compiler extensions) allow for similar looking initialisers, with some caveats, but in C, structs are POD, there’s no need to be concerned about hidden memory allocations or control flow while composing data. For a more experienced analysis of C99, see this article by floooh.
Dealing with DirectX
I’m going to render a solid colour to the screen. I’m sure there are trivial ways to do this with other APIs, but using DirectX 12 requires setting up many pipeline objects.
- (debug layer)
- factory
- adapter
- device
- command queue
- swap chain
- render target (2)
- command allocator
- command list
- fence
I won’t pretend to know the reasons why there are so many building blocks required to render the colour blue at this stage, but DirectX 12 is lower level – in line with Vulkan and Metal – than DirectX 11. While this increased flexibility is probably a good thing for larger projects, it is making the initial learning phase more convoluted; I have to set up a strict list of dependencies for a single visible result. I suppose the API is more accurately representing the graphics hardware pipelines now, so that’s a positive factor.
Aside: I’m unsure why we don’t have a general instruction set for GPUs. I can use a wide variety of languages to write a basic program that compiles for x86 and ARM CPUs. But for GPUs, I have to go through these proprietary APIs and specific shader languages for each wretched platform!
Using C instead of C++ with DirectX 12 can be verbose; the API is very object-oriented. Creating a command allocator requires accessing a v-table on the device struct and passing itself in as the first argument.
|
|
C style macros exist to avoid some verbosity and error potential, which is nicer to work with. Depending on how up to date your DirectX 12 headers and libraries are, these might just be broken 🥳.
|
|
There are common methods to most DirectX objects, the most prevalent being Release()
, used to free DirectX resources. Using the macros to call all the individual release methods for each object is frustrating.
|
|
Creating a simple and general macro is an enjoyable solution.
|
|
After more DirectX 12 and windowing nonsense, I have myself a render loop that clears the screen to a solid colour each frame. It’s about 400 lines of C code with notes and comments; wildly uninteresting C code, by design and requirement. It creates a number of system objects and frees them at the end of the program. There are no overlapping lifetimes or memory limitations or resource management. Those are more interesting challenges for the humble C programmer, of which I must face soon enough. That’s when the real work begins. But compared to Microsoft’s C++ sample, mine is breathtakingly straightforward. So to contrast, I’ll cover some of the difficulties I’ve had developing this application.
Exquisite.Zig as a C compiler
It has not been entirely smooth sailing. Well… I suppose the actual sailing was smooth, but I did have to buy a boat, cart it to sea, and learn how to sail first.
Coming from any modern language back to C can be a headache. What actually is C? It’s a programming language, but who decides what is and isn’t C? There’s no “c-lang.org” where I can download the compiler; there isn’t even a “the compiler”. There’s a specification, designed by whoever, and some mainstream compilers implementing various versions, features, and extensions; namely, GCC, Clang, and MSVC. Zig uses Clang under the hood, so I was required to learn just a little about that and LLVM, the larger project that Clang is under.
|
|
The command above will compile C just fine, but there are a few flags that I set for a nicer experience. I’m using Zig version 0.11.0-dev.1711
for reference.
|
|
First, set the C standard to C99, then enable every compiler warning. For debug builds, set the optimisation level to zero and set the debug symbol format to dwarf
(if on Windows). It took me many hours of tinkering to discover I had to add -gdwarf
for debugging to work on Windows.
If you’re writing pure C code, this setup works just fine, but I’m writing a DirectX 12 Windows application, so I have to deal with the titular system libraries. Once a C/C++ project gets more specific build requirements, one generally integrates CMake or Premake or SCons or the infinite amount of other build systems available. Many build systems use their own half-baked configuration language or format, which is truly frustrating. Luckily, we are already compiling with Zig, which provides us with a refreshing build command that runs a build.zig
file. This is plain Zig code using the standard library to configure various targets, modes, executables, system libraries, and of course, interdependent steps, all with the power of a general purpose systems programming language. No need for another tool, no need for too much domain specific nonsense.
|
|
That’s the whole build.zig
file. It’s cross-platform, so it should compile anywhere you can install Zig. That being said, Zig achieves this by bundling system headers from the MSYS project, and they might not be up-to-date with the latest headers for that system. For example, I was trying to integrate the DirectX 12 Agility SDK so I could use the latest rendering features, but I encountered this compilation error:
|
|
Version 500
is required for the Agility SDK, but Zig currently only has version 475
as part of LibC (so close). I could possibly link into my actual system libraries, which has version 501
, but I don’t really know how to do that without the dreaded Visual Studio, plus that would defeat cross-compilation.
Regardless, I can recommend Zig as a nice way to compile C, no matter your platform.
How to read C++
I have recreated Microsoft’s D3D12HelloWindow C++ sample application in C99, and mine is breathtakingly straightforward. A sample should contain the minimum amount of code to reasonably achieve the outcome. Let’s cloc!
Microsoft’s D3D12HelloWindow C++ sample:
|
|
Mine:
|
|
And that doesn’t include the C++ only extension file (d3dx12.h) that Microsoft’s sample uses, which would make it 4,000+ lines of code. The main issue is that it’s split over 10 different files. Ten (10) files! For 600 lines of code. That’s bad. Especially because this is the most basic DirectX 12 sample. I had to constantly switch between header files, follow function definitions, track down parent constructors, and consult the C++ reference documentation, just to figure out what was happening in what order! Both programs do the same thing! It’s entirely unnecessary cruft. So that’s how to read C++.
In more complete sample applications, it makes sense to split things up and abstract things away, to focus on specific aspects like shader bindings, etc. My code is an imperative, step-by-step example of how to render the colour blue. It does not concern itself with application architecture or software design patterns. It showcases what functions have to be called in what order, and that’s all anyone would want from a sample.
With that all done, I’ll move on to displaying a triangle and further basic rendering tasks. I imagine my application architecture will need to evolve as more and more constructs are introduced. I’m hopeful that it will stay sane, even if DirectX 12 ends up an unwieldy beast. Writing C is fun! It has its syntax quirks and dreaded undefined behaviour, but it’s refreshingly simple and unmatched in power and portability.
Bye.