Sol's Graphics Tutorial

02 - SDL Skeleton and Putting Pixels

Make copy of the development environment we set up in previous chapter. In Windows, the easiest way to do this is to close Visual Studio and use Windows Explorer to make a copy of the gptut directory, and rename it to ch02. Start Visual Studio by opening the gptut.sln in the new directory.

Replace the contents of the source file with the following:

#include <string.h>
#include <math.h>
#include <stdlib.h>
#ifdef __EMSCRIPTEN__
#include <emscripten/emscripten.h>
#endif
#include "SDL3/SDL.h"
#include "SDL3/SDL_main.h"

int* gFrameBuffer;
SDL_Window* gSDLWindow;
SDL_Renderer* gSDLRenderer;
SDL_Texture* gSDLTexture;
static int gDone;
const int WINDOW_WIDTH = 1920 / 2;
const int WINDOW_HEIGHT = 1080 / 2;

bool update()
{
  SDL_Event e;
  if (SDL_PollEvent(&e))
  {
    if (e.type == SDL_EVENT_QUIT)
    {
      return false;
    }
    if (e.type == SDL_EVENT_KEY_UP && e.key.key == SDLK_ESCAPE)
    {
      return false;
    }
  }

  char* pix;
  int pitch;
  
  SDL_LockTexture(gSDLTexture, NULL, (void**)&pix, &pitch);
  for (int i = 0, sp = 0, dp = 0; i < WINDOW_HEIGHT; i++, dp += WINDOW_WIDTH, sp += pitch)
    memcpy(pix + sp, gFrameBuffer + dp, WINDOW_WIDTH * 4);

  SDL_UnlockTexture(gSDLTexture);  
  SDL_RenderTexture(gSDLRenderer, gSDLTexture, NULL, NULL);
  SDL_RenderPresent(gSDLRenderer);
  SDL_Delay(1);
  return true;
}

void render(Uint64 aTicks)
{
  for (int i = 0, c = 0; i < WINDOW_HEIGHT; i++)
  {
    for (int j = 0; j < WINDOW_WIDTH; j++, c++)
    {
      gFrameBuffer[c] = (int)(i * i + j * j + aTicks) | 0xff000000;
    }
  }
}

void loop()
{
  if (!update())
  {
    gDone = 1;
#ifdef __EMSCRIPTEN__
    emscripten_cancel_main_loop();
#endif
  }
  else
  {
    render(SDL_GetTicks());
  }
}

int main(int argc, char** argv)
{
  if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS))
  {
    return -1;
  }

  gFrameBuffer = new int[WINDOW_WIDTH * WINDOW_HEIGHT];
  gSDLWindow = SDL_CreateWindow("SDL3 window", WINDOW_WIDTH, WINDOW_HEIGHT, 0);
  gSDLRenderer = SDL_CreateRenderer(gSDLWindow, NULL);
  gSDLTexture = SDL_CreateTexture(gSDLRenderer, SDL_PIXELFORMAT_ABGR8888, SDL_TEXTUREACCESS_STREAMING, WINDOW_WIDTH, WINDOW_HEIGHT);

  if (!gFrameBuffer || !gSDLWindow || !gSDLRenderer || !gSDLTexture)
    return -1;

  gDone = 0;
#ifdef __EMSCRIPTEN__
  emscripten_set_main_loop(loop, 0, 1);
#else
  while (!gDone)
  {
    loop();
  }
#endif

  SDL_DestroyTexture(gSDLTexture);
  SDL_DestroyRenderer(gSDLRenderer);
  SDL_DestroyWindow(gSDLWindow);
  SDL_Quit();

  return 0;
}

There's quite bit to go through here, but the code should compile and you should see something like this:

SDL3 window
SDL3 window

Let's go through the code from start to end. First we have some include directives:

#include <string.h>
#include <math.h>
#include <stdlib.h>
#ifdef __EMSCRIPTEN__
#include <emscripten/emscripten.h>
#endif
#include "SDL3/SDL.h"
#include "SDL3/SDL_main.h"

First we include some standard headers, for stuff like memcpy.

Next, inside the ifdef block we include Emscripten's header in case we want to compile our code as a web page. This will only be done if you're doing an Emscripten build, which you likely aren't doing right now, so you can just ignore this.

