The Object To NDC Pipeline


So far we've been drawing within the "NDC" square. All of our coordinates have been within a square with a bottom left of [-1, -1] and top right of [1, 1], except of course with our Fullscreen shader, which was really just a way to render into the entirety of that box with less geometry. Left under-discussed was that geometry partially outside the NDC square gets clipped to only have the parts within the square, and geometry entirely outside of it don't proceed past the Vertex stage at all.

Now we can't actually prevent that our coordinates need to be generally within that box if we want to see it, it's a fundamental part of all graphics APIs, even if the box might be slightly different in each one. With some relatively simple Linear Algebra though, we'll be able to work in coordinate spaces that make more sense to us, and then transform our vertices to match NDC in our Vertex Shader.

Generally in 2D we like to work in a space not entirely dissimilar to the NDC one, but we typically want each whole number to represent something relevant. Maybe it's pixels, or faux-pixels if we want a more Pixel Art aesthetic. It could be meters or yards, but regardless, a 2 by 2 square just isn't enough for us. In 3D it's usually similar, but with an added z coordinate, and the units are typically a unit of measure.

So in this chapter, lets revisit vertices, and discuss coordinate spaces and cameras.

Vertices, Exhaustively

As we've discussed, a lot of the GPU is dedicated to processing geometry, and while this is a bit tweakable, in general this means Triangles. Speaking to SDL_GPU, there's also Lines and Points, but we'll touch on that below. You might hear people referring to "Quads", if they're referring to real time rendering this refers to two triangles that form a rectangle. Or they're perhaps longing for GPUs to support quads, alas, we don't live in that world, sorry 3D Artists.

TDDO: Insert image of a triangle, in 2D and 3D, with labels for point positions.

So this geometry we pass to the GPU is made of many "Vertices", when using a Triangle lists we're going to need 3 verts per triangle. In 2D, an individual vertex can be two numbers, for {x, y}. Somewhat obviously in 3D we need 3 numbers, due to the added z dimension. Also these are almost always stored as floats.

Lets discuss why it needs to be processed. If you're here, you've likely played games before, you know that plenty of things are moving around all the time. Now we could reupload the geometry every frame, and for many 2D games, that's can be acceptable, and there's optimizations we'll talk about later to make this sort of thing the ideal. If you're making a 3D game, this is almost never going to work unless you've really constrained your art. There's just too many verts in modern 3D models, even indie ones, it's not practical.

To add to this, GPUs also want to do as little work as possible. So they agressively "cull" triangles that are outside of the viewport, The Viewport by the way is something you can think of as the Swapchain we discussed in second chapter, though in practice it's more abstract that that, we'll get there. Triangles fully outside of this Viewport will not proceed to be shaded by the Pixel/Fragment stage.

So we can't rely on always being able to upload our verts so we need to manipulate them on the GPU, and we know they're checked against a viewport, how does this happen? Well the short answer...is math.

TODO: Insert image of the SDL NDC

It starts, or rather, ends here. All of our geometry and thus everything we shade must fit within this 2x2 square. And we'll use some relatively simple Linear Algebra to get there. Let us first discuss Coordinate Spaces.

TODO: Image describing the main coordinate spaces in graphics Object Space -> World Space -> Camera Space -> NDC ^ ^ ^ Transform Matrix View Matrix Projection Matrix

We're not going to go deep on the math involved here, that's better suited to a Linear Algebra text. You'll need to understand the basics of Points and how to transform them using Matrices

Coordinate Spaces

Object

World

Camera

NDC

NDC space is the Unit cuboid the GPU API uses for rendering. Anything outside of it gets clipped during the vertex stage. In SDL GPU, it's a Cuboid with the bottom-left-front point is {-1, -1, 0}, it's top-right-front point is {1, 1, 0}, and it extends into the Z dimention to 1 unit which is farther away from the screen.

INSERT DIAGRAM

Transformations

Object to World (Model Matrix)

World to Camera (View Matrix)

Camera to NDC (Projection Matrix)

Finally we need a matrix that maps from the Camera space to NDC space.

Projections

Orthographic

Perspective

Orthographic Projection in Practice

Pull up your code from the last chapter, there might've been a lot to learn in this chapter, but the changes will be fairly minor.

