Issue 3-20, May 20, 1998

Be Engineering Insights: Splitting Device Drivers and Bus Managers

By Arve Hjønnevåg

The current release of the BeOS uses kernel add-ons to load device drivers. These device drivers communicate directly with the hardware to provide a standard interface for applications to use. The ISA and PCI buses can always be accessed directly by the CPU, and the kernel has built-in functions that let drivers access these.

Other buses, however, like SCSI, IDE, USB, PCMCIA, and 1394 are usually accessed through the devices on the PCI or ISA bus. If someone wants to write a driver for a device connected to one of these buses, it's preferable not to have to communicate directly with the hardware for that bus.

To help the implementation of devices connected to these buses, R4 will add the notion of loadable kernel modules that can be used to implement bus managers. A bus manager is a module that allows the driver to access a bus without detailed knowledge of the hardware that controls it. The bus manager will find modules that handle specific buses. The drivers can then scan all buses for the devices they can handle without having to know how to handle the controllers.

For R4 the IDE driver has been split up into ATA and ATAPI drivers, an IDE bus manager, and controller modules for generic PCI IDE, BeBox IDE, and Mac IDE. Not all modules have been implemented yet, but the ATA and ATAPI drivers that have been implemented are more functional than the old driver, while allowing third party developers to add support for specific controllers.

I will now show you what it takes to implement a module for an IDE controller, but keep in mind that every detail is subject to change.

IDE.h defines the interface for the module:

typedef struct {
  bus_manager_info  binfo;
  uint32 (*get_nth_cookie) (uint32 bus);
  uint32 (*get_bus_count) ();
  int32  (*get_abs_bus_num) (uint32 cookie);

  status_t (*acquire_bus) (uint32 cookie);
  status_t (*release_bus) (uint32 cookie);

  status_t (*write_command_block_regs)
    (uint32 cookie, ide_task_file *tf, ide_reg_mask mask);
  status_t (*read_command_block_regs)
    (uint32 cookie, ide_task_file *tf, ide_reg_mask mask);

  uint8 (*get_altstatus) (uint32 cookie);
  void (*write_device_control) (uint32 cookie, uint8 val);

  void (*write_pio_16)
    (uint32 cookie, uint16 *data, uint16 count);
  void (*read_pio_16)
    (uint32 cookie, uint16 *data, uint16 count);

  status_t (*intwait) (uint32 cookie, bigtime_t timeout);

  status_t (*prepare_dma)
    (uint32 cookie, void *buffer, size_t *size, bool to_device);
  status_t (*finish_dma)(uint32 cookie);
} ide_bus_info;

A module for an IDE controller exports this structure with all the function pointers pointing to the respective functions. All functions have to be implemented, but prepare_dma and finish_dma may return an error if DMA is not supported, and all device drivers need to handle this case.

binfo contains the generic bus manager module information. Currently this contains the module name, some flags and a function for initialization and uninitialization.

At initialization time, the module finds the hardware it supports, and allocates resources. Specifically it may scan the PCI bus, create areas for DMA tables, and initialize structures and semaphores used to access each bus. At closing time all resources should be freed.

The first three functions an IDE module implements are those that allow the IDE bus manager to iterate through all the buses a module handles and map them to a global bus number that the drivers will use. The functions are

Let's now look at the functions to access the bus. Since only one device can be active on an IDE bus at a given time, we define two functions that gives a driver exclusive access to the bus.

status_t acquire_bus(uint32 cookie);
status_t release_bus(uint32 cookie);

A normal implementation of these would look like this:

static status_t
acquire_bus(bus_info *cookie)
  return acquire_sem_etc(cookie->mutex,
                         1, B_CAN_INTERRUPT, 0);

static status_t
release_bus(bus_info *cookie)
  return release_sem_etc(cookie->mutex,
                         1, B_DO_NOT_RESCHEDULE);

All the following functions assume that the driver has successfully called acquire_bus.

The next four functions provide access to the IDE registers:

status_t write_command_block_regs(uint32 cookie,
  ide_task_file *tf, ide_reg_mask mask);

status_t read_command_block_regs(uint32 cookie,
  ide_task_file *tf, ide_reg_mask mask);

uint8 get_altstatus(uint32 cookie);

void write_device_control(uint32 cookie, uint8 val);

read_command_block_regs() and write_command_block_regs() allow multiple registers to be updated with one call. All these are straightforward to implement.

