Presentation for Ottawa Linux Symposium

Ottawa Linux Symposium: http://www.ottawalinuxsymposium.com/

I’m going to have this finished by the end of the afternoon, but I
welcome any comments you have. :slight_smile:

The final version will be in HTML, with the tips in a sidebar.–
-Sam Lantinga, Lead Programmer, Loki Entertainment Software
-------------- next part --------------

“Linux your game with Simple DirectMedia Layer”

  1. Introduction:

    Sam Lantinga, Lead Programmer for Loki Entertainment Software.
    Loki Entertainment is a company devoted to porting top games
    to Linux. You can find out more at http://www.lokigames.com/

    My background:

    • Author of the Simple DirectMedia Layer (SDL)
    • Ported Maelstrom from the Macintosh to Linux
    • Worked on port of the Macintosh emulator Executor to Win32
    • Ported some of the DOOM tools from DOS (DEU, DHE, etc.)
    • Other projects: http://www.devolution.com/~slouken/
  2. Simple DirectMedia Layer

  3. What is it?

  4. What can it do?

    • Video

      • Set a video mode at any depth (8-bpp or greater) with optional
        conversion, if the video mode is not supported by the hardware.
      • Write directly to a linear graphics framebuffer.
      • Create surfaces with colorkey or alpha blending attributes.
      • Surface blits are automatically converted to the target format
        using optimized blitters and are hardware accelerated, when
        possible. MMX optimized blits are available for the x86.
      • Hardware accelerated blit and fill operations are used if
        supported by the hardware.
        Tip:
        You can set your application’s title-bar (if any) and icon using the
        SDL_WM_SetCaption() and SDL_WM_SetIcon() functions respectively.
    • Events

      • Events provided for:
        • Application visibility changes
        • Keyboard input
        • Mouse input
        • User-requested quit
      • Each event can be enabled or disabled with SDL_EventState().
      • Events are passed through a user-specified filter function
        before being posted to the internal event queue.
      • Thread-safe event queue.
        Tip:
        Use SDL_PeepEvents() to search for an event of a particular type in the
        event queue.
    • Audio

      • Set audio playback of 8-bit and 16-bit audio, mono or stereo,
        with optional conversion if the format is not supported by the
        hardware.
      • Audio runs independently in a separate thread, filled via a
        user callback mechanism.
      • Designed for custom software audio mixers, but the example
        archive contains a complete audio/music output library.
        Tip:
        Use the SDL_LockAudio() and SDL_UnlockAudio() functions to synchronize
        access data shared by the audio callback and the rest of the program.
    • CD-ROM audio

      • Complete CD audio control API
        Tip:
        If you pass a NULL CD-ROM handle to the CD-ROM API functions, they will act
        on the last CD-ROM that was opened.
    • Threads

      • Simple thread creation API
      • Simple binary semaphores for synchronization
        Tip:
        Do not use C library functions like I/O and memory management from threads
        if you can help it - they lock resources used by other threads.
    • Timers

      • Get the number of milliseconds elapsed
      • Wait a specified number of milliseconds
      • Set a single periodic timer with 10ms resolution
        Tip:
        You can easily replace the Win32 GetTickCount() with SDL_GetTicks()
    • Endian independence

      • Detect the endianness of the current system
      • Routines for fast swapping of data values
      • Read and write data of a specified endianness
        Tip:
        When reading your data files, you may need to byteswap 16-bit graphics
  5. What platforms does it run on?

    • Linux, of course

      • Uses X11 for video display, taking advantage of XFree86 DGA
        extensions and new MTRR acceleration for fullscreen display.
      • Uses the OSS API for sound.
      • Threads are implemented using either the clone() system call
        and SysV IPC, or glibc-2.1 pthreads.
        Tip:
        You can get at the hidden portions of the SDL driver interface via the
        SDL_GetWMInfo() function. This allows you to do things like remove window
        decorations and programmatically iconify your window.
    • Win32

      • Two versions, one safe for all systems based on Win32 APIs,
        and one with higher performance, based on DirectX APIs.
      • Safe version uses GDI for video display. High performance version
        uses DirectDraw for video display, taking advantage of hardware
        acceleration if available.
      • Safe version uses waveOut APIs for sound. High performace version
        uses DirectSound for audio playback.
        Tip:
        You must call the SDL event functions periodically from your main thread
        to pump the Windows message queue and keep your application responsive.
    • BeOS

      • BWindow is used for video display.
      • BSoundPlayer API is used for sound.
        Tip:
        Linux and BeOS support the SDL_INIT_EVENTTHREAD flag which, when passed
        to SDL_Init(), asks the event loop to run asynchronously in another thread.
        This is useful for color cursors that respond even when the application
        is busy.
    • Unofficial ports, ports in progress

      • Solaris, IRIX, FreeBSD
      • MacOS
  6. Using the Simple DirectMedia Layer API

  7. Initializing the library

    Use SDL_Init() to dynamically load and initialize the library.
    This function takes a set of flags corresponding to the portions
    you want to activate:
    SDL_INIT_AUDIO
    SDL_INIT_VIDEO
    SDL_INIT_CDROM
    SDL_INIT_TIMER

    Use SDL_Quit() to clean up the library when you are done with it.

Example:
#include <stdlib.h>
#include “SDL.h”

main(int argc, char *argv[])
{
if ( SDL_Init(SDL_INIT_AUDIO|SDL_INIT_VIDEO) < 0 ) {
fprintf(stderr, “Unable to init SDL: %s\n”, SDL_GetError());
exit(1);
}
atexit(SDL_Quit);

...

}

Tip:
SDL dynamically loads the SDL library from the standard system library
locations. Use the SDL_SetLibraryPath() function to use an alternate
location for the dynamic libraries distributed with your application.

  1. Video
 * Choosing and setting video modes  (the easy way)

   You just choose your favorite bit-depth and resolution, and set it!

Example:
{ SDL_Surface *screen;

screen = SDL_SetVideoMode(640, 480, 16, SDL_SWSURFACE);
if ( screen == NULL ) {
	fprintf(stderr, "Unable to set 640x480 video: %s\n", SDL_GetError());
	exit(1);
}

}

Tip #1:
You can find the fastest video depth supported by the hardware with the
function SDL_GetVideoInfo().

Tip #2:
You can get a list of supported video resulutions at a particular bit-depth
using the function SDL_ListModes().

 * Drawing pixels on the screen

   Drawing to the screen is done by writing directly to the graphics
   framebuffer, and calling the screen update function.