Next up is the include for the main SDL header. That will let us use various SDL_ calls.

Finally there's SDL_main.h. If you're familiar with older versions of SDL, this is a new thing: instead of using sdlmain.lib, the main-function trickery is now all in a header file, simplifying things somewhat. This will take care of all those console vs windowed application differences, and will let us use normal-looking C main functions. Generally speaking, don't worry about it, it just works(tm).

Next we define a bunch of global variables:

int* gFrameBuffer;
SDL_Window* gSDLWindow;
SDL_Renderer* gSDLRenderer;
SDL_Texture* gSDLTexture;
static int gDone;
const int WINDOW_WIDTH = 1920 / 2;
const int WINDOW_HEIGHT = 1080 / 2;
  • gFrameBuffer will be our framebuffer. Each integer in the array will represent one pixel on the screen.
  • gSDLWindow, gSDLRenderer and gSDLTexture represent SDL's window, renderer and texture that we'll need to get stuff on screen.
  • gDone is a flag that represents whether we want to terminate the application.
  • WINDOW_WIDTH and WINDOW_HEIGHT set the size of the window. I'm defaulting to one quarter of HD resolution; feel free to play with the values.

Next we have the update function, which does all of the system related things in one go:

bool update()
{
  SDL_Event e;
  if (SDL_PollEvent(&e))
  {
    if (e.type == SDL_EVENT_QUIT)
    {
      return false;
    }
    if (e.type == SDL_EVENT_KEY_UP && e.key.key == SDLK_ESCAPE)
    {
      return false;
    }
  }

  char* pix;
  int pitch;
  
  SDL_LockTexture(gSDLTexture, NULL, (void**)&pix, &pitch);
  for (int i = 0, sp = 0, dp = 0; i < WINDOW_HEIGHT; i++, dp += WINDOW_WIDTH, sp += pitch)
    memcpy(pix + sp, gFrameBuffer + dp, WINDOW_WIDTH * 4);

  SDL_UnlockTexture(gSDLTexture);  
  SDL_RenderTexture(gSDLRenderer, gSDLTexture, NULL, NULL);
  SDL_RenderPresent(gSDLRenderer);
  SDL_Delay(1);
  return true;
}

The function starts by polling system events. If the event is SDL_EVENT_QUIT or user has pressed (and released) ESC, the update function returns false, meaning it's time to quit.

The rest of the function is related to updating the graphics in the window. First, we lock the texture for write access, getting pointer to the texture data as well as pitch. The width of the texture and pitch may differ. For example, if you have a 800 pixel wide texture, it's possible the system has actually allocated a 1024 pixel wide one, wasting a bunch of memory, in exchange of faster operations. Adding pitch always moves to the start of the next line. For our framebuffer, pitch is equal to width.

Once the texture is locked, we use memcpy() to copy our framebuffer into the texure.

Then it's time to unlock the texture (which will make SDL copy the data to video memory), ask SDL to render the texture to screen, and finally we ask SDL to present the result of the render, which will make our image show on the screen. Since the texture update is a write-only operation, and we might not update the whole display every frame, we need to have our separate framebuffer array.

We ask the program to sleep a bit after preset. The small delay doesn't affect our framerate (unless we're aiming for something ridiculously high), but it lets the CPU take a breather. Laptop users will thank you for doing this.

Lastly the function returns true, meaning that the application isn't quiting just yet.

Then we get to what we're really interested in:

void render(Uint64 aTicks)
{
  for (int i = 0, c = 0; i < WINDOW_HEIGHT; i++)
  {
    for (int j = 0; j < WINDOW_WIDTH; j++, c++)
    {
      gFrameBuffer[c] = (int)(i * i + j * j + aTicks) | 0xff000000;
    }
  }
}

The render function is what the tutorial will mostly be populating. The example code loops through every pixel in our framebuffer, and stores the pattern i * i + j * j + aTicks into the pixels. The i and j represent y and x coordinates of the pixels, and aTicks is current time value in milliseconds, causing the result to animate.

The downcast to (int) is required since aTicks is a 64 bit variable.

The final | 0xff000000 forces the high 8 bits to 1:s, so the pixels are always solid. Opaque. Visible.

Next we have the main loop:

