Replace the black window with a pixel buffer, wire the render cycle, and confirm the buffer works by rendering a gradient. The event loop established here is the pattern every SDL2 chapter reuses.
The render cycle
The SDL2 render cycle has four steps every frame:
- Fill the pixel buffer (
pixels[]) - Upload it to a streaming texture (
SDL_UpdateTexture) - Copy the texture to the renderer (
SDL_RenderCopy) - Present the renderer (
SDL_RenderPresent)
The pixel buffer is the working surface. You write colours into it; SDL2 puts them on screen. The GPU handles the upload and display — you never touch it directly.
render.h
Add the render state struct and function prototypes to a new header:
#ifndef RENDER_H
#define RENDER_H
#include <SDL2/SDL.h>
#include "game.h"
#include "view.h"
struct s_state {
SDL_Window *win;
SDL_Renderer *ren;
SDL_Texture *tex;
uint32_t pixels[WIDTH * HEIGHT];
view_t view;
int fractal;
int colour_scheme;
double julia_re;
double julia_im;
};
void render_frame(state_t *s);
void render_init(state_t *s);
void render_free(state_t *s);
#endifpixels[] is the flat array. fractal tracks which fractal mode is
active (0 = Mandelbrot, 1 = Julia, 2 = Burning Ship). julia_re and
julia_im hold the current Julia c parameter set by mouse click.
render.c
#include "render.h"
#include "view.h"
void render_init(state_t *s)
{
s->tex = SDL_CreateTexture(s->ren,
SDL_PIXELFORMAT_ARGB8888,
SDL_TEXTUREACCESS_STREAMING,
WIDTH, HEIGHT);
if (!s->tex)
SDL_Log("SDL_CreateTexture: %s", SDL_GetError());
s->fractal = 0;
s->colour_scheme = 0;
s->julia_re = -0.7;
s->julia_im = 0.27;
}
void render_frame(state_t *s)
{
int x;
int y;
int i;
double re;
double im;
uint32_t colour;
y = 0;
while (y < HEIGHT) {
x = 0;
while (x < WIDTH) {
re = pixel_to_re(x, &s->view);
im = pixel_to_im(y, &s->view);
i = 0; /* placeholder — iteration added next page */
colour = (uint32_t)i << 16; /* placeholder: red channel */
s->pixels[y * WIDTH + x] = colour;
x++;
}
y++;
}
SDL_UpdateTexture(s->tex, NULL, s->pixels, WIDTH * sizeof(uint32_t));
SDL_RenderClear(s->ren);
SDL_RenderCopy(s->ren, s->tex, NULL, NULL);
SDL_RenderPresent(s->ren);
}
void render_free(state_t *s)
{
SDL_DestroyTexture(s->tex);
SDL_DestroyRenderer(s->ren);
SDL_DestroyWindow(s->win);
SDL_Quit();
}Three functions, one lifecycle: initialise the SDL2 resources, run one frame, tear everything down at exit.
State as a pointer. Every function takes state_t *s — a pointer
to the struct that lives in main, not a copy of it. The reason is
size: pixels[] holds 480,000 uint32_t values — just under 2 MB.
Passing the struct by value would copy that 2 MB onto the stack on
every call. The pointer passes four bytes (the address) and all three
functions share the same memory.
The arrow operator s->tex means (*s).tex: dereference the pointer,
then access the member. Every write through s-> modifies the
original struct in main. There is no copy.
render_init. SDL_CreateTexture takes the renderer plus four
arguments: the pixel format, the access mode, width, and height.
SDL_PIXELFORMAT_ARGB8888 means each pixel is four bytes — one for
Alpha (transparency), Red, Green, and Blue, packed in that order. The
format name is the memory layout, not the channel order on screen.
SDL_TEXTUREACCESS_STREAMING tells SDL2 this texture will be updated
every frame from CPU memory; the driver allocates accordingly.
The remaining assignments set defaults: Mandelbrot mode, linear
colour, and a Julia c of (-0.7, 0.27). That last value is not
arbitrary — it is a point inside the Mandelbrot set that produces a
visually interesting connected Julia set. At runtime, this value
changes to the complex coordinate of whichever point the user clicks
in the Mandelbrot view.
render_frame — the pixel loop. The two nested while loops visit
every pixel exactly once. y is the row, x is the column.
pixels[] is a flat one-dimensional array, but the screen is
two-dimensional. The index y * WIDTH + x converts a screen
coordinate into an array offset: row y begins at position
y * WIDTH; x selects the column within that row.
pixel (0, 0) → 0 * 800 + 0 = 0 ← top-left
pixel (5, 3) → 3 * 800 + 5 = 2405
pixel (799, 599) → 599 * 800 + 799 = 479999 ← bottom-rightInside the loop, pixel_to_re and pixel_to_im take &s->view —
the address of the view member inside the struct. s is already a
pointer to the struct; s->view is the member; &s->view is a
pointer to that member. pixel_to_re needs a pointer because it reads
the centre and scale values stored inside view_t.
The placeholder (uint32_t)i << 16 slots a value into the red
channel via bit shifting — skip the mechanics for now. i is zero
here and becomes the escape count on the next page; pixel packing and
the real colour functions are covered in full on the
Bits
page, where this placeholder is replaced entirely.
The upload calls. After the loop fills s->pixels, four SDL calls
push it to screen. SDL_UpdateTexture copies CPU memory to the GPU
texture. The pitch argument — WIDTH * sizeof(uint32_t) — is the
number of bytes per row; SDL uses it to stride through the buffer
correctly regardless of any internal alignment. SDL_RenderClear
wipes the renderer, SDL_RenderCopy draws the texture onto it filling
the window, SDL_RenderPresent swaps the buffer to the display.
render_free. SDL resources are destroyed in reverse order of
creation: texture first, then renderer, then window. SDL_Quit
releases the subsystems initialised by SDL_Init in main.
The event loop
Expand main.c to handle keyboard and mouse events, and call
render_frame each iteration:
#include "game.h"
#include "render.h"
#include "view.h"
int main(void)
{
state_t s;
view_t v;
SDL_Event ev;
int running;
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
SDL_Log("SDL_Init: %s", SDL_GetError());
return (1);
}
s.win = SDL_CreateWindow("infinite", SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED, WIDTH, HEIGHT, 0);
s.ren = SDL_CreateRenderer(s.win, -1, SDL_RENDERER_ACCELERATED);
view_init(&v);
s.view = v;
render_init(&s);
running = 1;
while (running) {
while (SDL_PollEvent(&ev)) {
if (ev.type == SDL_QUIT)
running = 0;
if (ev.type == SDL_KEYDOWN) {
if (ev.key.keysym.sym == SDLK_ESCAPE)
running = 0;
if (ev.key.keysym.sym == SDLK_UP)
view_pan(&s.view, 0, -0.1);
if (ev.key.keysym.sym == SDLK_DOWN)
view_pan(&s.view, 0, 0.1);
if (ev.key.keysym.sym == SDLK_LEFT)
view_pan(&s.view, -0.1, 0);
if (ev.key.keysym.sym == SDLK_RIGHT)
view_pan(&s.view, 0.1, 0);
if (ev.key.keysym.sym == SDLK_EQUALS)
view_zoom(&s.view, 0.9);
if (ev.key.keysym.sym == SDLK_MINUS)
view_zoom(&s.view, 1.1);
if (ev.key.keysym.sym == SDLK_m)
s.fractal = 0;
if (ev.key.keysym.sym == SDLK_j)
s.fractal = 1;
if (ev.key.keysym.sym == SDLK_b)
s.fractal = 2;
if (ev.key.keysym.sym == SDLK_c)
s.colour_scheme = !s.colour_scheme;
}
if (ev.type == SDL_MOUSEBUTTONDOWN
&& ev.button.button == SDL_BUTTON_LEFT
&& s.fractal != 1) {
s.julia_re = pixel_to_re(ev.button.x, &s.view);
s.julia_im = pixel_to_im(ev.button.y, &s.view);
s.fractal = 1;
}
}
render_frame(&s);
}
render_free(&s);
return (0);
}Notice the mouse handler condition: s.fractal != 1. Clicking in
Mandelbrot or Burning Ship mode picks a Julia c and switches to
Julia mode. Clicking in Julia mode does nothing — the mouse has already
done its job.
Gradient test
The render loop produces a black screen because i is always zero.
As a visual sanity check before adding the iteration function, write
x directly into the red channel:
colour = (uint32_t)(x * 255 / WIDTH) << 16;make re && ./infiniteA gradient from black (left) to red (right) fills the window. The pixel buffer, texture upload, and render cycle are all working. Remove the test line — the real colour computation arrives on the next page.
view.h stub
render_frame and main.c reference view_t, view_init,
pixel_to_re, and pixel_to_im. Create a stub so the project
compiles:
#ifndef VIEW_H
#define VIEW_H
#include "game.h"
struct s_view {
double center_re;
double center_im;
double scale;
};
void view_init(view_t *v);
void view_pan(view_t *v, double dre, double dim);
void view_zoom(view_t *v, double factor);
double pixel_to_re(int x, view_t *v);
double pixel_to_im(int y, view_t *v);
#endifCreate a matching view.c stub with empty function bodies so the
linker is satisfied. The full implementation is on the next page.