release
dev builds
extras
themes manual
wiki
device status forums
mailing lists
IRC bugs
patches
dev guide



Search | Go
Wiki > Main > SdlPluginPort

SDL Plugin Port

This page concerns the port of SDL to run on Rockbox. It is not related to ports of Rockbox that rely on SDL.

Introduction

SDL (Simple DirectMedia? Library) is a software library which provides a generic interface for video and audio output, among other miscellaneous functionality. Many programs use SDL to be portable across different platforms. Rockbox has a port of version 1.2.15 of SDL as a static library against which various programs can be compiled. Having SDL support allows us to greatly accelerate the process of porting other software by supporting an API which they already use.

The version of SDL which we have does not support hardware acceleration; all video output is software rendered. There is no GL support, either.

Along with the main SDL library itself, we have ports of the auxiliary SDL_mixer and SDL_image libraries, which are commonly used by games.

The port resides in the apps/plugins/sdl/ directory and was largely done by me (FranklinWei). The purpose of this page is to document the history and technical details of the port, mostly for my own benefit, but hopefully these notes will be useful to others as well. If you're looking to port a program which uses SDL, see below for a guide.

The SDL runtime is currently used by PluginDuke3D, PluginQuake, and PluginWolf3D. Ports of other programs have also been attempted with varying degrees of success. They are still available in old Gerrit patch sets if anyone is interested.


History

I'm writing this about 2 years after the fact, so much of the following is reconstructed from commit history and IRC. From what I can gather, the idea for the port came around 21 January 2017.

2017-01-21:
<__builtin> hmm, it seems like the easiest way to approach this port [of Prince of Persia] is by writing a SDL wrapper
<__builtin> or maybe I could just port SDL, who knows? :D

... about 1 hour passes ...

<__builtin> woot! SDL compiles!!! (-ish)
<__builtin> though I don't have any drivers that actually do anything :P

...

<__builtin> it actually fits in the plugin buffer! :D

2017-01-22:
<pamaury>__builtin: are you *sure* you want to port SDL to rockbox ?
<__builtin> pamaury: too late now :)

It seems that I began porting Wolfenstein 3-D in early September 2017, and I had gotten Duke Nukem working by the middle of that same month. Quake was first pushed to Gerrit on 25 September 2017.

Duke3D was merged in December 2017, and Wolf3D and Quake sat in Gerrit for two years until I finally merged them both in July 2019.


Technical overview

Compilation

The sources of the SDL library reside in apps/plugins/sdl/src. There are several appropriately named drivers in this directory, including audio, thread, and video. The SDL_mixer and SDL_image libraries reside in apps/plugins/sdl.

Programs reside in their own directory under apps/plugins/sdl/progs/. The master branch currently has ports of duke3d, quake, and wolf3d in correspondingly named directories. Every program must have its main function renamed to my_main(int argc, char* argv[]).

The compilation of each program is governed by apps/plugins/sdl/sdl.make (see below for details on adding a new program). On native hosts, programs are compiled to overlays by default, which are loaded by a stub loader, such as apps/plugins/duke3d.c. This stub loader does nothing but load the actual program into the audio buffer (hence "overlay") and run it from there.

Compilation is done statically against the SDL sources. That is, each plugin has its own binary copy of the SDL library in its object file. This is inefficient because the SDL code is duplicated, but this has negligible impact (only few KB of wasted disk space) and greatly simplifies linking.

In order to prevent conflicts with system SDL libraries (in simulator builds, particularly), sdl.make has a build rule which inserts a rb_ prefix to every symbol in the ported SDL. This is done with the objcopy --redefine-syms command and the file redefines.txt. This renaming is transparent on the source code level, but might cause confusion when debugging.

Control flow

Execution begins in main.c:plugin_start(), which spawns a new Rockbox thread which your program's my_main() is run (this allows allocating a larger stack than default).