To do PIO data transfers we use two functions:

void read_pio_16(uint32 cookie, uint16 *data,
                 uint16 count);
void write_pio_16(uint32 cookie, uint16 *data,
                  uint16 count);

These functions read or write the data passed in from and to the IDE data register. The argument count specifies how many 16-bit words to transfer.

The last function that has to be implemented is

status_t intwait(uint32 cookie, bigtime_t timeout);

This function blocks the caller until an interrupt is received from the bus, or until the time specified in the timeout argument has elapsed.

Finally we have two functions that a driver uses to do DMA transfers:

status_t prepare_dma(uint32 cookie, void *buffer,
  size_t *size, bool to_device);

status_t finish_dma(uint32 cookie);

If DMA is not supported these functions return B_NOT_ALLOWED. If the controller supports DMA, prepare_dma() sets up the required DMA tables and prepares the controller for a DMA read or write. It is the driver's responsibility to lock the memory before locking the bus. This order is necessary since locking memory may cause disk access to occur.

You have now seen how the controller-specific part of an IDE driver can be separated from the device-specific parts. Since most PCs have motherboard IDE buses that are mostly hardware compatible, using modules for IDE controllers is not strictly necessary. It does, however provide cleaner device drivers, and an easy way to support controllers that deviate from the standard. Other bus types also exist where different controllers are incompatible, but it's not feasible for every device driver for these buses to know about different controllers.

Developers' Workshop: DynaDraw, Part One

By Michael Morrissey

Hello, everyone! I'm Michael Morrissey, the new DTS engineer here at Be. I've already enjoyed working with several of you during my first month on the job, and I hope that one day we'll meet at a developer conference or demo.

The first time I saw a Silicon Graphics machine (*), when I was a freshman in college, I was blown away. The graphics were so fast, so smooth! Those great SGI demos captivated me, and I spent hours tweaking the parameters to the programs, just to amuse myself.

Eventually, I wanted to write my own graphics programs. I looked at some of the sample code, but couldn't seem to get started. C wasn't the problem, it was the GL library—I didn't understand the building blocks necessary to writing a basic graphics program with it. Some older students who saw I was struggling set me straight and got me going.

I imagine a lot of people are in the same predicament with the Intel release of the BeOS that I was in with that SGI machine—tired of tweaking the demos and sample code, wanting to write their own code, but unsure how to start and feeling a little overwhelmed. I'd like to help newcomers to BeOS programming by building a small application which emphasizes the fundamentals but is large enough for experimentation.

The application we'll build (actually, port and modify) is DynaDraw, written originally in C and GL by Paul Haeberli of Silicon Graphics (fittingly, the person who first told me about BeOS). It's a fun little paint program which models the dynamics of a brush with mass on a paper with drag. (Don't worry, we'll skip the physics!)

DynaDraw turns your mouse into a calligraphy pen, and lets you make beautiful, smooth strokes easily. Rather than concentrating on any one aspect of the OS, we'll talk about good program structure and use objects that are common to almost all programs.

The sample code for this application can be found at:

Paul's original C source is also included in this zip file, and I highly recommend checking out his web page on DynaDraw:

When designing or porting an application, it's often easiest to write the program in stages, starting with minimal functionality, and building up from there. Keep in mind that you'll add more features later, and don't paint yourself into a corner.

For minimal functionality DynaDraw needs a window to draw in. But, since BWindow objects can't draw, we need a BView object as a child of the BWindow.

We'll want to draw with the left mouse button down. The BView API has a MouseDown() function, so if we derive a class from BView, we can override that function to handle mouse events the way we'd like.

Another important consideration is the ability to redraw the image if part or all of the view becomes invalidated, for example, if another window is on top of ours temporarily, or if we minimize and then restore our window. BViews can't redraw themselves; you have to tell them how -- and for this, we'll override the BView::Draw() function.

Something else to consider is where to put the state-specific variables for the "filter"—things such as brush mass, drag, velocity, etc. -- which were stored in the filter structure in Paul's original program. We'd like them to be as close as possible to the functions that need them -- the functions that draw (and redraw)—so we'll put them in our BView descendant. By the same logic, we'll also put the functions that operate on these variables in the same class (such as the Apply() method).

That's our minimum functionality, so let's look at what we'll need to implement it:

Returning to override BWindow::QuitRequested(): even in the simplest apps, with just one window, you need to override QuitRequested(). This is because the last window to close needs to alert the BApplication object to quit. BWindow::QuitRequested() doesn't do this automatically, because a window can't assume that it's the last one open. So we'll override QuitRequested() by creating a simple class derived from BWindow, called DDWindow:

class DDWindow : public BWindow
   DDWindow(BRect R, const char* title,
            window_type type, uint32 flags);
   bool QuitRequested();

DDWindow::DDWindow(BRect R, const char* title,
                   window_type type, uint32 flags)
  : BWindow(R, title, type, flags)
  // do nothing

  /* we're the last window open,
     so shut down the application */
   return true;

The constructor for the DDWindow doesn't really do anything but pass the arguments along to the BWindow. In the QuitRequested() method, we post a message to our BApplication object to quit. Remember, be_app is a global variable set in BApplication constructor to point to the instance of your application object.

Okay! Now on to the main() function:

int main()
  BApplication App("application/x-vnd.Be-dynadraw");
  DDWindow* W = new DDWindow(BRect(50,50,800,600),
                      B_TITLED_WINDOW, 0);

  FilterView* F = new FilterView(W->Bounds());
  return B_NO_ERROR;

This actually does an enormous amount of work for us. First, it connects us to the application sever and sets up our application identifier. Next, we create a DDWindow: the top-left corner is at (50,50) and the lower-right corner is at (800,600); the window title is "DynaDraw!"; the window has a yellow tab; and finally, the user can move, resize, close, and zoom the window. Remember, though, this window isn't displayed yet.

Next, we create an instance of our (still undefined) FilterView class, passing in an important BRect, namely, the bounds of the window it will be attached to. Then we display the window, and start the application's message loop.

The FilterView class should look like this:

class FilterView : public BView
  /* overridden functions from BView */
   FilterView(BRect R);
   void MouseDown(BPoint point);
   void Draw(BRect updateRect);

  /* state variables, formerly from the filter structure */
   float curmass, curdrag, width;
   float velx, vely, vel;
   float accx, accy, acc;
   float angx, angy;
   BPoint odel, m, cur, last;

  /* a list of polygons which make up our brushstokes */
   BList polyList;

  /* this is where the calculations get done,
     and the drawing */
   void DrawSegment();
   bool Apply(BPoint m, float curmass, float curdrag);

  /* little helper functions */
   inline float flerp(float f0, float f1, float p);
   inline void setpos(BPoint point);

Note that all the variables that were originally in the filter struct are now in the private section of the class. This is fine, since the only functions that need these variables are also in the class. Our brushstrokes are made up of polygons. We'll want to keep a list of them (so that we can redraw), so I've decided to use a BList object as a container. Every time we make a stroke, we'll add an item to this BList.

Then we have the two main methods, which remain largely unchanged from the original program. The first is Apply(), which decides whether or not a segment needs to be drawn. If it does, the other main method, DrawSegment() is called, and it draws the segment. Finally, there are two small helper methods, which don't do anything special.

The constructor for this class looks like this:

FilterView::FilterView(BRect R)
  : BView(R, "filter", B_FOLLOW_ALL_SIDES, B_WILL_DRAW)
  curmass = 0.50;
  curdrag = 0.46;
  width = 0.50;

Now we give the constructor a BRect object, which it passes on to the BView constructor. We name the view "filter", and instruct it to follow all sides. We'll do some drawing, so we need update notifications sent to us. Inside the constructor, we set initial values for the mass, drag, and width variables.

The MouseDown() method looks like this:

FilterView::MouseDown(BPoint point)
  uint32 buttons=0;
  bool  flag=0;
  float p;
  float mx, my;
  BRect B = Bounds();

  GetMouse(&point, &buttons, true);

  if(buttons == B_PRIMARY_MOUSE_BUTTON)
    mx = (float)point.x / B.right;
    my = (float)point.y / B.bottom;

    while(buttons == B_PRIMARY_MOUSE_BUTTON)
      GetMouse(&point, &buttons, true);

      mx = (float)point.x / B.right;
      my = (float)point.y / B.bottom;

      if(Apply(m, curmass, curdrag))
  else if (buttons == B_SECONDARY_MOUSE_BUTTON)
    int32 count = polyList.CountItems();
    for(int i=0; i < count; i++)

