Displaying Newsletter
Issue 36, 21 Feb 2003


  In This Issue:
 
Beatrice: Messages and the Storage Kit by Niels Reedijk 

Welcome back to yet another article of my hand. If you didn't catch the last one, it is still in the archive. Of more importance is that I will be speeding up a bit. The first few issues I have dedicated to getting started. I have also shown you how you should begin a BeOS application. However, every application has its special needs, and unfortunately I can't quite cover them all.

Luckily there are still quite a few concepts we need to conquer before I can let you loose into the wild. First of all, there is the concept of messages, which will be discussed in this issue, as well as the next. Secondly we will need to go over threads. The latter might send some chills through the spines of some, however, using threads in BeOS is easy and efficient, and as soon as you have opened the concept of them, you are ready for some genuine BeOS action.

So what are we going to do to Beatrice this time? First of all a note to people who have downloaded the source and looked through it. There was a CVS class there. You might have noticed I have not yet used it. The reason it is there is that I needed some place to play with opening a terminal app that doesn't have a BApplication object. Normally it is quite easy to spawn other BeOS apps through the BRoster (if you have some spare time on your hands, look through it). However, it is impossible to do that to terminal applications. Also I needed to catch the terminal output. The CVS class is not in the zipfile this time.

What we are going to use this week is the storage kit. We have already gotten acquainted with the Interface and the Application kits, now we will change to the Storage Kit. This kit deals with all aspects of files and directories and how they are stored on our disk. We can access files with it, and we can also 'watch' directories for changes. This means that we will get notifications when, for example, files change in a directory, or files are added to this directory. You can imagine that this suits all sorts of purposes in a wide range of applications.

We will also start using BMessages. Messages are an important concept in programming with the BeOS interface and we can approach many difficult programming problems with them to provide a proper solution. So let's get started!

Menu items and messages

An important aspect of a GUI is the interaction between two objects. We have, for example, the window and the button in it. The window contains the functionality of what should happen when the button is clicked. To let the window know that the button is clicked, the button object should call a method of the window directly. However, because this means that virtually every GUI element should be subclassed, a far more powerful means has been designed: messages.

In order to have a messaging concept you need a few things. First of all, you need a class that is the message itself. It must be possible to have different types of messages. It should also be possible that messages contain additional information, in the form of variables, to give additional parameters to the receiver. All of this functionality is encapsulated in the BMessage class.

You would also need a class that would know how to act on the messages it receives. This is done by BHandler.

Last of all, you would need a class that knows how to receive messages and that takes care of actually acting based on the type of message. This is done by BLooper. This class creates a seperate thread that waits until messages are received and then sends them to the BHandler.

The messaging architecture sounds fairly abstract, however, by giving concrete examples I will try to show how exactly messages work. Actually, we have been working with messages from the beginning on. Do you remember the BeatriceMainWindow::QuitRequested() method? Here it is:

 
bool BeatriceMainWindow::QuitRequested() 
{ 
    be_app->PostMessage(B_QUIT_REQUESTED); 
    return true; 
} 

If you look at BApplication, you will see that it inherits both BLooper and BHandler. What we are doing in this method is actually sending a message, thus we are using the BLooper functionality. You will see that the BLooper::PostMessage(BMessage *) method puts the message passed on into the message queue. The line also introduces you to another concept called the message type. Because it actually makes sense to have different messages, every message gets an identifier. This identifier is a unsigned int, in BeOS terms an uint32. The B_QUIT_REQUESTED constant is a number that always indicates that the message is a Quit message. What happens is that BApplication receives the message, then processes it as a call to quit. It then deletes all the windows and ends the message loop.

I also have extended the menu bar to contain another entry labelled 'Open', which can be used to select a CVS repository. Let's look at the code of the menubar.

 
1    //File Menu 
2    m_filemenu = new BMenu("File"); 
3    m_menubar->AddItem( m_filemenu ); 
4    m_openaction = new BMenuItem("Open", new BMessage( MENU_OPEN ) , 'O' ); 
5    m_filemenu->AddItem( m_openaction ); 
6    m_openaction->SetTarget(this); 
7    m_quitaction = new BMenuItem("Quit", new BMessage(B_QUIT_REQUESTED), 'Q') 
8    m_filemenu->AddItem( m_quitaction ); 
9    m_quitaction->SetTarget(this); 

