Next up we'll cover rendering textures, and extending our geometry into quads. This'll get us most of the way to rendering simple 2D scenes.
We'll be using SDL's built-in support for BMPs, but it also supports PNGs as of 3.4! First though, lets discuss some issues with texture formats you may not be familiar with if this is your first low-level graphics API.
Typically games use gpu compressed texture formats rather than things like PNG, BMP, TIFF, or JPEG. This comes down to data layouts and hardware support. The typical image formats you're familiar with are designed around minimizing file size, but this means it's essentially impossible for hardware to trivially sample individual pixels. This means that to use these formats on the gpu, we need to decompress them to a format such as RGBA where each component is 1 byte, with each Pixel being the Uint32 of those components combined side-by-side. This means the texture is significantly larger in memory, and slower to sample.
We have compressed formats such as various versions of DXT, ETC, and ATSC that compress textures as a series of blocks, which gpus have hardware support for sampling. There's various tradeoffs between the different formats, the hardware you're targeting, and what needs to be done on load time to use them. We'll look into some options further on in the series, but for now we'll not be worrying too much about these details, and just use the built-in formats.
First we'll start with a small function to create a Texture resource. We'll need to create textures outside of loading up files from time to time, so it's nice to have a little function to simplify creation a bit. As usual we'll pass a name in and set it in the properties, the rest is mostly straightforward.
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; textureCreateInfo.num_levels = levels; textureCreateInfo.usage = aUsage; textureCreateInfo.format = aFormat; textureCreateInfo.props = gContext.mProperties; return SDL_CreateGPUTexture(gContext.mDevice, &textureCreateInfo); }
Part of creating a texture is deciding how much memory to allocate, and for this the API will need to know nearly every other parameter here. Width and Height are self explanatory, and we've briefly discussed how format determines how the data is laid out, which should indicate that it's also relevant to the size of the data required, how much is dependent on the specific format.
Layers/Depth and Levels are a little less obvious. Layers and Depth are relatively simple to explain, they're essentially just additional textures of the same size and format. These can be used for any number of things, maybe you store the different faces of a skybox (a cube with textures on the inside that are used to display the sky), or you store several texture atlases together to reduce on texture rebindings. All we really need to consider for now is that we just need a single layer for most tasks currently, multiple levels and depth will only really come up when we use 2D Array textures or 3D textures. Levels on the other hand relate to what are referred to as mipmaps. They're somewhat similar to layers, but rather than being the same size, each additional mip level is another texture that's smaller than the last. Traditionally they're intended to be downsampled, although perhaps touched up by artists, copies of the full texture, to be automatically used when the triangle displaying the texture is of some size where the full texture's quality would go to waste. For various cache efficiency reasons this ends up being a large performance win for objects not close to the screen. We'll talk more about each of those in the future, but that should suffice for a high level explanation.
Finally there's the usage parameter, which is kind of what it sounds like. We need to tell SDL_GPU how we plan to use this texture, for now, we really only care about SDL_GPU_TEXTUREUSAGE_SAMPLER, but you may wish to look at the various other options of SDL_GPUTextureUsageFlags on your own. We'll be discussing them as they're relevant.
Next we'll do something similar for creating SDL_GPUTransferBuffers, which are intermediate buffers we'll use to copy our texture data to the GPU. And of course, we'll use them for other types of buffers in the future as well.
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; }
This one is a bit simpler, since we only care about usage and size. Usage just determines if this transfer buffer will be used for CPU -> GPU (upload), or GPU -> CPU (download) transfers. For now, we'll only be looking at uploading.
For the most part we'll be usually be loading files and dumping them into textures, so we'll write a function that does this for us. Like with shaders, we'll load them from a specific place. We can compose a path given a texture name, and then use SDL_LoadSurface to load an SDL_Surface with the texture data. For the sake of simplicity, we'll ensure we use RGBA32 format as discussed above, and if it's not, we can convert to it using SDL_ConvertSurface, and destroy the original afterword.
Regarding the copy pass parameter, we'll discuss that a bit down below, but just know that NULL is a perfectly valid argument here, and it's what we'll be doing for awhile.
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; }
Now that we've got the texture data in CPU memory in an expected format we can start working on uploading it to the GPU. For that we'll need a tranfer buffer, as discussed. We'll be using it on upload, and we can calculate the size we'll need by multiplying the height of the texture, by the pitch. The pitch is the size in bytes of a row (width) of pixels, which may require padding depending on the size and format of the image, not to mention the different sizes of pixels.
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);
Once we have the transfer buffer we can map it, which will give us a pointer to memory we're allowed to touch, and then we can simply memcpy our texture data into it. Then we just need to unmap it. Now there's a lot of discussion we could have about keeping around our transfer buffers, and persistently mapping them, but for our case here, we're going to be comfortable just creating them and throwing them away. Regarding persistent mapping, there's not a ton of immediate need for that unless you're creating a much more complicated upload system. Perhaps we can explore that and keeping around transfer buffers later, with the latter being much more generally applicable.
void* transferPtr = SDL_MapGPUTransferBuffer(gContext.mDevice, transferBuffer, false); memcpy(transferPtr, surface->pixels, textureSize); SDL_UnmapGPUTransferBuffer(gContext.mDevice, transferBuffer)
Next we need to make a texture resource, we talked a about the parameters above, so the calls should seem straightforward. We don't need any additional layers, and we're not dealing with mipmaps yet, so each of those are 1. We want to display our textures from a fragment shader, so our usage will be SDL_GPU_TEXTUREUSAGE_SAMPLER, and since we're using a RGBA32 pixel format on our surface, this maps to SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM in SDL_GPU.
SDL_GPUTexture* texture = CreateTexture(surface->w, surface->h, 1, 1, SDL_GPU_TEXTUREUSAGE_SAMPLER, SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM, aTextureName); SDL_assert(texture);
Now we get to discuss copy passes a bit. You'll remember we've been using render passes previously, this is a similar idea. Like how render passes contain many rendering operations, so too does a copy pass contain many copying operations. We'll need one of these to move data from our transfer buffer into the texture. To accommodate possible future use of this function, we took a copy pass. This is for future chapters where we might load a bunch of things all at the same time and this function is just one among many loads. In a situation like that we'd only want to create one copy pass and use it for all of our transfers. For our use case, it won't be uncommon that we want a one-off copy pass, so we'll accept a NULL copy pass, and create our own if so.
Thankfully getting them is pretty easy, we're also assuming that if we weren't handed a copy pass, there's likely no command buffer as well, so we'll treat that as a one-off as well.
SDL_GPUCommandBuffer* commandBuffer = NULL; SDL_GPUCopyPass* copyPass = aCopyPass; bool needsToSubmit = NULL == copyPass; if (needsToSubmit) { commandBuffer = SDL_AcquireGPUCommandBuffer(gContext.mDevice); copyPass = SDL_BeginGPUCopyPass(commandBuffer); }
Now that we've got a copypass, we can issue the actual copy command. In this case we're copying to a texture, so we need a SDL_GPUTextureTransferInfo and SDL_GPUTextureRegion to describe it. You can essentially think of these as the transfer info describing the source data and the region the destination texture. With that in mind, given we're not doing anything special, this should be rather straightforward. We use the surface width and height to describe the rows and columns of the source data, and the w and h of the destination texture. We need to specify that we're using the transfer buffer from above as the source, and the texture we created as the destination. The last two interesting pieces of this is the d field of the region and the false we pass to the upload command. The d field refers to the layer or depth we're copying to. We're only handling normal textures here, and we only passed 1 for layers, so that's the layer we're writing to. Regarding the false, this corresponds to something called cycling. For now we don't want to request cycling, but it's related to doing multiple uploads to the same buffer or texture. We'll discuss it more later on when we start doing those updates.
// 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 );
Finally we can close out the copy pass and command buffer if we needed to make them, as well as releasing the TransferBuffer. You don't need to worry about releasing it before submitting the command buffer if we had passed one, as it's internally refcounted by SDL.
if (needsToSubmit) { SDL_EndGPUCopyPass(copyPass); SDL_SubmitGPUCommandBuffer(commandBuffer); } SDL_ReleaseGPUTransferBuffer(gContext.mDevice, transferBuffer); return texture;
This time around we'll want to be able to both move and scale our texture, which we'll display on a "Quad", which is really just another term for Rect/Rectangle. We'll expand this in a bit, but for now we'll call this a Uniform since we'll upload the whole thing into a uniform buffer. We'll use this in place of the position in our context.
typedef struct ModelUniform { float2 mPosition; float2 mScale; } ModelUniform; typedef struct TechniqueContext { SDL_GPUGraphicsPipeline* mPipeline; SDL_GPUTexture* mTexture; SDL_GPUSampler* mSampler; ModelUniform mUniform; } TechniqueContext;
Next up we're changing our fragment shader a bit, we don't need the uniform buffer anymore, so we'll change that to 0, but we're now using a sampler, so we'll pass that in as a 1:
graphicsPipelineCreateInfo.fragment_shader = CreateShader( "Quad.frag", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0, 0, 0, SDL_PROPERTY_TYPE_INVALID );
Now that we've got a position and scale, we'll need to set them up. 0.5f will give us a reasonably sized unit square in NDC for our scale, and the origin seems a reasonable position.
context.mUniform.mPosition.x = 0.f; context.mUniform.mPosition.y = 0.f; context.mUniform.mScale.x = 0.5f; context.mUniform.mScale.y = 0.5f;
Finally we need to create a sampler for our texture, this is essentially how we'll tell the graphics card how we'd like it to read from the texture. For now we're okay with the defaults, but for context the sorts of things this decides are what happens when you try to sample "outside" of the texture (the default is that it repeats), what happens when you're not sampling from an exact texture position (the default is to find the nearest pixel to that sample and use it, rather than doing a linear interpolation between the various near pixels). There's a bunch more options, but that'll give you a general idea of what this is for.
SDL_GPUSamplerCreateInfo samplerCreateInfo; SDL_zero(samplerCreateInfo); context.mSampler = SDL_CreateGPUSampler(gContext.mDevice, &samplerCreateInfo); SDL_assert(context.mSampler);
And then of course we just need to make the texture itself, like was mentioned earlier, we won't bother with creating a copy pass since we're really just doing this one upload.
context.mTexture = CreateAndUploadTexture(NULL, "sample.bmp");
This time around we'll be uploading the uniform struct, as we want both the position and scale. We could in theory have decided this should be two different uniforms, but in general we'll try to pack what we can into each uniform, where appropriate.
SDL_BindGPUGraphicsPipeline(aRenderPass, aContext->mPipeline); SDL_PushGPUVertexUniformData(aCommandBuffer, 0, &aContext->mUniform, sizeof(aContext->mUniform));
Next we'll need to bind the texture we're going to use in this draw, we can see that SDL_GPUTextureSamplerBinding takes both a texture and a sampler, so you can both reuse samplers for different textures, and swap them out, if some situations need alternate settings for effects. We're just using a single texture here, in the first (0th) slot, so we're just setting the slot, passing a pointer to the binding, and letting it know it's just a single binding.
{ SDL_GPUTextureSamplerBinding textureBinding; SDL_zero(textureBinding); textureBinding.texture = aContext->mTexture; textureBinding.sampler = aContext->mSampler; SDL_BindGPUFragmentSamplers(aRenderPass, 0, &textureBinding, 1); }
Finally we can draw the triangles that make up the quad, which we'll discuss in the vertex shader section below. We'll need to issue a draw for 6 vertices to draw it, and only one instance.
SDL_DrawGPUPrimitives(aRenderPass, 6, 1, 0, 0);
We'll need to clean up the new resources we made in our destroy function:
SDL_ReleaseGPUSampler(gContext.mDevice, aContext->mSampler); SDL_ReleaseGPUTexture(gContext.mDevice, aContext->mTexture);
We don't need the coloring controls from the last example so if you still have that in your event handler, remove it. This time we'll adjust and expand out our controls a bit, one due to now using a struct that contains our position, and two so we can adjust the scale of our textured object:
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;
Up until now, we've only stored arrays of vertices, this made sense because we've only been dealing with a single triangle. Once you want to render a texture however, you'll immediately realize that you'd be duplicating two vertices for that quad you want to display it on! Now obviously with a Quad, this isn't such a big deal, two verts is essentially nothing, but it allows us to demonstrate a new technique. There's a tool called an index buffer which, as it sounds, holds indices into the vertex buffer. Instead of iterating through the vertices, we iterate through the indices and use them to pull the relevant vertex. Here's what this looks like for a quad, with the vertex positions in parentheses and the index in the index buffer on each position.
(-1, 1) (1, 1) 0------------------1 1 | / /| | Triangle 1 / / | | / / | | / / | | / / | | / / | | / / | | / / Triangle 2 | | / / | 2 2-----------------3 (-1, -1) (1, -1)
See how the first and second vertex are the same on each triangle? It doesn't look like much now, but when you start rendering models, this would really start increasing the size of them. So lets learn about indexing now, ahead of when we'll need them for some of the more serious 3D applications later on.
Similar to last time, we'll be declaring some static data in the shader, but this time with an additional vertex, as well as an array of indices to index with. We won't need the colors like last time, hence their omition.
static const float2 cVertexPositions[4] = { {-1.0f, 1.0f}, { 1.0f, 1.0f}, {-1.0f, -1.0f}, { 1.0f, -1.0f}, }; static const uint cVertexIndices[6] = { 0, 1, 2, // Triangle 1 1, 3, 2 // Triangle 2 };
We can see our indices align with the diagram above. An astute reader might also notice that these vertices match the dimensions of the screen in NDC space. Don't worry, we'll learn how to adjust the size of it in this chapter so it won't take up the whole screen!
struct Output { float2 UV : TEXCOORD1; float4 Position : SV_Position; };
As discussed above, we're now taking in a "Uniform" struct that contains all of the data we need. For now that's just two float2s for position and scale. Here in HLSL we can define a struct that matches the one we have in C. When you're defining your own you'll need to be careful of alignment, as discussed in the previous chapter. In this case with two sequential float2 and no previous members, we're safe to not need padding.
Once you have the struct, you can declare uniform variable just as before, but this time as the struct we defined.
struct ModelUniform { float2 mPosition; float2 mScale; }; cbuffer UBO : register(b0, space1) { ModelUniform cModelUniform; };
Incidentally, you don't need to use a struct here. You can have position and scale be individual variables within the UBO cbuffer block. I just prefer to model this more directly on the C code, and for that I feel more comfortable using a struct. It's ultimately a subjective style decision.
Now that we're using indices, we'll use our vertex ID to find which index we're currently rendering, remembering that we're rendering six this time, so using that for the mod. Then we can use the index to find the current vertex.
Output main(uint id : SV_VertexID) { uint indicesIndex = id % 6; uint vertexIndex = cVertexIndices[indicesIndex]; float2 position = cVertexPositions[vertexIndex];
We'll be getting deeper into the math of this in the next chapter, but for now we'll take it somewhat as given that we can simply piecewise multiply our scale against our vertex position to scale them up or down, and we've already seen we can do the same for piecewise addition to move things around.
float2 scaledPosition = position * cModelUniform.mScale; float2 transformedPosition = scaledPosition + cModelUniform.mPosition;
As for output, the position will be output like in the fullscreen triangle, this time using our adjusted vertex position, rather than the vertex position directly. With the UV coordinate, a reminder of which is it being the texture coordinates from the last chapter, we'll be computing it based on the vertex position, rather than just using an array. There's nothing wrong with storing it in an array, this is just for demonstration purposes as it's trivial to compute in this case. Remember that we want bottom left to be (0, 0) and top -right to be (1, 1), so we can just do a scalar addition of 1 to our vertices to get the bottom left where we want it. At this point top right will be (2, 2), if we scale it down by half, we'll be where we want at the top right.
Output output; output.Position = float4(transformedPosition, 0.0f, 1.0f); output.UV = (vertex + 1.0f) * 0.5f; return output; }
#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; ////////////////////////////////////////////////////// // 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; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Shared GPU Code ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// typedef struct GpuContext { SDL_Window* mWindow; SDL_GPUDevice* mDevice; SDL_PropertiesID mProperties; const char* mShaderEntryPoint; SDL_GPUShaderFormat mChosenBackendFormat; const char* mChosenBackendFormatExtension; } 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_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; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Technique Code ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// typedef struct ModelUniform { float2 mPosition; float2 mScale; } ModelUniform; typedef struct TechniqueContext { SDL_GPUGraphicsPipeline* mPipeline; float2 mPositon; int mColorIndex; SDL_GPUTexture* mTexture; SDL_GPUSampler* mSampler; ModelUniform mUniform; } TechniqueContext; 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.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST; graphicsPipelineCreateInfo.vertex_shader = CreateShader( "FullscreenTriangle.vert", "Quad.vert", SDL_GPU_SHADERSTAGE_VERTEX, 0, 1, 0, 0, SDL_PROPERTY_TYPE_INVALID ); SDL_assert(graphicsPipelineCreateInfo.vertex_shader); graphicsPipelineCreateInfo.fragment_shader = CreateShader( "FullscreenTriangle.frag", "Quad.frag", SDL_GPU_SHADERSTAGE_FRAGMENT, 0, 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.mPositon.x = 0.5f; context.mPositon.y = 0.5f; context.mUniform.mPosition.x = 0.f; context.mUniform.mPosition.y = 0.f; context.mUniform.mScale.x = 0.5f; context.mUniform.mScale.y = 0.5f; 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_FColor colors[] = { {1, 0, 0, 1}, {0, 1, 0, 1}, {0, 0, 1, 1} }; SDL_BindGPUGraphicsPipeline(aRenderPass, aContext->mPipeline); SDL_PushGPUVertexUniformData(aCommandBuffer, 0, &aContext->mPositon, sizeof(float2)); SDL_PushGPUFragmentUniformData(aCommandBuffer, 0, &colors[aContext->mColorIndex], sizeof(SDL_FColor)); SDL_DrawGPUPrimitives(aRenderPass, 3, 1, 0, 0); SDL_PushGPUVertexUniformData(aCommandBuffer, 0, &aContext->mUniform, sizeof(aContext->mUniform)); { SDL_GPUTextureSamplerBinding textureBinding; SDL_zero(textureBinding); textureBinding.texture = aContext->mTexture; textureBinding.sampler = aContext->mSampler; SDL_BindGPUFragmentSamplers(aRenderPass, 0, &textureBinding, 1); } 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); TechniqueContext context = CreateTechniqueContext(); const float speed = 1.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_KEY_DOWN: switch (event.key.scancode) { case SDL_SCANCODE_1: context.mColorIndex = 0; break; case SDL_SCANCODE_2: context.mColorIndex = 1; break; case SDL_SCANCODE_3: context.mColorIndex = 2; break; default: break; } break; } } if (key_map[SDL_SCANCODE_D]) context.mPositon.x += speed * dt * 1.0f; if (key_map[SDL_SCANCODE_A]) context.mPositon.x -= speed * dt * 1.0f; if (key_map[SDL_SCANCODE_W]) context.mPositon.y += speed * dt * 1.0f; if (key_map[SDL_SCANCODE_S]) context.mPositon.y -= 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; if (!SDL_WaitAndAcquireGPUSwapchainTexture(commandBuffer, gContext.mWindow, &swapchainTexture, NULL, NULL)) { SDL_Log("WaitAndAcquireGPUSwapchainTexture failed: %s", SDL_GetError()); continue; } 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; SDL_GPURenderPass* renderPass = SDL_BeginGPURenderPass( commandBuffer, &colorTargetInfo, 1, NULL ); DrawTechniqueContext(&context, commandBuffer, renderPass); SDL_EndGPURenderPass(renderPass); SDL_SubmitGPUCommandBuffer(commandBuffer); } DestroyTechniqueContext(&context); DestroyGpuContext(); SDL_Quit(); return 0; } #ifdef __cplusplus } // end cpp_test #endif
Download the source for this example here.