Re: [linux-audio-dev] API design again [MORE code]

New Message Reply About this list Date view Thread view Subject view Author view Other groups

Subject: Re: [linux-audio-dev] API design again [MORE code]
From: Paul Barton-Davis (pbd_AT_Op.Net)
Date: su loka   10 1999 - 00:07:00 EDT


>[...]
>> So the event is now queued in my local event port. What causes it to
>> become available to the engine, and thus get delivered to the plugins?
>
>The one sync point that your malicious example is trying to avoid! ;-)
>
>At some point, you have to tell the engine to "grab" the new events,
>and that will need to be a sync point of some kind... possibly a FIFO
>where the address of the "news" (first event in a linked list, or
>whatever it'll be in the end) is passed.

>In the case of audio streaming, the natural sync point is the end of
>the buffer cycle, and your thread will sleep there, or on the start
>of the cycle, if the client gets data from the engine as well.

absolutely not! one thread sleeping on another's thread activity is
death for the kind of response we want. also, since neither select nor
poll unify file descriptors with either msgsend and/or user-space
condition variables, it makes programming much harder.

but thats OK, we can still do this.

So, lets try to clarify this. Since the MIDI input thread can't mess
with the engine thread's data structures, what has to happen, as far
as I can see, is that the MIDI input thread has to tell the engine
thread to listen to its event port. So far, so good, since this is
basically what you describe above.

Next step: the engine finishes a control cycle, and sets about
preparing events to deliver to plugins for the next cycle. Presumably
it iterates over some list of event ports or FIFO or whatever that it
has been told to listen to. So far, so good.

But wait - how is the engine thread going to safely copy or otherwise
use this data when the MIDI input thread (or any other h/w-driven
thread) could modify the port's event list at any time ?

this, it seems, is where we might be able to use a lock free queue. we
have exactly one writer and one reader per queue, making them OK for
the kind of logic that Eli Brandt outlined (which was thankfully not
a Herlihy-like compare-and-swap system). as Eli pointed out, this
does rely on pointer writes being atomic and surrounding writes not
being reordered, which we can probably ensure.

so, the MIDI input thread can *append* its own event queue (if tail is
beyond head), but it cannot do anything to it the engine thread can
*pop* items from the front of the queue if the head is behind the
tail, but it cannot do anything else to it.

---------------------------------------------------

so here's some pseudo code to summarize what i understand so far:

notice that the use of '<' and '>' to test for head/tail
relations are nominal, not actual C code. if we could use fixed size
arrays of event_t's, then we could actually use '<' and '>', but this
seems unlikely to satisfy us.

MIDI input thread:
=================

     while (1) {
     
         ... set up fd_set ...
         select (...);

         timestamp = engine->timestamp ();

         if (ISSET(&readable_fds, midi_port_fd)) {

               read_midi_data_from (midi_port_fd);
               ... parse data ...

               foreach event to be generated from data {
                     event_t *ev;

                           ev = qm_alloc_event (my_event_port->heap, ...);
                    ev->timestamp = timestamp;
                    ... fill out rest of ev ...
                    if (my_event_port->events_tail >
                        my_event_port->events_head)
                             my_event_port->events_tail->next = ev;
                    } else {
                      /* what ? */
                    }
               }
         }
     }

Engine thread:
=============

     while (1) {

           lock (listen_lock); /* prevent any new ports being added
                                  while we traverse the listen list.
                                */

           foreach event_port_t subscribed to {

                event_t *current_tail;

                /* notice where the end of the event list for the port
                   is right now, so that all plugins get the same set
                   of events during this cycle.
                 */

                 current_tail = event_port->events_tail;

                 foreach port in event_port->subscriber_list
                    event_t *ev;
                      
                    ev = event_port->events_head;
                    while (ev < current_tail) {

                          /* hmm - can this ever be false ? and
                             what would we do if it was ?
                           */

                          if (port->events_tail > port->events_head) {
                                 port->events_tail = ev;
                          }
                          ev = ev->next;
                          event_port->events_head = ev;
                    }
                }
         }

         unlock (listen_lock);

         foreach plugin {
            plugin->process ();
         }

         ... remove all events from each plugin's delivery port ...
         ... global buffer allocation stuff ...

     }

and thats it.

out of this come some expanded data structures:

struct event_port_t {
       qm_heap_t *heap; /* how to allocate */
       event_port_t *engine_listen_next; /* link to engine listen list */
       event_port_t *subscriber_head; /* subscriber list */
       event_port_t *subscriber_tail; /* subscriber list */
       event_t *event_head;
       event_t *event_tail;
}

struct event_t {
       qm_timestamp_t timestamp;
       .. whatever the hell you want ...
}

and the four functions that forces another thread to take a lock on an
engine data structure:

    event_port_t engine_listen_tail = 0;
    event_port_t engine_listen_head = 0;

    engine_listen (event_port_t *port)

    {
           lock (listen_lock);

           /* possibly skip the conditional having "reserve"
              element(s) as head (and tail?) of the listen_list.
            */

           if (listen_head == 0) {
              engine_listen_head = port;
           }
           engine_listen_tail->next = port;
           engine_listen_tail = port;
           unlock (listen_lock);
    }

    engine_unlisten (event_port_t *port)

    {
            event_port_t *prev;
            event_port_t *p;

            lock (listen_lock);

            for (prev = 0, p = engine_listen_head; p; prev = p, p = p->next) {
                if (p == port) {
                   ... remove port from engine_list_next ...
                }
            }

            unlock (listen_lock);
    }

we also need:

    subscribe (event_port_t *target, event_port_t *subscriber)

    {
         /* the subscriber list is only ever read by the engine
            thread while it holds the listen_lock. so take it
            before playing with the subscriber list on the target
            port.
          */

         lock (listen_lock);

         if (!target->subscriber_head) {
              target->subscriber_tail = subscriber;
         }

         target->subscriber_tail = subscriber;

         unlock (listen_lock);
    }

    unsubscribe (event_port_t *target, event_port_t *desubscriber)

    {
         lock (listen_lock);
         ...
         unlock (listen_lock);
    }

Final note: we should come up with a new name for things like the MIDI
input thread. They are not plugins, because they run independently of
the engine's control cycle. But they are also candidates for "adding"
to the engine, not necessarily at run time, but perhaps at startup,
and certainly during compilation. One can imagine threads that handle
h/w events from various strange devices (say, the serial port, or some
A-D data acquisition card) etc.

--p


New Message Reply About this list Date view Thread view Subject view Author view Other groups

This archive was generated by hypermail 2b28 : pe maalis 10 2000 - 07:27:13 EST