Go to the previous, next section.

New Devices and New Machines

Adding New Devices

As we have discussed extensively (see section The Stdgraph Graphics Kernel), most plotting devices can be accommodated within the framework of stdgraph/graphcap without writing a line of C; raster devices can use the raster device driver. Sometimes, however, this is not possible and you must write a real driver. An example would be making SM run native under X11; running under a terminal emulator can be done with graphcap.

If you do make any changes, write any new drivers, or modify graphcap (except trivially) please send us copies of your modifications.

Let us assume that you really do need a new driver, how do you go about it? SM communicates with devices solely through a graphcap entry, some external variables (defined in sm.h) and a structure called DEVICES which is defined in devices.h and declared in plotsub/devices.c.

When you issue a DEVICE devname args command, the stdgraph driver is called with the string devname args, and it opens the graphcap entry for devname. If it finds an entry of the form :DV=driver: it tries to find a hardcoded device driver called "driver" (you'll see in a moment where the names come from). If it can find such a driver it calls its setup function (to be defined in a moment) with args as an argument. Your device driver is now in charge, and no more stdgraph routines are called until your device is closed; after you return from your close function stdgraph's close function is called to close the graphcap descriptor. What do you have to remember from this paragraph? That you need a graphcap entry, if only a trivial one along the lines of

my_device|all_mine:DV=my_driver:
which associates the driver my_driver with the device referred to as either my_device or all_mine. Because stdgraph's stg_open and stg_close functions are called around your device code the standard graphcap support for OW, IF, DC, OF, SY, and CW is automatically provided if you want it; see the imagen driver and graphcap entries for an example. You might also want to remember that you can directly access any graphcap entry for your device from within your driver, using the functions ttygets, ttygetr, ttygeti, and ttygetb.

The current definition of DEVICES looks like this:

typedef struct {             /* output device dependent functions */
   char d_name[40];           /* name of device */
   int (*dev_setup)();        /* initialisation  */
   void (*dev_enable)(),      /* enable graphics */
   void (*dev_line)(),        /* draw a line from x1, y1 to x2, y2 */
   void (*dev_reloc)(),       /* relocate current position */
   void (*dev_draw)(),        /* draw a line from current pos to x,y  */
   int (*dev_char)(),         /* hardware characters  */
   int (*dev_ltype)(),        /* hardware line type */
   int (*dev_lweight)(),      /* hardware line weight */
   void (*dev_erase)(),       /* erase graphics screen  */
   void (*dev_idle)(),        /* switch from graph to alpha mode */
   int (*dev_cursor)(),       /* cursor display */
   void (*dev_close)(),       /* close device */
   int (*dev_dot)(),          /* draw a single pixel dot */
   int (*dev_fill_pt)(),      /* draw a filled point */
   void (*dev_ctype)(),       /* set colour of line */
   int (*dev_set_ctype)(),    /* set possible colours of lines */
   void (*dev_gflush)();      /* flush graphics */
   void (*dev_page)();        /* start a new page */
   void (*dev_redraw)();      /* redraw the screen, if need be */
} DEVICES;   

Note that this is the DEVICES structure at the time of writing; be sure to see that it is still correct when you write your driver! To add a new device to SM, all you have to do is to write functions to fill as many of the slots in DEVICES as possible, add their definitions to `sm_declare.h', then add an element to the array devices[] in devices.c. After recompiling, SM will recognise your device by the name d_name that you gave it in devices[]. Traditionally the functions have the same name as those given above with the dev replaced by some mnemonic for your new device. You should surround both your driver and the entry in devices[] with #ifdef's so that all I have to do to totally lose your device is to not #define it to the compiler. In writing your device you may want to use graphcap to give it various snippets of information. This can be done; see the imagen driver for an example.

You should include the file `sm_declare.h' (which automatically includes `options.h') which provides declarations for all SM functions (including prototypes if the compiler supports them), and you should add your own declarations to this file.