The MouseDown() method is called when the parent window receives a mouse down message. In our case, we want to track the cursor as long as the primary mouse button is held down. If the second mouse button is pressed, we want to clear the screen. (For anyone using a one-button mouse, don't despair; we'll add a general clear-screen feature later on.)

First we decide which button is being pressed. If it's the primary button, we initialize some points. Then we enter a loop which will continue until the button is released. In that loop, we get the mouse position, update our point, and call the Apply() method to determine if a segment needs to be drawn. (I'll skip the Apply() method body, since it's all calculations, and identical to the original version.)

If we need to draw the segment, we call DrawSegment(), which I'll get to in a moment. Finally, we need to snooze() between mouse calls; otherwise, the responsiveness would be too high. If the secondary mouse button was pressed, we want to clear the screen. We do this by deleting all the polygons in the polyList, and invalidating the whole view (meaning the Draw() function is called on the entire view). There are three things to note here: first, the RemoveItem() function does not free the objects it holds pointers to—you must do that. Second, calling RemoveItem() compacts the list, so the length of the list decreases by one every time you call it. Third, you'll notice that I called RemoveItem() with an argument of 0L -- this is because RemoveItem() is overloaded, one version taking a void*, the other taking an int32. Calling it with a 0 is ambiguous; calling it with a 0L makes it clear we want the int32 version.

Now, back to DrawSegment(). Calculations removed, it looks like this:

  /* calculations removed */



  polyList.AddItem(new BPolygon(polypoints, 4));
  FillPolygon(polypoints, 4);
  StrokePolygon(polypoints, 4);

  odel = del;


We call SetHighColor(), which sets the pen color to black. Next, we set the coordinates for the four BPoints which make up the polygon. Then, we make a new BPolygon, constructed with our four-point array, and add it to our polygon list. Finally, we make a call to FillPolygon(), but also one to StrokePolygon. The reason is that if our polygon gets extremely small, flattened to the point were it lies on a single line, FillPolygon() will not draw anything (because the area is, after all, zero). StrokePolygon(), on the other hand, will draw the line, which is what we need.

The only thing left to look at is the Draw() method, which is called if part or all of the view is invalidated. Lifting the hood reveals a trivial function:

FilterView::Draw(BRect updateRect)
  BPolygon* bp;
  int32 count = polyList.CountItems();
  for(int i =0; i < count; i++)
    bp = (BPolygon*)polyList.ItemAt(i);

All we do here is loop through the polygon list, getting one item at a time, and calling FillPolygon() and StrokePolygon() for the item. This reconstructs our drawing.

We now have a small program that draws nice calligraphic strokes. Play with it for a while to see how it feels and how it reacts to movements. Adjust the mass, drag, and width parameters in the FilterView constructor and see how it changes the program.

There's lots more to do with this program. To start with, we should have a simple menu bar at the top that lets us clear the screen, bring up an About box, and quit (without using the close button on the window tab). Since I'm a "tweak-freak," I'd like a window where I can adjust the mass, drag, width, snooze factor, and other things on the fly, without recompiling. Being able to change the color of the pen would be nice, too, so we'll add a color preference panel.

I'll be back in two weeks with an article which adds these features. It will also show how starting with a simple, clean framework makes expanding your programs much easier. In the next article we'll be moving at a faster pace, so you may want to get a head start by checking out the BSlider class, the BColorControl class, and simple messaging.

See you in two weeks!

(*) - That first SGI machine was an Iris 4D, for the curious.

How Do I Explain This To My Mother?

By Jean-Louis Gassée

My mother is puzzled. She thinks this whole circus of DOJ and state lawsuits against Microsoft is unfair, greedy, and downright dangerous. My mother asks tough, common sense questions. And since she's not a geek -- not our kind of geek anyway—she won't stand for any of our industry encoded speech. No, she wants answers she can understand.

How come those bureaucrats want Microsoft to bundle Netscape's browser along with the one already offered with Windows? Isn't that, as Mr. Gates pointed out, like forcing Coca-Cola to include three cans of Pepsi inside each six-pack of Coke? It is very nice of Microsoft to keep making their product more and more useful to more and more people; why are these government lawyers trying to prevent Microsoft from innovating?

She also wonders why so many people who benefited from Microsoft's work are now turning against it. She reminds me of the times when there were two different voltages for appliances or when the rail gauge changed at the border between France and Spain. With all the good PCs running Windows and Office, customers don't have to worry about the voltage, the hardware guys know what to manufacture and everyone should be happy, she says.