NOTE: The line numbers aren't present in the real code!

In line four we create the new action. The second argument (which you can compare to line seven) is the message we would like to receive when the menu option is chosen. Note that the MENU_OPEN constant is defined by me in Constants.h as const uint32 MENU_OPEN = 'MFop' . If you define all your constants this way (with four characters closed in by single quotes), you can be pretty sure that your constants are unique in your application. Also note line six where we say that the message should be send to 'this', so to this window.

So, what happens when the user selects the Open menu entry? Every class that inherits BHandler has the void BHandler::MessageReceived( BMessage * ) method. This virtual method can be overridden by us to check if the messages send to the object are perhaps some of our own defined so that we can act on them. Look at the example of BeatriceMainWindow:

 
void BeatriceMainWindow::MessageReceived(BMessage *message) 
{ 
    switch(message->what) 
    { 
        case MENU_OPEN: // The open action was chosen 
            m_openpanel->Show(); 
            break; 
        default: // Else see if BWindow can handle it 
            BWindow::MessageReceived( message ); 
    } 
} 

You can see that we use a switch statement to determine what message we have here. The what variable of a BMessage is the public uint32 that determines its type. If the message states a MENU_OPEN message, then do the appropriate thing (what it is I will tell later), else let the default BWindow::MessageReceived go over it. This last thing is important. There will be other messages sent to your window that need to be processed in order to make your window functional, so don't forget to add a default case.

There are more advanced tricks to be played with messages, but we'll do that next time when we will be implementing some actual CVS processing code!

The Storage Kit: Some concepts

The second topic of this issue is the storage kit. In the BeBook there is a nice introduction on file systems, so if you aren't at home with file systems, read it. The concepts used in this article will not go beyond the basic knowledge of files and directories. But the way of thinking in BeOS is a bit different than you might be used to, so I will explain some pieces to you.

What we need to do is the following. First of all, we need to open a directory that we can process. Secondly, we need to check if the directory contains a CVS subdirectory (this CVS directory is present in all repositories). We use BDirectory and BEntry for this. Finally, we need to build the listview. But before I will show you the implementation, we first need to get a grip on entries and nodes.

Entries are references to the physical hard disk. Entries are related to the path. So the file /boot/home/file has an entry, as well as the directory /boot/home/config. The contents of the file and directory can be accessed via nodes. This may sound unclear, but you use nodes to change files. You also need nodes to change the contents of directories (for example to create new files).

The 'Open' Dialog

BeOS has a very convenient way of opening files for the users. All 'open' dialogs look and behave the same. For developers it means that they get a free functional dialog that needs little tweaking. The class we will be using is the BFilePanel class. Here's the prototype for the constructor:

 
BFilePanel(file_panel_mode mode = B_OPEN_PANEL, 
        BMessenger* target = NULL, 
        entry_ref *panel_directory = NULL, 
        uint32 node_flavors = 0, 
        bool allow_multiple_selection = true, 
        BMessage *message = NULL, 
        BRefFilter *filter = NULL, 
        bool modal = false, 
        bool hide_when_done = true) 

It is a pretty tweakable class, and it would take too long to explain all the options. Instead you can look it up in the BeBook. I have added a BFilePanel object to the BeatriceMainWindow class. Here's the part of the constructor that initialises it:

 
//Construct the open dialog 
m_openpanel =
new BFilePanel(B_OPEN_PANEL,     // It's an open dialog 
               NULL,             // We want be_app to receive messages 
               NULL,             // No specific wishes for where the dialog starts 
               B_DIRECTORY_NODE, // You only can select directories 
               false,            // No multiple selection 
               NULL,             // We want all dirs 
               NULL,             // No custom messages 
               true,             // Modal panel 
               true);            // Hide when done 

