Using SDL2 Streaming Textures for Real‑Time Pixel Updates
Learn how SDL_TEXTUREACCESS_STREAMING lets you write pixels directly into GPU‑owned memory each frame, reducing copy overhead for video or procedural textures.
20 Sept 2025, 17:40 UTC

Problem: Updating a texture every frame without stalling the GPU
When you need to display video frames, a camera feed, or a procedurally generated image at 60 fps, the naïve approach is to create a new SDL_Texture each frame with SDL_TEXTUREACCESS_STATIC and upload the whole image via SDL_UpdateTexture. This forces a CPU‑to‑GPU copy for every frame and can cause noticeable frame‑time jitter, especially on integrated GPUs or when the texture is large.
Thesis: Streaming textures give you direct access to GPU‑owned memory
By creating a texture with the flag SDL_TEXTUREACCESS_STREAMING, you obtain a pointer to memory that the GPU can DMA directly. You lock the texture, write your pixel data into the returned buffer, unlock, and then render. The driver can transfer the locked region to the GPU without an extra copy, which lowers CPU overhead and stabilizes frame times when the whole texture is updated each frame.
Setting up a streaming texture
First, initialize SDL, create a window and a renderer. Then create the texture:
SDL_Window *win = SDL_CreateWindow("Streaming demo", 100, 100, 640, 480, 0);
SDL_Renderer *ren = SDL_CreateRenderer(win, -1, SDL_RENDERER_ACCELERATED);
SDL_Texture *tex = SDL_CreateTexture(ren,
SDL_PIXELFORMAT_ARGB8888,
SDL_TEXTUREACCESS_STREAMING,
640, 480);
if (!tex) { /* handle error */ }
The pixel format must match the data you will write; SDL_PIXELFORMAT_ARGB8888 is a common choice. The width and height define the texture size.
Updating the texture each frame
In your main loop, lock the texture, fill the buffer, unlock, render, and present:
Uint32 frameStart = SDL_GetPerformanceCounter();
int pitch; // bytes per row, may include padding
void *pixels; // pointer to the locked memory
if (SDL_LockTexture(tex, NULL, &pixels, &pitch) != 0) {
/* handle lock failure */
}
/Example: moving horizontal gradient /
static int offset = 0;
offset = (offset + 2) % 640;
for (int y = 0; y < 480; ++y) {
Uint32 *row = (Uint32 *)((uint8_t *)pixels + y * pitch);
for (int x = 0; x < 640; ++x) {
Uint8 intensity = (Uint8)((x + offset) * 255 / 640);
row[x] = SDL_MapRGBA(SDL_PIXELFORMAT_ARGB8888,
intensity, intensity, intensity, 255);
}
}
SDL_UnlockTexture(tex);
SDL_RenderClear(ren);
SDL_RenderCopy(ren, tex, NULL, NULL);
SDL_RenderPresent(ren);
Uint32 frameEnd = SDL_GetPerformanceCounter();
double ms = (frameEnd - frameStart) * 1000.0 / SDL_GetPerformanceFrequency();
/ms should stay near 16.6 for 60 fps/
The pitch value accounts for any row‑stride padding the driver requires; you must offset each row by y * pitch when writing.
Trade‑offs and limitations
- GPU stall: Locking a streaming texture stalls the GPU until the texture is unlocked. Keep the lock duration short (just the memcpy) and avoid locking large regions multiple times per frame.
- Pitch alignment: The returned pointer may not point to a tightly packed array; each row is separated by
pitchbytes. Ignoring this leads to corrupted images. - Pixel‑format match: The source data must exactly match the texture’s format. If you need to convert (e.g., from YUV to RGB), do the conversion before locking or use a shader.
- Partial updates: You can lock a sub‑rectangle by passing a non‑
NULLSDL_Rect*toSDL_LockTexture, which reduces the stall time when only part of the texture changes.
Practical verification
To confirm the streaming approach is beneficial:
- Run the program above and note the per‑frame time printed (or log it). It should stay around 16 ms with low variance.
- Replace the streaming texture creation with
SDL_TEXTUREACCESS_STATICand, each frame, callSDL_UpdateTexturewith the same pixel data. Compare the average frame time; the static version will typically show higher CPU usage and more jitter. - Repeat the test on at least two OSes (e.g., Windows 10 and Ubuntu 22.04) using SDL 2.0.22 or later to ensure consistent behavior.
If you observe frame‑time spikes, try locking only the changed region or reducing the texture size.
Actionable closing
For any high‑frequency image source—video decode output, camera frames, or procedural effects—create the texture once with SDL_TEXTUREACCESS_STREAMING, lock/write/unlock each frame, and render. Monitor lock time with SDL_GetPerformanceCounter and keep the locked region as small as practical to avoid GPU stalls. This pattern gives you the lowest‑overhead path for uploading dynamic pixel data in SDL2.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.