Matrix Representations, Column Major or Row Major (It's Column)

The Orthographic Projection Matrix

Object To World, in-shader

#include <SDL3/SDL.h>
#include <SDL3/SDL_stdinc.h>

  
// This is for testing to ensure the code works in both C and C++,
// this entire preprocessor block should just be the #include
// in your own code.
#ifndef __cplusplus
#include <SDL3/SDL_main.h>
#else
namespace cpp_test {
#endif

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// MATH
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
typedef struct float2 {
  float x, y;
} float2;

typedef struct float3 {
  float x, y, z;
} float3;

typedef struct float4 {
  float x, y, z, w;
} float4;

typedef struct float4x4 {
  union {
    float4 columns[4];
    float data[4][4];
  };
} float4x4;

typedef struct Transform {
  float4 mPosition;
  float4 mScale;
  float4 mRotation;
} Transform;

//////////////////////////////////////////////////////
// Downcasts

float2 Float3_XY(float3 aValue) {
  float2 toReturn = { aValue.x, aValue.y };
  return toReturn;
}

float2 Float4_XY(float4 aValue) {
  float2 toReturn = { aValue.x, aValue.y };
  return toReturn;
}

float3 Float4_XYZ(float4 aValue) {
  float3 toReturn = { aValue.x, aValue.y, aValue.z };
  return toReturn;
}

//////////////////////////////////////////////////////
// Subtraction

float2 Float2_Subtract(float2 aLeft, float2 aRight) {
  float2 toReturn = { aLeft.x - aRight.x, aLeft.y - aRight.y };
  return toReturn;
}

float3 Float3_Subtract(float3 aLeft, float3 aRight) {
  float3 toReturn = { aLeft.x - aRight.x, aLeft.y - aRight.y, aLeft.z - aRight.z };
  return toReturn;
}

float4 Float4_Subtract(float4 aLeft, float4 aRight) {
  float4 toReturn = { aLeft.x - aRight.x, aLeft.y - aRight.y, aLeft.z - aRight.z, aLeft.w - aRight.w };
  return toReturn;
}

//////////////////////////////////////////////////////
// Addition

float2 Float2_Add(float2 aLeft, float2 aRight) {
  float2 toReturn = { aLeft.x + aRight.x, aLeft.y + aRight.y };
  return toReturn;
}

float3 Float3_Add(float3 aLeft, float3 aRight) {
  float3 toReturn = { aLeft.x + aRight.x, aLeft.y + aRight.y, aLeft.z + aRight.z };
  return toReturn;
}

float4 Float4_Add(float4 aLeft, float4 aRight) {
  float4 toReturn = { aLeft.x + aRight.x, aLeft.y + aRight.y, aLeft.z + aRight.z, aLeft.w + aRight.w };
  return toReturn;
}

//////////////////////////////////////////////////////
// Multiplication

float2 Float2_Multiply(float2 aLeft, float2 aRight) {
  float2 toReturn = { aLeft.x * aRight.x, aLeft.y * aRight.y };
  return toReturn;
}

float3 Float3_Multiply(float3 aLeft, float3 aRight) {
  float3 toReturn = { aLeft.x * aRight.x, aLeft.y * aRight.y, aLeft.z * aRight.z };
  return toReturn;
}

float4 Float4_Multiply(float4 aLeft, float4 aRight) {
  float4 toReturn = { aLeft.x * aRight.x, aLeft.y * aRight.y, aLeft.z * aRight.z, aLeft.w * aRight.w };
  return toReturn;
}

//////////////////////////////////////////////////////
// Scalar Addition

float2 Float2_Scalar_Add(float2 aLeft, float aRight) {
  float2 toReturn = { aLeft.x + aRight, aLeft.y + aRight };
  return toReturn;
}

float3 Float3_Scalar_Add(float3 aLeft, float aRight) {
  float3 toReturn = { aLeft.x + aRight, aLeft.y + aRight, aLeft.z + aRight };
  return toReturn;
}

float4 Float4_Scalar_Add(float4 aLeft, float aRight) {
  float4 toReturn = { aLeft.x + aRight, aLeft.y + aRight, aLeft.z + aRight, aLeft.w + aRight };
  return toReturn;
}

//////////////////////////////////////////////////////
// Scalar Multiplication

float2 Float2_Scalar_Multiply(float2 aLeft, float aRight) {
  float2 toReturn = { aLeft.x * aRight, aLeft.y * aRight };
  return toReturn;
}

float3 Float3_Scalar_Multiply(float3 aLeft, float aRight) {
  float3 toReturn = { aLeft.x * aRight, aLeft.y * aRight, aLeft.z * aRight };
  return toReturn;
}

float4 Float4_Scalar_Multiply(float4 aLeft, float aRight) {
  float4 toReturn = { aLeft.x * aRight, aLeft.y * aRight, aLeft.z * aRight, aLeft.w * aRight };
  return toReturn;
}

//////////////////////////////////////////////////////
// Scalar Divison

float2 Float2_Scalar_Division(float2 aLeft, float aRight) {
  float2 toReturn = { aLeft.x / aRight, aLeft.y / aRight };
  return toReturn;
}

float3 Float3_Scalar_Division(float3 aLeft, float aRight) {
  float3 toReturn = { aLeft.x / aRight, aLeft.y / aRight, aLeft.z / aRight };
  return toReturn;
}

float4 Float4_Scalar_Division(float4 aLeft, float aRight) {
  float4 toReturn = { aLeft.x / aRight, aLeft.y / aRight, aLeft.z / aRight, aLeft.w / aRight };
  return toReturn;
}

//////////////////////////////////////////////////////
// Dot Product

float Float2_Dot(float2 aLeft, float2 aRight) {
  return
    (aLeft.x * aRight.x) +
    (aLeft.y * aRight.y);
}

float Float3_Dot(float3 aLeft, float3 aRight) {
  return
    (aLeft.x * aRight.x) +
    (aLeft.y * aRight.y) +
    (aLeft.z * aRight.z);
}

float Float4_Dot(float4 aLeft, float4 aRight) {
  return
    (aLeft.x * aRight.x) +
    (aLeft.y * aRight.y) +
    (aLeft.z * aRight.z) +
    (aLeft.w * aRight.w);
}

//////////////////////////////////////////////////////
// Cross Product

float3 Float3_Cross(float3 aLeft, float3 aRight) {
  float3 toReturn = {
    (aLeft.y * aRight.z) - (aLeft.z * aRight.y),
    (aLeft.z * aRight.x) - (aLeft.x * aRight.z),
    (aLeft.x * aRight.y) - (aLeft.y * aRight.x)
  };

  return toReturn;
}

// Convience function that ignores the 4th component, assuming it was irrelevant.
float3 Float4_Cross(float4 aLeft, float4 aRight) {
  float3 toReturn = {
    (aLeft.y * aRight.z) - (aLeft.z * aRight.y),
    (aLeft.z * aRight.x) - (aLeft.x * aRight.z),
    (aLeft.x * aRight.y) - (aLeft.y * aRight.x)
  };

  return toReturn;
}

//////////////////////////////////////////////////////
// Magnitude

float Float2_Magnitude(float2 aValue) {
  return SDL_sqrt(Float2_Dot(aValue, aValue));
}

float Float3_Magnitude(float3 aValue) {
  return SDL_sqrt(Float3_Dot(aValue, aValue));
}

float Float4_Magnitude(float4 aValue) {
  return SDL_sqrt(Float4_Dot(aValue, aValue));
}

//////////////////////////////////////////////////////
// Normalization

float2 Float2_Normalize(float2 aValue) {
  float magnitude = Float2_Magnitude(aValue);

  float2 toReturn = {
    aValue.x / magnitude,
    aValue.y / magnitude
  };

  return toReturn;
}

float3 Float3_Normalize(float3 aValue) {
  float magnitude = Float3_Magnitude(aValue);

  float3 toReturn = {
    aValue.x / magnitude,
    aValue.y / magnitude,
    aValue.z / magnitude
  };

  return toReturn;
}

float4 Float4_Normalize(float4 aValue) {
  float magnitude = Float4_Magnitude(aValue);

  float4 toReturn = {
    aValue.x / magnitude,
    aValue.y / magnitude,
    aValue.z / magnitude,
    aValue.w / magnitude
  };

  return toReturn;
}

//////////////////////////////////////////////////////
// Matrix Operations

float4 Float4x4_Float4_Multiply(const float4x4* aLeft, const float4 aRight)
{
  float4 toReturn;
  toReturn.x =
    (aLeft->data[0][0] * aRight.x) +
    (aLeft->data[1][0] * aRight.y) +
    (aLeft->data[2][0] * aRight.z) +
    (aLeft->data[3][0] * aRight.w);
  toReturn.y =
    (aLeft->data[0][1] * aRight.x) +
    (aLeft->data[1][1] * aRight.y) +
    (aLeft->data[2][1] * aRight.z) +
    (aLeft->data[3][1] * aRight.w);
  toReturn.z =
    (aLeft->data[0][2] * aRight.x) +
    (aLeft->data[1][2] * aRight.y) +
    (aLeft->data[2][2] * aRight.z) +
    (aLeft->data[3][2] * aRight.w);
  toReturn.w =
    (aLeft->data[0][3] * aRight.x) +
    (aLeft->data[1][3] * aRight.y) +
    (aLeft->data[2][3] * aRight.z) +
    (aLeft->data[3][3] * aRight.w);

  return toReturn;
}

float4x4 Float4x4_Multiply(const float4x4* aLeft, const float4x4* aRight)
{
  float4x4 toReturn;
  SDL_zero(toReturn);

  for (size_t i = 0; i < 4; ++i)
  {
    toReturn.data[i][0] =
      aLeft->data[0][0] * aRight->data[i][0] +
      aLeft->data[1][0] * aRight->data[i][1] +
      aLeft->data[2][0] * aRight->data[i][2] +
      aLeft->data[3][0] * aRight->data[i][3];

    toReturn.data[i][1] =
      aLeft->data[0][1] * aRight->data[i][0] +
      aLeft->data[1][1] * aRight->data[i][1] +
      aLeft->data[2][1] * aRight->data[i][2] +
      aLeft->data[3][1] * aRight->data[i][3];

    toReturn.data[i][2] =
      aLeft->data[0][2] * aRight->data[i][0] +
      aLeft->data[1][2] * aRight->data[i][1] +
      aLeft->data[2][2] * aRight->data[i][2] +
      aLeft->data[3][2] * aRight->data[i][3];

    toReturn.data[i][3] =
      aLeft->data[0][3] * aRight->data[i][0] +
      aLeft->data[1][3] * aRight->data[i][1] +
      aLeft->data[2][3] * aRight->data[i][2] +
      aLeft->data[3][3] * aRight->data[i][3];
  }
  return toReturn;
}


////////////////////////////////////////////////////////////
/// Core Matrices

float4x4 IdentityMatrix() {
  float4x4 toReturn;
  SDL_zero(toReturn);

  toReturn.data[0][0] = 1.0f;
  toReturn.data[1][1] = 1.0f;
  toReturn.data[2][2] = 1.0f;
  toReturn.data[3][3] = 1.0f;

  return toReturn;
}

float4x4 TranslationMatrix(float4 aPosition) {
  float4x4 toReturn = IdentityMatrix();

  toReturn.data[3][0] = aPosition.x;
  toReturn.data[3][1] = aPosition.y;
  toReturn.data[3][2] = aPosition.z;

  return toReturn;
}

float4x4 ScaleMatrix(float4 aScale) {
  float4x4 toReturn = IdentityMatrix();

  toReturn.data[0][0] = aScale.x;
  toReturn.data[1][1] = aScale.y;
  toReturn.data[2][2] = aScale.z;

  return toReturn;
}

float4x4 RotationMatrixX(float aAngle) {
  float4x4 toReturn = IdentityMatrix();

  toReturn.data[1][1] = SDL_cosf(aAngle);
  toReturn.data[1][2] = SDL_sinf(aAngle);
  toReturn.data[2][1] = -SDL_sinf(aAngle);
  toReturn.data[2][2] = SDL_cosf(aAngle);

  return toReturn;
}

float4x4 RotationMatrixY(float aAngle) {
  float4x4 toReturn = IdentityMatrix();

  toReturn.data[0][0] = SDL_cosf(aAngle);
  toReturn.data[0][2] = -SDL_sinf(aAngle);
  toReturn.data[2][0] = SDL_sinf(aAngle);
  toReturn.data[2][2] = SDL_cosf(aAngle);

  return toReturn;
}

float4x4 RotationMatrixZ(float aAngle) {
  float4x4 toReturn = IdentityMatrix();

  toReturn.data[0][0] = SDL_cosf(aAngle);
  toReturn.data[0][1] = SDL_sinf(aAngle);
  toReturn.data[1][0] = -SDL_sinf(aAngle);
  toReturn.data[1][1] = SDL_cosf(aAngle);

  return toReturn;
}

float4x4 RotationMatrix(float4 aPosition) {
  float4x4 xRotation = RotationMatrixX(aPosition.x);
  float4x4 yRotation = RotationMatrixY(aPosition.y);
  float4x4 zRotation = RotationMatrixZ(aPosition.z);

  float4x4 xyRotation = Float4x4_Multiply(&yRotation, &xRotation);

  return Float4x4_Multiply(&zRotation, &xyRotation);
}

float4x4 CreateModelMatrix(float4 aPosition, float4 aScale, float4 aRotation) {
  float4x4 translation = TranslationMatrix(aPosition);
  float4x4 rotation = RotationMatrix(aRotation);
  float4x4 scale = ScaleMatrix(aScale);

  float4x4 scale_rotation = Float4x4_Multiply(&rotation, &scale);

  return Float4x4_Multiply(&translation, &scale_rotation);
}

float4x4 CreateModelMatrixFromTransform(const Transform* aTransform) {
  return CreateModelMatrix(aTransform->mPosition, aTransform->mScale, aTransform->mRotation);
}

float4x4 OrthographicProjectionLHZO(float aLeft, float aRight, float aBottom, float aTop, float aNear, float aFar) {
  float4x4 toReturn;
  SDL_zero(toReturn);

  toReturn.data[0][0] = 2.0f / (aRight - aLeft);
  toReturn.data[1][1] = 2.0f / (aTop - aBottom);
  toReturn.data[2][2] = 1.0f / (aFar - aNear);

  toReturn.data[3][0] = -(aRight + aLeft) / (aRight - aLeft);
  toReturn.data[3][1] = -(aTop + aBottom) / (aTop - aBottom);
  toReturn.data[3][2] = -aNear / (aFar - aNear);

  toReturn.data[3][3] = 1.0f;

  return toReturn;
}

float4x4 PerspectiveProjectionLHZO(float aFovY, float aAspectRatio, float aNear, float aFar) {
  float4x4 toReturn;
  SDL_zero(toReturn);

  const float focalLength = 1.0f / SDL_tan(aFovY * .5f);
  const float k = aFar / (aFar - aNear);

  toReturn.data[0][0] = focalLength / aAspectRatio;
  toReturn.data[1][1] = focalLength;
  toReturn.data[2][2] = k;
  toReturn.data[2][3] = 1.0f;
  toReturn.data[3][2] = -aNear * k;

  return toReturn;
}

float4x4 PerspectiveProjectionLHOZ(float aFovY, float aAspectRatio, float aNear, float aFar) {
  float4x4 toReturn;
  SDL_zero(toReturn);

  const float focalLength = 1.0f / SDL_tan(aFovY * .5f);
  const float k = aNear / (aNear - aFar);

  toReturn.data[0][0] = focalLength / aAspectRatio;
  toReturn.data[1][1] = focalLength;
  toReturn.data[2][2] = k;
  toReturn.data[2][3] = 1.0f;
  toReturn.data[3][2] = -aFar * k;

  return toReturn;
}

float4x4 InfinitePerspectiveProjectionLHOZ(float aFovY, float aAspectRatio, float aNear) {
  float4x4 toReturn;
  SDL_zero(toReturn);

  const float focalLength = 1.0f / SDL_tan(aFovY * .5f);

  // For ease of use we're hardcoding the epsilon to what's recommended in Foundations of Game Engine
  // Development: Rendering, which is 2^(-20).
  const float epsilon = SDL_powf(2, -20);

  toReturn.data[0][0] = focalLength / aAspectRatio;
  toReturn.data[1][1] = focalLength;
  toReturn.data[2][2] = epsilon;
  toReturn.data[2][3] = 1.0f;
  toReturn.data[3][2] = aNear/(1.0f - epsilon);

  return toReturn;
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Shared GPU Code
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
typedef struct GpuContext {
  SDL_Window* mWindow;
  SDL_GPUDevice* mDevice;
  SDL_PropertiesID mProperties;
  const char* mShaderEntryPoint;
  SDL_GPUShaderFormat mChosenBackendFormat;
  const char* mChosenBackendFormatExtension;
  float4x4 WorldToNDC;
} GpuContext;

GpuContext gContext;

void CreateGpuContext(SDL_Window* aWindow) {
  SDL_zero(gContext);

  gContext.mWindow = aWindow;
  gContext.mDevice = SDL_CreateGPUDevice(SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_DXIL | SDL_GPU_SHADERFORMAT_MSL, true, NULL);
  SDL_assert(gContext.mDevice);

  SDL_assert(SDL_ClaimWindowForGPUDevice(gContext.mDevice, gContext.mWindow));

  gContext.mProperties = SDL_CreateProperties();
  SDL_assert(gContext.mProperties);

  SDL_GPUShaderFormat availableFormats = SDL_GetGPUShaderFormats(gContext.mDevice);
  gContext.mShaderEntryPoint = NULL;

  if (availableFormats & SDL_GPU_SHADERFORMAT_SPIRV)
  {
    gContext.mChosenBackendFormat = SDL_GPU_SHADERFORMAT_SPIRV;
    gContext.mShaderEntryPoint = "main";
    gContext.mChosenBackendFormatExtension = "spv";
  }
  else if (availableFormats & SDL_GPU_SHADERFORMAT_MSL)
  {
    gContext.mChosenBackendFormat = SDL_GPU_SHADERFORMAT_MSL;
    gContext.mShaderEntryPoint = "main0";
    gContext.mChosenBackendFormatExtension = "msl";
  }
  else if (availableFormats & SDL_GPU_SHADERFORMAT_DXIL)
  {
    gContext.mChosenBackendFormat = SDL_GPU_SHADERFORMAT_DXIL;
    gContext.mShaderEntryPoint = "main";
    gContext.mChosenBackendFormatExtension = "dxil";
  }
}

void DestroyGpuContext() {
  SDL_DestroyProperties(gContext.mProperties);
  SDL_DestroyGPUDevice(gContext.mDevice);
  SDL_DestroyWindow(gContext.mWindow);
  SDL_zero(gContext);
}

SDL_GPUShader* CreateShader(
  const char* aShaderFilename,
  SDL_GPUShaderStage aShaderStage,
  Uint32 aSamplerCount,
  Uint32 aUniformBufferCount,
  Uint32 aStorageBufferCount,
  Uint32 aStorageTextureCount,
  SDL_PropertiesID aProperties)
{
  char shader_path[4096];
  SDL_snprintf(shader_path, SDL_arraysize(shader_path), "Assets/Shaders/%s/%s.%s", TARGET_NAME, aShaderFilename, gContext.mChosenBackendFormatExtension);

  size_t fileSize = 0;
  void* fileData = SDL_LoadFile(shader_path, &fileSize);
  SDL_assert(fileData);

  SDL_PropertiesID properties = gContext.mProperties;

  if (aProperties != 0) {
    properties = aProperties;
  }

  SDL_assert(SDL_SetStringProperty(properties, SDL_PROP_GPU_SHADER_CREATE_NAME_STRING, aShaderFilename));

  SDL_GPUShaderCreateInfo shaderCreateInfo;
  SDL_zero(shaderCreateInfo);
  
  shaderCreateInfo.entrypoint = gContext.mShaderEntryPoint;
  shaderCreateInfo.format = gContext.mChosenBackendFormat;
  shaderCreateInfo.code = (Uint8*)fileData;
  shaderCreateInfo.code_size = fileSize;
  shaderCreateInfo.stage = aShaderStage;
  shaderCreateInfo.num_samplers = aSamplerCount;
  shaderCreateInfo.num_uniform_buffers = aUniformBufferCount;
  shaderCreateInfo.num_storage_buffers = aStorageBufferCount;
  shaderCreateInfo.num_storage_textures = aStorageTextureCount;
  shaderCreateInfo.props = properties;

  SDL_GPUShader* shader = SDL_CreateGPUShader(gContext.mDevice, &shaderCreateInfo);

  SDL_free(fileData);
  SDL_assert(shader);

  return shader;
}

SDL_GPUBuffer* CreateGPUBuffer(Uint32 aSize, SDL_GPUBufferUsageFlags aUsage, const char* aName)
{
  SDL_GPUBufferCreateInfo createInfo;

  SDL_SetStringProperty(gContext.mProperties, SDL_PROP_GPU_BUFFER_CREATE_NAME_STRING, aName);
  createInfo.props = gContext.mProperties;
  createInfo.size = aSize;
  createInfo.usage = aUsage;

  SDL_GPUBuffer* buffer = SDL_CreateGPUBuffer(gContext.mDevice, &createInfo);
  SDL_assert(buffer);

  return buffer;
}

SDL_GPUTransferBuffer* CreateTransferBuffer(Uint32 aSize, SDL_GPUTransferBufferUsage aUsage, const char* aName)
{
  SDL_SetStringProperty(gContext.mProperties, SDL_PROP_GPU_TRANSFERBUFFER_CREATE_NAME_STRING, aName);

  SDL_GPUTransferBufferCreateInfo transferBufferCreateInfo;
  SDL_zero(transferBufferCreateInfo);
  transferBufferCreateInfo.props = gContext.mProperties;
  transferBufferCreateInfo.size = aSize;
  transferBufferCreateInfo.usage = aUsage;

  SDL_GPUTransferBuffer* transferBuffer = SDL_CreateGPUTransferBuffer(gContext.mDevice, &transferBufferCreateInfo);
  SDL_assert(transferBuffer);

  return transferBuffer;
}

SDL_GPUTexture* CreateTexture(Uint32 aWidth, Uint32 aHeight, Uint32 layers_or_depth, Uint32 levels, SDL_GPUTextureUsageFlags aUsage, SDL_GPUTextureFormat aFormat, const char* aName)
{
  SDL_SetStringProperty(gContext.mProperties, SDL_PROP_GPU_TEXTURE_CREATE_NAME_STRING, aName);

  SDL_GPUTextureCreateInfo textureCreateInfo;
  SDL_zero(textureCreateInfo);
  textureCreateInfo.width = aWidth;
  textureCreateInfo.height = aHeight;
  textureCreateInfo.layer_count_or_depth = layers_or_depth;
  textureCreateInfo.num_levels = levels;
  textureCreateInfo.usage = aUsage;
  textureCreateInfo.format = aFormat;
  textureCreateInfo.props = gContext.mProperties;
  return SDL_CreateGPUTexture(gContext.mDevice, &textureCreateInfo);
}

SDL_GPUTexture* CreateAndUploadTexture(SDL_GPUCopyPass* aCopyPass, const char* aTextureName) {
  char stringBuffer[4096];
  SDL_snprintf(stringBuffer, SDL_arraysize(stringBuffer), "Assets/Images/%s", aTextureName);
  SDL_Surface* surface = SDL_LoadSurface(stringBuffer);
  if (surface->format != SDL_PIXELFORMAT_RGBA32)
  {
    SDL_Surface* temp = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_RGBA32);
    SDL_DestroySurface(surface);
    surface = temp;
  }

  Uint32 textureSize = surface->h * surface->pitch;

  SDL_snprintf(stringBuffer, SDL_arraysize(stringBuffer), "CreateAndUploadTexture Transfer Buffer for %s", aTextureName);

  SDL_GPUTransferBuffer* transferBuffer = CreateTransferBuffer(textureSize, SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD, stringBuffer);

  void* transferPtr = SDL_MapGPUTransferBuffer(gContext.mDevice, transferBuffer, false);
  memcpy(transferPtr, surface->pixels, textureSize);
  SDL_UnmapGPUTransferBuffer(gContext.mDevice, transferBuffer);

  SDL_GPUCommandBuffer* commandBuffer = NULL;
  SDL_GPUCopyPass* copyPass = aCopyPass;
  bool needsToSubmit = NULL == copyPass;
  if (needsToSubmit) {
    commandBuffer = SDL_AcquireGPUCommandBuffer(gContext.mDevice);
    copyPass = SDL_BeginGPUCopyPass(commandBuffer);
  }

  SDL_GPUTexture* texture = CreateTexture(surface->w, surface->h, 1, 1, SDL_GPU_TEXTUREUSAGE_SAMPLER, SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM, aTextureName);
  SDL_assert(texture);

  // Copy to GPU
  SDL_GPUTextureTransferInfo textureTransferInfo;
  SDL_zero(textureTransferInfo);
  textureTransferInfo.pixels_per_row = surface->w;
  textureTransferInfo.rows_per_layer = surface->h;
  textureTransferInfo.transfer_buffer = transferBuffer;

  SDL_GPUTextureRegion textureRegion;
  SDL_zero(textureRegion);
  textureRegion.texture = texture;
  textureRegion.w = surface->w;
  textureRegion.h = surface->h;
  textureRegion.d = 1;

  SDL_UploadToGPUTexture(
    copyPass,
    &textureTransferInfo,
    &textureRegion,
    false
  );

  if (needsToSubmit) {
    SDL_EndGPUCopyPass(copyPass);
    SDL_SubmitGPUCommandBuffer(commandBuffer);
  }

  SDL_ReleaseGPUTransferBuffer(gContext.mDevice, transferBuffer);
  SDL_DestroySurface(surface);

  return texture;
}

SDL_GPUTextureFormat GetSupportedDepthFormat()
{
  SDL_GPUTextureFormat possibleFormats[] = {
    SDL_GPU_TEXTUREFORMAT_D32_FLOAT_S8_UINT,
    SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT,
    SDL_GPU_TEXTUREFORMAT_D32_FLOAT,
    SDL_GPU_TEXTUREFORMAT_D24_UNORM,
    SDL_GPU_TEXTUREFORMAT_D16_UNORM,
  };

  for (size_t i = 0; i < SDL_arraysize(possibleFormats); ++i) {
    if (SDL_GPUTextureSupportsFormat(gContext.mDevice,
      possibleFormats[i],
      SDL_GPU_TEXTURETYPE_2D,
      SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET))
    {
      return possibleFormats[i];
    }
  }

  // Didn't find a suitable depth format.
  SDL_assert(false);

  return SDL_GPU_TEXTUREFORMAT_INVALID;
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Technique Code
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
typedef struct ModelUniform {
  float2 mPosition;
  float2 mScale;
} ModelUniform;

typedef struct TechniqueContext {
  SDL_GPUGraphicsPipeline* mPipeline;
  SDL_GPUTexture* mTexture;
  SDL_GPUSampler* mSampler;
  Transform mUniform[2];
  ModelUniform mUniform;
} TechniqueContext;

TechniqueContext CreateTechniqueContext(SDL_GPUTextureFormat aDepthFormat) {
TechniqueContext CreateTechniqueContext() {
  SDL_GPUColorTargetDescription colorTargetDescription;
  SDL_zero(colorTargetDescription);
  colorTargetDescription.format = SDL_GetGPUSwapchainTextureFormat(gContext.mDevice, gContext.mWindow);

  SDL_GPUGraphicsPipelineCreateInfo graphicsPipelineCreateInfo;
  SDL_zero(graphicsPipelineCreateInfo);

  graphicsPipelineCreateInfo.target_info.num_color_targets = 1;
  graphicsPipelineCreateInfo.target_info.color_target_descriptions = &colorTargetDescription;
  graphicsPipelineCreateInfo.target_info.depth_stencil_format = aDepthFormat;
  graphicsPipelineCreateInfo.target_info.has_depth_stencil_target = true;
  graphicsPipelineCreateInfo.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
  graphicsPipelineCreateInfo.rasterizer_state.front_face = SDL_GPU_FRONTFACE_CLOCKWISE;
  graphicsPipelineCreateInfo.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_BACK;

  // Remember to come back to this later in the tutorial, don't show it off immediately.
  graphicsPipelineCreateInfo.depth_stencil_state.compare_op = SDL_GPU_COMPAREOP_GREATER_OR_EQUAL;

  graphicsPipelineCreateInfo.depth_stencil_state.enable_depth_test = true;
  graphicsPipelineCreateInfo.depth_stencil_state.enable_depth_write = true;

  graphicsPipelineCreateInfo.vertex_shader = CreateShader(
    "Depth.vert",
    "TransformedQuad.vert",
    SDL_GPU_SHADERSTAGE_VERTEX,
    0,
    2,
    0,
    0,
    SDL_PROPERTY_TYPE_INVALID
  );
  SDL_assert(graphicsPipelineCreateInfo.vertex_shader);

  graphicsPipelineCreateInfo.fragment_shader = CreateShader(
    "Depth.frag",
    "TransformedQuad.frag",
    SDL_GPU_SHADERSTAGE_FRAGMENT,
    1,
    0,
    0,
    0,
    SDL_PROPERTY_TYPE_INVALID
  );
  SDL_assert(graphicsPipelineCreateInfo.fragment_shader);

  SDL_assert(SDL_SetStringProperty(gContext.mProperties, SDL_PROP_GPU_GRAPHICSPIPELINE_CREATE_NAME_STRING, "TechniqueContext"));

  TechniqueContext context;
  SDL_zero(context);
  context.mPipeline = SDL_CreateGPUGraphicsPipeline(gContext.mDevice, &graphicsPipelineCreateInfo);
  SDL_assert(context.mPipeline);
  
  context.mUniform[0].mPosition.x =  0.f;
  context.mUniform[0].mPosition.y = -1.f;
  context.mUniform[0].mPosition.z =  5.f;
  context.mUniform[0].mPosition.w =  0.f;
  context.mUniform[0].mScale.x = 0.5f;
  context.mUniform[0].mScale.y = 0.5f;
  context.mUniform[0].mScale.z = 0.5f;
  context.mUniform[0].mScale.w = 0.5f;
  context.mUniform[0].mRotation.x = 0.f;
  context.mUniform[0].mRotation.y = 0.f;
  context.mUniform[0].mRotation.z = 0.f;
  context.mUniform[0].mRotation.w = 0.f;

  context.mUniform[1].mPosition.x = 0.f;
  context.mUniform[1].mPosition.y = -1.f;
  context.mUniform[1].mPosition.z = 10.f;
  context.mUniform[1].mPosition.w = 0.f;
  context.mUniform[1].mScale.x = 2.f;
  context.mUniform[1].mScale.y = 2.f;
  context.mUniform[1].mScale.z = 2.f;
  context.mUniform[1].mScale.w = 2.f;
  context.mUniform[1].mRotation.x = 0.f;
  context.mUniform[1].mRotation.y = 0.f;
  context.mUniform[1].mRotation.z = 0.f;
  context.mUniform[1].mRotation.w = 0.f;
  context.mUniform.mPosition.x = 128.f;
  context.mUniform.mPosition.y = 128.f;
  context.mUniform.mScale.x = 256.f;
  context.mUniform.mScale.y = 256.f;

  SDL_GPUSamplerCreateInfo samplerCreateInfo;
  SDL_zero(samplerCreateInfo);
  context.mSampler = SDL_CreateGPUSampler(gContext.mDevice, &samplerCreateInfo);
  SDL_assert(context.mSampler);

  context.mTexture = CreateAndUploadTexture(NULL, "sample.bmp");

  SDL_ReleaseGPUShader(gContext.mDevice, graphicsPipelineCreateInfo.vertex_shader);
  SDL_ReleaseGPUShader(gContext.mDevice, graphicsPipelineCreateInfo.fragment_shader);

  return context;
}

void DrawTechniqueContext(TechniqueContext* aContext, SDL_GPUCommandBuffer* aCommandBuffer, SDL_GPURenderPass* aRenderPass)
{
  SDL_BindGPUGraphicsPipeline(aRenderPass, aContext->mPipeline);
  
  float4x4 model = CreateModelMatrixFromTransform(&aContext->mUniform[0]);

  SDL_PushGPUVertexUniformData(aCommandBuffer, 0, &model, sizeof(model));
  SDL_PushGPUVertexUniformData(aCommandBuffer, 0, &aContext->mUniform, sizeof(aContext->mUniform));
  SDL_PushGPUVertexUniformData(aCommandBuffer, 1, &gContext.WorldToNDC, sizeof(gContext.WorldToNDC));

  {
    SDL_GPUTextureSamplerBinding textureBinding;
    SDL_zero(textureBinding);
    textureBinding.texture = aContext->mTexture;
    textureBinding.sampler = aContext->mSampler;
    SDL_BindGPUFragmentSamplers(aRenderPass, 0, &textureBinding, 1);
  }

  // Draw the first cube
  SDL_DrawGPUPrimitives(aRenderPass, 6 /* 6 per face */ * 6 /* 6 sides of our cube */, 1, 0, 0);

  // Draw the second cube, make sure to recalculate the model matrix for it and reupload it.
  model = CreateModelMatrixFromTransform(&aContext->mUniform[1]);
  SDL_PushGPUVertexUniformData(aCommandBuffer, 0, &model, sizeof(model));
  SDL_DrawGPUPrimitives(aRenderPass, 6 /* 6 per face */ * 6 /* 6 sides of our cube */, 1, 0, 0);
  SDL_DrawGPUPrimitives(aRenderPass, 6, 1, 0, 0);
}

void DestroyTechniqueContext(TechniqueContext* aContext)
{
  SDL_ReleaseGPUSampler(gContext.mDevice, aContext->mSampler);
  SDL_ReleaseGPUTexture(gContext.mDevice, aContext->mTexture);
  SDL_ReleaseGPUGraphicsPipeline(gContext.mDevice, aContext->mPipeline);
  SDL_zero(*aContext);
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Main
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
int main(int argc, char** argv)
{
  (void)argc;
  (void)argv;
  SDL_assert(SDL_Init(SDL_INIT_VIDEO));

  SDL_Window* window = SDL_CreateWindow(TARGET_NAME, 1280, 720, 0);
  SDL_assert(window);

  CreateGpuContext(window);

  SDL_GPUTexture* depthTexture = NULL;
  Uint32 depthWidth = 0;
  Uint32 depthHeight = 0;
  SDL_GPUTextureFormat depthFormat = GetSupportedDepthFormat();
  TechniqueContext context = CreateTechniqueContext();

  TechniqueContext context = CreateTechniqueContext(depthFormat);

  const float speed = 5.f;
  const float speed = 200.f;
  Uint64 last_frame_ticks_so_far = SDL_GetTicksNS();
  int keys;
  const bool* key_map = SDL_GetKeyboardState(&keys);
  bool running = true;

  while (running) {
    Uint64 current_frame_ticks_so_far = SDL_GetTicksNS();
    float dt = (current_frame_ticks_so_far - last_frame_ticks_so_far) / 1000000000.f;
    last_frame_ticks_so_far = current_frame_ticks_so_far;

    SDL_Event event;
    while (SDL_PollEvent(&event)) {
      switch (event.common.type) {
        case SDL_EVENT_QUIT:
          running = false;
          break;
      case SDL_EVENT_QUIT:
        running = false;
        break;
      }
    }
    

    int w = 0, h = 0;
    SDL_GetWindowSizeInPixels(gContext.mWindow, &w, &h);

    gContext.WorldToNDC = InfinitePerspectiveProjectionLHOZ(
      45.0f * SDL_PI_F / 180.0f,
      (float)w / (float)h,
      0.1f
    gContext.WorldToNDC = OrthographicProjectionLHZO(
      0, w,
      0, h,
      0.0f, 1.0f
    );
      
    if (key_map[SDL_SCANCODE_D])        context.mUniform[0].mPosition.x += speed * dt * 1.0f;
    if (key_map[SDL_SCANCODE_A])        context.mUniform[0].mPosition.x -= speed * dt * 1.0f;
    if (key_map[SDL_SCANCODE_W])        context.mUniform[0].mPosition.y += speed * dt * 1.0f;
    if (key_map[SDL_SCANCODE_S])        context.mUniform[0].mPosition.y -= speed * dt * 1.0f;
    if (key_map[SDL_SCANCODE_E])        context.mUniform[0].mPosition.z += speed * dt * 1.0f;
    if (key_map[SDL_SCANCODE_Q])        context.mUniform[0].mPosition.z -= speed * dt * 1.0f;
    if (key_map[SDL_SCANCODE_R])        context.mUniform[0].mScale.x += speed * dt * 1.0f;
    if (key_map[SDL_SCANCODE_F])        context.mUniform[0].mScale.x -= speed * dt * 1.0f;
    if (key_map[SDL_SCANCODE_T])        context.mUniform[0].mScale.y += speed * dt * 1.0f;
    if (key_map[SDL_SCANCODE_G])        context.mUniform[0].mScale.y -= speed * dt * 1.0f;
    if (key_map[SDL_SCANCODE_INSERT])   context.mUniform[0].mRotation.x += speed * dt * 1.0f;
    if (key_map[SDL_SCANCODE_DELETE])   context.mUniform[0].mRotation.x -= speed * dt * 1.0f;
    if (key_map[SDL_SCANCODE_HOME])     context.mUniform[0].mRotation.y += speed * dt * 1.0f;
    if (key_map[SDL_SCANCODE_END])      context.mUniform[0].mRotation.y -= speed * dt * 1.0f;
    if (key_map[SDL_SCANCODE_PAGEUP])   context.mUniform[0].mRotation.z += speed * dt * 1.0f;
    if (key_map[SDL_SCANCODE_PAGEDOWN]) context.mUniform[0].mRotation.z -= speed * dt * 1.0f;

    if (key_map[SDL_SCANCODE_D]) context.mUniform.mPosition.x += speed * dt * 1.0f;
    if (key_map[SDL_SCANCODE_A]) context.mUniform.mPosition.x -= speed * dt * 1.0f;
    if (key_map[SDL_SCANCODE_W]) context.mUniform.mPosition.y += speed * dt * 1.0f;
    if (key_map[SDL_SCANCODE_S]) context.mUniform.mPosition.y -= speed * dt * 1.0f;
    if (key_map[SDL_SCANCODE_R]) context.mUniform.mScale.x += speed * dt * 1.0f;
    if (key_map[SDL_SCANCODE_F]) context.mUniform.mScale.x -= speed * dt * 1.0f;
    if (key_map[SDL_SCANCODE_T]) context.mUniform.mScale.y += speed * dt * 1.0f;
    if (key_map[SDL_SCANCODE_G]) context.mUniform.mScale.y -= speed * dt * 1.0f;

    SDL_GPUCommandBuffer* commandBuffer = SDL_AcquireGPUCommandBuffer(gContext.mDevice);
    if (!commandBuffer)
    {
      SDL_Log("AcquireGPUCommandBuffer failed: %s", SDL_GetError());
      continue;
    }

    SDL_GPUTexture* swapchainTexture;
    Uint32 swapchainWidth = 0;
    Uint32 swapchainHeight = 0;
    if (!SDL_WaitAndAcquireGPUSwapchainTexture(commandBuffer, gContext.mWindow, &swapchainTexture, &swapchainWidth, &swapchainHeight))
    if (!SDL_WaitAndAcquireGPUSwapchainTexture(commandBuffer, gContext.mWindow, &swapchainTexture, NULL, NULL))
    {
      SDL_Log("WaitAndAcquireGPUSwapchainTexture failed: %s", SDL_GetError());
      continue;
    }

    if (depthWidth != swapchainWidth || depthHeight != swapchainHeight)
    {
      SDL_ReleaseGPUTexture(gContext.mDevice, depthTexture);
      depthTexture = CreateTexture(swapchainWidth, swapchainHeight, 1, 1, SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET, depthFormat, "DepthTexture");
      SDL_assert(depthTexture);

      depthWidth = swapchainWidth;
      depthHeight = swapchainHeight;
    }

    SDL_GPUColorTargetInfo colorTargetInfo;
    SDL_zero(colorTargetInfo);

    colorTargetInfo.texture = swapchainTexture;
    colorTargetInfo.load_op = SDL_GPU_LOADOP_CLEAR;
    colorTargetInfo.store_op = SDL_GPU_STOREOP_STORE;
    colorTargetInfo.clear_color.r = 0.2f;
    colorTargetInfo.clear_color.g = 0.2f;
    colorTargetInfo.clear_color.b = 0.85f;
    colorTargetInfo.clear_color.a = 1.0f;


    // Remember to come back to this later in the tutorial, don't show it off immediately.
    SDL_GPUDepthStencilTargetInfo depthStencilTargetInfo;
    SDL_zero(depthStencilTargetInfo);

    depthStencilTargetInfo.texture = depthTexture;
    depthStencilTargetInfo.clear_depth = 0.f;
    depthStencilTargetInfo.clear_stencil = 0.f;
    depthStencilTargetInfo.load_op = SDL_GPU_LOADOP_CLEAR;
    depthStencilTargetInfo.store_op = SDL_GPU_STOREOP_DONT_CARE;
    depthStencilTargetInfo.stencil_load_op = SDL_GPU_LOADOP_CLEAR;
    depthStencilTargetInfo.stencil_store_op = SDL_GPU_STOREOP_DONT_CARE;
    depthStencilTargetInfo.cycle = true; // NOTE: Introduce cycling

    SDL_GPURenderPass* renderPass = SDL_BeginGPURenderPass(
      commandBuffer,
      &colorTargetInfo,
      1,
      &depthStencilTargetInfo
      NULL
    );

    DrawTechniqueContext(&context, commandBuffer, renderPass);

    SDL_EndGPURenderPass(renderPass);
    SDL_SubmitGPUCommandBuffer(commandBuffer);
  }

  SDL_ReleaseGPUTexture(gContext.mDevice, depthTexture);

  DestroyTechniqueContext(&context);

  DestroyGpuContext();

  SDL_Quit();
  return 0;
}

#ifdef __cplusplus
} // end cpp_test
#endif

Download the source for this example here.