Streaming Textures in SDL2: Updating Pixels Every Frame Without Recreating the Texture
SDL2 streaming textures let you update dynamic 2D pixel data every frame without recreating textures. Learn SDL_UpdateTexture vs SDL_LockTexture, partial updates, and how to verify the win.
25 Jun 2026, 18:42 UTC

If your SDL2 app generates its own pixels each frame — a software-rendered framebuffer, a video decoder, an oscilloscope trace, a tile map with animated cells — the naive approach is to create a new texture, upload your buffer, render, and destroy it. That works, but it forces the driver to allocate and free GPU resources constantly, and it tends to show up as jitter in your frame times. SDL's streaming texture access exists precisely for this case: create the texture once, then push only the pixels that changed.
The thesis is simple: SDL_TEXTUREACCESS_STREAMING plus SDL_UpdateTexture (or SDL_LockTexture) turns per-frame texture churn into a bounded memory upload, which is cheaper and more predictable.
What "streaming" actually means
When you create a texture with SDL_CreateTexture, the third argument is the access mode. SDL_TEXTUREACCESS_STATIC means "set it once, draw it many times" — SDL is free to put it in memory that's fast for the GPU to read but expensive to write. SDL_TEXTUREACCESS_STREAMING tells the driver the pixel contents will change frequently, so it should keep the texture in memory that's cheap to update from the CPU, even if it's marginally slower to sample.
There are two ways to get new pixels in:
SDL_UpdateTexture(texture, &rect, pixels, pitch)— you keep a client-side buffer and ask SDL to copy a rectangular region of it into the texture. Therectcan beNULLfor the whole texture, or a small box covering only what changed.SDL_LockTexture/SDL_UnlockTexture— SDL hands you a pointer to a mapped buffer, you write pixels directly into it, and unlocking triggers the upload. This avoids one copy when you don't need to keep your own copy of the pixels.
Both require the texture to have been created with the streaming flag; calling them on a static texture is an error.
A worked example: a scrolling plasma buffer
The following sketch (SDL2, C, run on any desktop platform) maintains an ARGB8888 buffer on the CPU and uploads it each frame. Compile with your usual SDL2 flags, e.g. cc plasma.c $(sdl2-config --cflags --libs).
SDL_Texture *tex = SDL_CreateTexture(renderer,
SDL_PIXELFORMAT_ARGB8888,
SDL_TEXTUREACCESS_STREAMING,
640, 480);
if (!tex) { /* check SDL_GetError() */ }
uint32_t pixels[640 * 480];
/* per frame: */
for (int y = 0; y < 480; y++)
for (int x = 0; x < 640; x++)
pixels[y * 640 + x] = plasma(x, y, tick); /* your generator */
/* full-frame upload: rect = NULL, pitch in BYTES */
if (SDL_UpdateTexture(tex, NULL, pixels, 640 * sizeof(uint32_t)) != 0) {
/* log SDL_GetError(); a failure here usually means a pitch
or format mismatch */
}
SDL_RenderClear(renderer);
SDL_RenderCopy(renderer, tex, NULL, NULL);
SDL_RenderPresent(renderer);Two details trip people up. First, pitch is bytes per row, not pixels — for tightly packed ARGB8888 that's width * 4. Second, the buffer you pass must actually be in the texture's pixel format. If your generator produces RGB24 and the texture is ARGB8888, SDL will not silently fix that for SDL_UpdateTexture; you must convert yourself (or use SDL_ConvertPixels) before uploading.
Partial updates are where the win is
Uploading a full 640×480×4 frame is about 1.2 MB — fine. But if only a 64×64 tile changed (say, a blinking cursor in a terminal emulator), you can upload just that:
SDL_Rect dirty = { cursor_x, cursor_y, 64, 64 };
SDL_UpdateTexture(tex, &dirty,
pixels + dirty.y * 640 + dirty.x, /* offset to region start */
640 * sizeof(uint32_t)); /* pitch of the FULL buffer */Note the pointer arithmetic: the source pointer must point at the first pixel of the dirty region, while the pitch still describes the full row width of your buffer. Keeping a dirty-rectangle list and coalescing it into one or two update calls per frame is the standard pattern for emulators and software UIs.
Lock or Update?
SDL_UpdateTexture is the better default when you already own a pixel buffer — video decoders and software rasterizers usually do. SDL_LockTexture shines when you don't: you write straight into SDL's mapped memory and skip one full-frame copy. The trade-off is that the locked pointer's layout is not guaranteed to be contiguous the way you expect — you must respect the pitch it returns, and you can't hold the lock across a render call. If your drawing code is row-oriented (it usually is), either path is fine; pick Update for simplicity and Lock when profiling shows the extra copy matters.
Limitations and how to verify
Streaming textures are not free. A full-frame upload every frame at 1080p is ~8 MB per frame, and at 60 fps that's roughly half a gigabyte per second across the bus — real, but not unlimited. If you update more than you draw, or upload regions larger than what actually changed, you'll saturate the upload path and the "optimization" becomes the bottleneck. There is also no guarantee of asynchronous upload on every backend; on some drivers the copy can stall the pipeline.
To confirm it's actually helping, measure rather than assume:
- Wrap the frame in
SDL_GetPerformanceCounter()/SDL_GetPerformanceFrequency()and log the update-call duration separately from total frame time. - Compare against a variant that destroys and recreates a static texture each frame — the streaming version should show lower and steadier update cost.
- For deeper confirmation, capture a frame in a graphics debugger such as RenderDoc and check that partial updates produce small texture uploads rather than full-texture writes.
Behavior described here is stable across SDL2's lifetime; if you're on SDL3, the API was renamed (SDL_CreateTexture still exists but signatures and constants differ), so check the migration guide before porting this snippet.
Takeaway
Create the texture once with SDL_TEXTUREACCESS_STREAMING, keep your pixel format consistent end to end, and upload the smallest rectangle that covers what changed. Reach for SDL_LockTexture only when the extra copy shows up in a profile. That combination turns per-frame texture management from an allocation problem into a bounded memcpy — which is exactly what a software-rendered frame needs.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.