Software Rasterization: Setup
This is the first blog post of the Project 2D Software Rasterization. In this step, we will set up our project and display a color instead of black.
Requirements:
- CPP20 (I chose C++20 because it's more common nowadays)
- CMake (who uses Make in 2025?)
- SDL2 (It's more common than SDL3, and the internet is full of resources in case you get stuck)
- VCPkg (I am on Windows, so I need a package manager
- I have installed SDL2 already using
vcpkg install sdl2
- I have installed SDL2 already using
Step 1: Setting Up the Project
Before writing any code, we need to organize our project and set up CMake. A clean project structure makes development easier as the project grows.
I will go with this project structure:
├── CMakeLists.txt ├── include/ │ └── (header files go here) ├── src/ │ └── main.cpp │ └── renderer.cpp │ └── (other files) └── build/ (CMake will generate build files here)
Now let's setup cmake. I will go with this simple cmake script
cmake_minimum_required(VERSION 3.29)
project(rasterizer)
# Set C++ standard
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Include directories
include_directories(${CMAKE_SOURCE_DIR}/include)
# Collect all cpp files in src/
file(GLOB_RECURSE SOURCES ${CMAKE_SOURCE_DIR}/src/*.cpp)
# Create executable from all cpp files
add_executable(rasterizer ${SOURCES})
# Find SDL2
find_package(SDL2 REQUIRED)
# Linking to SDL2
target_include_directories(rasterizer PRIVATE ${SDL2_INCLUDE_DIRS})
target_link_libraries(rasterizer PRIVATE ${SDL2_LIBRARIES})
Step 2: Getting a Window
Let's write inside our main
function
constexpr int width = 800, height = 600;
SDL_Window* window = SDL_CreateWindow(
"Software Rasterizer",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
width,
height,
SDL_WINDOW_RESIZABLE | SDL_WINDOW_SHOWN
);
if (!window) {
std::cerr << "SDL_CreateWindow Error: " << SDL_GetError() << std::endl;
SDL_Quit();
return 1;
}
We are just creating a window but our program still does nothing. To keep it alive, we need an event loop so it won’t quit immediately.
bool running = true;
while (running) {
SDL_Event event;
while (SDL_PollEvent(&event)) {
switch (event.type) {
case SDL_QUIT:
running = false;
break;
default:
break;
}
}
}
Step 3: Displaying Something
In older SDL tutorials you would see SDL_GetWindowSurface
. That was fine, but surfaces do not support real alpha blending on the window. The better way in 2025 is to use SDL_Renderer
and SDL_Texture
. They let us write our own pixels into a framebuffer and then render them to the window with full RGBA support.
SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
SDL_Texture* texture = SDL_CreateTexture(renderer,
SDL_PIXELFORMAT_RGBA32,
SDL_TEXTUREACCESS_STREAMING,
width, height);
while (running) {
SDL_Event event;
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) running = false;
}
// Lock texture to get direct access to pixels
uint32_t* pixels;
int pitch;
SDL_LockTexture(texture, NULL, reinterpret_cast<void**>(&pixels), &pitch);
pitch /= 4;
// Clear to white
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
pixels[y * pitch + x] = 0xFFFFFFFF;
}
}
// Draw a semi transparent reddish box
for (int y = 0; y < height / 2; ++y) {
for (int x = 0; x < width / 2; ++x) {
uint8_t r = 189, g = 55, b = 15, a = 128;
uint8_t rb = (r * a + 255 * (255 - a)) / 255;
uint8_t gb = (g * a + 255 * (255 - a)) / 255;
uint8_t bb = (b * a + 255 * (255 - a)) / 255;
pixels[y * pitch + x] = (0xFF << 24) | (rb << 16) | (gb << 8) | bb;
}
}
SDL_UnlockTexture(texture);
// Render the texture to the window
SDL_RenderClear(renderer);
SDL_RenderCopy(renderer, texture, NULL, NULL);
SDL_RenderPresent(renderer);
}
Now we are drawing directly into the texture, and SDL takes care of blending it properly when rendered. Notice the alpha value of 128 which makes the box semi transparent on top of the white background.
Step 4: Implement clear function
We can extract the white filling into a helper function. This makes it cleaner and prepares us for more complex rendering later.
void clear_pixels(uint32_t* pixels, int width, int height, uint32_t color) {
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
pixels[y * width + x] = color;
}
}
}
Now in the event loop we just call clear_pixels
instead of writing the white fill manually.
Step 5: Resizing the Window
At this point if you resize the window, your box will not redraw correctly because the texture does not match the new size. We can fix this by catching the resize event and recreating the texture with the new width and height.
else if (event.type == SDL_WINDOWEVENT &&
event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
width = event.window.data1;
height = event.window.data2;
SDL_DestroyTexture(texture);
texture = SDL_CreateTexture(renderer,
SDL_PIXELFORMAT_RGBA32,
SDL_TEXTUREACCESS_STREAMING,
width, height);
}
Now every time the window size changes, we destroy the old texture and create a new one that matches. This way our program keeps drawing correctly regardless of how the user resizes the window.
This resizing step is critical in graphics programming. Without it, your frame-buffer is stuck at the old resolution and things will stretch or break when the window changes size.