Native Client

How to Port SDL Games to Native Client

by Robert Muth

Simple Directmedia Layer (SDL) is a popular library that many games and applications use to access sound and video capabilities on end-user machines. Native Client bindings for SDL have recently become available on naclports; thus it is now possible to port SDL-based games to Native Client. This article describes how to complete such a port. The focus of the article is on writing the glue code for fusing your game with PPAPI (the bridge between Native Client modules and the browser, also known as "Pepper"). Other important aspects, such as how to load resources and files, are covered in other articles listed in the Links section.

What SDL components are supported?

The SDL bindings for Native Client currently support the following components:

  • 2D graphics (SDL_INIT_VIDEO)
  • audio (SDL_INIT_AUDIO)
  • input events (mouse, keyboard)
  • timer events (SDL_INIT_TIMER)

At present, the SDL bindings for Native Client do not support the following components:

  • SDL_INIT_JOYSTICK
  • SDL_INIT_CDROM

Step 1: Install the Native Client SDK and the SDL bindings for Native Client.

In order to port an SDL-based game to Native Client, you must:

Step 2: Modify the main() function in your game's code.

Native Client modules are event-driven and do not use main() as an entry point. Thus, you must rename the main() function to something like game_main().

You must also move the initialization of SDL out of main() and into your new PPAPI glue code (listed below). Thus, remove the call to SDL_Init() from main(). This is a good time to check whether the SDL bindings for Native Client support the SDL components your game uses – make sure that the arguments to SDL_Init are on the list of supported components shown above.

Step 3: Write glue code to fuse your game with PPAPI.

Native Client uses PPAPI to play audio and render graphics in the browser (see the Pepper C++ reference for additional information). The Native Client port of SDL hides most of the use of PPAPI from developers, but you still need to fuse the game code with PPAPI. The code samples below illustrate how to do so. Note that the code samples use the C++ version of PPAPI. You can put the code samples in a new file, say nacl_glue.cc, which you can compile and link with the game code as described in the next section of this article.

As with all Native Client modules, your code must include a Module class and an Instance class. These classes provide an entry point into your module, and represent multiple instances of your module that could in theory be embedded into a web page. The code fragment below shows subclasses called GameModule and GameInstance:

class GameModule : public pp::Module {
 public:
  GameModule() : pp::Module() {}
  virtual ~GameModule() {}

  virtual pp::Instance* CreateInstance(PP_Instance instance) {
    return new GameInstance(instance);
  }
};

namespace pp {
Module* CreateModule() {
  return new GameModule();
}
}  // namespace pp

The function pp::CreateModule() is actually the only real entry point into your module; PPAPI bootstraps all other entry points from this function. As alluded to above, in theory a Native Client module could be instantiated multiple times within the same web page; all instances would then be handled by a single process. In reality this rarely works with ported applications because of global variables and other considerations. The code fragment below explicitly guards against the creation of multiple instances:

class GameInstance : public pp::Instance {
 private:
  static int num_instances_;       // Ensure we only create one instance.
  pthread_t game_main_thread_;     // This thread will run game_main().
  int num_changed_view_;           // Ensure we initialize an instance only once.
  int width_; int height_;         // Dimension of the SDL video screen.
  pp::CompletionCallbackFactory cc_factory_;

  // Launches the actual game, e.g., by calling game_main().
  static void* LaunchGame(void* data);

  // This function allows us to delay game start until all
  // resources are ready.
  void StartGameInNewThread(int32_t dummy);

 public:

  explicit GameInstance(PP_Instance instance)
    : pp::Instance(instance),
      game_main_thread_(NULL),
      num_changed_view_(0),
      width_(0), height_(0),
      cc_factory_(this) {
    // Game requires mouse and keyboard events; add more if necessary.
    RequestInputEvents(PP_INPUTEVENT_CLASS_MOUSE|
                       PP_INPUTEVENT_CLASS_KEYBOARD);
    ++num_instances_;
    assert (num_instances_ == 1);
  }

  virtual ~GameInstance() {
    // Wait for game thread to finish.
    if (game_main_thread_) { pthread_join(game_main_thread_, NULL); }
  }

  // This function is called with the HTML attributes of the embed tag,
  // which can be used in lieu of command line arguments.
  virtual bool Init(uint32_t argc, const char* argn[], const char* argv[]) {
    [Process arguments and set width_ and height_]
    [Initiate the loading of resources]
    return true;
  }