void loop()
{
  if (!update())
  {
    gDone = 1;
#ifdef __EMSCRIPTEN__
    emscripten_cancel_main_loop();
#endif
  }
  else
  {
    render(SDL_GetTicks());
  }
}

The main loop function is called repeatedly from main() (or from Emscripten). This is a bit convoluted, but since it enables building for the web, we'll just have to accept it.

The function calls update(), setting done to 1 if we're done, as well as signaling Emscripten that we're done. If we're not, in fact, done, it calls render(), passing the time counter as a parameter (via SDL_GetTicks()).

And finally we have the main() function that largely sets things up (and tears it down) for us:

int main(int argc, char** argv)
{
  if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS))
  {
    return -1;
  }

  gFrameBuffer = new int[WINDOW_WIDTH * WINDOW_HEIGHT];
  gSDLWindow = SDL_CreateWindow("SDL3 window", WINDOW_WIDTH, WINDOW_HEIGHT, 0);
  gSDLRenderer = SDL_CreateRenderer(gSDLWindow, NULL);
  gSDLTexture = SDL_CreateTexture(gSDLRenderer, SDL_PIXELFORMAT_ABGR8888, SDL_TEXTUREACCESS_STREAMING, 
                                  WINDOW_WIDTH, WINDOW_HEIGHT);

  if (!gFrameBuffer || !gSDLWindow || !gSDLRenderer || !gSDLTexture)
    return -1;

  gDone = 0;
#ifdef __EMSCRIPTEN__
  emscripten_set_main_loop(loop, 0, 1);
#else
  while (!gDone)
  {
    loop();
  }
#endif

  SDL_DestroyTexture(gSDLTexture);
  SDL_DestroyRenderer(gSDLRenderer);
  SDL_DestroyWindow(gSDLWindow);
  SDL_Quit();

  return 0;
}

First up, SDL_Init() is called. If it fails, we quit. I've never seen the function fail, but who knows!

Next, we allocate our framebuffer and set up window, renderer and texture. If any of these fail, we quit. Could we do more error checking? Yes, yes we could.

Next block is the main loop, which is, again, convoluted by Emscripten. In case of Emscripten, we tell Emscripten where the main loop function is. Otherwise we loop until gDone is something else than zero.

Finally we tear down gSDLTexture, gSDLRenderer, gSDLWindow, and finally SDL itself.

Putting Pixels

Phew! Now that that the skeleton has been dealt with, let's talk pixels. The pixel format we've chosen is a very common one, SDL_PIXELFORMAT_ABGR8888. The ABGR8888 means there's 32 bits, split into 8 bits of alpha, blue, green and red. Value of 0 means dark and 255 means bright. Alpha is typically transparency, but we won't care about it, and we should always set it as 255 (or 0xff).

So let's put some pixels. Add this function before the render() function:

void putpixel(int x, int y, int color)
{
  if (x < 0 ||
      y < 0 ||
      x >= WINDOW_WIDTH ||
      y >= WINDOW_HEIGHT)
  {
      return;
  }
  gFrameBuffer[y * WINDOW_WIDTH + x] = color;
}

This function first checks if the coordinates are within our window, returning without doing anything if they aren't, and finally changes the framebuffer array to change the target pixel. This makes our putpixel() function safe, but a very, very slow way of producing graphics.

To find the location in the array, the y coordinate is multiplied by the width of the framebuffer to get to the start of the horizontal line we're interested in, and then x is added to get to the pixel we want.

Add the following to the end of the render function:

for (int i = 0; i < 100; i++)
    for (int j = 0; j < 100; j++)
      putpixel(j + 20, i + 20, 0xffff0000);

If you build and run, you should see a blue 100 by 100 pixel block near the top left corner of the window. If you want the block to be green, try 0xff00ff00, or 0xff0000ff for red. Mix them up for various colors.

Additional things to try:

  • Try changing the i * i + j * j + ticks to something else and see what happens. Change a plus to a minus, add multipliers, remove multipliers.. or mix in some sin() and cos() functions. Go wild. As long as you're not dividing by zero, you should be ok, so there's little to be afraid of.
  • We're animating the background using "aTicks". Can you animate the blue dot? Note that if you move outside the screen, you won't see where the dot is, but since we're using a very safe putpixel(), your application shouldn't crash.

Next up: 03 - What Little Sprites Are Made Of..

Any comments etc. can be emailed to me.