Example:
Drawing a pixel on a screen of arbitrary format

void DrawPixel(SDL_Surface *screen, Uint8 R, Uint8 G, Uint8 B)
{
Uint32 color = SDL_MapRGB(screen->format, R, G, B);

if ( SDL_MUSTLOCK(screen) ) {
	if ( SDL_LockSurface(screen) < 0 ) {
		return;
	}
}
switch (screen->format->BytesPerPixel) {
	case 1: { /* Assuming 8-bpp */
		Uint8 *bufp;

		bufp = (Uint8 *)screen->pixels + y*screen->pitch + x;
		*bufp = color;
	}
	break;

	case 2: { /* Probably 15-bpp or 16-bpp */
		Uint16 *bufp;

		bufp = (Uint16 *)screen->pixels + y*screen->pitch/2 + x;
		*bufp = color;
	}
	break;

	case 3: { /* Slow 24-bpp mode, usually not used */
		Uint8 *bufp;

		bufp = (Uint8 *)screen->pixels + y*screen->pitch + x;
		*(bufp+screen->format->Rshift/8) = R;
		*(bufp+screen->format->Gshift/8) = G;
		*(bufp+screen->format->Bshift/8) = B;
	}
	break;

	case 4: { /* Probably 32-bpp */
		Uint32 *bufp;

		bufp = (Uint32 *)screen->pixels + y*screen->pitch/4 + x;
		*bufp = color;
	}
	break;
}
if ( SDL_MUSTLOCK(screen) ) {
	SDL_UnlockSurface(screen);
}
SDL_UpdateRect(screen, x, y, 1, 1);

}

Tip:
If you know that you will be doing a lot of drawing, it is best to lock
the screen (if necessary) once before drawing, draw while keeping a list
of areas that need to be updated, and unlock the screen again before
updating the display.

 * Loading and displaying images

   SDL provides a single image loading routine for your convenience,
   SDL_LoadBMP().  A library for image loading can be found in the
   SDL demos archive.

   You can display images by using SDL_BlitSurface() to blit them
   into the graphics framebuffer.  SDL_BlitSurface() automatically
   clips the blit rectangle, which should be passed to SDL_UpdateRects()
   to update the portion of the screen which has changed.

Example:
void ShowBMP(char *file, SDL_Surface *screen, int x, int y)
{
SDL_Surface *image;
SDL_Rect dest;

/* Load the BMP file into a surface */
image = SDL_LoadBMP(file);
if ( image == NULL ) {
	fprintf(stderr, "Couldn't load %s: %s\n", file, SDL_GetError());
	return;
}

/* Blit onto the screen surface.
   The surfaces should not be locked at this point.
 */
dest.x = x;
dest.y = y;
dest.w = image->w;
dest.h = image->h;
SDL_BlitSurface(image, NULL, screen, &dest);

/* Update the changed portion of the screen */
SDL_UpdateRects(screen, 1, &dest);

}

Tip #1:
If you are loading an image to be displayed many times, you can improve
blitting speed by convert it to the format of the screen. The function
SDL_DisplayFormat() does this conversion for you.

Tip #2:
Many sprite images have a transparent background. You can enable
transparent blits (colorkey blit) with the SDL_SetColorKey() function.

  1. Events
 * Waiting for events

   Wait for events using the SDL_WaitEvent() function.

Example:
{
SDL_Event event;

SDL_WaitEvent(&event);

switch (event.type) {
	case SDL_KEYDOWN:
		printf("The %s key was pressed!\n",
		       SDL_GetKeyName(event.key.keysym.sym));
		break;
	case SDL_QUIT:
		exit(0);
}

}

Tip:
SDL has international keyboard support, translating key events and placing
the UNICODE equivalents into event.key.keysym.unicode. Since this has some
processing overhead involved, it must be enabled using SDL_EnableUNICODE().

 * Polling for events

   Poll for events using the SDL_PollEvent() function.

Example:
{
SDL_Event event;

while ( SDL_PollEvent(&event) ) {
	switch (event.type) {
		case SDL_MOUSEMOTION:
			printf("Mouse moved by %d,%d to (%d,%d)\n", 
			       event.motion.xrel, event.motion.yrel,
			       event.motion.x, event.motion.y);
			break;
		case SDL_MOUSEBUTTONDOWN:
			printf("Mouse button %d pressed at (%d,%d)\n"),
			       event.button.button, event.button.x, event.button.y);
			break;
		case SDL_QUIT:
			exit(0);
	}
}

}

Tip:
You can peek at events in the event queue without removing them by passing
the SDL_PEEKEVENT action to SDL_PeepEvents().

 * Polling event state

   In addition to handling events directly, each type of event has a
   function which allows you to check the application event state.

   If you use this exclusively, you should ignore all events with the
   SDL_EventState() function, and call SDL_PumpEvents() periodically
   to update the application event state.

Example:
{
SDL_EventState(SDL_MOUSEMOTION, SDL_IGNORE);
}

void CheckMouseHover(void)
{
int mouse_x, mouse_y;

SDL_PumpEvents();

SDL_GetMouseState(&mouse_x, &mouse_y);
if ( (mouse_x < 32) && (mouse_y < 32) ) {
	printf("Mouse in upper left hand corner!\n");
}

}

Tip:
You can hide or show the system mouse cursor using SDL_ShowCursor().

  1. Sound
 * Opening the audio device

   You need to have a callback function written which mixed your
   audio data and puts it in the audio stream.  After that, choose
   your desired audio format and rate, and open the audio device.

   The audio won't actually start playing until you call SDL_PauseAudio(0),
   allowing you to perform other audio initialization as needed before
   your callback function is run.

   After you are done using the sound output, you should close it
   with the SDL_CloseAudio() function.

Example:
#include “SDL.h”
#include “SDL_audio.h”
{
extern void mixaudio(void *unused, Uint8 *stream, int len);
SDL_AudioSpec fmt;

/* Set 16-bit stereo audio at 22Khz */
fmt.freq = 22050;
fmt.format = AUDIO_S16;
fmt.channels = 2;
fmt.samples = 512;		/* A good value for games */
fmt.callback = mixaudio;
fmt.userdata = NULL;

/* Open the audio device and start playing sound! */
if ( SDL_OpenAudio(&fmt, NULL) < 0 ) {
	fprintf(stderr, "Unable to open audio: %s\n", SDL_GetError());
	exit(1);
}
SDL_PauseAudio(0);

...

SDL_CloseAudio();

}

