C-MANSHIP PROGRAMMING
by Clayton Walnum
Everyone who's tired of studying GEM's windows, please raise your hand. Yeah, that's what I thought. Okay, it's time to take up a new subject, something that, though it'll give you a lot of information on how your computer works, won't give you a headache trying to understand it.
One of the more useful things about the ST is the ability to have many screens of data in memory at once and flip between them as you like. I thought this would be a good subject to tackle, since it enables us to not only see how we can accomplish "screen flipping" (which is really a simple process), but how to apply some of the other techniques we've learned, such as the programming of file selector boxes. We'll also take a look at some new information, such as the DEGAS picture file format.
Type in this month's listing and compile it. Note that the program was developed using the Megamax C compiler. If you have a different compiler, you may need to make some small changes to the code. Once you've got the program compiled and linked, go ahead and run it.
What we're going to do is load two DEGAS format pictures into memory, and then use an alert box to choose which picture to view. We'll have to tell the program which files to load, so the first thing the program will do is bring up a file selector box. Use it in the normal way to select two DEGAS pictures for loading.
While you're doing this, keep in mind that the program presented here is a very stripped-down model. In other words, it doesn't incorporate much in the way of error checking. In fact, it'll let you load just about any type of file into memory, whether it's DEGAS or not. So do your own error checking, and make sure you're selecting the right type of file.
If you click on the file-selector box's Cancel button for either picture, or if the program gets a file error, you'll be returned to the desktop.
Once you get two files loaded, an alert box with three buttons will appear. Clicking on the first button will cause the first loaded picture to be displayed. Clicking on the second button will show the second picture. The Quit button should be used to leave the program and return to the desktop. Once a picture is displayed on the screen, clicking the left button will bring the alert box back, allowing you to make another choice or quit the program.
Hey! That space is reserved!
The first step is getting our picture files loaded into the computer is figuring out where we're going to store them. We need a lot of space—32K for each picture—and we have to make sure that, wherever we store the picture information, it doesn't get in the way of our program or its data. Also, since we're going to be displaying a couple of different screens, we have to make sure we store the address of the original screen, as well as its color palette, so that we can restore it when the program's finished.
Take a look at the function init__screens() in Listing 1. The first thing we do here is store the desktop's color palette with the line:
for (x = 0; x<16; desk_palette[x++] = Setcolor(x,-1));
The function Setcolor() is an XBIOS function and is defined in the OSBIND.H file. This function requires two integers as arguments. The first is the index of the color you want to change (from 0 to 15), and the second is the color to change it to.
Colors on the ST are formed by mixing the correct proportions of red, green and blue, each of which can have a value from 0 (minimum) to 7 (maximum). The color value for blue is placed in the first nibble (four bits) of the integer; the value for green is placed in the second nibble; and the value for the red is placed in the third. This works out well in hexadecimal: 0x007 is the brightest blue; 0x070 is the brightest green; and 0x700 is the brightest red. White is all the values at their maximum (0x777), while black is formed by setting all colors to the minimum 0x000). By combining the three basic colors in varying intensities, we can conjure up any of the ST's 512 possible colors.
But all that is besides the point (go ahead and boo; I deserve it). We don't want to change the colors (at least, not yet); we want to know what value they're currently set at, so we can store them for later retrieval. One thing I didn't tell you about the Setcolor() function is that it always returns a color's previous setting (its color value before we changed it). If we make the second argument a negative number, it won't change the color register at all; it'll just return the color's setting.
Now you can see how the above code segment works. We use a for loop to step through all 16 possible elements of the color palette, calling Setcolor () in each iteration with a color value of -1, in order to have the current color returned to us. Each of these colors is stored in the array desk__palette[], where they'll be when we're ready to restore the desktop's colors.
Now that we've gotten that taken care of, we have to store the address of the desktop's screen (we do want to get back there eventually, you know). This line takes care of that:
scrn = Physbase();
Here, the variable scrn is a long integer that'll hold the address returned from Physbase(). The function Physbase() returns the address of the physical screen, the area of memory currently displayed on your monitor. The function Logbase() returns the address of the logical screen, an area of memory where all output to the screen is to go.
In most cases, the physical and logical screens are in the same location. For example, as I'm writing this article, I can see the new text I'm typing appearing on the screen. That means that the displayed screen and the one the program is sending text to are at the same address. Sometimes, though, you may find it handy to direct data to a different place in memory, so you can do the screen updating "behind the user's back." Once the logical screen has been set up the way you want it, you can simply flip to it, creating the illusion of the screen being intently updated. We'll see how all this works a little later on.
Now that we know where our physical screen is, we're ready to allocate some memory for a couple of logical screens. You allowed only one physical screen, but you can have as many logical screens as you can store in memory. In the function init__screens(), we set up a while loop that first allocates a block of screen memory, then calls a function to read the picture data into it. To allocate a block of memory, we use the call:
addr = Malloc (bytes);
Here, the pointer addr will hold the address of the block of memory, and the long integer bytes is the number of bytes you wish to reserve. This function returns a 0 if the amount of memory you've requested isn't available. One variation on the Malloc() call, making bytes equal to -IL, will return the total amount of memory available.
You've probably noticed, though, that our call to Malloc() in Listing 1 looks quite a bit more complex:
pic[x] = [Malloc(32768L) & 0xffffff00 ) + 0 × 0100;
First, even though pic[x] doesn't look like a pointer, it is. In fact, pic[] is an array of pointers (actually, long integers, but for our use that amounts to the same thing). For programming purposes, it's very convenient to store the addresses of our screens in an array, so that we can get at them easily with some sort of loop.
Next comes that strange looking Malloc() call. It looks strange to you because there's one little detail I've yet to mention, the fact that the ST's screen memory must always start on a 256-byte boundary. And, since Malloc() doesn't know or care about this little requirement, it's up to us to smooth things over.
The first step in getting to a safe 256-byte boundary is to use C's AND operator to mask off the eight right-most bits of the address, using the hex value OxFFFFFF00 as our mask. This value has every bit set except the right-most eight. The AND operator compares the bits of two values, returning a true (1) when both bits are on and a false (0) when either or both the bits are off. What that means for us is that every bit we have off in the mask will result in a 0 in the bit it's being AND-ed with. Let's say the address returned from Malloc() was 0x0034CC3E2. After ANDing it with our mask, we'd have 0x034CC300, which is an address on a 256-byte boundary.
But even though we're now on the boundary we wanted, it's not a safe boundary. Why? Because the address we have now is lower than the one returned from Malloc(). We're no longer in the area we just reserved; we're actually before it. If we try to load data there, we'll probably end up clomping all over our program—and get a delightful string of bombs up on the screen.
That's why, after completing the AND operation, we add 0x00000100 (256 decimal) to the resultant address. That pushes it back into our reserved area.
"Ah!" you cry in that smug manner you use when you think you've caught the professor with his foot in it. "If we're pushing the address forward, doesn't that mean that, when we load our picture data, the last few bytes will be placed outside the reserved area, beyond the other end?"
Nope. You see, we've reserved 32768 bytes (that's a full 32K), and we only really need 32000 bytes for our picture data. When people tell you that screen memory on the ST is 32K, they're not telling you the whole truth. It's actually a bit short of a full 32K. We just like to round it off when we speak. (You ever hear people refer to the SF314 disk drive as a one-meg drive, even though you can only story 720,000 bytes on the disk? Same idea.)
One thing we do have to watch out for, though, is how we handle any subsequent calls to Malloc(), because it doesn't know we've finagled the address it gave us the first time around. The next time we allocate some memory, we have to remember to add the same amount to the returned address, or we're sure to make digital footprints in the previous areas. And digital footprints often result in the Big Kablooey. (In our case, since we're using those areas only for a screen display, we'd simply end up with some funny looking pictures.)
Okay, we've got the memory we need to store our pictures. Now let's think about how we're going to load them. The first step is to get the picture's file-name, and the obvious way to do that is with GEM's handy file selector box. Included in Listing 1 is a function called select__file(). This is a generic file selector box routine that I came up with that you can use in your own prograrns. It handles some of the minor details for you, allowing you to just call a file selector box and have the complete filename (including the path) returned to you. (You're welcome.)
If you look at the function get__pic(), you'll see how we get started. First, because it's required by select__file(), we have to come up with a default filename. This will be tacked on to the end of the pathname field in the file selector box, and allowing us to narrow the number of files shown when the box first comes up. In our example, we start with the string " * .P1" then finish the default name by adding the proper DEGAS resolution indicator. Adding the ASCII value of "1" to the value returned from Getrez() performs that trick.
Our file selector function, select__file(), returns the complete chosen filename and the button that was clicked to exit the file selector box. The call to the function looks like this:
select-file(path, file, default, flag);
Here, path is a pointer to a 64-byte character array where the function will store the completed filename. The pointer file is the address of a 13-byte character array that'll hold the selected filename after the call to fsel__input(). You may also, before the function call, store a filename here that you want to appear in the filename field of the file selector box. The pointer default contains the address of a string containing the text you want added to the selector box's pathname field. And finally, flag is a Boolean value that tells the function whether you want the string pointed to by file to appear in the file-selector box's file field.
It sounds a little complicated at first, but I've found that using this function is a lot easier than trying to remember how to handle the file-selector box each time I need it.
As I mentioned before, select—file() returns the value of the file-selector button that was clicked. Strangely enough (or perhaps it was done purposely), these values also correspond to obvious Boolean values; the Cancel button returns 0, and the OK button returns 1. In the function get—pic(), we use this returned value as a Boolean to evaluate an if statement. In other words, if the user clicks on the file selector box's Cancel button for either of the two files we're going to be loading, we'll know not to read the file and instead exit the program.
If the user clicks the file selector's OK button, we call the function read—degas() to attempt to load the file chosen. If the file loads all right, this function will return a value of TRUE. If an error is encountered (maybe the file doesn't exist), it returns a value of FALSE. We use this returned value in another if statement to determine whether we should continue or return to the desktop. In a full-scale application program, you would want to give the user a message if you ran into an error, but for the sake of brevity, we've kept things to a minimum in the example program.
Turn your attention now to read__degas(). It's here that we actually read the selected picture file into memory. This function needs to know which picture we're loading and the complete filename. The first thing we must do is open the file, but we have to make sure we open it to read binary. We covered the open() function way back in Issue 4, but we didn't talk about the 0__BINARY flag. When we open the file with this flag (it's defined at the top of the listing as 8192), we're telling the system that we want the file read from the disk in an untranslated form, as a continuous block of data, rather than a series of lines ending with carriage returns and line feeds.
Before we go any further, we need to discuss the format in which DEGAS pictures (the unsqueezed variety) are saved to disk. If you've ever looked at a disk directory containing these picture files, you've undoubtedly noticed that they are 32034 bytes. In order to get the picture up on the screen properly, we have to know what each of these bytes is.
The first two bytes of a DEGAS file indicate the picture's resolution. It's interpreted as a word value: 0x0000, 0x0001 or 0x0002, for low, medium or high resolution, respectively. Normally, we'd want to check the resolution of the picture against the computer's current resolution, to make sure they aren't different, and if they are, give the user an error message. But, as I said before, for the sake of brevity, we're going to do things quick and sloppy and just throw away those two bytes after we've read them.
The next 32 bytes (16 words) are the picture's color palette. That we don't want to throw away; we want to read it into the array we've set up for storing this information.
Finally, the last 32000 bytes are the actual picture data. We read that information into the area of memory starting at the address stored in the appropriate element of the pic[] array.
Now that we've got all the data read, we close the file and return a value of TRUE to the calling function. Notice that, in the function read__degas(), we're using the value returned from the open() function in an if statement. Doing this makes sure, in the case of a file error, that we skip over all the subsequent file handling code, and just return from the function a value of FALSE.
Once we get two picture files loaded okay, program execution gets turned over to the function flip__screens(), where we get a chance to actually view the pictures. We begin by calling up an alert box with three buttons, one button for each picture plus a Quit button. We use the value returned from the alert box as an index into the pic[] array, where the pointers to the screens are stored. To flip between the different screens, we use the call:
Setscreen (log, phys, res);
Here, log is the address of the logical screen, phys is the address of the physical screen, and res is the screen resolution we want to switch to. If we don't want to switch screen resolutions, we just give res a negative value. In fact, all parameters with a negative value will be ignored.
In most cases, you would set both the logical and physical screen to the same address. As for the resolution, you'll almost always want to leave it unchanged (use a negative value) because GEM isn't ever informed of resolution changes, and that little bit of ignorance on its part can lead to some pretty nasty complications.
Exactly flipping the screen, we wait for a mouse button click using a call to evnt__button(), after which we bring up the alert box to get another choice. We keep displaying the selected picture until the Quit button is clicked. Then we close things up and return to the desktop.
Putting it back where we found it
But we can't just go blithely on our way, returning to the desktop by just closing the virtual workstation and calling appl__exit() as we've gotten used to doing. Nosiree. We've got some cleaning up to do first. We've allocated a bunch of memory for our picture files, and before we leave, we have to get it back. Not a tough thing to do. The following call will return a block of memory (one that was allocated with Malloc() to the system:
Mfree (adr);
The pointer adr is the address of the block we want to de-allocate. You need to make a separate call to Mfree() for each block allocated, and you must return the blocks in the reverse order you allocated them.
Once we've returned all the memory to the system, we can exit the program in the usual manner. You can see all this being done in Listing 1 in the function clean__up().
The whoops department
I was going over a couple of the old installments of C-manship the other day and found that I had left something out of our discussion of resource files. So let's correct that oversight, shall we?
When you load a resource file, it takes up a certain amount of memory, and, just like the picture files we were working with this month, that memory should be returned to the system before we exit our program. The call to free the memory allocated for a resource file is:
rsrc_free ();
You should use this call (whenever you've loaded a file with rsrc__load()) before you exit a program or before you attempt to load a different resource file.