Tips for Writing Your Main (select, event) Loop

By Chris Pirazzi and Michael Portuesi.

The VL source code examples we ship demonstrate several possible ways to write one's main event dispatching loop, none of which are sufficient for typical real applications. This is because typical real applications:

In additon, there are some outright bugs in some of our sample programs that happen to surface only very rarely in those programs, but may surface more commonly in real app code.

This document shows you how to roll your own main event loop in a way which will navigate you around the most common pitfalls of VL programming, and in a way which will not yank control of the main select() loop away from you. Then it shows you how to deal if you are unfortunate enough to be stuck with XtMainLoop().

This document will assume that your app creates a VL path involving memory. Much of what is said here is useful even if that is not the case.

Some of the tips described here depend on whether you are using the classic VL buffering API or a DMbuffer-based VL buffering API. Check out What Are the SGI Video-Related Libraries? for information on what that means.

Use Your Own select() Loop

You must avoid these pitfalls in writing a select() loop:

Here is some sample code using the classic buffering API which illustrates these points in action:

{
  int vlFD, vlBufferFD;

  /* open VL */
  /* set up VL path */
  /* create and associate VLBuffer of size "buffer_size" items */
  /* vlSelectEvents: mask out VLTransferComplete/VLSequenceLost events */
  /* vlBeginTransfer */

  vlFD = vlGetFD(vid->vlSvr);
  vlBufferFD = vlBufferGetFd(vid->rb);
  
  while (1)
    {
      int i;

      /* first check to see if VLEvents are available */
      while (vlPending(vid->vlSvr))
        {
          VLEvent ev;
          VC(vlNextEvent(vid->vlSvr, &ev));
          
          switch (ev.reason)
            {
            case VLTransferFailed:    
              ... /* transfer has now stopped.  deal with that. */
              break;

            /* ... other events ...*/
            }
        }

      /* now check to see if we have some data/space available */
      if (path_is_video_to_memory_path)
        {
          int nfilled = vlGetFilled(vid->vlSvr, vid->buf);
          for(i=0; i < nfilled; i++)
            {
              VLInfoPtr info;
              info = vlGetNextValid(vid0>vlSvr, vid->rb);
              assert(info); /* filled count sez there must be data */

              ...; /* use the data! */

              vlPutFree(vid->vlSvr, vid->rb);
            }
        }
      else if (path_is_memory_to_video_path)
        {
          /* classic VL buffering API has no "vlGetFillable()" operation */
          /* process at most N items */
          int nfillable = buffer_size;
          for(i=0; i < nfillable; i++)
            {
              VLInfoPtr info;
              info = vlGetNextFree(vid0>vlSvr, vid->rb);
              /* we're not sure exactly how many times we'll succeed */
              if (!info)
                break;

              ...; /* use the space! */

              vlPutValid(vid->vlSvr, vid->rb);
            }
        }

      /* now do the select() */
      {
        fd_set readset, writeset, exceptset;
        
        FD_ZERO(&readset);
        FD_ZERO(&writeset);
        FD_ZERO(&exceptset);
        
        /* video */
        FD_SET(vid->vlFD, &readset);
        FD_SET(vid->vlBufferFD, &readset);
        
        /* do FD_SETs for other agents: audio, serial, X, gfx, ... */
        
        if (-1 == select(getdtablehi(), &readset, &writeset, &exceptset, NULL))
          ...;
      }

      /* now service the events that woke up the select() */
      {
        /* at this point, we don't even have to call FD_ISSET() on any 
         * of the video fd's if we don't want to---vlPending and 
         * vlGetFilled will tell us all FD_ISSET() would and more.
         */

        /* act on FD_ISSET for other agents: audio, serial, X, gfx, ... */
      }
    }

One thing to notice is that upon entering this main loop for the first time, we call vlPending() before we select() for the first time. This is crucial. As explained by the obscure paragraph from qgetfd(3G) quoted above, there are circumstances where select() could hang forever even though VL events are available. We chose to also put the call to vlGetFilled() before select() just because it was convenient. There could very well be data/space in the buffer before the first select(), but in this case the buffer fd would unblock immediately, so it's not a big deal.

How to Get the Same Effect with XtMainLoop() in the Way

As any Motif programmer knows, Xt owns the main loop for your application, and it calls select() for you. You have little control over that. However, it is still possible for an Xt program to respond to VL events. It is also possible to handle VL events which may already be in the queue before XtMainLoop() calls select(). Finally, you can even do all of this in a ViewKit program, without munging VkApp:run() (ViewKit's wrapper around the Xt event loop).

To respond to VL events, you must register Xt callbacks for the VL event queue, and also for the transfer buffer, if your application uses one. For each, you provide a callback handler function. Here is some sample code to set up the callbacks.

void setupVLEvents()
{
    //
    // register an Xt callback for the VL's event queue.
    // EventHandlerFunc is an Xt input callback which will
    // process VL events.
    //
    videoInputId = XtAppAddInput( appContext,
                                 vlConnectionNumber(svr),
                                 (XtPointer) XtInputReadMask,
                                 &EventHandlerFunc,
                                 (XtPointer) clientData);

    //
    // Register an Xt callback for the file descriptor associated
    // with the VL buffer.  We do this instead of asking for
    // TransferComplete events.
    //
    // BufferFullHandlerFunc is an Xt input callback which will
    // take the next frame from the buffer and display it on the
    // screen.
    //
    videoBufferId = XtAppAddInput(appContext,
                                 vlBufferGetFd(buf),
                                 (XtPointer) XtInputReadMask,
                                 &BufferFullHandlerFunc,
                                 (XtPointer) clientData);

    //
    // Choose the VL events we want to listen to.  Your application
    // might have a different set of events it cares about than the
    // ones shown in this example.
    // 
    vlSelectEvents(svr, displayPath,
                  VLDeviceEventMask |
                  VLDefaultSourceMask |
                  VLStreamPreemptedMask |
                  VLStreamAvailableMask |
                  VLControlChangedMask |
                  VLControlPreemptedMask |
                  VLControlAvailableMask |
                  VLTransferFailedMask
                  );
}

void removeVLEvents()
{
    if (videoInputId != NULL) {
        XtRemoveInput(videoInputId);
        videoInputId = NULL;
    }
    if (videoBufferId != NULL) {
        XtRemoveInput(videoBufferId);
        videoBufferId = NULL;
    }
}

A sample callback handler for VL events might look something like this:

void EventHandlerFunc(XtPointer client_data, int*, XtInputId*)
{
    EventHandler();
}

void EventHandler()
{
    VLEvent ev;
    while (vlPending(svr)) {
        vlNextEvent(svr, &ev);

        switch (ev.reason) {
        case VLTransferComplete:
            break;
        case VLTransferFailed:
            transferFailed(ev);
            break;
        case VLDeviceEvent:
            deviceEvent(ev);
            break;
        case VLDefaultSource:
            defaultSourceChanged(ev);
            break;
        case VLStreamPreempted:
            streamPreempted(ev);
            break;
        case VLStreamAvailable:
            streamAvailable(ev);
            break;
        case VLControlChanged:
            controlChanged(ev);
            break;
        case VLControlPreempted:
            controlPreempted(ev);
            break;
        case VLControlAvailable:
            controlAvailable(ev);
            break;
        default:
            printf("unhandled VL event, reason %d\n", ev.reason);
            break;
        }
    }
}

The callback handler for the VL transfer buffer looks something like this:

void BufferFullHandlerFunc(XtPointer client_data, int*, XtInputId*)
{
    BufferFullHandler();
}

void BufferFullHandler()
{
    VLInfoPtr info = NULL;
    info = vlGetLatestValid(svr, buf);

    if (info != NULL) {
        // do something with the video data.  Here, we draw it to the
        // screen.
        void* dataPtr = vlGetActiveRegion(svr, buf, info);
        glRasterPos2i(originX, originY);
        glPixelZoom(1.0, -1.0);
        glDrawPixels(sizeW, sizeH,
                     GL_ABGR_EXT, GL_UNSIGNED_BYTE, dataPtr);
        vlPutFree(svr, buf);
    }
}

To make sure you properly handle VL events which may already queued before select is called, you should explicitly call the Xt event handlers immediately after starting the transfer on the path:

void startVideo()
{

    // ... other VL setup code ...

    setupVLEvents();

    // start video transfer.

    vlBeginTransfer(svr, displayPath, 0, NULL);

    // do a first pass check to handle any
    // queued events from the VL or the transfer buffer

    BufferFullHandler();
    EventHandler();
}

When Exactly Does the Buffer FD Unblock?

Classic and cross-platform buffering API:

O2 buffering API:

Caveat about Spinning, Especially for High-Priority Processes

One tricky thing about writing any program that uses select(), and in particular one that uses the VL buffer fd, is avoiding spinloops. Spinloops are accidental circumstances where your program is essentially always executing, usually because select() is always returning immediately. Spinloops are bad because they burn away CPU power on the basic overhead of processing thousands of select() system calls per second. Spinloops are really bad for non-degrading high-priority processes (see npri(1), schedctl(2), or Seizing Higher Scheduling Priority): since such processes want to run all the time, and since they run at a higher priority than all normal processes (including the X server), they essentially lock up the system, rendering it completely useless while the program is spinning.

The caveat: say your app wants to wait until there are N (N>1) items in an input buffer, and then dequeue them all at once. For example, it may want to coalesce buffers for disk writing or interleaving. Say your app selects on the buffer or event fd. After the first item arrives in the VL buffer, these fds will always unblock immediately, and your app will begin to spinloop. At the moment, the only recourse offered to a VL app is not to select() on the buffer fd when there are already items available (which you can tell from vlGetFilled()), and instead to provide select() with a timeout on the order of one field time. This is rather gross. Another gross solution is to call vlGetNextValid() or vlGetNextFree() as soon as any data or spaces become available, keeping track of the resulting VLInfoPtrs yourself. To the VL, each time you call select(), there is no data or no space, so your app will block for a reasonable amount of time waiting for the next data/space. When you have accumulated N VLInfoPtrs, you act on the data.

A similar thing happens if your app wishes to wait until there are N spaces in an output buffer. Since there is no vlGetFillable(), you have to use the second technique above---grab free data immediately and keep track of VLInfoPtrs yourself.

The real solution to this problem has been in the AL and other libraries for some time---fillpoints (see ALsetfillpoint(3)). Perhaps someday we will have fillpoints in the VL.

This caveat also applies to the DMbuffer buffering APIs. The buffer/event fd and the dmBufferGetPoolFD() fd will unblock immediately if their condition is true. The dmBufferSetPoolSelectSize() function lets you set the threshold of free DMpool space for unblocking, but this function was not implemented as of 12/16/97 (the select size was fixed at one item).

Detecting Dropped or Duplicated Data

When an input buffer overflows (the application fails to dequeue data fast enough), or an output buffer underflows (the application fails to enqueue data fast enough), or there is some device failure, video frames can be dropped on input and duplicated on output.

The VL events VLTransferComplete and VLSequenceLost indicate that one or more items of video data transferred or failed to transfer. There is not (for all VL devices) a one-to-one correspondence between these events the individual fields/frames that transferred or failed to transfer. Therefore, these VL events are not useful for determining exactly which items were dropped or duplicated.

For video to memory paths with the classic buffering API, the easiest way to detect dropped data is to look at the "sequence" field of the DMediaInfo struct returned by vlGetDMediaInfo() for a particular item of video data (a particular VLInfoPtr). Every VL device keeps a field counter which increments by one every time a field crosses an input jack of the machine (whether or not that field is successfully transferred into memory). When a transfer is successful, the VL will snap the current value of the sequence value and place it in the DMediaInfo struct of that particular VL item (for frames you get the DMediaInfo struct for the first field of the frame).

For video to memory paths with the DMbuffer buffering APIs, you can retrieve similar timestamp information from a DMbuffer using dmBufferGetUSTMSCpair(). In this case the counter value has the units of MSC, which may be fields or frames depending on VL_CAP_TYPE (see vlGetFrontierMSC(3dm)).

Therefore, by watching for jumps in the "sequence" or MSC values of successive pieces of video data, you can detect all failed transfers, you can tell exactly where they fell in the sequence of data, and you can tell exactly how many fields were dropped. This is the recommended method for detecting drops on input.

At the moment, there is no equivalent queue of DMediaInfo or USTMSCpair structures coming back to the application in a memory to video path. Perhaps one day there will be.

But it turns out that there is a second mechanism (part of the UST/MSC implementation in the VL), called vlGetFrontierMSC(), which can be used to determine all the same statistics for both input and output in a symmetric manner. The mechanism works identically for both buffering APIs. See the Lurker Page Introduction to UST and UST/MSC and the man page vlGetFrontierMSC(3dm) to find out how to detect all the failed transfers and the length of all the failures. You can also use vlGetFrontierMSC(3dm) to determine exactly which video items were dropped or duplicated, but this is more tricky and is explained in UST/MSC: Using the Frontier MSC to Detect Errors.