Tip:
If your application can handle different audio formats, pass a second
SDL_AudioSpec pointer to SDL_OpenAudio() to get the actual hardware
audio format. If you leave the second pointer NULL, the audio data
will be converted to the hardware audio format at runtime.

 * Loading and playing sounds

   SDL provides a single sound loading routine for your convenience,
   SDL_LoadWAV().

   After you load your sounds, you should convert them to the audio
   format of the sound output using SDL_ConvertAudio(), and make them
   available to your mixing function.

Example:

#define NUM_SOUNDS 2
struct sample {
Uint8 *data;
Uint32 dpos;
Uint32 dlen;
} sounds[NUM_SOUNDS];

void mixaudio(void *unused, Uint8 *stream, int len)
{
int i;
Uint32 amount;

for ( i=0; i<NUM_SOUNDS; ++i ) {
	amount = (sounds[i].dlen-sounds[i].dpos);
	if ( amount > len ) {
		amount = len;
	}
	SDL_MixAudio(stream, &sounds[i].data[sounds[i].dpos], amount, SDL_MIX_MAXVOLUME);
	sounds[i].dpos += amount;
}

}

void PlaySound(char *file)
{
int index;
SDL_AudioSpec wave;
Uint8 *data;
Uint32 dlen;
SDL_AudioCVT cvt;

/* Look for an empty (or finished) sound slot */
for ( index=0; index<NUM_SOUNDS; ++index ) {
	if ( sounds[index].dpos == sounds[index].dlen ) {
		break;
	}
}
if ( index == NUM_SOUNDS )
	return;

/* Load the sound file and convert it to 16-bit stereo at 22kHz */
if ( SDL_LoadWAV(file, &wave, &data, &dlen) == NULL ) {
	fprintf(stderr, "Couldn't load %s: %s\n", file, SDL_GetError());
	return;
}
SDL_BuildAudioCVT(&cvt, wave.format, wave.channels, wave.freq,
                        AUDIO_S16,   2,             22050);
cvt.buf = malloc(dlen*cvt.len_mult);
memcpy(cvt.buf, data, dlen);
cvt.len = dlen;
SDL_ConvertAudio(&cvt);
SDL_FreeWAV(data);

/* Put the sound data in the slot (it starts playing immediately) */
if ( sounds[index].data ) {
	free(sounds[index].data);
}
SDL_LockAudio();
sounds[index].data = cvt.buf;
sounds[index].dlen = cvt.len_cvt;
sounds[index].dpos = 0;
SDL_UnlockAudio();

}

Tip:
SDL audio facilities are designed for a low level software audio mixer.
A complete example mixer implementation available under the LGPL license
can be found in the SDL demos archive.

  1. CD-ROM audio
 * Opening a CD-ROM drive for use

   You can find out how many CD-ROM drives are on the system with
   the SDL_CDNumDrives() function, and then pick which one to use
   with SDL_CDOpen().  The system default CD-ROM is always drive 0.

   The CD-ROM drive may be opened for use even if there is no disk
   in the drive.  You should use the SDL_CDStatus() function to
   determine the state of the drive.

   After you are done using the CD-ROM drive, close it with the
   SDL_CDClose() function.

Example:
{
SDL_CD *cdrom;

if ( SDL_CDNumDrives() > 0 ) {
	cdrom = SDL_CDOpen(0);
	if ( cdrom == NULL ) {
		fprintf(stderr, "Couldn't open default CD-ROM: %s\n" SDL_GetError());
		return;
	}

	...

	SDL_CDClose(cdrom);
}

}

Tip:
You can get the system-dependent name for a CD-ROM drive using the
SDL_CDName() function.

 * Playing the CD-ROM

   CD-ROM drives specify time either in MSF format (mins/secs/frames)
   or directly in frames.  A frame is a standard unit of time on the
   CD, corresponding to 1/75 of a second.  SDL uses frames instead of
   the MSF format when specifying track lengths and offsets, but you
   can convert between them using the FRAMES_TO_MSF() and MSF_TO_FRAMES()
   macros.

   SDL doesn't update the track information in the SDL_CD structure
   until you call SDL_CDStatus(), so you should always use SDL_CDStatus()
   to make sure there is a CD in the drive and determine what tracks
   are available before playing the CD.  Note that track indexes start
   at 0 for the first track.

   SDL has two functions for playing the CD-ROM.  You can either play
   specific tracks on the CD using SDL_CDPlayTracks(), or you can play
   absolute frame offsets using SDL_CDPlay().

   SDL doesn't provide automatic notification of CD insertion or play
   completion.  To detect these conditions, you need to periodically
   poll the status of the drive with SDL_CDStatus().
   Since this call reads the table of contents for the CD, it should 
   not be called continuously in a tight loop.

Example:

void PlayTrack(SDL_CD *cdrom, int track)
{
if ( CD_INDRIVE(SDL_CDStatus(cdrom)) ) {
SDL_CDPlayTracks(cdrom, track, 0, track+1, 0);
}
while ( SDL_CDStatus(cdrom) == CD_PLAYING ) {
SDL_Delay(1000);
}
}

Tip:
You can determine which tracks are audio tracks and which are data
tracks by looking at the cdrom->tracks[track].type, and comparing
it to SDL_AUDIO_TRACK and SDL_DATA_TRACK.

  1. Threads

    • Create a simple thread

      Creating a thread is done by passing a function to SDL_CreateThread().
      When the function returns, if successful, your function is now
      running concurrently with the rest of your application, in its
      own running context (stack, registers, etc.) and able to access
      memory and file handles used by the rest of the application.

Example:
#include “SDL_thread.h”

int global_data = 0;

int thread_func(void *unused)
{
int last_value = 0;

while ( global_data != -1 ) {
	if ( global_data != last_value ) {
		printf("Data value changed to %d\n", global_data);
		last_value = global_data;
	}
	SDL_Delay(100);
}
printf("Thread quitting\n");
return(0);

}

{
SDL_Thread *thread;
int i;

thread = SDL_CreateThread(thread_func, NULL);
if ( thread == NULL ) {
	fprintf(stderr, "Unable to create thread: %s\n", SDL_GetError());
	return;
}

for ( i=0; i<5; ++i ) {
	printf("Changing value to %d\n", i);
	global_data = i;
	SDL_Delay(1000);
}

printf("Signaling thread to quit\n");
global_data = -1;
SDL_WaitThread(thread, NULL);

}