We have instructed it to be a B_OPEN_PANEL, the message that contains what directory is selected is sent to be_app the user will only be able to open directories, with single selection, we will not filter out any directories, we want the ordinary B_REFS_RECEIVED message to be sent, we want a modal panel (e.g. the user can't do anything else with the application while the open panel is on screen) and the panel should hide when the user has selected something and the message is sent.

As you have seen above the panel is shown when the user selects 'Open' from the file menu. The user selects a directory and a message is sent to be_app. Our BeatriceApplication knows the B_REFS_RECEIVED message and it will automatically pass it on to the virtual void RefsReceived(BMessage *message) method. This method is also called when the user drops a file on your app. So this method is rather interesting. Look at the implementation I have:

 
void BeatriceApplication::RefsReceived(BMessage *message) 
{ 
    int32      refNum; 
    entry_ref  ref; 
    status_t   err; 

    refNum = 0; 
    do { 
        err = message->FindRef("refs", refNum, &ref); 
        if (err != B_OK) 
            return; 
        m_mainwindow->SetRepository(&ref); 
        refNum++; 
    } while (true); 
} 

The message contains data on what references it carries. Messages may carry more references. What happens in this loop is that entry_refs are taken out of the message and that these are passed on to a method in our class that is named SetRepository(). What happens is that every reference is passed on to that same window. There is a huge side note you need to take with this, because this implementation is quite clumsy. It makes absolutely no sense to have the window open all references one after another, discarding the previous, in other words, it is futile to pass all the references on because only the last one will be displayed. But I will introduce a multiple window architecture soon, so stay put.

Using BDirectories and BEntries

After the user has chosen the directory, the open window will hide again. The next time the user selects Open, it will be shown again. I bet you can guess the bigger picture here. But what, of course, is the most interesting here is what BeatriceMainWindow does with this. Prepare yourself, because here she comes:

 
void BeatriceMainWindow::SetRepository( entry_ref *ref ) 
{ 
1    m_dir.SetTo( ref ); 
2    if ( m_dir.InitCheck() != B_OK ) 
3    { 
4        m_dir.Unset(); // Clear pointer to repository 
5        BAlert *alert = new BAlert( "Error opening repository" , 
6                    "The repository you specified is not a valid directory" , 
7                    "Cancel" ); // Create Alert that it failed 
8        alert->SetShortcut( 0 , B_ESCAPE ); // Map the escape key to the cancel button 
9        alert->Go(); // Set off the alert 
10       return; 
11   } 
12 
13   // Check if the CVS subdir is present 
14   BEntry direntry; 
15   if ( m_dir.FindEntry( "CVS" , &direntry , false ) != B_OK ) 
16   { 
17       m_dir.Unset(); // Clear pointer to repository 
18       BAlert *alert = new BAlert( "Error opening repository" , 
19                   "The repository you specified is not a CVS repository" , 
20                   "Cancel" ); // Create Alert that it failed 
21       alert->SetShortcut( 0 , B_ESCAPE ); // Map the escape key to the cancel button 
22       alert->Go(); // Set off the alert 
23       return; 
24   } 
25   else 
26       direntry.Unset(); // Clear open pointer to CVS dir 
27 
28   // Set the title of the window based on the repository 
29   BString windowtitle = "Beatrice: "; 
30   char name[B_FILE_NAME_LENGTH]; 
31   m_dir.GetEntry( &direntry ); 
32   direntry.GetName( name ); 
33   windowtitle.Append( name ); 
34   this->SetTitle( windowtitle.String() ); 
} 

This method accepts an entry_ref. This is a struct that is the basic type of an entry. BEntry is a wrapper around an entry_ref that adds some functions to it. The entry_refs we receive can be virtually anything. Users can only select directories using the open dialog, however, they can drop anything on our icon using the tracker, so we need to check if the entry we receive is actually a directory. What happens is the following. In line one we create a BDirectory based on the entry_ref. BDirectory is a node that handles the contents of directories. It is logical that the object only constructs if the entry_ref is pointing to a directory. Therefore in line two we call status_t BDirectory::InitCheck() to check if the object constructed properly. If it did not, we notify the user using the BAlert class, the explanation of which is beyond the scope of the article.

Another thing that need to be checked before we can say it is a valid repository, is if there is a CVS subdirectory present. In line fourteen I create a BEntry. We don't need a BNode, because we only need to check if it is present, and not what content it has. I use the BDirectory::FindEntry to check if the entry exists. If it doesn't, in lines seventeen through twenty-four we tell the user the repository is invalid. If it is valid, in line twenty-six we clear the entry so that we can use it again.

In lines twenty-eight through thirty-four we set the title of the window to the leafname (which is the name behind the slash) of the repository. We need to prefix this with "Beatrice: " and in order to avoid some dirty char calculations, I have used the BString class to do all this without a big hassle. Because BDirectory is a node, we need to get its BEntry in order to determine its name. Normally a BNode doesn't know what entry it has, but BDirectory is an exception for some obvious reasons (which I wasn't able to find out). I get the entry with the BDirectory::GetEntry() method. Then I called BEntry::GetName() to fetch the name of the entry. Please note that I used a character array with the length of B_FILE_NAME_LENGTH, because that's the longest a filename can get, plus one for the termination character. Finally, I set the window title using BWindow::SetTitle() .