There are several rockbox-specific drivers in SDL, located in apps/plugins/sdl/src/{audio,thread,timer,video}/. The video driver is likely of the most interest to a game port, as it in fact controls button input as well (i.e. which keycodes are generated by your player's buttons).

Video driver

The video driver is located in src/video/rockbox. The video driver handles not only video, but also button input (probably not good design, but pfft... whatever). The driver additionally handles rotation of the screen and keymap.

The driver accepts any size video mode, but only the device's native pixel format is supported (see the driver code for more). The RGB565SWAPPED pixel format is also supported, but it relies on a final swapping step done in the update routine which adds some overhead, as I don't believe non-contiguous color channels are supported by SDL.

Special screen sizes

There are two special cases for the video mode, which offer performance benefits:

1. requested_h is LCD_HEIGHT && requested_w is LCD_WIDTH: In this case, writes are done directly to the LCD framebuffer and is called direct mode. This is very fast, and is likely what you want. To trigger this, make sure the program you are porting requests a screen size of LCD_WIDTHxLCD_HEIGHT when calling SDL_SetVideoMode.

2. requested_h is LCD_WIDTH && requested_w is LCD_HEIGHT: The driver detects this case and automatically rotates both the keymap and framebuffer by 90 degrees. This mode requires a copy, so it is not as efficient as direct mode. This is useful for rotating the screen in games, a la PluginPacbox.

All other screen resolutions will be scaled to LCD_WIDTHxLCD_HEIGHT transparently using simple_resize_bitmap, which incurs a performance penalty.

Audio driver

Our audio driver supports only 16-bit signed integer PCM audio, with stereo interleave (even indexes: left channel, odd: right). Any hardware-supported sample rate is supported by the audio driver. Attempting to set an unsupported rate will lead to severe audio distortion. To simplify things for games, there is a global RB_SAMPR define which defaults to a reasonable frequency which should be supported (currently 22.1 KHz).

You should take care that your program does not request an unsupported sample rate, as doing so will force SDL to interpolate your samples to RB_SAMPR in realtime.

The SDL audio code and the Rockbox audio driver run in a long-running thread (separate from both the Rockbox main thread and the SDL main thread) which is created through the thread driver (see below).

The audio driver works as follows:

1. Our driver tells SDL to place samples in an internally allocated mixing buffer (ROCKBOXAUD_GetAudioBuf()).

2. SDL audio thread calls the program's callback to get samples.

3. SDL calls WaitAudio to wait until the callback has completed and the samples have been fully written.

4. SDL calls our PlayAudio function to tell the driver it wants the samples currently in the mixing buffer to be played.

5. Our driver has a circular queue of audio buffers (currently 4), including the currently playing buffer, in order to reduce skipping at the cost of latency. PlayAudio finds the next free buffer and copies the samples from the mixing buffer into it. (We could probably optimize this by telling SDL to directly place samples into the next free buffer, but it's complicated).

6. Finally, an asynchronous Rockbox IRQ calls our get_more to request samples. get_more does nothing but return a pointer to the next buffer in line, which is then played.

This design avoids copying in the IRQ context. There is, however, potential for optimization by reducing copying between the mixing buffer and the circular queue and having ROCKBOXAUD_GetAudioBuf point to a free buffer instead.

Thread driver

The SDL audio driver needs to run in a separate thread. We provide a bare-bones thread driver in apps/plugins/sdl/src/thread/rockbox to allow this. This driver defaults to creating a thread with a priority of PRIORITY_BUFFERING, which is exactly what the SDL audio driver does. The thread driver is not currently used by anything other than the audio driver.

We do not support killing threads.


Porting new programs

Prerequisites

When deciding to port a program, there are a couple things you should make sure of first:

  • The program uses SDL 1.2 (as opposed to the more modern SDL 2.0, which we do not support).

  • The program does not rely on any outside libraries which we do not have (or which you are not willing to port).

  • You have the source code of the program and it is written in C, or you are willing to rewrite it in C (this is not impossible if the original source is C++).

  • The source code is licensed under a GPLv2+-compatible license. If not, it may still be possible to port, but it will not be merged.

  • You are able to compile and run the source code on a modern Linux machine. If not, you will find debugging to be a very painful experience.

  • The program does not rely on any hardware absent from our targets (i.e. anything that has to do with hardware rendering, etc.). This basically rules out any 3D game without a software renderer (unless you're looking to also write an OpenGL? driver, of course).

  • The program runs without requiring too much memory. This is very hard to define exactly, but the currently ported programs use about ~10-20MB of heap space when running.

Once you have verified these facts, you can begin the port process. Before you begin, keep in mind that ports of large, complex programs to run on new platforms are non-trivial: from experience, they can take anywhere from half an afternoon to get working to several months to perfect.

Porting process

The general outline of the porting process is given below. Note that this guide may not be totally accurate or complete (ask __builtin in IRC before attempting this).

1. Copy your program's sources into its own directory under apps/plugins/sdl/progs/. You can leave out any irrelevant files.

2. Create apps/plugins/sdl/SOURCES.yourprogram, modeled after SOURCES.duke. This file should have all your program source files listed.

3. Edit sdl.make by duplicating all lines referring to "DUKE3D" and editing them to refer to your program. You will need duplicate build rules as well. Make sure you are thorough and do not miss anything.

4. Rename your program's main() to be my_main(int argc, char *argv[])

5. Copy the duke3d stub loader apps/plugins/duke3d.c to apps/plugins/MYPROGRAM.c, and edit the path to point to your program. If you are porting a game, you just need to change "duke3d.ovl" to "myprogram.ovl." If it's in a different category, you will need to change the directory as appropriate.

6. Add your program to apps/plugins/CATEGORIES.

7. Try compiling Rockbox.

8. Fix any errors or suspicious warnings generated in step 7. Repeat until the program compiles and links successfully. This will take many compilation cycles.

9. Test and debug.

Common issues when porting

Unaligned accesses

ARM cannot do unaligned loads correctly. Reading an unaligned value will give undefined behavior. SDL currently builds with -Wcast-align to catch these errors -- fix them.

The SDL runtime enables the ARM processor's alignment trap, which will trigger a data abort whenever an unaligned read is attempted. However, a data abort will likely render the LCD driver useless, preventing any debug information from being output through the screen (the usual backtrace). I devised a workaround to use the ipod6g's piezo speaker (which requires very little code to access) to log the program counter and address of the fault in binary via high/low tones. To use this, cherry-pick the linked patch. Upon a crash, the device will beep out the PC, fault address, and type of fault in binary from least-signficant to most-significant bit.

Filesystem paths

Rockbox has no concept of relative paths or a "working directory". All paths must be absolute (i.e. begin with "/").

Missing functions

The SDL library includes a set of POSIX-like wrapper functions which tries to make programs happy. They are not complete.

The usual procedure is to #define symbols to what you would like them to be, in include/SDL_config_rockbox.h, and add the corresponding wrapper function in wrappers.c.

Simulator builds

The UI sim does not play well with the default settings. It is still possible to run Duke (and other SDL programs) under the UI simulator, by enabling it in SUBDIRS, using SDL threads (pass --sdl-threads to tools/configure), and compiling for 32-bit (pass --32-bit to tools/configure). This solves the Thread creation failed. Retryingmake_context(): Cannot allocate memory issue when running in the sim. (The need for 32-bit is only due to Duke3D?, not a limitation of the SDL library itself.)

In summary, to build for simulator, run:

tools/configure --32-bit --sdl-threads

-- FranklinWei - 15 Jun 2019
r9 - 05 Aug 2019 - 04:09:59 - FranklinWei

Copyright by the contributing authors.