  // This crucial function forwards PPAPI events to SDL.
  virtual bool HandleInputEvent(const pp::InputEvent& event) {
    SDL_NACL_PushEvent(event);
    return true;
  }

  // This function is called for various reasons, e.g. visibility and page
  // size changes. We ignore these calls except for the first
  // invocation, which we use to start the game.
  virtual void DidChangeView(const pp::Rect& position, const pp::Rect& clip) {
    ++num_changed_view_;
    if (num_changed_view_ > 1) return;
    // NOTE: It is crucial that the two calls below are run here
    // and not in a thread.
    SDL_NACL_SetInstance(pp_instance(), width_, height_);
    // This is SDL_Init call which used to be in game_main()
    SDL_Init(SDL_INIT_TIMER|SDL_INIT_AUDIO|SDL_INIT_VIDEO);
    StartGameInNewThread(0);
  }

}; 

For simplicity reasons, the function StartGameInNewThread(), shown below, uses polling to wait until all resources are available. In most circumstance it is possible to avoid polling and use a scheme based on PPAPI's asynchronous callbacks.

void StartGameInNewThread(int32_t dummy) {
  if ([All Resourced Are Ready]) {
    pthread_create(&game_main_thread_, NULL, &LaunchGame, this);
  } else {
    // Wait some more (here: 100ms).
    pp::Module::Get()->core()->CallOnMainThread(
      100, cc_factory_.NewCallback(&GameInstance::StartGameInNewThread), 0);
  }
}

static void* LaunchGame(void* data) {
  // Use "thiz" to get access to instance object.
  GameInstance* thiz = reinterpret_cast(data);
  // Craft a fake command line.
  const char* argv[] = { "game",  ...   };
  game_main(sizeof(argv) / sizeof(argv[0]), argv);
  return 0;
}

Step 4: Compile and link your code.

Native Client modules are currently processor-specific, which means that you must provide both a 32-bit and a 64-bit version of your module. Assuming your SDK is located at $(NACL_SDK_ROOT), you can create different versions of your module by using the two compiler settings shown below:

CC = $(NACL_SDK_ROOT)/toolchain/linux_x86/bin/i686-nacl-g++  -m32

or

CC = $(NACL_SDK_ROOT)/toolchain/linux_x86/bin/i686-nacl-g++  -m64

Note that the compiler sets the following pre-processor symbol, which you can use to enable Native Client-specific conditional compilation:

#define __native_client__ 1

Once you've compiled your game code and the PPAPI glue code (e.g., the nacl_glue.cc file described in the previous section), you can create an executable Native Client module by linking the following files:

nacl_glue.o
the PPAPI glue code discussed above
-lSDL
part of the Native Client SDL port
-lSDLmain
part of the Native Client SDL port
-lppapi
PPAPI C bindings
-lppapi_cpp
PPAPI C++ bindings
-lnosys
library with stubs for common functions like kill(), which are not available in Native Client (note that these functions will cause asserts when actually called)

If you're using autoconf-based software, you can avoid typing these file names by directing the software to the correct sdl-config, e.g.:

./configure --with-sdl-exec-prefix=$(NACL_SDK_ROOT)/toolchain/linux_x86/i686-nacl/usr

Because you renamed the main() function, the linker might get confused and report undefined symbols during the final link (this is especially true when the exact link line is not completely under your control, e.g., when using autotools/configure). In such cases you can work around the problem by using the "‑u <symbol>" option, e.g., ‑u game_main.

Note again that you must create two versions of the Native Client executable module, e.g., game32.nexe and game64.nexe.

Step 5: Create an HTML file and a manifest file.

After you have generated the 32- and 64-bit versions of your Native Client module, you must create a manifest file to tell the browser which version of the module to load based on the end-user's processor. A sample manifest file, say game.nmf, looks as follows:

{
  "program": {
    "x86-32": {"url": "game32.nexe"},
    "x86-64": {"url": "game64.nexe"}
  }
}

The manifest file is in turn referenced by an HTML file, which can be as simple as this:

<!DOCTYPE html>
<html>
<body>

<!-- Note: Attributes are passed to GameInstance::Init(). -->
<embed
  width="640"
  height="480"
  src="game.nmf"
  type="application/x-nacl"
/>

</body>
</html>

Step 6: Run your game in Chrome.

See How to Test-Run Web Applications for instructions on how to run your game.

Authentication required

You need to be signed in with Google+ to do that.

Signing you in...

Google Developers needs your permission to do that.