Some of my fellow engineers tell me that I'm lucky to have so many ideas for my newsletter articles. They couldn't be more wrong. Having to choose between two ideas is much harder (at least for me) than having to find one idea. After whittling a pile of ideas down to just two, I couldn't choose between them, so I'm using both for this article.
You can find the source for them here: ftp://ftp.be.com/pub/samples/graphics/gamma.zip
Something that BeOS developers must keep in mind when programming their
user interface is that users don't like interfaces to feel slow or
sluggish. One key to interface responsiveness is to let the BLooper
run
freely and process messages. To achieve this goal, you must make sure
that your program doesn't spend too much time sitting in the various
virtual functions that the BLooper
will call
in its BHandler
s. The sample
code for this article shows two techniques that should cover a reasonable
number of common cases.
In older versions of BeOS, the only way to track the mouse to dynamically
change a control was to repeatedly call GetMouse()
in a loop. The BeBook
mistakenly suggested doing this in MouseMoved()
, which would prevent the
BLooper from processing message as long as the mouse button was held
down, which could be a very long time. The correct way to do this was to
poll in a separate thread, a tricky thing to set up and to tear down
without deadlocking.
Fortunately, R4 added an elegant way to solve the problem: the view event mask, which lets you create asynchronous controls. Since this has already been explained in other Newsletter articles, I won't say much about it, but you can look at the sample code to see one of those beasts in action. For more information,
see: Be Engineering Insights: That BeOS is one baaad mother-[Shut your mouth!]
...just talking 'bout BeOS
and
Be Engineering Insights: The Kitchen Sink
We're now reaching the heart of the problem. The naive way to implement
real time user feedback with asynchronous controls is to redraw the
display each time MouseMoved()
is called.
Change the #if
in the sample
code to see what happens in that case. This is not what a user wants to
see, and it gets worse as the processing time gets longer. A temporary
solution might be to eliminate some of the B_MOUSE_MOVED
messages by hand
in the BMessageQueue
before they're dispatched; this only works if the
processing stays reasonably short and if the BLooper
is a BWindow
.
A more general solution is to have another thread do the work for the
BLooper
, so the BLooper
can continue processing messages (e.g., update
messages) as they come along. Since setting up and tearing down threads
can be painful, and can easily create deadlocks and other race
conditions, I've encapsulated all that work in two classes—called
SignalSender
and SignalCatcher
--
that I hope will be reasonably easy to
use. Those classes are an example of how easy it is to create your own
synchronization classes, and you might even find them useful in their
current state.
The sample code is not only interesting because of the programming techniques it uses, but also because of what it does. This small program is actually intended to calibrate a gamma correction system and to use this system to draw anti-aliased lines.
Gamma is a very simple process. The intensity of light created by a CRT is a nonlinear function of its input voltage. Gamma is a parameter that represents this nonlinearity, and gamma correction is the process of compensating for this nonlinearity. Surprisingly, this nonlinearity is the same for all CRTs: the intensity of light produced by the CRT is proportional to the input voltage raised to the power 2.5. You can thus repeat after me: "Gamma is 2.5."
There are, however, many components between the framebuffer (or, more precisely, the DAC) and the CRT's input: various amplifiers, filters, brighness/contrast controls. Overall, those components scale and offset the signal between the DAC and the CRT.
All the math is done in a GammaCorrect
class, so you don't have to know
exactly how it works if you don't want to.
The patterns displayed by the program are these:
In the top-left corner, a median blend of black and white (note that I'm using lines, because some monitors, although expensive, have some limitations that make other patterns yield incorrect results).
In the top-right corner, a plain gray that you can select using the control in the bottom of the screen and that you must try to adjust to the brightness of the black and white pattern.
In the bottom-left corner, a system of high-frequency ripples that show some circular moire patterns if the gamma correction is not properly calibrated.
In the bottom-right corner, some gamma-corrected anti-aliased lines, compared to the standard lines.
Right now, nothing is done by the OS, and everything has to be done by the applications. This will probably change in a future release, but this sample code shows how to use some gamma correction without any OS support.
If you want to know more about how it really works, here's the beginning
of an explanation. If we call f
the value for a pixel in the
framebuffer and l
the lightness on the screen for this pixel, we can
write that l
is proportional to
(f+e)^2.5
. Since it's more convenient to
use values between 0 and 1, we'll renormalize l
in that range with to
renormalization factors that we'll call a
and
b
, so that l=a*l'+b
(l'
is
the normalized version of l
). We can finally write that
f=((l'-b)/a)^0.4-e
, where f
will
vary between 0 and 1 when l'
varies
between 0 and 1.
The sample code will compute a
, b
,
and e
from another value m
that I call
the "mid-light" value; i.e., the gray level that is as light as a median
blend of black and white. With ideal monitor settings, m
should be
approximately 3/4, so that we get a=1
, b=0
and e=0
.
If you look in the file be/drivers/Drivers.h
in your system headers,
you'll find a list of Be-defined opcodes for ioctls. A driver may choose
to implement any of these, but all implemented ioctls need to behave as
defined by us. Some ioctls are required for certain types of devices.
Let's go through the opcodes defined for Release 4.5.
B_GET_DRIVER_FOR_DEVICE
, B_GET_PARTITION_INFO
,
and B_SET_PARTITION
are
implemented in devfs. You should not implement any of these.
B_GET_DEVICE_SIZE
and B_SET_DEVICE_SIZE
are obsolete calls that operate
on 32-bit numbers.
B_SET_NONBLOCKING_IO
and B_SET_BLOCKING_IO
, are used to change a file
descriptor to blocking or nonblocking mode. Devices where a read or write
operation depends on other events to complete should implement these. The
driver also sets the mode on open according to the O_NONBLOCK
flag.
B_GET_READ_STATUS
and B_GET_WRITE_STATUS
are used, in blocking IO mode,
to determine if a following read or write will block. A pointer to a bool
is passed as the argument, and the driver writes set the value to false
if the operation will block, and true if not.
B_GET_GEOMETRY
is used to get information about a disk device. All
drivers that publish their device under /dev/disk/ should implement this.
The argument to this call is a pointer to the following structure:
typedef struct { uint32bytes_per_sector
; /* sector size in bytes */ uint32sectors_per_track
; /* # sectors per track */ uint32cylinder_count
; /* # cylinders */ uint32head_count
; /* # heads */ uchardevice_type
; /* type */ boolremovable
; /* non-zero if removable */ boolread_only
; /* non-zero if read only */ boolwrite_once
; /* non-zero if write-once */ } device_geometry;
For block devices, bytes per sector should match the block size of the physical device. A file system mounted on your device will not try to read partial blocks. Most devices do not expose accurate information about the relation of cylinders, heads, and sectors, but the number of usable blocks in the device should match sectors per track*cylinder count*head count.
When using this call to determine the size of a device, make sure to use 64-bit precision for the calculation.
device_geometryg
; off_tdevsize
; if(ioctl
(devfd
,B_GET_GEOMETRY
, &g
) >= 0) {devsize
= (off_t)g
.bytes_per_sector
* (off_t)g
.sectors_per_track
* (off_t)g
.cylinder_count
* (off_t)g
.head_count
; }
The device type field indicates the type of the device. The most common
values here are B_DISK
for hard drives and
B_CD
for CD-ROMs.
If the media in the device is removable, the removable field should be true. Some devices support media of different size and block size. If there is no media in the drive, report a size and block size of 0.
The read only field is set if the driver knows that the media is not writeable. Some devices may not be able to determine this, so make sure your application deals with write operations failing.
write_once should normally be set to false.
B_GET_BIOS_GEOMETRY
, B_GET_BIOS_DRIVE_ID
: On the x86 platform, hard
drives can be accessed though the bios at boot time and from other
operating systems. B_GET_BIOS_GEOMETRY
will fill in the same structure as
B_GET_GEOMETRY
, but sectors per track, cylinder count, and head count
need to match the numbers used by the bios. This information is needed to
create a partition table that the bios can use. B_GET_BIOS_DRIVE_ID
will
return the drive number the bios uses for this drive. This information is
needed when adding a disk to the boot menu.
B_FORMAT_DEVICE
is used to format the media in the device. If your device
needs to prepare the media before it can be read and written to,
implement B_FORMAT_DEVICE
.
B_GET_ICON
allows a device to have a custom icon. Currently, our disk
drivers have the Be disk and CD-ROM icons built in, but we will provide a
simple way to specify standard icons in the future.
B_EJECT_DEVICE
and B_LOAD_MEDIA
are used to eject or load media.
B GET MEDIA STATUS is used to determine the state of the media in the drive. This is described in detail in my article "Removable Media" <http://www-classic.be.com/aboutbe/benewsletter/volume II/Issue44.html#Insight>.
B_SET_UNINTERRUPTABLE_IO
and
B_SET_INTERRUPTABLE_IO
change how the driver
responds to signals. By default a driver should return to user space as
soon as possible when it receives SIGINT
. A driver can use the
B_CAN_INTERRUPT
flag when acquiring semaphores and can abort its operation if
B_INTERRUPTED
is returned. However, when using the file system cache, a
read or write operation may carry data that doesn't belong to the current
thread. The file system uses B_SET_UNINTERRUPTABLE_IO
to tell the driver
not to abort any operations. B_SET_INTERRUPTABLE_IO
restores the default
state. A disk driver that wants to abort IO operations on signals needs
to implement these ioctls.
B_FLUSH_DRIVE_CACHE
needs to be implemented by disk drivers if the device
has a write cache. If the device has a write cache, either a software
cache or hardware cache, a call to B_FLUSH_DRIVE_CACHE
should not return
until all data in the cache has been written to the media.
If an opcode is not supported by your driver, return B_DEV_INVALID_IOCTL
.
This will cause the ioctl call to return -1 with errno set to
B_DEV_INVALID_IOCTL
. For all opcodes above you
should return B_OK
or B_NO_ERROR
if the operation was successful. If the operation failed you need to
return an error. In the file be/support/Errors.h under your system
headers, you'll find error codes for a variety of error conditions. You
should try to pick the error code that best describes why the operation
failed. Let's look at the most common cases for drivers.
If you don't know why an operation failed, return B_ERROR
.
If an operation failed because you could not allocate memory, return
B_NO_MEMORY
.
If an IO operation fails, return B_IO_ERROR
, or if your device has
additional information you can return more specific errors such as
B_DEV_NO_MEDIA
, B_DEV_UNREADABLE
,
B_DEV_FORMAT_ERROR
, B_DEV_TIMEOUT
,
B_DEV_RECALIBRATE_ERROR
, B_DEV_SEEK_ERROR
,
B_DEV_READ_ERROR
, B_DEV_WRITE_ERROR
,
B_DEV_NOT_READY
, and B_READ_ONLY_DEVICE
.
If someone tries to write to a read-only device or to write-protected
media, return B_READ_ONLY_DEVICE
.
For devices with removable media it's also important that
B_DEV_NO_MEDIA
and B_DEV_MEDIA_CHANGED
are returned when appropriate. Once opened, read
and write operations should fail with B_DEV_MEDIA_CHANGED
if the media
changes. See the article "Be Engineering Insights: Removable Media" for more info on how this
should be implemented.
Recently, the DTS oraA long time ago, in an office far, far away (or at least, on the other side of the building), I wrote a newsletter article about making and using menus in BeOS GUI applications. One of the goals of that article was to see how people responded to a more tutorial style of documentation--something that didn't necessarily go into every last, cotton-pickin' finicky little detail, but that did provide the basic information necessary to use a class or classes in your program, in a compact, easy -to-read form.
We were rather surprised at the number of responses generated by that article, almost all of them positive. Well, it's been a while, and other things came up of course, but now we're back in the tutorial game. In fact, there will hopefully be quite a few more tutorial style documents coming down the pipe in the future.
In light of this (and due to the fact that I didn't have the time or the imagination to do anything else), it seemed apropos to do a sequel to my previous newsletter tutorial. Think of this one as "Looking for Tutorial Feedback II: Nightmare on BView Street". The first writeup in this new tutorial series will be about the BView class, and the first part of this writeup is a short, very high-level introduction located here:
<http://www-classic.be.com/aboutbe/benewsletter/volume_III/resources/articl e.htm l>
I hope the technical content is useful, but the main purpose of presenting this here is to elicit comments from you, the reader. We expect to put quite a bit of effort into this series (hopefully resulting in a complete tutorial for programming the BeOS), and given this, it makes sense to develop a format which will make the information as useful as possible. For example, one of the things I've done in the tutorial is to include a sidebar on the left, giving easy access to the section and subsection headings within the document. Is this a good idea? Can you suggest other features which might be better, or complementary? As the series progresses, I will be locked in to a certain format and approach, in order to maintain consistency--but right now, the sky is the limit. (Well, subject to resource constraints, of course :-) ) So suggest away!
And, of course, please make comments on the writing style and content also. If nothing else, please tell me:
What I'm doing right in the tutorial.
What I'm doing wrong.
What most needs to be covered by this sort of documentation.
You can email directly to me, ken@be.com. (Nope, you don't get my home phone number :-) ). If you could begin the subject line of any feedback with the words "TUTORIAL COMMENTS", it would let me use the cool indexing features of the Be file system to keep responses nicely sorted.
Our application-building exercise begins with Rephrase,
a BTextView
based
text-processing application. Over the next few months, I'll add features
in each installment, and explain the BeOS coding philosophy behind them.
You'll find the first version of Rephrase at
<ftp://ftp.be.com/pub/samples/tutorials/rephrase/rephrase0.1d1.zip>
Rephrase 0.1d1
New Features:
Edit text within the window. Drag and drop for open and save. About box.
This limited version of Rephrase is essentially an application wrapped
around a standard BTextView
. It
has a BApplication
and a BWindow
for the
text view to live in. There is an About window, with a basic set of menu
items in the main window to bring up the about window, quit the
application, and edit the text.
The application's real functionality (limited as it is) comes from the
BTextView
. It lets you cut, copy, paste, and select the text. It even
provides limited file i/o through inherent drag and drop capabilites:
select text and drag it to the Tracker to save a text clipping, or drag a
file to the view to paste in its contents.
Although it has only 164 lines of code (many of them blank or comments), the application is surprisingly functional. With the addition of resizing, scrollbars and real open and save capabilities, Rephrase could even be useful.
BeOS uses a system of messaging to manage most aspects of communication
and event handling. BLooper
s run an event loop in a thread that receives
incoming messages and dispatches them to associated BHandler
s to be acted
on.
BApplication
provides a connection to the Application Server, which
enables the messaging system for the app. It also serves as the main
looper for interapplication communication and application global
services, such as displaying an About window. A global pointer to the
BApplication
object (be_app
),
is available throughout the application.
BWindow
s and BView
s
are the specialized loopers and handlers for managing
the graphical user interface. When visible, a BWindow
represents an area
on screen. A BWindow
maintains a hierarchy of
BView
s that draw in and
handle messages for sections of that area. User interaction generates
messages that are delivered to the window, which passes them to the
BView
s to handle as appropriate.
For example, when you select a menu item in Rephrase, a message is sent
to the BMenuItem
's target. The menu item's window is the default target.
However, the menu item can be told to send its message elsewhere. For
instance, when "About Rephrase" is selected from the File menu, a message
is sent to the BApplication
. All the menu items in the Edit menu send
their messages to the BTextView
.
In main()
, create the application on the stack, call
Run()
and
return after run completes.
Don't hard-code your interface details. Some views resize themselves to match their contents. Menus, for example, resize to match the size of the characters in their font. After you create and add the menu, determine its size and use that information to place the rest of your views. Change the size of your font in the Menu preferences panel, run Rephrase and see.
Do not delete a BLooper
. Send it a
B_QUIT_REQUESTED
message
instead. This calls its QuitRequested()
hook function, and if that
returns true
, will call the Quit()
function which tears down the
looper.
Windows own their child views and will properly delete them when
destroyed. Only delete a view if you have already called RemoveChild()
to remove it from its parent.
Invokers own their model messages and will properly delete them when destroyed.
When an app's last window is asked to quit, it should request the app to quit as well. If not, the application may hang around in the Deskbar with no windows.
Next week: Opening a file.