Tip:
The second argument to SDL_CreateThread() is passed as a parameter to
the thread function. You can use this to pass in values on the stack,
or just a pointer to data for use by the thread.

  * Synchronizing access to a resource

    You can prevent more than one thread from accessing a resource
    by creating a mutex and surrounding access to that resource with
    lock (SDL_mutexP()) and unlock (SDL_mutexV()) calls.

Example:
#include “SDL_thread.h”
#include “SDL_mutex.h”

int potty = 0;
int gotta_go;

int thread_func(void *data)
{
SDL_mutex *lock = (SDL_mutex *)data;
int times_went;

times_went = 0;
while ( gotta_go ) {
	SDL_mutexP(lock);	/* Lock  the potty */
	++potty;
	printf("Thread %d using the potty\n", SDL_ThreadID());
	if ( potty > 1 ) {
		printf("Uh oh, somebody else is using the potty!\n");
	}
	--potty;
	SDL_mutexV(lock);
	++times_went;
}
printf("Yep\n");
return(times_went);

}

{
const int progeny = 5;
SDL_Thread *kids[progeny];
SDL_mutex *lock;
int i, lots;

/* Create the synchronization lock */
lock = SDL_CreateMutex();

gotta_go = 1;
for ( i=0; i<progeny; ++i ) {
	kids[i] = SDL_CreateThread(thread_func, lock);
}

SDL_Delay(5*1000);
SDL_mutexP(lock);
printf("Everybody done?\n");
gotta_go = 0;
SDL_mutexV(lock);

for ( i=0; i<progeny; ++i ) {
	SDL_WaitThread(kids[i], &lots);
	printf("Thread %d used the potty %d times\n", i+1, lots);
}
SDL_DestroyMutex(lock);

}

Tip:
You can make SDL surfaces thread-safe by passing SDL_THREADSAFE as a
flag to SDL_AllocSurface(). If you do this, SDL_LockSurface() and
SDL_UnlockSurface() serialize access so only one thread can access
it at a time.

  1. Timers

    • Get the current time, in milliseconds

      SDL_GetTicks() tells how many milliseconds have past since an
      arbitrary point in the past.

Example:

#define TICK_INTERVAL 30

Uint32 TimeLeft(void)
{
static Uint32 next_time = 0;
Uint32 now;

now = SDL_GetTicks();
if ( next_time <= now ) {
	next_time = now+TICK_INTERVAL;
	return(0);
}
return(next_time-now);

}

Tip:
In general, when implementing a game, it is better to move objects in
the game based on time rather than on framerate. This produces consistent
gameplay on both fast and slow systems.

  * Wait a specified number of milliseconds

    SDL_Delay() allows you to wait for some number of milliseconds.

    Since the operating systems supported by SDL are multi-tasking,
    there is no way to guarantee that your application will delay
    exactly the requested time.  This should be used more as a way
    of idling for a while rather than to wake up at a particular time.

Example:
{
while ( game_running ) {
UpdateGameState();
SDL_Delay(TimeLeft());
}
}

Tip:
Most operating systems have a scheduler timeslice of about 10 ms.
You can use SDL_Delay(1) as a way of giving up CPU for the current
timeslice, allowing other threads to run. This is important if you
have a thread in a tight loop but want other threads (like audio)
to keep running.

  1. Endian independence

    • Determine the endianness of the current system

      The C preprocessor define SDL_BYTEORDER is defined to be either
      SDL_LIL_ENDIAN or SDL_BIG_ENDIAN, depending on the byte order of
      the current system.

      A little endian system that writes data to disk has it laid out:
      [lo-bytes][hi-bytes]
      A big endian system that writes data to disk has it laid out:
      [hi-bytes][lo-bytes]

Example:
#include “SDL_endian.h”

#if SDL_BYTEORDER == SDL_LIL_ENDIAN
#define SWAP16(X) (X)
#define SWAP32(X) (X)
#else
#define SWAP16(X) SDL_Swap16(X)
#define SWAP32(X) SDL_Swap32(X)
#endif

Tip:
x86 systems are little-endian, PPC systems are big-endian.

  * Swap data on systems of differing endianness

    SDL provides a set of fast macros in SDL_endian.h, SDL_Swap16()
    and SDL_Swap32(), which swap data endianness for you.  There are
    also macros defined which swap data of particular endianness to
    the local system's endianness.

Example:
#include “SDL_endian.h”

void ReadScanline16(FILE *file, Uint16 *scanline, int length)
{
fread(scanline, length, sizeof(Uint16), file);
if ( SDL_BYTEORDER == SDL_BIG_ENDIAN ) {
int i;
for ( i=length-1; i >= 0; --i )
scanline[i] = SDL_SwapLE16(scanline[i]);
}
}

Tip:
If you just need to know the system byte-order, but don’t need all
the swapping functions, include SDL_byteorder.h instead of SDL_endian.h

The rest of the talk is an example of porting a DirectX sample game
to Linux, using SDL.

  1. Process of porting games to Linux:
  • Compile the source code
  • Identify major subsystems
  • Rewrite major components
  • Polish the code
  • Add Linux specific features
  • Optimize and debug
  • Port to other architectures
  1. Get the code to compile – adding in any C library shims and stubs
    necessary to get the code to the link stage.
  2. Identify major subsystems. Much of this work is done in stage 1.
  3. Rewrite major components to run natively on Linux. Major components
    are: CD-ROM support, audio, 2D video, 3D video, networking, filesystem
    code, etc. SDL has been useful in implementing some of these
    subsystems. Other portions are being coded in-house, often tailored to
    each individual game.
  4. Polish. Clean up code which is designed for Win32, remove code that
    is no longer relevant.
  5. Add features so that the program runs well in the Linux environment.
  6. Optimize and debug. gdb and dmalloc are very useful for this stage.
  7. Port to other Linux architectures (PPC, Alpha, Sparc, etc.)
  8. Beta test, beta test, beta test
  9. Packaging, duplication, PR
  10. Release!
  11. It’s not over 'till the fat lady sings (bug reports, support, etc.)

Looks awesome!

Maybe you could show some demos at the end of the presentation or something
and show what SDL can really do.

See ya.> ----- Original Message -----