This introduction to the storage kit is a meagre one, however, you will have some sort of idea what you can do with it, and I haven't even discussed the parts to change a file. We will get to that later, meanwhile try to understand what I have shown you this time.

Concluding

As always, here's a short list of what we have done this time:

  • Messages are objects that are transfered between other objects in order to notify eachother of something
  • Messages have a constant that describes them: it's a uint32 directly accessible by BMessage::what
  • Messages can contain data
  • BHandler is the base class of all objects that handle messages
  • Subclasses of BHandler should reimplement the BHandler::MessageReceived( BMessage * ) method
  • BLooper is the base class of all objects that process messages
  • To post messages to a BLooper or a BLooper derative, use the BLooper::PostMessage( BMessage * ) method
  • BApplication is a BHandler and a BLooper at the same time
  • The B_QUIT_REQUESTED message is the message with the B_QUIT_REQUESTED constant, which implies that it asks the window or the application to quit
  • BMenuItem constructors take as the second argument the message it will need to send
  • Using BMenuItem::SetTarget( BLooper *) you can set an appropriate target where the message will be sent to when the item is selected.
  • The Storage Kit implements all functionality to work with files and filesystems
  • The BFilePanel class can be used to provide an open dialog for the users
  • Entries represent the location of a file or directory
  • Nodes represent the data in a file or directory
  • BDirectory represents the node of a directory
  • You can set the window title with BWindow::SetTitle( char * )

That's all for today. As always, you can email me. Source can be found here

 
Introduction to MIDI, Part 2 by Matthijs Hollemans 

This second part of my introduction to MIDI programming on (Open)BeOS is about timing. If you read part 1 and tried out the demo app, you may have noticed that playback of the notes really lagged behind if you quickly pressed several keys in a row. That is bad, because we want MIDI processing to be as real-time as possible. This article explains where the lag comes from, and shows you how to prevent it. Be warned: some of this discussion goes into gory low-level details and the guts of the Midi Kit, but that is the price you pay for low latency MIDI performance. Actually, it ain't all that bad.

How events are consumed

As you'll recall, the BeOS Midi Kit lets you create two types of MIDI endpoints: producers that send out MIDI events (comparable to a MIDI Out socket) and consumers that receive these events (a la MIDI In). So how does a consumer, aka the BMidiConsumer class, handle incoming MIDI events? Using Kernel Kit ports, that is.

Each consumer object has a Kernel Kit port associated with it to receive MIDI events from connected producers. A port is a low-level communications device for sending data between different applications. They are also used for sending and receiving BMessages, for example. You don't really need to know how ports work to use the Midi Kit, but you do need to understand how they affect the flow and timing of MIDI events.

In addition to opening a port, each consumer also creates a separate thread whose only task is to read incoming MIDI events from the port, and to call one of BMidiLocalConsumer's hook functions in response. As long as nothing is sent to the port, this thread blocks. When some producer sends an event to the consumer, for example "note on", the thread wakes up and calls the corresponding NoteOn() hook. After it is done with the hook function, the thread tries to read from the port again and the cycle repeats itself.

Inside the hook function, a typical consumer will first snooze_until() the performance time of the event. When that time has come, the thread wakes up again and actually "performs" the event, whatever that means. The consumer thread has a real-time priority, so needless to say, the hook functions should deal with the events as quickly as possible. (This may sound contradictory; first I'm telling you that the consumer must snooze, and now it should hurry up? Snoozing is fine because it takes up no processor time, and the real-time thread won't bog down the rest of the system while it is sleeping. It is performing the actual event that should be as fast as possible.)

Lousy timing

Now, back to our problem: what causes the lag? Obviously, while the consumer is snoozing, it won't deal with any other MIDI events that arrive at its port. Its thread is asleep and won't wake up until it is time to perform the current event. This works fine if the other pending events are all scheduled to be performed after the current one, but in our case that isn't necessarily true.

