Let's Make an Engine: Graphics API History


We briefly discussed our choice of DirectX 12 in our previous entry. In order to more fully understand how we’re going to proceed to add graphics support to our engine we need to understand a bit of the history and evolution of graphics APIs.

A Brief History of Graphics APIs

Graphics APIs evolved through the decades.

They start in the early 1990s with Iris GL followed by OpenGL. These are fixed-function immediate mode APIs. In immediate mode your calls and data are communicating directly with the driver and you are having to supply that data on every frame. This is the API model of the ImGui library we just integrated. The fixed-function pipeline refers to the internal processing that happens on every piece of of geometry we send to be rendered. Effectively the API is a gigantic state machine that gets configured and then information flows through it, but you have relatively little control over the work that is being done.

After a while in the mid 90s you get DirectX and it comes with immediate mode and alternatively introduces retained mode APIs. This retained mode allows you to send your data to the driver once and refer to it in subsequent frames reducing overhead in exchange for complex resource management. This retained mode made its way to OpenGL as well.

Around the turn of the millennium you start getting a push towards programmability first with Register Combiners and then later with small shaders written in assembly code. A shader is essentially a little function that gets executed by the GPU. Vertex shaders execute for every vertex of your geometry. And pixel or fragment shaders execute for every resulting fragment that needs shading.

The desire for more complex and robust shaders eventually resulted in massive improvements to the capabilities of vertex and pixel shaders. This gave rise to the advent of OpenGL 2.0 and DirectX 9 which both include a specialized C++-like language for writing shaders, named GLSL and HLSL respectively, which compiled into code the GPU could execute. Additionally rendering to textures and being able to read those textures back became somewhat faster and support for floating point pixel formats lead to the invention of general purpose GPU programming (GPGPU).

Around this time (after Xbox 360) GPUs started to switch from specialized shader cores for pixel and vertex processing to unified shader cores that could handle any workload.1 Then we finally got a new stage of programmability with geometry shaders but in practice they were uninteresting as the performance almost always sucked.2 This is the DirectX 10 / OpenGL 3.2 era which most developers generally skipped out on. The fact that DX10 was limited to Vista which had very low adoption at the time made it very unappealing to target. In parallel GPGPU was taking off with technologies like CUDA allowing a much easier time utilizing the computational resources of the GPU outside of the games industry.

Finally we enter the golden age of traditional APIs.3 Largely these are still with us today side-by-side with new APIs and still causing endless debate wherever gamers neckbeard. DirectX 11 launched on Windows 7 which, unlike Vista, was widely loved by everyone. With DX11 (and OpenGL 4) we get Tessellation shaders, which are situationally extremely useful for adding a ton of perceived detail with high performance. And finally in the corresponding And in GPGPU realm we get compute shaders here which can actually be used to great effect in games as well.

Unfortunately it is not all sunshine and rainbows. Throughout this process the graphics drivers themselves have become wildly complex. Drivers are constantly having to validate state, track resources, and guess at usage. Generally every major game release is followed by a driver release with code explicitly designed to work around bugs and improve performance. Since at least 2003 we’ve been told to “Batch, Batch, Batch”4 but there is a limit to what can be batched in a typical frame update. State changes do occur and usually require additional draw calls. You get enough of these and the driver CPU can be your game’s performance bottleneck.

Enter explicit APIs. Beginning with Mantle5 in 2014 we saw the rise of the modern explicit graphics API. We have the idea of moving much of the validation, compilation, and resource management out of the driver layer and into the application or engine layers.6 This massively complicates the API usage but the upside is the potential for huge performance gains by giving expert level control over the GPU resources to graphics experts. This process culminated in the release of DirectX 12, Vulkan, and Metal.

OpenGL vs Vulkan Performance

And finally bringing us up to the present times we have the introduction of 4 major new technologies to the graphics APIs.

  • Mesh Shaders
  • Async Compute
  • Raytracing
  • Bindless

Mesh Shaders replace most of the traditional geometry processing pipeline making it function more like a compute shader.7

Mesh Shaders

Async compute which allows use to run compute shaders asynchronously with the rest of our normal graphics GPU work and utilize cycles that would otherwise go to waste.8

And Raytracing has the potential to replace everything we’ve done with our silly polygon rasterization for 30 years but at least it can already create amazing shadows and reflections.

Okay Bindless isn’t technically that new, but I ws first introduced to it with Dan Baker’s talk on Mantle. but the new APIs allow us to actually use it portably and with good performance. Alex Tardif has a great primer on Bindless.9 Essentially we used to have to change out resource bindings for nearly every draw call, but now we can just bind them all and let the shader decide what resources to use.

Wrapping Up

Okay that was a lot of history and believe me I left out a ton. But here we are in the modern era and the evolution and trends in graphics API design will inform our implementation choices going forward as we make our graphics renderer.

Series: Lets Make an Engine