Doesn't everyone see how the Microsoft standard fueled the PC explosion? Look at your old cronies at Apple. They didn't participate and they were left behind. In my mind it's clear, she tells me. Microsoft is the industry benefactor, so why bite the hand that feeds you millions?

And she knows. She not only looks and speaks like Scott McNealy's mother, she's also been following Sun for a while. In fact, when Sun went public, she heard a rumor that one of Sun's founders, Andy Bechtolsheim, took his IPO money and bought Microsoft stock. So, she followed suit and invested her retirement savings in Microsoft stock, thinking Andy had to be onto something. He was. She likes what the stock has done for her and her fellow MSFT holders. As for people who attack Microsoft, I can't print the names she has for them in this family publication.

What do you say for yourself and your so-called friends in the industry, she demands to know? (Needless to say, when she learned I invested my time and, worse, her grandchildren's inheritance in a non-Windows operating system company, she was distraught, thinking all efforts to educate me had been a waste. But that's another story.)

So, I tell her the cans of Pepsi in a Coke six-pack argument is admirable. She smiles, and I'm her good son again. Then I bring up the software that ships with Windows on any new PC these days. Among other things, there is an icon for Microsoft's own network, MSN, and a folder of on-line services -- AOL, Compuserve, and AT&T's WorldNet. Isn't that a bunch of cans from brands directly competing with the MSN brand? She's puzzled.

But what about the apparatchiki who want to prevent Microsoft from innovating, she asks? I sigh. Maman, this is complicated. She gives me a withering look. Don't talk down to me. If you can't explain yourself to your mother, either you haven't done your homework, or you're trying to hide something from me. So, what is it?

Well, it goes like this. Some biased and ill-informed people say Microsoft never innovates, they just copy very well and use these imitations to eliminate competitors. Myself, I'd say they have a very good eye. And I admire the way they implement once they've decided on a target.

Take Microsoft's conversion to the Internet, I say, showing her the '95 Pearl Harbor speech. I attempt to explain how Microsoft's official & quot;embrace and extend" becomes lethal. Microsoft can extend its operating system in ways that use its position of power in the OS domain to generate a sure win. Some biased minds consider that unfair competition.

What's so wrong with that, she asks? Customers get a better product, a more complete offering from their favorite vendor. It makes no impression when I try and explain thatthis sterilizes innovation, just as when a culture occupies the entire Petri dish, there is no more room for other life forms.

Yes, but what if I get all I need at a good price? Then, I try to explain what a per-system license means. Allegedly, Microsoft gives its OEMs a good price for the "everything-on-it" combo of Windows and Office if they agree to calculate the license fee on the basis of the number of systems you ship, regardless of what you actually put on the disk. Accounting is simplified, the OEM can market, promote, mix, and match what they want without incurring complicated reporting requirements.

Isn't this gracious, asks my mother, low price, simplicity? Certainement, I say, but try selling a word processor to that OEM. He'll ask why he should pay for it, since he's already paid for Office. My mother is a little troubled by this. She's not sure what a word processor better than Word could be, but she sees that making a business of a non-Microsoft word processor could be even harder than her eldest son's OS venture, perhaps.

But she quickly recovers her equanimity. On second thought, she says, this lawsuit is a big win for Microsoft. First, Microsoft will win and, even if they don't win, they win. Now I'm the one who's puzzled.

The first win is simple. Does Microsoft have good lawyers, who know the law inside out? Yes, of course, Bill's father is a lawyer of fine repute, Microsoft's chief legal eagle Bill Neukom is talented, and the company can afford the best litigators. The DOJ may also have fine lawyers, but, does Joel Klein live and breathe technology as well as Bill and his people? No. Therefore, Bill will win.

And the second win, she says, is even better. Imagine Microsoft's situation without these lawsuits. Their ever-increasing presence—she doesn't like to say dominance—would only generate more and more hostility, which is not good for business.

Now, my dear son, let's assume for a moment your Silicon Valley cronies are right. Let's assume Microsoft does indeed embrace and extend and smother its competition. These suits give Microsoft two things that are invaluable. One is time. The legal battles will last for years, while Microsoft goes on doing what it does. The other is a mantle of martyrdom. As victims of bureaucratic oppression, they can exterminate with even greater impunity.