From: slouken@libsdl.org (Sam Lantinga)
To: ; ;
Sent: Friday, July 09, 1999 1:49 PM
Subject: [SDL] Presentation for Ottawa Linux Symposium

Ottawa Linux Symposium: http://www.ottawalinuxsymposium.com/

I’m going to have this finished by the end of the afternoon, but I
welcome any comments you have. :slight_smile:

The final version will be in HTML, with the tips in a sidebar.


-Sam Lantinga, Lead Programmer, Loki Entertainment Software



“Linux your game with Simple DirectMedia Layer”

  1. Introduction:

    Sam Lantinga, Lead Programmer for Loki Entertainment Software.
    Loki Entertainment is a company devoted to porting top games
    to Linux. You can find out more at http://www.lokigames.com/

    My background:

    • Author of the Simple DirectMedia Layer (SDL)
    • Ported Maelstrom from the Macintosh to Linux
    • Worked on port of the Macintosh emulator Executor to Win32
    • Ported some of the DOOM tools from DOS (DEU, DHE, etc.)
    • Other projects: http://www.devolution.com/~slouken/
  2. Simple DirectMedia Layer

  3. What is it?

  4. What can it do?

    • Video

      • Set a video mode at any depth (8-bpp or greater) with optional
        conversion, if the video mode is not supported by the hardware.
      • Write directly to a linear graphics framebuffer.
      • Create surfaces with colorkey or alpha blending attributes.
      • Surface blits are automatically converted to the target format
        using optimized blitters and are hardware accelerated, when
        possible. MMX optimized blits are available for the x86.
      • Hardware accelerated blit and fill operations are used if
        supported by the hardware.
        Tip:
        You can set your application’s title-bar (if any) and icon using the
        SDL_WM_SetCaption() and SDL_WM_SetIcon() functions respectively.
    • Events

      • Events provided for:
        • Application visibility changes
        • Keyboard input
        • Mouse input
        • User-requested quit
      • Each event can be enabled or disabled with SDL_EventState().
      • Events are passed through a user-specified filter function
        before being posted to the internal event queue.
      • Thread-safe event queue.
        Tip:
        Use SDL_PeepEvents() to search for an event of a particular type in the
        event queue.
    • Audio

      • Set audio playback of 8-bit and 16-bit audio, mono or stereo,
        with optional conversion if the format is not supported by the
        hardware.
      • Audio runs independently in a separate thread, filled via a
        user callback mechanism.
      • Designed for custom software audio mixers, but the example
        archive contains a complete audio/music output library.
        Tip:
        Use the SDL_LockAudio() and SDL_UnlockAudio() functions to synchronize
        access data shared by the audio callback and the rest of the program.
    • CD-ROM audio

      • Complete CD audio control API
        Tip:
        If you pass a NULL CD-ROM handle to the CD-ROM API functions, they will
        act
        on the last CD-ROM that was opened.
    • Threads

      • Simple thread creation API
      • Simple binary semaphores for synchronization
        Tip:
        Do not use C library functions like I/O and memory management from threads
        if you can help it - they lock resources used by other threads.
    • Timers

      • Get the number of milliseconds elapsed
      • Wait a specified number of milliseconds
      • Set a single periodic timer with 10ms resolution
        Tip:
        You can easily replace the Win32 GetTickCount() with SDL_GetTicks()
    • Endian independence

      • Detect the endianness of the current system
      • Routines for fast swapping of data values
      • Read and write data of a specified endianness
        Tip:
        When reading your data files, you may need to byteswap 16-bit graphics
  5. What platforms does it run on?

    • Linux, of course

      • Uses X11 for video display, taking advantage of XFree86 DGA
        extensions and new MTRR acceleration for fullscreen display.
      • Uses the OSS API for sound.
      • Threads are implemented using either the clone() system call
        and SysV IPC, or glibc-2.1 pthreads.
        Tip:
        You can get at the hidden portions of the SDL driver interface via the
        SDL_GetWMInfo() function. This allows you to do things like remove window
        decorations and programmatically iconify your window.
    • Win32

      • Two versions, one safe for all systems based on Win32 APIs,
        and one with higher performance, based on DirectX APIs.
      • Safe version uses GDI for video display. High performance
        version
        uses DirectDraw for video display, taking advantage of hardware
        acceleration if available.
      • Safe version uses waveOut APIs for sound. High performace
        version
        uses DirectSound for audio playback.
        Tip:
        You must call the SDL event functions periodically from your main thread
        to pump the Windows message queue and keep your application responsive.
    • BeOS

      • BWindow is used for video display.
      • BSoundPlayer API is used for sound.
        Tip:
        Linux and BeOS support the SDL_INIT_EVENTTHREAD flag which, when passed
        to SDL_Init(), asks the event loop to run asynchronously in another
        thread.
        This is useful for color cursors that respond even when the application
        is busy.
    • Unofficial ports, ports in progress

      • Solaris, IRIX, FreeBSD
      • MacOS
  6. Using the Simple DirectMedia Layer API

  7. Initializing the library

    Use SDL_Init() to dynamically load and initialize the library.
    This function takes a set of flags corresponding to the portions
    you want to activate:
    SDL_INIT_AUDIO
    SDL_INIT_VIDEO
    SDL_INIT_CDROM
    SDL_INIT_TIMER

    Use SDL_Quit() to clean up the library when you are done with it.

Example:
#include <stdlib.h>
#include “SDL.h”

main(int argc, char *argv[])
{
if ( SDL_Init(SDL_INIT_AUDIO|SDL_INIT_VIDEO) < 0 ) {
fprintf(stderr, “Unable to init SDL: %s\n”, SDL_GetError());
exit(1);
}
atexit(SDL_Quit);


}

Tip:
SDL dynamically loads the SDL library from the standard system library
locations. Use the SDL_SetLibraryPath() function to use an alternate
location for the dynamic libraries distributed with your application.

  1. Video
 * Choosing and setting video modes  (the easy way)

   You just choose your favorite bit-depth and resolution, and set it!

Example:
{ SDL_Surface *screen;

screen = SDL_SetVideoMode(640, 480, 16, SDL_SWSURFACE);
if ( screen == NULL ) {
fprintf(stderr, “Unable to set 640x480 video: %s\n”, SDL_GetError());
exit(1);
}
}

Tip #1:
You can find the fastest video depth supported by the hardware with the
function SDL_GetVideoInfo().

