thecodingidiot.com

The InfiniteThe Loop

The Loop

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:

  1. Fill the pixel buffer (pixels[])
  2. Upload it to a streaming texture (SDL_UpdateTexture)
  3. Copy the texture to the renderer (SDL_RenderCopy)
  4. 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);
 
#endif

pixels[] 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-right

Inside 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 && ./infinite

A 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);
 
#endif

Create a matching view.c stub with empty function bodies so the linker is satisfied. The full implementation is on the next page.