Native Client

Case Study: Porting XaoS to Native Client

by Robert Muth

This article describes our experience porting XaoS, an interactive graphical exploration tool for fractals, to Native Client. Many of the porting problems we encountered are quite common, and the techniques described here should help with other similar porting efforts. Some of the background information provided here might also benefit those who are writing new Native Client applications.

Note: This document describes how we ported XaoS using tools on the Linux platform. The resulting code runs in the Google Chrome browser on all currently supported Native Client platforms (Windows, Mac, and Linux).

Introduction

Here is the XaoS tool in a nutshell:

  • open-source; approximately 47,000 lines of C code
  • math-intensive; requires high performance to allow responsive, real-time zooming of fractals
  • does not have any additional mandatory dependencies—the tool even comes with its own GUI that needs little more than a bitmap to draw into

There are a number of complications that our port has to deal with:

  • XaoS assumes it is driving user interaction—that is, it is explicitly asking for user input events. This "pull" model is contrary to the PPAPI model which is designed to push events into the application.
  • Native Client currently does not support making PPAPI calls on any other thread besides the main one. This makes it slightly harder to accommodate screen updates.
  • Native Client does not support the notion of the function main().
  • Some changes to the underlying Autotools configuration files are necessary.

Here are a few caveats before we dive into the details of the port:

  • XaoS supports the use of files for various purposes but also works without them. Hence we completely ignore the issue of reading and writing files in this port.
  • We limit user interaction to mouse events. Adding keyboard support is straightforward but is omitted from this article.
  • We cut some corners to keep the code changes small but do not introduce any busy waiting loops.
  • XaoS does not interact with the HTML page it is embedded in—that is, there is no DOM tree manipulation or JavaScript invocation.

Basic (Autotools-related) configuration changes

Native Client defines its own platform which is advertised to Autotools by small changes to config.sub. A similar change will be necessary for almost all ports of Autotools-based applications and libraries. We also add a new user interface backend. The corresponding directories and files are introduced in configure.in and are discussed in detail below.

A number of special defines are added to src/include/config/config.autoconf:

#ifdef __native_client__
#define HAVE_GETTIMEOFDAY 1
#define HAVE_UNISTD_H 1
#define MAIN_FUNCTION original_main
#define NOASSEMBLY
#endif

where

__native_client__
Is a symbol that is always TRUE when compiling using the Native Client toolchain.
#define MAIN_FUNCTION original_main
This allows us to hijack main(), since the PPAPI model does not support the notion of a traditional main()— more on this later.
#define NOASSEMBLY
While Native Client supports assembly language as input, it does require extra work, so we ask configure not to use assembly language.

Running configure

To enable all the changes from the previous section, simply run the following:

autoconf

The actual configure invocation looks like this:

export CC=<point to SDK compiler>
...
export LIBS="-lppruntime -limc -lplatform -lpthread -lgio -lsrpc -lstdc++ -lm -u PPP_GetInterface \
  -u PPP_ShutdownModule -u PPP_InitializeModule -u original_main"
./configure\
     --with-png=no\
     --host=nacl\
     --with-x11-driver=no\
     --with-sffe=no
ppruntime

This command selects nacl as the target configuration platform and also tells the linker to include certain libraries. We need to specify -lstdc++ because, although XaoS is a C program, the ppruntime library (Pepper) is largely written in C++.

We also make sure that certain symbols, specified via -u, are linked into the final executable. The symbols starting with PPP_ are entry points that must be present in all Native Client executables. original_ main is the new symbol for main. Usually, the presence of ppruntime will force those symbols to be linked in automatically, but XaoS only lists libraries (not object files) on the command line for the final link. Combined with the renaming of main, the linker gets confused without this workaround.

New UI backend using PPAPI

XaoS supports a variety of different user interface backends. It does so by abstracting away the essential functionality into a fixed set of functions. A backend must implement these functions and copy pointers to them to a well-known place.

The Native Client/PPAPI backend is implemented in the following files:

ppapi.c does all the heavy lifting, while ui_nacl.c interfaces with XaoS.

ui_nacl.h exports the following four functions from ppapi.c to be used by ui_nacl.c:

/* Get screen width and height */
int GetWidth();
int GetHeight();
/* flush video buffer to screen */
void CopyImageDataToVideo(void* data);
/* get next ppapi event to process, may block if wait == 1 */ 
int GetEvent(struct PP_InputEvent* event, int wait);

Discussion of ppapi.c

Like all PPAPI applications, ppapi.c exports three mandatory functions:

PP_EXPORT int32_t PPP_InitializeModule()
used to initialize the application. It receives an argument for importing APIs callable by the application.
PP_EXPORT void PPP_ShutdownModule()
used to close the application.
PP_EXPORT const void* PPP_GetInterface()
used to export APIs callable by the browser.

In our implementation, PPP_InitializeModule() imports four API families: PPB_Core, PPB_Instance, PPB_Instance, PPB_Graphics2D, which we will not discuss further. Details can be found in the header files.

Note: The "B" in PPB stands for "browser" and indicates that these APIs are provided by the browser. Compare this with the PPP_xxx functions provided by the application, where "P" == "plugin" == application.

In our implementation, PPP_GetInterface() exports the following functions, which the browser uses to drive the application:

DidCreate()
This function is called to instantiate an application. In theory, each application could have several instantiations, if it occurs multiple times on one page. In practice, this will rarely work with legacy programs since all instances would share the same global variables. For the XaoS port, we simply do not allow multiple instances. While this function is typically used to initialize an application, we deviate from the model and delay initialization until DidChangeView() is called.
DidDestroy()
This function destroys an instance of the application.
DidChangeView()
This function is called when the application becomes visible and/or the visible area changes in any way. We use this call to initialize the application, since this is where the window dimensions are first known. (As a simplification, we assert that this function is called only once, hence resizing is not supported.)
DidChangeFocus()
This function is called when the application gains or loses focus.
HandleInputEvent()
This function is called to push input events (keyboard, mouse, etc.) into the application.
HandleDocumentLoad()
This function is not used in this application.
GetInstanceObject()
This function is not used in this application.

Dealing with main()

As mentioned earlier, PPAPI does not support the traditional main() function, and hence we have renamed the main() function in XaoS to original_main(). We spawn this function in its own thread very late in DidChangeView() after we set up all other data structures, as follows:

static void* ThreadForRunningXaosMain(void* arg) {
  char* argv[] = { "xaos", 0};
  original_main(1, argv);
  return 0;
}

...
DidChangeView() {
...
   pthread_create(&Global.tid, NULL, ThreadForRunningXaosMain, 0);
...
}

Dealing with events

Since the event models used by PPAPI and XaoS are quite different, we use a simple queue to decouple them:

static struct {
  pthread_mutex_t mutex;
  pthread_cond_t condvar;
  int tail;
  int num;
  struct PP_InputEvent queue[kMaxEvents];
} EventQueue;

Events pushed into the application by PPAPI are handled as follows:

static PP_Bool HandleInputEvent(PP_Instance instance,
                                const struct PP_InputEvent* event){
  pthread_mutex_lock(&EventQueue.mutex);
  if (EventQueue.num >= kMaxEvents) {
    NaClLog(LOG_ERROR, "dropping events because of overflow\n");
  } else {
    int head = (EventQueue.tail + EventQueue.num) % kMaxEvents;
    /* structure copy */
    EventQueue.queue[head] = *event;
    ++EventQueue.num;
    if (EventQueue.num >= kMaxEvents) EventQueue.num -= kMaxEvents;
    pthread_cond_signal(&EventQueue.condvar);
  }

  pthread_mutex_unlock(&EventQueue.mutex);
  return PP_TRUE;
}

Event consumption by XaoS is facilitated using the following helper:

int GetEvent(struct PP_InputEvent* event, int wait) {
  int result = 0;
  pthread_mutex_lock(&EventQueue.mutex);
  if (EventQueue.num == 0 && wait) {
    pthread_cond_wait(&EventQueue.condvar, &EventQueue.mutex);
  }

  if (EventQueue.num > 0) {
    result = 1;
    *event = EventQueue.queue[EventQueue.tail];
    ++EventQueue.tail;
    if (EventQueue.tail >= kMaxEvents) EventQueue.tail -= kMaxEvents;
    --EventQueue.num;
  }
  pthread_mutex_unlock(&EventQueue.mutex);
  return result;
}