Tip #2:
You can get a list of supported video resulutions at a particular
bit-depth
using the function SDL_ListModes().

 * Drawing pixels on the screen

   Drawing to the screen is done by writing directly to the graphics
   framebuffer, and calling the screen update function.

Example:
Drawing a pixel on a screen of arbitrary format

void DrawPixel(SDL_Surface *screen, Uint8 R, Uint8 G, Uint8 B)
{
Uint32 color = SDL_MapRGB(screen->format, R, G, B);

if ( SDL_MUSTLOCK(screen) ) {

if ( SDL_LockSurface(screen) < 0 ) {
return;
}
}
switch (screen->format->BytesPerPixel) {
case 1: { /* Assuming 8-bpp */
Uint8 *bufp;

bufp = (Uint8 )screen->pixels + yscreen->pitch + x;
*bufp = color;
}
break;

case 2: { /* Probably 15-bpp or 16-bpp */
Uint16 *bufp;

bufp = (Uint16 )screen->pixels + yscreen->pitch/2 + x;
*bufp = color;
}
break;

case 3: { /* Slow 24-bpp mode, usually not used */
Uint8 *bufp;

bufp = (Uint8 )screen->pixels + yscreen->pitch + x;
*(bufp+screen->format->Rshift/8) = R;
*(bufp+screen->format->Gshift/8) = G;
*(bufp+screen->format->Bshift/8) = B;
}
break;

case 4: { /* Probably 32-bpp */
Uint32 *bufp;

bufp = (Uint32 )screen->pixels + yscreen->pitch/4 + x;
*bufp = color;
}
break;
}
if ( SDL_MUSTLOCK(screen) ) {
SDL_UnlockSurface(screen);
}
SDL_UpdateRect(screen, x, y, 1, 1);
}

Tip:
If you know that you will be doing a lot of drawing, it is best to lock
the screen (if necessary) once before drawing, draw while keeping a list
of areas that need to be updated, and unlock the screen again before
updating the display.

 * Loading and displaying images

   SDL provides a single image loading routine for your convenience,
   SDL_LoadBMP().  A library for image loading can be found in the
   SDL demos archive.

   You can display images by using SDL_BlitSurface() to blit them
   into the graphics framebuffer.  SDL_BlitSurface() automatically
   clips the blit rectangle, which should be passed to

SDL_UpdateRects()

   to update the portion of the screen which has changed.

Example:
void ShowBMP(char *file, SDL_Surface *screen, int x, int y)
{
SDL_Surface *image;
SDL_Rect dest;

/* Load the BMP file into a surface */
image = SDL_LoadBMP(file);
if ( image == NULL ) {
fprintf(stderr, “Couldn’t load %s: %s\n”, file, SDL_GetError());
return;
}

/* Blit onto the screen surface.
   The surfaces should not be locked at this point.
 */
dest.x = x;
dest.y = y;
dest.w = image->w;
dest.h = image->h;
SDL_BlitSurface(image, NULL, screen, &dest);

/* Update the changed portion of the screen */
SDL_UpdateRects(screen, 1, &dest);

}

Tip #1:
If you are loading an image to be displayed many times, you can improve
blitting speed by convert it to the format of the screen. The function
SDL_DisplayFormat() does this conversion for you.

Tip #2:
Many sprite images have a transparent background. You can enable
transparent blits (colorkey blit) with the SDL_SetColorKey() function.

  1. Events
 * Waiting for events

   Wait for events using the SDL_WaitEvent() function.

Example:
{
SDL_Event event;

SDL_WaitEvent(&event);

switch (event.type) {
case SDL_KEYDOWN:
printf(“The %s key was pressed!\n”,
SDL_GetKeyName(event.key.keysym.sym));
break;
case SDL_QUIT:
exit(0);
}
}

Tip:
SDL has international keyboard support, translating key events and placing
the UNICODE equivalents into event.key.keysym.unicode. Since this has
some
processing overhead involved, it must be enabled using
SDL_EnableUNICODE().

 * Polling for events

   Poll for events using the SDL_PollEvent() function.

Example:
{
SDL_Event event;

while ( SDL_PollEvent(&event) ) {
switch (event.type) {
case SDL_MOUSEMOTION:
printf(“Mouse moved by %d,%d to (%d,%d)\n”,
event.motion.xrel, event.motion.yrel,
event.motion.x, event.motion.y);
break;
case SDL_MOUSEBUTTONDOWN:
printf(“Mouse button %d pressed at (%d,%d)\n”),
event.button.button, event.button.x, event.button.y);
break;
case SDL_QUIT:
exit(0);
}
}
}

Tip:
You can peek at events in the event queue without removing them by passing
the SDL_PEEKEVENT action to SDL_PeepEvents().

 * Polling event state

   In addition to handling events directly, each type of event has a
   function which allows you to check the application event state.

   If you use this exclusively, you should ignore all events with the
   SDL_EventState() function, and call SDL_PumpEvents() periodically
   to update the application event state.

Example:
{
SDL_EventState(SDL_MOUSEMOTION, SDL_IGNORE);
}

void CheckMouseHover(void)
{
int mouse_x, mouse_y;

SDL_PumpEvents();

SDL_GetMouseState(&mouse_x, &mouse_y);
if ( (mouse_x < 32) && (mouse_y < 32) ) {
printf(“Mouse in upper left hand corner!\n”);
}
}

Tip:
You can hide or show the system mouse cursor using SDL_ShowCursor().

  1. Sound
 * Opening the audio device

   You need to have a callback function written which mixed your
   audio data and puts it in the audio stream.  After that, choose
   your desired audio format and rate, and open the audio device.

   The audio won't actually start playing until you call

SDL_PauseAudio(0),

   allowing you to perform other audio initialization as needed before
   your callback function is run.

   After you are done using the sound output, you should close it
   with the SDL_CloseAudio() function.

Example:
#include “SDL.h”
#include “SDL_audio.h”
{
extern void mixaudio(void *unused, Uint8 *stream, int len);
SDL_AudioSpec fmt;

/* Set 16-bit stereo audio at 22Khz /
fmt.freq = 22050;
fmt.format = AUDIO_S16;
fmt.channels = 2;
fmt.samples = 512; /
A good value for games */
fmt.callback = mixaudio;
fmt.userdata = NULL;

/* Open the audio device and start playing sound! */
if ( SDL_OpenAudio(&fmt, NULL) < 0 ) {
fprintf(stderr, “Unable to open audio: %s\n”, SDL_GetError());
exit(1);
}
SDL_PauseAudio(0);

SDL_CloseAudio();
}

