ObjectivelyGPU
Object oriented graphics framework for SDL3 and C
Loading...
Searching...
No Matches
ObjectivelyGPU User Guide

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:

RenderDevice *renderDevice = $(alloc(RenderDevice), initWithWindow, window, NULL);
static RenderDevice * initWithWindow(RenderDevice *self, SDL_Window *window, const char *driverName)
Definition RenderDevice.c:464
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:

CommandBuffer *commands = $(renderDevice, beginFrame);
if (commands) {
// ... record passes against `commands` ...
$(renderDevice, endFrame);
}
static CommandBuffer * beginFrame(RenderDevice *self)
Definition RenderDevice.c:153
static void endFrame(RenderDevice *self)
Definition RenderDevice.c:211
A recorded sequence of GPU commands for a single frame.
Definition CommandBuffer.h:81

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.

Buffer *vertexBuffer = $(renderDevice, createBufferWithConstMem,
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,
});
// ... build a pipeline from the shaders, then ...
vertexShader = release(vertexShader);
fragmentShader = release(fragmentShader);
static Buffer * createBufferWithConstMem(RenderDevice *self, SDL_GPUBufferUsageFlags usage, const void *mem, Uint32 size)
Definition RenderDevice.c:230
static Shader * loadShader(RenderDevice *self, const char *name, const SDL_GPUShaderCreateInfo *info)
Definition RenderDevice.c:477
An SDL_GPUBuffer (vertex, index, indirect, storage, etc.) and its metadata.
Definition Buffer.h:61
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:

const SDL_GPUTextureFormat colorFormat = $(renderDevice, getSwapchainTextureFormat);
Framebuffer *framebuffer = $(renderDevice, createFramebuffer, &(GPU_FramebufferCreateInfo) {
.size = MakeSize(w, h),
.colorAttachments = {
{ .format = colorFormat, .clearColor = { 0.1f, 0.1f, 0.2f, 1.f } },
},
.numColorTargets = 1,
.depthAttachment = { .format = SDL_GPU_TEXTUREFORMAT_D16_UNORM, .clearDepth = 1.f },
.sampleCount = SDL_GPU_SAMPLECOUNT_4,
});
$(renderDevice, setFramebuffer, framebuffer);
static SDL_GPUTextureFormat getSwapchainTextureFormat(const RenderDevice *self)
Definition RenderDevice.c:428
static Framebuffer * createFramebuffer(RenderDevice *self, const GPU_FramebufferCreateInfo *info)
Definition RenderDevice.c:257
static void setFramebuffer(RenderDevice *self, Framebuffer *framebuffer)
Definition RenderDevice.c:573
#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:177
SDL_Size size
The framebuffer dimensions.
Definition Framebuffer.h:198
Parameters for creating a Framebuffer.
Definition Framebuffer.h:108

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 — each attachment clears to the clearColor/clearDepth given at creation time, so there's nothing further to pass at draw time; the MSAA bookkeeping is handled for you, and endFrame presents the resolved result.

Building the matching GraphicsPipeline needs the same formats, but as an SDL_GPUGraphicsPipelineTargetInfo rather than render-pass target info — pipelineTargetInfo derives that from the Framebuffer too, given one blend state per color attachment:

SDL_GPUColorTargetDescription descriptions[1];
SDL_GPUGraphicsPipelineTargetInfo targetInfo;
$(framebuffer, pipelineTargetInfo, &GPU_BlendStateOpaque, descriptions, &targetInfo);
SDL_GPUGraphicsPipelineCreateInfo info = GPU_GraphicsPipeline3D;
info.vertex_shader = vertexShader->shader;
info.fragment_shader = fragmentShader->shader;
info.target_info = targetInfo;
GraphicsPipeline *pipeline = $(renderDevice, createGraphicsPipeline, &info);
static void pipelineTargetInfo(const Framebuffer *self, const SDL_GPUColorTargetBlendState *blendStates, SDL_GPUColorTargetDescription *descriptions, SDL_GPUGraphicsPipelineTargetInfo *targetInfo)
Definition Framebuffer.c:119
OBJECTIVELYGPU_EXPORT_DATA const SDL_GPUGraphicsPipelineCreateInfo GPU_GraphicsPipeline3D
Definition GraphicsPipeline.c:67
OBJECTIVELYGPU_EXPORT_DATA const SDL_GPUColorTargetBlendState GPU_BlendStateOpaque
Definition GraphicsPipeline.c:33
static GraphicsPipeline * createGraphicsPipeline(RenderDevice *self, const SDL_GPUGraphicsPipelineCreateInfo *info)
Definition RenderDevice.c:266
An SDL_GPUGraphicsPipeline: the compiled vertex/fragment program plus all fixed-function render state...
Definition GraphicsPipeline.h:51

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);
const SDL_GPUDepthStencilTargetInfo depth = $(framebuffer, depthTargetInfo, SDL_GPU_LOADOP_CLEAR, SDL_GPU_STOREOP_DONT_CARE);
RenderPass *pass = $(commands, beginRenderPass, &color, 1, &depth);
$(pass, bindPipeline, pipeline);
$(pass, bindVertexBuffers, 0, &(SDL_GPUBufferBinding) { .buffer = vertexBuffer->buffer }, 1);
$(pass, drawPrimitives, 36, 1, 0, 0);
pass = release(pass);
static RenderPass * beginRenderPass(CommandBuffer *self, const SDL_GPUColorTargetInfo *colorTargets, Uint32 numColorTargets, const SDL_GPUDepthStencilTargetInfo *depthStencil)
Definition CommandBuffer.c:117
static void bindPipeline(ComputePass *self, ComputePipeline *pipeline)
Definition ComputePass.c:57
static SDL_GPUDepthStencilTargetInfo depthTargetInfo(const Framebuffer *self, SDL_GPULoadOp loadOp, SDL_GPUStoreOp storeOp)
Definition Framebuffer.c:103
static SDL_GPUColorTargetInfo colorTargetInfo(const Framebuffer *self, Uint32 index, SDL_GPULoadOp loadOp, SDL_GPUStoreOp storeOp)
Definition Framebuffer.c:68
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:77
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:

$(commands, pushVertexUniformData, 0, modelViewProjection.f, sizeof(modelViewProjection));
static void pushVertexUniformData(const CommandBuffer *self, Uint32 slot, const void *data, Uint32 length)
Definition CommandBuffer.c:250

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.