A tour of drawing with ObjectivelyGPU.
Object oriented GPU programming in C
ObjectivelyGPU is built on Objectively, a lightweight object oriented framework for C. The raw SDL3 GPU API is a bag of opaque handles that you create, track, and destroy by hand; ObjectivelyGPU wraps them in reference-counted objects with strongly typed methods, so resources clean up after themselves and the API guides you toward correct usage.
Everything starts with a RenderDevice, created for the window you intend to draw to:
static RenderDevice * initWithWindow(RenderDevice *self, SDL_Window *window)
Definition RenderDevice.c:462
The RenderDevice encapsulates an SDL_GPUDevice and provides methods for allocating GPU resources,...
Definition RenderDevice.h:63
The device claims the GPU, configures the swapchain for that window, and becomes the factory for every other resource.
The render device owns the swapchain
ObjectivelyGPU draws to a Framebuffer and lets the RenderDevice manage the swapchain for you. Each frame is bracketed by beginFrame and endFrame:
if (commands) {
}
static CommandBuffer * beginFrame(RenderDevice *self)
Definition RenderDevice.c:152
static void endFrame(RenderDevice *self)
Definition RenderDevice.c:209
A recorded sequence of GPU commands for a single frame.
Definition CommandBuffer.h:80
beginFrame acquires the swapchain texture and returns a CommandBuffer, or NULL if the swapchain is unavailable this frame (e.g. the window is minimized) — just skip the frame. endFrame resolves the active multisampled Framebuffer, blits it to the swapchain, and submits. Applications that would rather render straight to the swapchain can ignore this loop entirely and drive the CommandBuffer themselves — beginFrame / endFrame are a convenience, not a requirement.
Resource objects with automatic lifecycle
Buffers, textures, samplers, shaders, and pipelines are all objects, created through the device and released when you are done. Reference counting means release returns NULL, so the foo = release(foo) idiom keeps dangling pointers from biting you.
SDL_GPU_BUFFERUSAGE_VERTEX, vertices, sizeof(vertices));
Shader *vertexShader = $(renderDevice,
loadShader,
"Hello.vert", &(SDL_GPUShaderCreateInfo) {
.
stage = SDL_GPU_SHADERSTAGE_VERTEX,
.num_uniform_buffers = 1,
});
Shader *fragmentShader = $(renderDevice,
loadShader,
"Hello.frag", &(SDL_GPUShaderCreateInfo) {
.
stage = SDL_GPU_SHADERSTAGE_FRAGMENT,
});
vertexShader = release(vertexShader);
fragmentShader = release(fragmentShader);
static Buffer * createBufferWithConstMem(RenderDevice *self, SDL_GPUBufferUsageFlags usage, const void *mem, Uint32 size)
Definition RenderDevice.c:228
static Shader * loadShader(RenderDevice *self, const char *name, const SDL_GPUShaderCreateInfo *info)
Definition RenderDevice.c:475
An SDL_GPUBuffer (vertex, index, indirect, storage, etc.) and its metadata.
Definition Buffer.h:60
An SDL_GPUShader: one compiled programmable stage of a graphics pipeline.
Definition Shader.h:51
SDL_GPUShaderStage stage
The shader stage (vertex or fragment).
Definition Shader.h:83
The underlying SDL handle is always reachable as a field (vertexBuffer->buffer, shader->shader) for the places SDL still wants one — you get encapsulation without losing access to the metal.
Framebuffers, MSAA, and automatic resolve
A Framebuffer bundles one or more color targets, an optional depth target, and a sample count. Create one through the device with a GPU_FramebufferCreateInfo, then make it the device's active framebuffer:
.colorFormats = { colorFormat },
.numColorTargets = 1,
.depthFormat = SDL_GPU_TEXTUREFORMAT_D16_UNORM,
.sampleCount = SDL_GPU_SAMPLECOUNT_4,
});
static SDL_GPUTextureFormat getSwapchainTextureFormat(const RenderDevice *self)
Definition RenderDevice.c:426
static Framebuffer * createFramebuffer(RenderDevice *self, const GPU_FramebufferCreateInfo *info)
Definition RenderDevice.c:255
static void setFramebuffer(RenderDevice *self, Framebuffer *framebuffer)
Definition RenderDevice.c:571
#define MakeSize(w, h)
Creates an SDL_Size with the given dimensions.
Definition Types.h:64
An off-screen render target grouping a color and/or depth texture.
Definition Framebuffer.h:127
SDL_Size size
The framebuffer dimensions.
Definition Framebuffer.h:143
Parameters for creating a Framebuffer.
Definition Framebuffer.h:56
When sampleCount is greater than one, the framebuffer allocates the resolve targets and promotes your STORE operations to RESOLVE_AND_STORE automatically. You ask for target descriptors with colorTargetInfo and depthTargetInfo; the MSAA bookkeeping is handled for you, and endFrame presents the resolved result.
Typed passes with lifecycle validation
A CommandBuffer vends three kinds of pass — RenderPass, ComputePass, and CopyPass — each a distinct type exposing only the methods that are legal within it. The framework asserts on misuse (recording into a finished buffer, leaving a pass open, submitting twice), turning a class of silent GPU errors into immediate, located failures.
const SDL_GPUColorTargetInfo color = $(framebuffer,
colorTargetInfo, 0, SDL_GPU_LOADOP_CLEAR, SDL_GPU_STOREOP_STORE, &clearColor);
const SDL_GPUDepthStencilTargetInfo depth = $(framebuffer,
depthTargetInfo, SDL_GPU_LOADOP_CLEAR, SDL_GPU_STOREOP_DONT_CARE, 1.f);
pass = release(pass);
static RenderPass * beginRenderPass(CommandBuffer *self, const SDL_GPUColorTargetInfo *colorTargets, Uint32 numColorTargets, const SDL_GPUDepthStencilTargetInfo *depthStencil)
Definition CommandBuffer.c:114
static void bindPipeline(ComputePass *self, ComputePipeline *pipeline)
Definition ComputePass.c:57
static SDL_GPUColorTargetInfo colorTargetInfo(const Framebuffer *self, Uint32 index, SDL_GPULoadOp loadOp, SDL_GPUStoreOp storeOp, const SDL_FColor *clearColor)
Definition Framebuffer.c:59
static SDL_GPUDepthStencilTargetInfo depthTargetInfo(const Framebuffer *self, SDL_GPULoadOp loadOp, SDL_GPUStoreOp storeOp, float clearDepth)
Definition Framebuffer.c:90
static void drawPrimitives(const RenderPass *self, Uint32 numVertices, Uint32 numInstances, Uint32 firstVertex, Uint32 firstInstance)
Definition RenderPass.c:179
static void bindVertexBuffers(const RenderPass *self, Uint32 firstSlot, const SDL_GPUBufferBinding *bindings, Uint32 num)
Definition RenderPass.c:103
SDL_GPUBuffer * buffer
The underlying SDL buffer.
Definition Buffer.h:76
A scoped render pass for recording draw commands into a CommandBuffer.
Definition RenderPass.h:57
Per-frame uniform data goes through the command buffer — for example, pushing a model-view-projection matrix to vertex uniform slot 0:
static void pushVertexUniformData(const CommandBuffer *self, Uint32 slot, const void *data, Uint32 length)
Definition CommandBuffer.c:224
Shaders in any language
ObjectivelyGPU loads compiled shaders in whatever format the active backend expects — SPIR-V for Vulkan, MSL for Metal, DXIL for Direct3D 12 — through the Objectively Resource system. loadShader looks the blob up by name and selects the right variant for the running backend, so your code names a shader once and runs everywhere.
How you author those shaders is entirely up to you. Write GLSL, HLSL, or Metal; hand-write the target language; or cross-compile a single source with SDL_shadercross. ObjectivelyGPU has no opinion — it only consumes the compiled result.
Examples
- Hello — a spinning, multisampled 3D cube, start to finish: device, framebuffer, pipeline, and the frame loop.
- HelloCompute — a compute shader animates a particle system that is then drawn as points.