Tip:
If your application can handle different audio formats, pass a second
SDL_AudioSpec pointer to SDL_OpenAudio() to get the actual hardware
audio format. If you leave the second pointer NULL, the audio data
will be converted to the hardware audio format at runtime.

 * Loading and playing sounds

   SDL provides a single sound loading routine for your convenience,
   SDL_LoadWAV().

   After you load your sounds, you should convert them to the audio
   format of the sound output using SDL_ConvertAudio(), and make them
   available to your mixing function.

Example:

#define NUM_SOUNDS 2
struct sample {
Uint8 *data;
Uint32 dpos;
Uint32 dlen;
} sounds[NUM_SOUNDS];

void mixaudio(void *unused, Uint8 *stream, int len)
{
int i;
Uint32 amount;

for ( i=0; i<NUM_SOUNDS; ++i ) {
amount = (sounds[i].dlen-sounds[i].dpos);
if ( amount > len ) {
amount = len;
}
SDL_MixAudio(stream, &sounds[i].data[sounds[i].dpos], amount,
SDL_MIX_MAXVOLUME);
sounds[i].dpos += amount;
}
}

void PlaySound(char *file)
{
int index;
SDL_AudioSpec wave;
Uint8 *data;
Uint32 dlen;
SDL_AudioCVT cvt;

/* Look for an empty (or finished) sound slot */
for ( index=0; index<NUM_SOUNDS; ++index ) {
if ( sounds[index].dpos == sounds[index].dlen ) {
break;
}
}
if ( index == NUM_SOUNDS )
return;

/* Load the sound file and convert it to 16-bit stereo at 22kHz /
if ( SDL_LoadWAV(file, &wave, &data, &dlen) == NULL ) {
fprintf(stderr, “Couldn’t load %s: %s\n”, file, SDL_GetError());
return;
}
SDL_BuildAudioCVT(&cvt, wave.format, wave.channels, wave.freq,
AUDIO_S16, 2, 22050);
cvt.buf = malloc(dlen
cvt.len_mult);
memcpy(cvt.buf, data, dlen);
cvt.len = dlen;
SDL_ConvertAudio(&cvt);
SDL_FreeWAV(data);

/* Put the sound data in the slot (it starts playing immediately) */
if ( sounds[index].data ) {
free(sounds[index].data);
}
SDL_LockAudio();
sounds[index].data = cvt.buf;
sounds[index].dlen = cvt.len_cvt;
sounds[index].dpos = 0;
SDL_UnlockAudio();
}

Tip:
SDL audio facilities are designed for a low level software audio mixer.
A complete example mixer implementation available under the LGPL license
can be found in the SDL demos archive.

  1. CD-ROM audio
 * Opening a CD-ROM drive for use

   You can find out how many CD-ROM drives are on the system with
   the SDL_CDNumDrives() function, and then pick which one to use
   with SDL_CDOpen().  The system default CD-ROM is always drive 0.

   The CD-ROM drive may be opened for use even if there is no disk
   in the drive.  You should use the SDL_CDStatus() function to
   determine the state of the drive.

   After you are done using the CD-ROM drive, close it with the
   SDL_CDClose() function.

Example:
{
SDL_CD *cdrom;

if ( SDL_CDNumDrives() > 0 ) {
cdrom = SDL_CDOpen(0);
if ( cdrom == NULL ) {
fprintf(stderr, “Couldn’t open default CD-ROM: %s\n” SDL_GetError());
return;
}

SDL_CDClose(cdrom);
}
}

Tip:
You can get the system-dependent name for a CD-ROM drive using the
SDL_CDName() function.

 * Playing the CD-ROM

   CD-ROM drives specify time either in MSF format (mins/secs/frames)
   or directly in frames.  A frame is a standard unit of time on the
   CD, corresponding to 1/75 of a second.  SDL uses frames instead of
   the MSF format when specifying track lengths and offsets, but you
   can convert between them using the FRAMES_TO_MSF() and

MSF_TO_FRAMES()

   macros.

   SDL doesn't update the track information in the SDL_CD structure
   until you call SDL_CDStatus(), so you should always use

SDL_CDStatus()

   to make sure there is a CD in the drive and determine what tracks
   are available before playing the CD.  Note that track indexes start
   at 0 for the first track.

   SDL has two functions for playing the CD-ROM.  You can either play
   specific tracks on the CD using SDL_CDPlayTracks(), or you can play
   absolute frame offsets using SDL_CDPlay().

   SDL doesn't provide automatic notification of CD insertion or play
   completion.  To detect these conditions, you need to periodically
   poll the status of the drive with SDL_CDStatus().
   Since this call reads the table of contents for the CD, it should
   not be called continuously in a tight loop.

Example:

void PlayTrack(SDL_CD *cdrom, int track)
{
if ( CD_INDRIVE(SDL_CDStatus(cdrom)) ) {
SDL_CDPlayTracks(cdrom, track, 0, track+1, 0);
}
while ( SDL_CDStatus(cdrom) == CD_PLAYING ) {
SDL_Delay(1000);
}
}

Tip:
You can determine which tracks are audio tracks and which are data
tracks by looking at the cdrom->tracks[track].type, and comparing
it to SDL_AUDIO_TRACK and SDL_DATA_TRACK.

  1. Threads

    • Create a simple thread

      Creating a thread is done by passing a function to
      SDL_CreateThread().
      When the function returns, if successful, your function is now
      running concurrently with the rest of your application, in its
      own running context (stack, registers, etc.) and able to access
      memory and file handles used by the rest of the application.

Example:
#include “SDL_thread.h”

int global_data = 0;

int thread_func(void *unused)
{
int last_value = 0;

while ( global_data != -1 ) {
if ( global_data != last_value ) {
printf(“Data value changed to %d\n”, global_data);
last_value = global_data;
}
SDL_Delay(100);
}
printf(“Thread quitting\n”);
return(0);
}

{
SDL_Thread *thread;
int i;

thread = SDL_CreateThread(thread_func, NULL);
if ( thread == NULL ) {
fprintf(stderr, “Unable to create thread: %s\n”, SDL_GetError());
return;
}

for ( i=0; i<5; ++i ) {
printf(“Changing value to %d\n”, i);
global_data = i;
SDL_Delay(1000);
}

printf(“Signaling thread to quit\n”);
global_data = -1;
SDL_WaitThread(thread, NULL);
}

