How to draw pixels in SDL 2.0?
Categories:
Mastering Pixel Drawing in SDL 2.0 for C/C++ Applications

Learn the fundamental techniques for drawing individual pixels and manipulating surfaces directly in SDL 2.0, essential for custom rendering and graphics effects.
Drawing individual pixels is a foundational skill in computer graphics, especially when working with low-level libraries like SDL 2.0. While SDL provides higher-level primitives for drawing lines, rectangles, and textures, understanding how to manipulate individual pixels directly opens up possibilities for custom rendering algorithms, procedural generation, and unique visual effects. This article will guide you through the process of setting up an SDL window and renderer, accessing pixel data, and drawing pixels efficiently in C and C++.
Understanding SDL 2.0 Rendering Basics
Before diving into pixel manipulation, it's crucial to grasp SDL 2.0's rendering model. SDL 2.0 primarily uses a hardware-accelerated renderer, which is generally faster than direct software rendering. However, drawing individual pixels often requires direct access to a texture's pixel data, which can then be rendered by the hardware accelerator. The typical workflow involves creating a texture with SDL_TEXTUREACCESS_STREAMING
or SDL_TEXTUREACCESS_STATIC
, locking it to access its pixel buffer, modifying the pixels, unlocking it, and finally copying it to the renderer.
flowchart TD A[Initialize SDL] --> B[Create Window] B --> C[Create Renderer] C --> D[Create Streaming Texture] D --> E{Game Loop} E --> F[Lock Texture for Pixel Access] F --> G[Modify Pixel Data] G --> H[Unlock Texture] H --> I[Clear Renderer] I --> J[Copy Texture to Renderer] J --> K[Present Renderer] K --> E E --> L{Quit?} L -->|Yes| M[Destroy Resources] M --> N[Quit SDL]
SDL 2.0 Pixel Drawing Workflow
Setting Up the SDL Environment
To begin, you need to initialize SDL, create a window, and then create a renderer. The renderer is responsible for drawing to the window. For pixel manipulation, we'll also need an SDL_Texture
that we can directly write to. This texture will be created with SDL_TEXTUREACCESS_STREAMING
to allow frequent updates.
#include <SDL.h>
#include <iostream>
// Screen dimensions
const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;
// Global variables for SDL window, renderer, and texture
SDL_Window* gWindow = nullptr;
SDL_Renderer* gRenderer = nullptr;
SDL_Texture* gTexture = nullptr;
bool init()
{
// Initialize SDL
if (SDL_Init(SDL_INIT_VIDEO) < 0)
{
std::cerr << "SDL could not initialize! SDL_Error: " << SDL_GetError() << std::endl;
return false;
}
// Create window
gWindow = SDL_CreateWindow("SDL Pixel Drawing", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN);
if (gWindow == nullptr)
{
std::cerr << "Window could not be created! SDL_Error: " << SDL_GetError() << std::endl;
return false;
}
// Create renderer for window
gRenderer = SDL_CreateRenderer(gWindow, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if (gRenderer == nullptr)
{
std::cerr << "Renderer could not be created! SDL_Error: " << SDL_GetError() << std::endl;
return false;
}
// Initialize renderer color
SDL_SetRenderDrawColor(gRenderer, 0xFF, 0xFF, 0xFF, 0xFF);
// Create streaming texture for pixel manipulation
gTexture = SDL_CreateTexture(gRenderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, SCREEN_WIDTH, SCREEN_HEIGHT);
if (gTexture == nullptr)
{
std::cerr << "Texture could not be created! SDL_Error: " << SDL_GetError() << std::endl;
return false;
}
return true;
}
void close()
{
// Destroy texture
SDL_DestroyTexture(gTexture);
gTexture = nullptr;
// Destroy renderer
SDL_DestroyRenderer(gRenderer);
gRenderer = nullptr;
// Destroy window
SDL_DestroyWindow(gWindow);
gWindow = nullptr;
// Quit SDL subsystems
SDL_Quit();
}
int main(int argc, char* args[])
{
if (!init())
{
std::cerr << "Failed to initialize!" << std::endl;
}
else
{
// Main loop flag
bool quit = false;
// Event handler
SDL_Event e;
// While application is running
while (!quit)
{
// Handle events on queue
while (SDL_PollEvent(&e) != 0)
{
// User requests quit
if (e.type == SDL_QUIT)
{
quit = true;
}
}
// Clear screen
SDL_SetRenderDrawColor(gRenderer, 0x00, 0x00, 0x00, 0xFF); // Black background
SDL_RenderClear(gRenderer);
// Update screen
SDL_RenderPresent(gRenderer);
}
}
close();
return 0;
}
SDL_TEXTUREACCESS_STREAMING
is crucial for textures that will be frequently updated with new pixel data. For textures that are loaded once and rarely change, SDL_TEXTUREACCESS_STATIC
is more appropriate. SDL_PIXELFORMAT_ARGB8888
is a common pixel format representing 32-bit ARGB (Alpha, Red, Green, Blue) data.Drawing Individual Pixels
To draw pixels, you need to lock the texture to gain direct access to its pixel buffer. SDL_LockTexture
provides a pointer to the pixel data and the pitch (number of bytes in a row). You can then iterate through this buffer and set individual pixel colors. Remember to unlock the texture with SDL_UnlockTexture
after you're done modifying it, so SDL can use it for rendering.
// Function to put a pixel on the texture
void putPixel(int x, int y, Uint32 color)
{
if (gTexture == nullptr) return;
void* pixels;
int pitch;
// Lock texture for direct pixel access
if (SDL_LockTexture(gTexture, nullptr, &pixels, &pitch) != 0)
{
std::cerr << "Unable to lock texture! SDL_Error: " << SDL_GetError() << std::endl;
return;
}
// Calculate the pixel's memory address
// pitch is in bytes, so divide by sizeof(Uint32) to get pitch in pixels
Uint32* pixelBuffer = static_cast<Uint32*>(pixels);
pixelBuffer[y * (pitch / sizeof(Uint32)) + x] = color;
// Unlock texture
SDL_UnlockTexture(gTexture);
}
// Example usage in main loop (replace the existing main loop content):
// ... inside the while(!quit) loop ...
// Clear screen
SDL_SetRenderDrawColor(gRenderer, 0x00, 0x00, 0x00, 0xFF); // Black background
SDL_RenderClear(gRenderer);
// Draw a red pixel at (100, 100)
putPixel(100, 100, 0xFFFF0000); // ARGB: Alpha=FF (opaque), Red=FF, Green=00, Blue=00
// Draw a blue pixel at (150, 150)
putPixel(150, 150, 0xFF0000FF);
// Draw a green line using putPixel
for (int i = 0; i < 50; ++i)
{
putPixel(200 + i, 200, 0xFF00FF00);
}
// Copy texture to renderer
SDL_RenderCopy(gRenderer, gTexture, nullptr, nullptr);
// Update screen
SDL_RenderPresent(gRenderer);
// ... rest of main function ...
Optimized Pixel Drawing
For drawing many pixels, you should lock the texture once, get a pointer to the pixel buffer, and then directly manipulate the pixels. The pitch
value is crucial here; it tells you how many bytes are in one row of the texture. You'll typically cast the void* pixels
pointer to a Uint32*
(for ARGB8888
format) and then access it like a 2D array, taking pitch
into account.
// Optimized function to draw multiple pixels
void drawPixelsOptimized()
{
if (gTexture == nullptr) return;
void* pixels;
int pitch;
// Lock texture once for all pixel operations
if (SDL_LockTexture(gTexture, nullptr, &pixels, &pitch) != 0)
{
std::cerr << "Unable to lock texture! SDL_Error: " << SDL_GetError() << std::endl;
return;
}
Uint32* pixelBuffer = static_cast<Uint32*>(pixels);
int pixelPitch = pitch / sizeof(Uint32);
// Clear the texture to black (optional, if you want to redraw everything)
for (int y = 0; y < SCREEN_HEIGHT; ++y)
{
for (int x = 0; x < SCREEN_WIDTH; ++x)
{
pixelBuffer[y * pixelPitch + x] = 0xFF000000; // Black
}
}
// Draw a diagonal line (red)
for (int i = 0; i < std::min(SCREEN_WIDTH, SCREEN_HEIGHT); ++i)
{
pixelBuffer[i * pixelPitch + i] = 0xFFFF0000; // Red
}
// Draw a checkerboard pattern (blue and green)
for (int y = 0; y < SCREEN_HEIGHT; ++y)
{
for (int x = 0; x < SCREEN_WIDTH; ++x)
{
if ((x / 10 + y / 10) % 2 == 0)
{
pixelBuffer[y * pixelPitch + x] = 0xFF0000FF; // Blue
}
else
{
pixelBuffer[y * pixelPitch + x] = 0xFF00FF00; // Green
}
}
}
// Unlock texture after all modifications
SDL_UnlockTexture(gTexture);
}
// Example usage in main loop (replace the existing putPixel calls):
// ... inside the while(!quit) loop ...
// Clear screen
SDL_SetRenderDrawColor(gRenderer, 0x00, 0x00, 0x00, 0xFF); // Black background
SDL_RenderClear(gRenderer);
// Call the optimized drawing function
drawPixelsOptimized();
// Copy texture to renderer
SDL_RenderCopy(gRenderer, gTexture, nullptr, nullptr);
// Update screen
SDL_RenderPresent(gRenderer);
// ... rest of main function ...
SDL_PIXELFORMAT_ARGB8888
format means the bytes are ordered Alpha, Red, Green, Blue. When you create a Uint32
color, it should be in the format 0xAARRGGBB
. For example, 0xFFFF0000
is opaque red, 0xFF00FF00
is opaque green, and 0xFF0000FF
is opaque blue.Full Example: Drawing a Simple Gradient
Let's put it all together to draw a simple horizontal gradient across the screen. This demonstrates how to iterate through the pixel buffer and calculate colors dynamically.
#include <SDL.h>
#include <iostream>
#include <algorithm> // For std::min
// Screen dimensions
const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;
// Global variables for SDL window, renderer, and texture
SDL_Window* gWindow = nullptr;
SDL_Renderer* gRenderer = nullptr;
SDL_Texture* gTexture = nullptr;
bool init()
{
if (SDL_Init(SDL_INIT_VIDEO) < 0)
{
std::cerr << "SDL could not initialize! SDL_Error: " << SDL_GetError() << std::endl;
return false;
}
gWindow = SDL_CreateWindow("SDL Gradient Drawing", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN);
if (gWindow == nullptr)
{
std::cerr << "Window could not be created! SDL_Error: " << SDL_GetError() << std::endl;
return false;
}
gRenderer = SDL_CreateRenderer(gWindow, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if (gRenderer == nullptr)
{
std::cerr << "Renderer could not be created! SDL_Error: " << SDL_GetError() << std::endl;
return false;
}
SDL_SetRenderDrawColor(gRenderer, 0xFF, 0xFF, 0xFF, 0xFF);
gTexture = SDL_CreateTexture(gRenderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, SCREEN_WIDTH, SCREEN_HEIGHT);
if (gTexture == nullptr)
{
std::cerr << "Texture could not be created! SDL_Error: " << SDL_GetError() << std::endl;
return false;
}
return true;
}
void close()
{
SDL_DestroyTexture(gTexture);
gTexture = nullptr;
SDL_DestroyRenderer(gRenderer);
gRenderer = nullptr;
SDL_DestroyWindow(gWindow);
gWindow = nullptr;
SDL_Quit();
}
void drawGradient()
{
if (gTexture == nullptr) return;
void* pixels;
int pitch;
if (SDL_LockTexture(gTexture, nullptr, &pixels, &pitch) != 0)
{
std::cerr << "Unable to lock texture! SDL_Error: " << SDL_GetError() << std::endl;
return;
}
Uint32* pixelBuffer = static_cast<Uint32*>(pixels);
int pixelPitch = pitch / sizeof(Uint32);
for (int y = 0; y < SCREEN_HEIGHT; ++y)
{
for (int x = 0; x < SCREEN_WIDTH; ++x)
{
// Calculate color based on x position (red gradient)
Uint8 red = (Uint8)(255.0f * x / SCREEN_WIDTH);
Uint32 color = (0xFF << 24) | (red << 16) | (0x00 << 8) | 0x00; // ARGB
pixelBuffer[y * pixelPitch + x] = color;
}
}
SDL_UnlockTexture(gTexture);
}
int main(int argc, char* args[])
{
if (!init())
{
std::cerr << "Failed to initialize!" << std::endl;
}
else
{
bool quit = false;
SDL_Event e;
// Draw the gradient once
drawGradient();
while (!quit)
{
while (SDL_PollEvent(&e) != 0)
{
if (e.type == SDL_QUIT)
{
quit = true;
}
}
SDL_SetRenderDrawColor(gRenderer, 0x00, 0x00, 0x00, 0xFF);
SDL_RenderClear(gRenderer);
// Copy the texture to the renderer
SDL_RenderCopy(gRenderer, gTexture, nullptr, nullptr);
SDL_RenderPresent(gRenderer);
}
}
close();
return 0;
}
std::vector<Uint32>
) to store your pixel data. You can then copy this buffer to the locked texture's pixel memory using memcpy
for even faster updates, especially if your pixel data is generated in a contiguous block.