(If you are wondering where these "pending events" go in the mean time, they don't go anywhere. Kernel ports have a first-in/first-out message queue, and if the queue is full, any subsequent senders will block until their messages can be delivered. BMidiConsumers have a queue that has room for only one message. This means that at most one producer at a time can put a MIDI event in the queue, and all the others will simply block. Our EchoFilter's producer, for example, performs several SprayNoteOn()'s in a row. But what really happens is that it blocks on the second call to SprayNoteOn() until the first event has been processed by the consumer. Then it blocks on the third call until the second event is performed, and so on.)

An example: Suppose we have hooked up a (virtual) MIDI keyboard to the input of our echo filter, and the BeOS softsynth to its output. You press the "C" key on the keyboard at time 1. This event arrives at the echo filter, which passes it on to the softsynth. The echo filter also schedules three additional echo notes at performance times 2, 3, and 4. The softsynth consumer now snoozes until time 1 and performs the first event. Then it snoozes until time 2 and performs the second event. So far so good.

Imagine what happens if we now press the "D" key on the keyboard. We are still at time 2, so that sends a new "note on" event to the synth with time 2, and the echo filter adds three more echo notes with times 3, 4, and 5. But the softsynth first has to work through the "old" events. So when the consumer is finally ready to perform the "D" note, it is already time 4. Et voila, there is the lag. The "D" note on event should be performed at time 2, but there is no way to tell the consumer this. It is happily snoozing away, and there is no way to interrupt it.

Now what!?

There are two possible solutions: either consumers queue up incoming events and sort them in time before performing, or producers sort outgoing events before actually sending them. By design, the Midi Kit places this burden on the producers. Not on all producers, mind you; just on those that may spray events in the "wrong order". It just so happens that our echo filter is one of them. (In my opinion, this is a deficiency in the design of the Midi Kit. Making sure that MIDI events arrive and are processed in the proper order is really something that the Midi Kit should automatically take care of. Something to keep in mind for a future revision of the Midi Kit.)

Fortunately for you, I already have written the code to do this. The class MidiQueue is a filter object that receives incoming events, puts them on a queue (a real queue, not the kernel port thing), snoozes until it is time to perform them, and finally sends them out to any connected consumers. Of course, it sorts the events as it puts them on the queue, because that is the whole point of this exercise. Now you can simply put this MidiQueue object between your producer and your consumer, and it will automatically take care of everything.

In the EchoDemo sample project, I hook up the output of the EchoFilter to the input of the MidiQueue. Unlike before, these two endpoints are not Register()'ed, so you can't connect anything else to them. Of course, the input of the EchoFilter and the output of the MidiQueue are published. The easiest way to try this out is to connect the input of the EchoFilter to the output of a MIDI keyboard and the output of the MidiQueue to InternalMidi. You'll find that you now can now hammer on the keyboard all you want and still get near-instant playback. Woohoo!

To be honest, my implementation of MidiQueue is not ideal. A more complete implementation would give the queue a finite length to prevent producers from running wild and generating thousands of events in advance. I also used a rather simplistic method to find the next event to perform. MidiQueue's producer launches a thread that sleeps for a bit, checks if it is already time to perform the first event in the queue, and repeats that ad infinitum. (The sleep is short enough not to be noticeable, and long enough not to drag the CPU too much.) A better solution would be to snooze_until() it is time to perform the top-most event on the queue. If in the mean time another event is received that needs to be performed earlier, the snooze could be interrupted by sending the thread a SIGCONT signal.

More goodies

I made several other changes to the source code as well. Last time I just wanted to introduce the various Midi Kit concepts without cluttering the code too much and scaring you off. Now that you already more-or-less know how the Midi Kit works, I have taken the liberty to add some goodies. First of all, the EchoFilter now extends from a base class called SimpleMidiFilter. This class provides several convenient facilities for making your own MIDI event filters. In addition, if you fire up PatchBay you will see that the EchoFilter's endpoints now have pretty little icons. Anything to please the eye.

Download the sample code for this article here. The various utility classes that are used by the sample project are part of MidiUtil, a package I put together especially for making MIDI programming easier. You can get it here. And with that I conclude this short introduction to MIDI programming on the BeOS. For more info, check out the (preliminary) midi2 kit documentation. Now go and make some noise ;-)

 
Focus by Michael Phipps 

