
You already know how to create the window and the device, if not you can refer to the previous tutorial: Getting stated with SDL3_gpu
SDL_Init(SDL_INIT_VIDEO);
SDL_GPUDevice* device = SDL_CreateGPUDevice(
SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_DXIL | SDL_GPU_SHADERFORMAT_MSL,
false,
NULL
);
if (device == NULL)
{
SDL_Log("CreateDevice failed");
return -1;
}
SDL_Window* window = SDL_CreateWindow("Display a triangle with SDL3_gpu", 800, 600, SDL_WINDOW_RESIZABLE);
if (window == NULL)
{
SDL_Log("CreateWindow failed: %s", SDL_GetError());
return -1;
}
if (!SDL_ClaimWindowForGPUDevice(device, window))
{
SDL_Log("ClaimWindow failed");
return -1;
}
To display a texture we need a vertex shader and a fragment shader
InitializeAssetLoader();
SDL_GPUShader* vertexShader = LoadShader(device, "TexturedQuad.vert", 0, 0, 0, 0);
if (vertexShader == NULL)
{
SDL_Log("Failed to create vertex shader!");
return -1;
}
SDL_GPUShader* fragmentShader = LoadShader(device, "TexturedQuad.frag", 1, 0, 0, 0);
if (fragmentShader == NULL)
{
SDL_Log("Failed to create fragment shader!");
return -1;
}
The vertex shader is simple it's take as input the position and the texture coordinate and pass it to the output to be used in the fragment shader.
You can download the shaders already compiled: shaders.zip
struct Input
{
float3 Position : TEXCOORD0;
float2 TexCoord : TEXCOORD1;
};
struct Output
{
float2 TexCoord : TEXCOORD0;
float4 Position : SV_Position;
};
Output main(Input input)
{
Output output;
output.TexCoord = input.TexCoord;
output.Position = float4(input.Position, 1.0f);
return output;
}
The fragment shader is also simple, it use a texture2d and a sampler: it's sampling the texture at the given TexCoord using the Sampler.
Texture2D<float4> Texture : register(t0, space2);
SamplerState Sampler : register(s0, space2);
float4 main(float2 TexCoord : TEXCOORD0) : SV_Target0
{
return Texture.Sample(Sampler, TexCoord);
}
To load the image we need to create a function LoadImage, we will use the fomat bmp since we do not want to use SDL3_image.
SDL_Surface* LoadImage(const char* imageFilename, int desiredChannels)
{
char fullPath[256];
SDL_Surface *result;
SDL_PixelFormat format;
SDL_snprintf(fullPath, sizeof(fullPath), "%s%s", BasePath, imageFilename);
result = SDL_LoadBMP(fullPath);
if (result == NULL)
{
SDL_Log("Failed to load BMP: %s", SDL_GetError());
return NULL;
}
if (desiredChannels == 4)
{
format = SDL_PIXELFORMAT_ABGR8888;
}
else
{
SDL_assert(!"Unexpected desiredChannels");
SDL_DestroySurface(result);
return NULL;
}
if (result->format != format)
{
SDL_Surface *next = SDL_ConvertSurface(result, format);
SDL_DestroySurface(result);
result = next;
}
return result;
}
Then we can call this function to load the image:
You can download the image : lettuce.bmp
SDL_Surface *imageData = LoadImage("lettuce.bmp", 4);
if (imageData == NULL)
{
SDL_Log("Could not load image data!");
return -1;
}
For the pipeline creation we will use a struct to represent the vertices:
typedef struct PositionTextureVertex
{
float x, y, z;
float u, v;
} PositionTextureVertex;
We use this struct in the pipeline creation in the vertex attributes:
SDL_GPUGraphicsPipelineCreateInfo pipelineCreateInfo = {
.target_info = {
.num_color_targets = 1,
.color_target_descriptions = (SDL_GPUColorTargetDescription[]){{
.format = SDL_GetGPUSwapchainTextureFormat(device, window)
}},
},
.vertex_input_state = (SDL_GPUVertexInputState){
.num_vertex_buffers = 1,
.vertex_buffer_descriptions = (SDL_GPUVertexBufferDescription[]){{
.slot = 0,
.input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX,
.instance_step_rate = 0,
.pitch = sizeof(PositionTextureVertex)
}},
.num_vertex_attributes = 2,
.vertex_attributes = (SDL_GPUVertexAttribute[]){{
.buffer_slot = 0,
.format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3,
.location = 0,
.offset = 0
}, {
.buffer_slot = 0,
.format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2,
.location = 1,
.offset = sizeof(float) * 3
}}
},
.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST,
.vertex_shader = vertexShader,
.fragment_shader = fragmentShader
};
Then we can create the pipeline from this informations and free the shaders:
SDL_GPUGraphicsPipeline *Pipeline = NULL;
Pipeline = SDL_CreateGPUGraphicsPipeline(device, &pipelineCreateInfo);
if (Pipeline == NULL)
{
SDL_Log("Failed to create pipeline!");
return -1;
}
SDL_ReleaseGPUShader(device, vertexShader);
SDL_ReleaseGPUShader(device, fragmentShader);
It's time to create a simple sampler and use neighboor interpolation.
SDL_GPUSamplerCreateInfo sampler_info{
.min_filter = SDL_GPU_FILTER_NEAREST,
.mag_filter = SDL_GPU_FILTER_NEAREST,
.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST,
.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE,
.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE,
.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE,
};
SDL_GPUSampler *Samplers = SDL_CreateGPUSampler(device, &sampler_info);
The function for the creation of the sampler is simple enough, but maybe you will want more informations about SDL_GPUSamplerCreateInfo:
typedef struct SDL_GPUSamplerCreateInfo
{
SDL_GPUFilter min_filter; // The minification filter to apply to lookups.
SDL_GPUFilter mag_filter; // The magnification filter to apply to lookups.
SDL_GPUSamplerMipmapMode mipmap_mode; // The mipmap filter to apply to lookups.
SDL_GPUSamplerAddressMode address_mode_u; // The addressing mode for U coordinates outside [0, 1).
SDL_GPUSamplerAddressMode address_mode_v; // The addressing mode for V coordinates outside [0, 1).
SDL_GPUSamplerAddressMode address_mode_w; // The addressing mode for W coordinates outside [0, 1).
float mip_lod_bias; // The bias to be added to mipmap LOD calculation.
float max_anisotropy; // The anisotropy value clamp used by the sampler.
// If enable_anisotropy is false, this is ignored.
SDL_GPUCompareOp compare_op; // The comparison operator to apply to fetched data before filtering.
float min_lod; // Clamps the minimum of the computed LOD value.
float max_lod; // Clamps the maximum of the computed LOD value.
bool enable_anisotropy; // true to enable anisotropic filtering.
bool enable_compare; // true to enable comparison against a reference value during lookups.
Uint8 padding1;
Uint8 padding2;
SDL_PropertiesID props; // A properties ID for extensions. Should be 0 if no extensions are needed.
} SDL_GPUSamplerCreateInfo;
More informations about SDL_GPUFilter:
typedef enum SDL_GPUFilter
{
SDL_GPU_FILTER_NEAREST, // Point filtering.
SDL_GPU_FILTER_LINEAR // Linear filtering.
} SDL_GPUFilter;
More informations about SDL_GPUSamplerMipmapMode:
typedef enum SDL_GPUSamplerMipmapMode
{
SDL_GPU_SAMPLERMIPMAPMODE_NEAREST, // Point filtering.
SDL_GPU_SAMPLERMIPMAPMODE_LINEAR // Linear filtering.
} SDL_GPUSamplerMipmapMode;
More informations about
typedef enum SDL_GPUSamplerAddressMode
{
SDL_GPU_SAMPLERADDRESSMODE_REPEAT, // Specifies that the coordinates will wrap around.
SDL_GPU_SAMPLERADDRESSMODE_MIRRORED_REPEAT, // Specifies that the coordinates will wrap around mirrored.
SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE // Specifies that the coordinates will clamp to the 0-1 range.
} SDL_GPUSamplerAddressMode;
More informations about SDL_GPUCompareOp
typedef enum SDL_GPUCompareOp
{
SDL_GPU_COMPAREOP_INVALID,
SDL_GPU_COMPAREOP_NEVER, // The comparison always evaluates false.
SDL_GPU_COMPAREOP_LESS, // The comparison evaluates reference < test.
SDL_GPU_COMPAREOP_EQUAL, // The comparison evaluates reference == test.
SDL_GPU_COMPAREOP_LESS_OR_EQUAL, // The comparison evaluates reference <= test.
SDL_GPU_COMPAREOP_GREATER, // The comparison evaluates reference > test.
SDL_GPU_COMPAREOP_NOT_EQUAL, // The comparison evaluates reference != test.
SDL_GPU_COMPAREOP_GREATER_OR_EQUAL, // The comparison evaluates reference >= test.
SDL_GPU_COMPAREOP_ALWAYS // The comparison always evaluates true.
} SDL_GPUCompareOp;
Next we create the vertex buffer:
SDL_GPUBufferCreateInfo buffer_info {
.usage = SDL_GPU_BUFFERUSAGE_VERTEX,
.size = sizeof(PositionTextureVertex) * 4
};
SDL_GPUBuffer* VertexBuffer = SDL_CreateGPUBuffer(
device,
&buffer_info
);
SDL_SetGPUBufferName(
device,
VertexBuffer,
"Lettuce Vertex Buffer"
);
Then the index buffer:
SDL_GPUBufferCreateInfo buffer_info2 {
.usage = SDL_GPU_BUFFERUSAGE_INDEX,
.size = sizeof(Uint16) * 6
};
SDL_GPUBuffer* IndexBuffer = SDL_CreateGPUBuffer(
device,
&buffer_info2
);
We need to create the texture from the SDL_Surface
SDL_GPUTextureCreateInfo texture_info{
.type = SDL_GPU_TEXTURETYPE_2D,
.format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM,
.width = static_cast<Uint32>(imageData->w),
.height = static_cast<Uint32>(imageData->h),
.layer_count_or_depth = 1,
.num_levels = 1,
.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER
};
SDL_GPUTexture* Texture = SDL_CreateGPUTexture(device, &texture_info);
SDL_SetGPUTextureName(
device,
Texture,
"Lettuce Texture"
);
Here some more informations about SDL_GPUTextureCreateInfo:
typedef struct SDL_GPUTextureCreateInfo
{
SDL_GPUTextureType type; // The base dimensionality of the texture.
SDL_GPUTextureFormat format; // The pixel format of the texture.
SDL_GPUTextureUsageFlags usage; // How the texture is intended to be used by the client.
Uint32 width; // The width of the texture.
Uint32 height; // The height of the texture.
Uint32 layer_count_or_depth; // The layer count or depth of the texture.
// This value is treated as a layer count on 2D array textures,
// and as a depth value on 3D textures.
Uint32 num_levels; // The number of mip levels in the texture.
SDL_GPUSampleCount sample_count; // The number of samples per texel.
// Only applies if the texture is used as a render target.
SDL_PropertiesID props; // A properties ID for extensions.
// Should be 0 if no extensions are needed.
} SDL_GPUTextureCreateInfo;
We create the transfer buffer with the right size.
SDL_GPUTransferBufferCreateInfo transfer_info {
.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD,
.size = (sizeof(PositionTextureVertex) * 4) + (sizeof(Uint16) * 6)
};
SDL_GPUTransferBuffer* bufferTransferBuffer = SDL_CreateGPUTransferBuffer(
device,
&transfer_info
);
We can now set the vertex data and the index data in the buffer:
PositionTextureVertex* transferData = (PositionTextureVertex*)SDL_MapGPUTransferBuffer(
device,
bufferTransferBuffer,
false
);
transferData[0] = (PositionTextureVertex) { -1, 1, 0, 0, 0 };
transferData[1] = (PositionTextureVertex) { 1, 1, 0, 1, 0 };
transferData[2] = (PositionTextureVertex) { 1, -1, 0, 1, 1 };
transferData[3] = (PositionTextureVertex) { -1, -1, 0, 0, 1 };
Uint16* indexData = (Uint16*) &transferData[4];
indexData[0] = 0;
indexData[1] = 1;
indexData[2] = 2;
indexData[3] = 0;
indexData[4] = 2;
indexData[5] = 3;
SDL_UnmapGPUTransferBuffer(device, bufferTransferBuffer);
The data is now ready to be uploaded to the gpu
We also need to upload the data for the texture, for that we create another transfer buffer:
SDL_GPUTransferBufferCreateInfo transfer_info2 {
.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD,
.size = static_cast<Uint32>(imageData->w * imageData->h * 4)
};
SDL_GPUTransferBuffer* textureTransferBuffer = SDL_CreateGPUTransferBuffer(
device,
&transfer_info2
);
We need to copy the pixels of the texture inside the buffer:
Uint8* textureTransferPtr = (Uint8*)SDL_MapGPUTransferBuffer(
device,
textureTransferBuffer,
false
);
SDL_memcpy(textureTransferPtr, imageData->pixels, imageData->w * imageData->h * 4);
SDL_UnmapGPUTransferBuffer(device, textureTransferBuffer);
We create a command buffer to upload the vertex buffer:
SDL_GPUCommandBuffer* uploadCmdBuf = SDL_AcquireGPUCommandBuffer(device);
SDL_GPUCopyPass* copyPass = SDL_BeginGPUCopyPass(uploadCmdBuf);
SDL_GPUTransferBufferLocation location {
.transfer_buffer = bufferTransferBuffer,
.offset = 0
};
SDL_GPUBufferRegion region {
.buffer = VertexBuffer,
.offset = 0,
.size = sizeof(PositionTextureVertex) * 4
};
SDL_UploadToGPUBuffer(
copyPass,
&location,
®ion,
false
);
SDL_GPUTransferBufferLocation location2 {
.transfer_buffer = bufferTransferBuffer,
.offset = sizeof(PositionTextureVertex) * 4
};
SDL_GPUBufferRegion region2 {
.buffer = IndexBuffer,
.offset = 0,
.size = sizeof(Uint16) * 6
};
SDL_UploadToGPUBuffer(
copyPass,
&location2,
®ion2,
false
);
The last buffer to upload, the texture buffer :
SDL_GPUTextureTransferInfo transfer_info3 {
.transfer_buffer = textureTransferBuffer,
.offset = 0, /* Zeros out the rest */
};
SDL_GPUTextureRegion region3{
.texture = Texture,
.w = static_cast<Uint32>(imageData->w),
.h = static_cast<Uint32>(imageData->h),
.d = 1
};
SDL_UploadToGPUTexture(
copyPass,
&transfer_info3,
®ion3,
false
);
We can end the copy pass,detoy the surfarce and the transfer buffers:
SDL_EndGPUCopyPass(copyPass);
SDL_SubmitGPUCommandBuffer(uploadCmdBuf);
SDL_DestroySurface(imageData);
SDL_ReleaseGPUTransferBuffer(device, bufferTransferBuffer);
SDL_ReleaseGPUTransferBuffer(device, textureTransferBuffer);
Again the event loop has only one event: SDL_EVENT_QUIT
Same thing as before we create a command buffer and aquire the swapchain texture:
SDL_GPUCommandBuffer* cmdbuf = SDL_AcquireGPUCommandBuffer(device);
if (cmdbuf == NULL)
{
SDL_Log("AcquireGPUCommandBuffer failed: %s", SDL_GetError());
return -1;
}
SDL_GPUTexture* swapchainTexture;
if (!SDL_WaitAndAcquireGPUSwapchainTexture(cmdbuf, window, &swapchainTexture, NULL, NULL)) {
SDL_Log("WaitAndAcquireGPUSwapchainTexture failed: %s", SDL_GetError());
return -1;
}
This time the render pass is intersting because we need to bind the pipeline, the vertex buffer, the index buffer, the sampler.
The draw function is different: SDL_DrawGPUIndexedPrimitives because we have two triangle to render
if (swapchainTexture != NULL)
{
SDL_GPUColorTargetInfo colorTargetInfo = { 0 };
colorTargetInfo.texture = swapchainTexture;
colorTargetInfo.clear_color = (SDL_FColor){ 0.0f, 0.0f, 0.0f, 1.0f };
colorTargetInfo.load_op = SDL_GPU_LOADOP_CLEAR;
colorTargetInfo.store_op = SDL_GPU_STOREOP_STORE;
SDL_GPURenderPass* renderPass = SDL_BeginGPURenderPass(cmdbuf, &colorTargetInfo, 1, NULL);
SDL_GPUBufferBinding binding1 { .buffer = VertexBuffer, .offset = 0 };
SDL_GPUBufferBinding binding2 { .buffer = IndexBuffer, .offset = 0 };
SDL_GPUTextureSamplerBinding texture_binding { .texture = Texture, .sampler = Samplers };
SDL_BindGPUGraphicsPipeline(renderPass, Pipeline);
SDL_BindGPUVertexBuffers(renderPass, 0, &binding1, 1);
SDL_BindGPUIndexBuffer(renderPass, &binding2, SDL_GPU_INDEXELEMENTSIZE_16BIT);
SDL_BindGPUFragmentSamplers(renderPass, 0, &texture_binding, 1);
SDL_DrawGPUIndexedPrimitives(renderPass, 6, 1, 0, 0, 0);
SDL_EndGPURenderPass(renderPass);
}
After that we can submit the command buffer
SDL_SubmitGPUCommandBuffer(cmdbuf);
Need another OS ? => Windows, Mac, Linux