Dealing with screen updates

Again, we have to work around some difference in the way PPAPI and XaoS see the world. Xaos wants to refresh the screen on its own terms at arbitrary times, while our current PPAPI implementation allows refreshs only when PPAPI has granted "control" to the application.

To solve this problem, we introduce a global screen buffer which, as far as XaoS is concerned, is the "screen." We blit this buffer periodically into the actual (PPAPI) screen.

static struct {
  pthread_mutex_t flush_mutex;
  int width, height;
  void* image_data;
  int dirty;
  PP_Resource image;
  PP_Resource device;
} Video;

This simple helper is used by XaoS to update the screen:

void CopyImageDataToVideo(void* data) {
    pthread_mutex_lock(&Video.flush_mutex);
    Video.dirty = 1;
    memcpy(Video.image_data,
           data,
           Video.width * Video.height * BYTES_PER_PIXEL);
    /* do not let anybody write into video buffer while flush in progress*/
    pthread_mutex_unlock(&Video.flush_mutex);
}

The following functions implement the periodic refresh using PPAPI:

struct PP_CompletionCallback ScreenUpdateCallback = { ScreenUpdateCallbackFun, NULL };

static void ScheduleScreenRefresh() {
  Global.if_core->CallOnMainThread(kRefreshInterval, ScreenUpdateCallback, 0);
}

static void FlushCallbackFun(void* user_data, int32_t result) {
  /* it is now safe to use the video buffer */
  Video.dirty = 0;
  pthread_mutex_unlock(&Video.flush_mutex);
  ScheduleScreenRefresh();
}


struct PP_CompletionCallback FlushCallback = { FlushCallbackFun, NULL };

void ScreenUpdateCallbackFun(void* user_data, int32_t result) {
  if (!Video.dirty) {
    ScheduleScreenRefresh();
    return;
  }

  pthread_mutex_lock(&Video.flush_mutex);
  
  struct PP_Point top_left = PP_MakePoint(0, 0);
  Global.if_graphics_2d->PaintImageData(Video.device,
                                        Video.image,
                                        &top_left,
                                        NULL);
  Global.if_graphics_2d->Flush(Video.device, FlushCallback);
} 

The Video struct is initialized and the initial refresh is scheduled by the code below which is called from DidChangeView() just before we spawn original_main:

static InitScreenRefresh(PP_Instance instance,
                         const struct PP_Size* size) {
  NaClLog(LOG_INFO, "initialize screen refresh\n");
  Video.width = size->width;
  Video.height = size->height;

  NaClLog(LOG_INFO, "create PPAPI graphics device\n");
  Video.device = Global.if_graphics_2d->Create(instance,
                                                size,
                                                PP_FALSE);
  CHECK(Video.device != 0);
  NaClLog(LOG_INFO, "create PPAPI image");
  CHECK(Global.if_instance->BindGraphics(Global.instance, Video.device));
  Video.image = Global.if_image_data->Create(
    instance, PP_IMAGEDATAFORMAT_BGRA_PREMUL, size, PP_TRUE);
  CHECK(Video.image != 0);
  NaClLog(LOG_INFO, "map image into shared memory\n");
  Video.image_data = (void*)Global.if_image_data->Map(Video.image);
  CHECK(Video.image_data != NULL);
  NaClLog(LOG_INFO, "map is %p\n", Video.image_data);

  /* assert some simplifying assumptions */
  struct PP_ImageDataDesc desc;
  Global.if_image_data->Describe(Video.image, &desc);
  CHECK(desc.stride == size->width * BYTES_PER_PIXEL);

  pthread_mutex_init(&Video.flush_mutex, NULL);

  ScheduleScreenRefresh();
} 

Conclusion

This port took about two days to complete, with most of the time spent debugging. Familiarity with the Autotools suite was very helpful, and we expect this to hold for many other ports as well.

We benefited greatly from a previous port to Native Client’s older NPAPI-based plugin. The GUI abstraction layer in the original application also facilitated this port. In general, the closer your application is to a POSIX-like environment, the easier the Native Client porting process will be.

Authentication required

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

Signing you in...

Google Developers needs your permission to do that.