Tip:
The second argument to SDL_CreateThread() is passed as a parameter to
the thread function. You can use this to pass in values on the stack,
or just a pointer to data for use by the thread.

  * Synchronizing access to a resource

    You can prevent more than one thread from accessing a resource
    by creating a mutex and surrounding access to that resource with
    lock (SDL_mutexP()) and unlock (SDL_mutexV()) calls.

Example:
#include “SDL_thread.h”
#include “SDL_mutex.h”

int potty = 0;
int gotta_go;

int thread_func(void *data)
{
SDL_mutex *lock = (SDL_mutex *)data;
int times_went;

times_went = 0;
while ( gotta_go ) {
SDL_mutexP(lock); /* Lock the potty */
++potty;
printf(“Thread %d using the potty\n”, SDL_ThreadID());
if ( potty > 1 ) {
printf(“Uh oh, somebody else is using the potty!\n”);
}
–potty;
SDL_mutexV(lock);
++times_went;
}
printf(“Yep\n”);
return(times_went);
}

{
const int progeny = 5;
SDL_Thread *kids[progeny];
SDL_mutex *lock;
int i, lots;

/* Create the synchronization lock */
lock = SDL_CreateMutex();

gotta_go = 1;
for ( i=0; i<progeny; ++i ) {
kids[i] = SDL_CreateThread(thread_func, lock);
}

SDL_Delay(5*1000);
SDL_mutexP(lock);
printf(“Everybody done?\n”);
gotta_go = 0;
SDL_mutexV(lock);

for ( i=0; i<progeny; ++i ) {
SDL_WaitThread(kids[i], &lots);
printf(“Thread %d used the potty %d times\n”, i+1, lots);
}
SDL_DestroyMutex(lock);
}

Tip:
You can make SDL surfaces thread-safe by passing SDL_THREADSAFE as a
flag to SDL_AllocSurface(). If you do this, SDL_LockSurface() and
SDL_UnlockSurface() serialize access so only one thread can access
it at a time.

  1. Timers

    • Get the current time, in milliseconds

      SDL_GetTicks() tells how many milliseconds have past since an
      arbitrary point in the past.

Example:

#define TICK_INTERVAL 30

Uint32 TimeLeft(void)
{
static Uint32 next_time = 0;
Uint32 now;

now = SDL_GetTicks();
if ( next_time <= now ) {
next_time = now+TICK_INTERVAL;
return(0);
}
return(next_time-now);
}

Tip:
In general, when implementing a game, it is better to move objects in
the game based on time rather than on framerate. This produces consistent
gameplay on both fast and slow systems.

  * Wait a specified number of milliseconds

    SDL_Delay() allows you to wait for some number of milliseconds.

    Since the operating systems supported by SDL are multi-tasking,
    there is no way to guarantee that your application will delay
    exactly the requested time.  This should be used more as a way
    of idling for a while rather than to wake up at a particular time.

Example:
{
while ( game_running ) {
UpdateGameState();
SDL_Delay(TimeLeft());
}
}

Tip:
Most operating systems have a scheduler timeslice of about 10 ms.
You can use SDL_Delay(1) as a way of giving up CPU for the current
timeslice, allowing other threads to run. This is important if you
have a thread in a tight loop but want other threads (like audio)
to keep running.

  1. Endian independence

    • Determine the endianness of the current system

      The C preprocessor define SDL_BYTEORDER is defined to be either
      SDL_LIL_ENDIAN or SDL_BIG_ENDIAN, depending on the byte order of
      the current system.

      A little endian system that writes data to disk has it laid out:
      [lo-bytes][hi-bytes]
      A big endian system that writes data to disk has it laid out:
      [hi-bytes][lo-bytes]

Example:
#include “SDL_endian.h”

#if SDL_BYTEORDER == SDL_LIL_ENDIAN
#define SWAP16(X) (X)
#define SWAP32(X) (X)
#else
#define SWAP16(X) SDL_Swap16(X)
#define SWAP32(X) SDL_Swap32(X)
#endif

Tip:
x86 systems are little-endian, PPC systems are big-endian.

  * Swap data on systems of differing endianness

    SDL provides a set of fast macros in SDL_endian.h, SDL_Swap16()
    and SDL_Swap32(), which swap data endianness for you.  There are
    also macros defined which swap data of particular endianness to
    the local system's endianness.

Example:
#include “SDL_endian.h”

void ReadScanline16(FILE *file, Uint16 *scanline, int length)
{
fread(scanline, length, sizeof(Uint16), file);
if ( SDL_BYTEORDER == SDL_BIG_ENDIAN ) {
int i;
for ( i=length-1; i >= 0; --i )
scanline[i] = SDL_SwapLE16(scanline[i]);
}
}

Tip:
If you just need to know the system byte-order, but don’t need all
the swapping functions, include SDL_byteorder.h instead of SDL_endian.h

The rest of the talk is an example of porting a DirectX sample game
to Linux, using SDL.

  1. Process of porting games to Linux:
  • Compile the source code
  • Identify major subsystems
  • Rewrite major components
  • Polish the code
  • Add Linux specific features
  • Optimize and debug
  • Port to other architectures
  1. Get the code to compile – adding in any C library shims and stubs
    necessary to get the code to the link stage.
  2. Identify major subsystems. Much of this work is done in stage 1.
  3. Rewrite major components to run natively on Linux. Major components
    are: CD-ROM support, audio, 2D video, 3D video, networking, filesystem
    code, etc. SDL has been useful in implementing some of these
    subsystems. Other portions are being coded in-house, often tailored to
    each individual game.
  4. Polish. Clean up code which is designed for Win32, remove code that
    is no longer relevant.
  5. Add features so that the program runs well in the Linux environment.
  6. Optimize and debug. gdb and dmalloc are very useful for this stage.
  7. Port to other Linux architectures (PPC, Alpha, Sparc, etc.)
  8. Beta test, beta test, beta test
  9. Packaging, duplication, PR
  10. Release!
  11. It’s not over 'till the fat lady sings (bug reports, support, etc.)

Looks awesome!

Maybe you could show some demos at the end of the presentation or something
and show what SDL can really do.

Yup, absolutely. :slight_smile:

-Sam Lantinga				(slouken at devolution.com)

Lead Programmer, Loki Entertainment Software–
“Any sufficiently advanced bug is indistinguishable from a feature”
– Rich Kulawiec