Recently, I was curious about animals' visual abilities. So I searched for some websites on animal vision. The variety of different types of eyes is really amazing. Frogs, for example, are highly drawn to movement. As one article put it, a frog could die of hunger in a room full of dead bugs because it couldn't see them. But if you threw one by him, he would nail it in mid air. Eagles have millions of cones in an area the size of the tip of a ball point pen, enabling them to spot small animals from hundreds of feet up in the air. Bees have multifaceted vision that uses the location of the sun as a naviagtion point.

Each of these animals has a visual perception that is specifically attuned to their environment. Frogs can catch rapidly moving insects. Eagles can see far away. One question that might occur to the reader is "why not have all of those advantages"? I am certainly not a biologist, but I would suspect that the animal's other systems could not handle it. Could a frog's brain handle the amount of data that a bee's eyes produces? Could an eagle, which has to focus on one prey animal afford to be distracted when it is plummeting toward the earth at a high rate of speed?

Each animal is a highly tuned machine, dedicated to its one purpose. While a human is capable of most of these tasks (sometimes with artificial enhancement), they aren't as good at it as specific animals. The fastest human is slower than the slowest healthy cheetah. The toughest human couldn't go toe to toe with a bear. A particular design will always beat a general design, if all other things are equal - skill of the designer, raw material, etc.

Operating systems, I believe, fall into the same catagory. Windows was designed, from 1.0 up through ME were designed for one thing - to put a visual shell around DOS to improve the user experience. MacOS was and is designed to be a desktop OS. These were/are focused products. Compare and contrast that with Win2K and WinXP. The same code base now must support a variety of users. From an 8 processor server down to a P133 to play Wind Surfing Willy on, the code is the same. This is not a focused product.

Sometimes the design ideals vary from user type to user type. Servers require different trade offs - performance under heavy load is more important than being lightweight, stability is far more important than performance, and throughput is more important than low latency. Linux has many of these same issues - it was originally designed for more server-like situations. When the needs of multimedia users became a more vocal part of the Linux community, patches and changes had to be introduced to reduce the latency of the system. The smart money says that there are still trade offs occuring somewhere. Writing better code can improve performance, but you can't change the laws of physics.

Where should OpenBeOS focus? Assuming that we want to take our lead from Be, Inc, we have our selection of different foci. Originally (at least, as far back as I remember), the BeBox and BeOS were to be a Geek Machine. I still have my Be pocket protector. Then there was the MacOS-alternative OS. Then the Gamer's OS (sort of - the Game Kit and the 3D kit). Then the Media OS. Then the IA OS. So we certainly have a variety of choices that we could choose from. None of them worked really well for Be, although many people have argued that Media OS was doing just fine until Be shifted their focus.

It seems to me that the place that we should focus is the desktop of the "average" home and light office worker. What would those needs be? Many of those needs are things that R5 is particularly good at: media, boot speed, reliability, low maintenance, etc. Some it is not, like games, office suite availability, etc. There is, IMHO, a need out there for a system that is easy to setup, easy to keep running, and easy to use. Windows is not really any of those things. While MacOS is much better about those things, it has other issues - it costs a lot of money to switch, unless you were about to buy a new machine, in which case it only costs too much extra money, instead.

What challenges does this present to us, the OBOS community? First, the realization that R1 is not "it". R1 will not be the release to go out and evangelize to the entire world. As has been so often brought up to me, it will be a multiple year old system, when it is brand new. There are some new and improved things (like, for example, our shiny new JPEG2000 translator), but overall, it is still what Be tried to sell > 3 years ago. R2 or maybe even R3 will be the first release where people will really say "wow - they did something new and different!".

The next challenge is to have the OS design ready. Once again, I will bang the "Glass Elevator" drum. If you want to have any voice in what > R1 looks/acts like, Glass Elevator is the focal point for that. I would like to see some really revolutionary and interesting ideas over there. Especially from people who can do some detail/design work. Some Gonx style mockups would be nice, too. Something to really blow people's minds.

Thirdly, we will need apps. Real apps - something of the kind of quality you would expect from a professional company. Fortuantely, we don't need to replicate every application ever written - that is the advantage of focus. I would bet that the right 10 apps and the right 10 games would make all of the difference.

I believe that OBOS could be the greatest server ever made. Or the greatest real time OS. Or the greatest IA OS ever made. Or the greatest desktop ever made. But I don't believe that it can be all of those things simultaneously. I believe that a single direction needs to be chosen.