One might think my mother is a real cynic.

BeDevTalk Summary

BeDevTalk is an unmonitored discussion group in which technical information is shared by Be developers and interested parties. In this column, we summarize some of the active threads, listed by their subject lines as they appear, verbatim, in the mail.

To subscribe to BeDevTalk, visit the mailing list page on our web site:


Subject: How to make a BeOS floppy?

How do you make a BFS floppy...and should you? Young Ficus Kirkpatrick of Be thinks maybe you shouldn't:

"...the journal eats up a large portion of the disk. I would be loath to use a floppy because they are notoriously slow, seemingly always corrupt, etc..."

Master Kirkpatrick goes on to suggest using CDs rather than floppies, which prompted Chris Herborth to plead for a CD writing app. Other suggestions, other voices: Use network download to distribute software, tar to the floppy, use HFS/MS-DOS floppies.

Subject: Release 3 on BeBox

Is Release 3 much less stable than PR2 on a BeBox? Slower? Hard to install? A number of correspondents wrote in to mention that they couldn't get the ROM Updater to drop-launch—they had to launch it from the shell. Others had no problem installing or updating the ROM.

THE BE LINE: The Release 3 boot ROM for the BeBox was broken. However, there really wasn't any need to update the ROM; you can use the PR2 boot ROM to run Release 3 on a BeBox.

Subject: Printing (from Be Newsletter)

Printing comments, prompted by Benoit Schillings' Newsletter article,

  • When should a print job pop up the Print Setup or Page Setup panel? Some folks think the Print menu item should print, period. Others think that "Print..." is a well-known convention and that users understand that another panel is on its way.

  • Tyler Riti suggests a no-fuss, no-panel "Print one copy" item in addition to the expected "Print..."

  • Mr. Riti would also like more flexibility in choosing a printer. The current Mac-influenced bureaucracy (fill out a form and submit it to SelectPrinter) is barbaric. Adam Lloyd, who's thought about these things, submitted a plan for how as document-based choose-printer UI might look and feel.

  • Michael Crawford would like multi-device printing: "I'd like to be able to choose a different printer for different pages, for example to print envelopes and letters in a single print job."

  • A number of listeners would like to mix and match layout types within a document. For example, they'd like to be able to mix portrait and landscape orientation.

Subject: select() spaws [sic] threads?

From Jason Jasmin:

"Given that the os appears to spawn a separate thread for each socket being select()ed, is there any performance loss to just spawn a reader thread for each socket and have the thread sit in a blocking recv() loop?"

Also sprach Jon Watte:

"[spawning rather than selecting] is actually considered a performance GAIN. Threads are cheap on BeOS. Really cheap. It definitely makes sense to spawn one thread per socket if you're expecting < 100 simultaneous connections."

Luc Andre pointed out that this doesn't help when you're porting code.

Subject: URL handling AKA: message/rfc822

What MIME type should an URL be? Should it assume the type of the data it points to? Most listeners think an URL should have its own type, perhaps of supertype (or "media" type, in archaic RFC lingo) "message". Some folks suggest using "message/rfc822", the standard email type. Nearly everyone agreed that an URL shouldn't take the type of the data it represents.

How should the system support URL handling? Some folks would like to find and launch protocol-handling apps, something like:

be_roster->Launch(B_HTTP_PROTOCOL, >>URL<<, ...)

The objection, here, is that this assumes that you know the protocol. So how about a call that takes an URL and deduces protocol for you?

Subject: Laying out GUI components

Anyone want to offer some distance-between-UI-objects guidelines for Wendell Beckwith? Tinic Uro suggests...

"...a minimum 8 pixels for the bordersize of group of elements and a minimum of 4 pixels for the space between elements... Leave enough space for future localisation..."

Thorsten Seitz:

"You shouldn't hardcode the layout at all but instead always make it font-sensitive (liblayout or something else). This way you won't have problems with localization either."

Dan from Cornell, you're on the air:

"Use the height of be_plain_font as one 'display unit.' One display unit then becomes the vertical distance between radio buttons and checkboxes, the horizontal distance between 'OK' and 'Cancel', the margin of a BBox, etc. Unrelated control groups are separated by two display units."

Creative Commons License
Legal Notice
This work is licensed under a Creative Commons Attribution-Non commercial-No Derivative Works 3.0 License.