If you don't provide some of the functions, SM will try to emulate them in its software. For example, of your ltype function returns -1, then we will chop your lines for you. There are a set of functions for nodevice that have the correct types, you can use them for functions that you don't want to provide (e.g. if you don't support dev_ltype you can use no_ltype). Some of the routines, e.g. dev_char are sometimes passed NULL arguments to inquire if they can provide particular capabilities. In the following paragraphs we discuss all the functions, their arguments, what they do, and what they return.

All coordinates are given in screen units, where the device is 32768 pixels along a side.

dev_setup(str) (char *str;)
Open the plotting device. Str contains the rest of the command line, so if the DEVICE command was DEVICE newdev abcde zyxwvut, setup will be passed abcde zyxwvut. If the device can't be opened for some reason setup should return -1, otherwise 0. Setup has a number of book-keeping responsibilities: setting the value of termout to 1 if the device is a terminal, otherwise 0; setting the value of ldef to the maximum spacing between lines if they are to appear as one thick line rather than a set of parallel ones, (used if we have to emulate LWEIGHT); setting the variables xscreen_to_pix and yscreen_to_pix to give the conversion from SCREEN to device coordinates; calling default_ctype with the name of the default colour for lines (e.g. default_ctype("white")). There is no need to look in the `.sm' file for a foreground entry, this is done for you (by set_dev()) after dev_setup returns, but you should deal with any background entry if you feel so inclined. There is a function parse_color that converts a colour name into r, g, and b values (see the sunwindows setup function for an example of its use) that may help. Note that you should not call stg_setup -- this was done for you automatically before your setup function was called. No return value.

dev_enable()
Enable plotting on the device. For a graphics terminal this means switching from alpha to graphics mode, but may well not be required for other devices. No arguments, no return value.

dev_line(x1,y1,x2,y2) (int x1, y1, x2, y2;)
Draw a line from (x1,y1) to (x2,y2). No return value.

dev_reloc(x,y) (int x,y;)
Move the plot marker to (x,y). No return value.

dev_draw(x,y) (int x,y;)
Draw a line from the current plot marker to (x,y) which becomes the new plot marker. No return value.

dev_char(str,x,y) (char *str; int x,y;)
Write the string at the position (x,y). The position given is just to the left of the first character, at about the height of the centre of a capital letter. Return 0 if you support hardware characters, otherwise -1 in which case we'll use the software character set instead. If str is NULL, this is just an enquiry as to whether the device supports a hardware character set. Char is also called (with NULL) whenever the value of expand or angle is changed, to allow you to adjust the sizes of cheight and cwidth (the height and width of a character in SCREEN units) for a hardware character set, as the sizes of hardware sets can be very different from the SM fonts. You should only change cheight and cwidth if we are really going to use your hardware characters (expand == 1, angle == 0).

dev_ltype(i) (int i;)
Set the LTYPE to be i, as in the interactive command. Return 0 if you support the ltype asked for, otherwise -1. If you can't support a linetype, make sure that you are drawing solid lines, so that SM can emulate it for you. LTYPE 10 is special, it is generated by the LTYPE ERASE command, and asks you to delete lines as they are drawn, LTYPE 11 is used to indicate the end of LTYPE ERASE mode.

dev_lweight(i) (int i;)
Set the LWEIGHT to be i, as in the interactive command. Lines with lweight 0 should be the natural thickness of a line on your device, higher lweight lines are usually drawn with a thickness of LDEF*lweight in SCREEN coordinates. Return 0 if you support the lweight asked for, otherwise -1.

dev_erase()
Erase the screen. No return value.

dev_idle()
Return to alpha mode, the opposite of dev_enable(). No return value.

dev_cursor(x,y) (int *x,*y)
Find the current position of the cursor, if there is one. If there is, the return value is the character struck by the user, otherwise return -1. If your device has a mouse, you should try to make the buttons correspond to `e', `p', and `q' from left to right. If you only have one button, `p' is probably the best choice.

dev_close()
Close the device, the opposite of dev_open(). Note that you should not call stg_close -- this is done for you automatically. No return value.

dev_dot(x,y) (int x,y)
Draw a dot at (x,y), (i.e. a real dot, not like the DOT command). You'll have to do the move to (x,y) yourself. Return 0 if you can draw dots, otherwise -1.

dev_fill_pt(n) (int n;)
Draw a filled point at the current position. This is the routine that draws PTYPE n 3 points. Remember to allow for EXPAND and ANGLE. Return 0 if you can draw the point, otherwise -1;

dev_ctype(r,g,b) (int r,g,b;)
Set the current line colour. The interpretation of (r,g,b) depends on whether you have a dev_set_ctype function. If you don't, then r, g, and b are the red, green, and blue intensities in the range 0-255. If you do, then r is an index into the array defined by set_ctype and g and b are irrelevant. No return value.

dev_set_ctype(col,n) (COLOR *col; int n;)
Define a set of colours to be accessed by set_ctype. Col is an array of n elements, each of which consists of 3 unsigned chars (COLOR is defined in mongo.h) corresponding to red, green, and blue in the range 0-255. Ctype will be used to access this array to change colours. Return 0 if you do support set_ctype, otherwise -1. If col is NULL this is just an enquiry. If you are asked for more colours than you are prepared to support, you should scale the request in a user transparent way (e.g. if she wants 256 colours, and you are only giving her 128, you should arrange that dev_ctype divides her requests by 2 so it looks as if she got all 256).

dev_gflush()
Flush graphics, update graphics on screen. No return value.

dev_page()
Start a new page. If your device produces hardcopy you should print the current page and start a new one; if you are writing a window system driver you probably should simply raise the window so that it is visible. No return value.

dev_redraw(fildes) (int fd)
Redraw the screen, but only if it needs it. This function is called by get1char() before it tries to read a character from SM's standard input (file descriptor fd). It is intended for the use of drivers that need to keep an eye on `their' window. For example, the Silicon Graphics driver needs to redraw a window every time that it is moved. Such device drivers will probably want to poll fildes (e.g. using select()) to see that there really is input before returning; look at the sgi or x11 driver if you want an example. No return value.

If you are still confused, look at some of the hardware drivers that are already available.

Porting to New Machines

Porting to new Unix machines should be relatively simple. If the machine runs BSD Unix the only changes that should be necessary are to the exception handler routines in main.c, to reflect the error conditions signalled by your new machine. For a Sys V Unix, you'll have to edit `options.h' to define SYS_V. Otherwise, your only problems should be hidden bugs which flourish in new architectures.

Otherwise there isn't all that much that I can tell you. SM runs on Unix (BSD and SysV) and VMS machines, and the places where the code is #ifdef'd for those operating systems is your best bet for places where there are machine dependencies. That said, there are a few obvious problem areas.

First consider get1char, which is the routine that reads terminal input. It has to be able to get one character a time from the terminal, and not interpret control characters. This is obviously operating system dependent (CBREAK under 4.3BSD, PASSALL under VMS, ...). Get1char expects to be passed ^A to start operations, and EOF to end them. Anything else means `return a character please'. The terminal output functions used by stdgraph are also machine dependent, for example they use QIO calls under VMS.

As mentioned above, the exception handlers try to interpret the exceptions that they receive and this may require a little modification in main.c.

Hardcopy devices need the system() system call to send commands to the operating system, and if you don't have one you are in trouble.

In a few places SM needs to make assumptions about how file names are put together, and these will need changing. On a similar note, some operating systems (e.g. VMS) are very picky about opening files and you may have to be careful. Note the use of the DT graphcap capability to signal particular requirements to the programme.

A machine that didn't use ascii would be rather a nuisance as makeyyl assumes that the characters for A-Z are contiguous, and although this could be easily fixed I am not sure that other problems wouldn't arise.

A final rather horrifying thought is that Bison might not compile on a machine that doesn't have YACC as an alternative. I don't know how to fix that, you'd just have to hope for the best, or else run Bison/YACC on a different machine and copy over control.c.

Go to the previous, next section.