Hugi Magazine #33: MP3 Power

Hugi #33 header graphic

Creating Demos in XPL0 (By Boreal)

How often have you been in a bookstore thinking about buying a book, and just wanting to see the example programs on the CD in the back? It would tell you in a glance if the book was worth buying or not.

Well, you're in luck! Not only do you not have to buy anything; but if you go to the file in the bonus pack, you can quickly and easily run the programs. There's no seal to break and no BS legal mumbo-jumbo to ignore.

You can even modify the code, with your favorite editor, and recompile and run it.

The example programs are essentially Polaris's excellent demos from Hugi 31 that have been translated into XPL0. However in some cases I couldn't resist making a few slight changes.

What's XPL0?

XPL0 is a similar to Pascal. It was initially created by my computer club (the 6502 Group) back in the mid 1970s. Since then, it has been enhanced and implemented on a variety of processors, especially those used in PCs. I'm the current maintainer of the language with a webpage at:

There's lots of information there, including a 127-page manual, but let me give you a brief introduction here. We'll begin with the traditional Hello World program:

        code Text=12;
        Text(0, "Hello World!")

"Text" is a built-in routine that outputs strings of characters. The zero (0) tells where to send the string. In this case it's sent to the display screen, but it could just as easily have been sent to the printer or out a serial port by using a different number.

In XPL0 all names must be declared before they can be used. The command word 'code' associates the name "Text" to built-in routine number 12, which is the one that outputs strings. There are about 80 of these built-in routines.

Is XPL0 worth messing with?

I can easily understand why you wouldn't want to bother learning yet another computer language. But I hope you'll find the following examples intriguing enough to at least consider it.

I'm not trying to sell you on XPL0. I don't make any money from it. It's just that it's the high-level language I use all the time. XPL0 has been tailored to my needs, and consequently I rarely use other high-level languages.

A possible objection to using XPL0 is that it's based on DOS rather than Windows. It doesn't make Windows applications. Fortunately Windows is still able to run most DOS programs.

The best feature of XPL0, besides being easy to learn, is you get all the source code. You can tailor the language to your needs.

Getting your feet wet

I'm assuming that you're at least somewhat familiar with running DOS. Under Windows XP, you can get a DOS window by going to Start -> Accessories -> "C:\ Command Prompt". I'm also assuming you have some kind of text editor (such as EDIT or Notepad) that makes plain-vanilla ASCII files.

To compile and run the Hello program, you need to extract the files in the accompanying bonus pack into a directory called CXPL. XPL0 programs expect to find certain files in this directory, immediately below your root directory, like this: C:\CXPL

The Hello program is in the file called HELLO.XPL, and it can be compiled and run like this (type in the lowercase letters):

        C:\CXPL>x hello

The Matrix has you!

Now let's get into something more interesting - some oldskool demos. For this we'll borrow heavily from Polaris's examples. Here's his Matrix program translated into XPL0:

  include c:\cxpl\codesi; \standard library 'code' definitions
  int     PosX, PosY;     \X and Y position (column and row) of text on screen

  begin   \Main
  while not ChkKey do                   \loop until a key is pressed
          PosX:= Ran(80);               \calculate random position to draw text
          \Ran(80) gives a range 0..79, which is what we want
          PosY:= Ran(25);               \Y position is similar

          Attrib(if Ran(2) then 10 else 2);\randomly select light or dark green
          Cursor(PosX, PosY);              \where we want our text to show up
          ChOut(6, Ran(2)+^0);             \randomly output an ASCII 0 or 1
  end;    \Main

XPL0 provides built-in (library) routines called "intrinsics". The 'code' declarations for all the intrinsics are in the file CODESI.XPL. This file is usually 'include'd at the top of every XPL0 program, and it gives the standardized names to all of the intrinsics. Here we're using these intrinsics:

ChkKey checks to see if a key has been struck on the keyboard and returns either 'false" (zero) or 'true' (non-zero).

Ran is the random number generator. It returns a number between 0 and the argument minus 1.

Attrib specifies the attributes (usually foreground and background colors) used for displaying characters.

Cursor specifies the column and row on the screen where characters will appear. The upper-left corner is 0,0.

ChOut outputs a character. The "6" indicates that it goes to the screen and uses the colors set up by Attrib. Characters can be sent to other output devices - such as printers, serial ports, or files - merely by using different numbers.

Note that names are capitalized. XPL0 requires that all names be capitalized - like the good proper nouns that they are. This avoids any possible conflict with reserved words, which are in lowercase. For example, there's no problem having a variable called "End".

The integer variables used by the program are declared by the command word 'int'. This could just as well have been written out as 'integer', but since only the first three letters of commands are significant, XPL0 honchos typically use abbreviations.

The code above is a fairly straightforward translation of Polaris's C++ program. However the argument to the Attrib intrinsic "if Ran(2) then 10 else 2" is slightly unusual. It's an example of an 'if' expression, which is different than the more common 'if' statement. It's equivalent to the C expression: "rand()%2 ? 10 : 2".

If Ran(2) returns a zero value it's interpreted as being 'false', while any non-zero value (such as 1) is treated as being 'true'. Thus the argument evaluates to either 2 (for false) or 10 (for true). These are the values of the two shades of green for EGA (and greater) video modes.

This Matrix example can be simplified to five lines (greetz! Fable Fox):

  include c:\cxpl\codesi;                   \standard library 'code' definitions
  repeat  Cursor(Ran(80), Ran(25));         \randomly select location on screen
          Attrib(if Ran(2) then 10 else 2); \randomly select light or dark green
          ChOut(6, Ran(2)+^0);              \randomly output an ASCII 0 or 1
  until   ChkKey;                           \run until a key is struck

When either of the examples above are run, you get something like this:

Let it snow

Here's a translation of Polaris's Snow demo. This introduces arrays and procedures. It begins by defining a name for the constant value that specifies the size of the arrays. 'def' is an abbreviation for the command word 'define'.

include c:\cxpl\codesi;                 \standard library 'code' definitions

\Data structures for snow flakes = very simple
def     Total_flakes=900;
def     Total_layers=3;

int     FlakesX(Total_flakes),

proc    Initialize_particle_flakes;      \Initialize the particle flakes
int     I;
for I:= 0, Total_flakes-1 do
        FlakesX(I):= Ran(320);          \0-319 (x)
        FlakesY(I):= Ran(200);          \0-199 (y)
        FlakesLayer(I):= Ran(Total_layers); \(0-2) (layer)

proc    Draw_particle_flakes;           \Draw all the particle flakes
int     I;
for I:= 0, Total_flakes-1 do
        Point(FlakesX(I), FlakesY(I), FlakesLayer(I)*4+23); \(bright flakes)
        \Note - we are drawing according to the default color palette

proc    Update_particle_flakes;         \Update the particle flakes
int     I;
for I:= 0, Total_flakes-1 do
        \Drop the particle down - depending on layer.
        \Add one since layer zero would result in no motion
        FlakesY(I):= FlakesY(I) + FlakesLayer(I) + 1;

        \Check for wrap around
        if FlakesY(I) > 199 then
                FlakesX(I):= Ran(320);
                FlakesY(I):= 0;
                FlakesLayer(I):= Ran(Total_layers);

        \New X position
        FlakesX(I):= Rem((FlakesX(I)+2-Ran(5)) / 320);

proc    VSync;                  \Wait for vertical retrace to begin
while PIn($3DA,0) & $08 do [];  \wait for vertical retrace signal to go away
repeat until PIn($3DA,0) & $08; \wait for vertical retrace
end;    \VSync

begin   \Main
SetVid($13);                    \set graphics display for 320x200x8-bit color
while not ChkKey do
        VSync;                  \(slow flakes)
SetVid($03);                    \restore normal text mode (for DOS)

Notice that the program is divided into procedures, which are specified by the 'proc' abbreviations. The term "procedure" is just another name for "subroutine". Unlike C, XPL0 distinguishes between procedures and functions. Functions are subroutines that return a value, and when they are called they are used as values.

Since in XPL0 all named things must be declared before they are used, procedures (and functions) are written before they're called. This tends to make programs look upside down. Generally you want to read an XPL0 program starting with the Main routine, which is at the bottom, and work your way upward to get the details.

Five new intrinsics are used by this program.

SetVid is used to change the video display mode. By passing hex 13, it changes to 320x200 graphics with 256 colors. By passing 3, it changes back to text mode. (When running under Windows, it's not necessary to restore text mode if the program exits back to the GUI; however, it might be running in a DOS box where it will return to a window that expects to be in text mode.)

Clear erases the entire screen.

Point is a subroutine that draws pixels on the graphic screen.

Rem returns the remainder of an integer division. It's equivalent to C's modulo (%) operator.

PIn reads from an input port.

SetVid, Clear and Point are examples of intrinsics that are used as subroutines, while Rem, Ran and ChkKey are examples where they're used as functions because they return a value.

This Snow program contains a procedure that is not shown in Polaris's code. It's the VSync procedure. In Polaris's code, Allegro provides the VSync routine, like the way XPL0 provides intrinsic routines. Since VSync is not provided, we must write it ourselves. Fortunately it's simple. The PIn intrinsic is used to access the hardware register that contains video status information. Bit 3 of this port indicates if vertical retrace is occurring. This typically occurs 60 times per second, so we use it to regulate the speed of our program.

XPL0 only has three data types: int, real and char. It does not have structures (or records). Instead of writing (as in Polaris's C++ code):

        struct particle
        {  int x, y;
           int layer;
        particle flakes[total_flakes];

you must declare names explicitly, by doing something like this:

        int     FlakesX(Total_flakes),

Another possibility for faking structures is to use arrays. For example, PointX, PointY, PointZ might be implemented as: Point(X), Point(Y), Point(Z).

When you run SNOW.COM, you should see something like this:


Here's another little demo. It shows lots of action by merely manipulating the color registers. Don't get sucked in!

include c:\cxpl\codesi;         \standard library 'code' definitions

int     Fade;                   \color intensity (0..128)
char    Palette(3*256);         \copy of color registers

proc    Rotate;                 \Rotate Palette and adjust Fade intensity
int     I, J, K,                \indexes
        R, G, B;                \red, green, blue
for I:= 0, 240*3-1 do           \shift down 16*3 bytes
        Palette(I):= Palette(I+16*3);
for I:= 0, 16*3-1 do            \wrap 16*3 bytes to the end
        Palette(I+240*3):= Palette(I);
K:= 0;                          \rotate 16*3 bytes in each of 16 groups
for I:= 0, 16-1 do
        R:= Palette(K);
        G:= Palette(K+1);
        B:= Palette(K+2);
        for J:= 0, 15*3-1 do
                Palette(K):= Palette(K+3);
                K:= K+1;
        Palette(K):= R;
        Palette(K+1):= G;
        Palette(K+2):= B;
        K:= K+3;
VSync;                          \regulate speed
POut(16, $3C8, 0);              \copy Palette to color registers
for I:= 16*3, 256*3-1 do        \ but don't alter the border color (0)
        POut(Palette(I)*Fade/128, $3C9, 0);
end;    \Rotate

proc    LoadBmp(Name);          \Load a 320x200x8 .bmp file but don't display it
char    Name;                   \file name
int     Hand, I, T,
        X, Y;                   \coordinates
Hand:= FOpen(Name, 0);                  \open file for input
FSet(Hand, ^I);                         \set device 3 to handle

for I:= 0, 53 do T:= ChIn(3);           \skip unused header info

for I:= 0, 255 do
        Palette(I*3+2):= ChIn(3)>>2;    \blue
        Palette(I*3+1):= ChIn(3)>>2;    \green
        Palette(I*3+0):= ChIn(3)>>2;    \red
        T:= ChIn(3);                    \unused

for Y:= -(200-1), 0 do                  \.bmp files are upside down!
    for X:= 0, 320-1 do
        Point(X, -Y, ChIn(3));

end;    \LoadBmp

begin   \Main
SetVid($13);                            \320x200x8-bit color
Fade:= 0;                               \set all color registers to black

repeat  if Fade < 128 then Fade:= Fade+1; \fade in
until   ChkKey;                         \loop until keystroke

repeat  Fade:= Fade-1;                  \fade out
until   Fade <= 0;

SetVid($03);                            \restore normal text mode
end;    \Main

The 'char' declaration sets up memory space for characters, or any kind of bytes. Here we're using it in two places: one to set up an array of bytes specifying the colors in the Palette; and another (in LoadBmp) to receive the string of characters that specifies the name of the file to load.

The Rotate procedure uses the POut intrinsic to write to the output port. The repeated writes to port $3C9 might seem strange. The PC has a hardware counter that increments each time this port is written. That way successive color registers are written. There are 256 groups each containing a red, green and blue register. Each register can have a value from 0 to 63. Thus you can get 63 shades of red (plus one shade of black), or 64*64*64 = 262144 total colors. However only 256 different colors can be displayed at one time.

The LoadBmp procedure uses several intrinsics to read in an image file. FOpen, FSet and OpenI are used to set up a file to be read in, one character at a time by ChIn. The .bmp file format is used here because it's simple (it doesn't use compression), and it's a standard Windows format that's universally supported.

If this program doesn't work (you might see ERROR 3), it probably means that you tried to run it directly from Windows without extracting the files to a folder. The accompanying file, Wormhole.bmp, must be in the same directory (or folder) as Wormhole.exe. Incidentally, any jitteriness you see is entirely due to Windows. I prefer to run these kinds of programs under pure DOS.

The Wormhole should look like this, but with lots of action:

32-bit XPL0

Now we're going to shift into high gear and use a much more powerful version of the XPL0 compiler.

Up until now we've been using the interpreted version. It has the advantage of being complete and self-contained (and is included in the bonus pack). However it's slow, it only has 16-bit integers, and it only provides about 60K of memory for variables and arrays. Actually, it does have double-precision floating point, and it will handle arrays that are much larger than 60K by using segment addressing; but for simplicity we're going to use the 32-bit version of XPL0 from now on.

Unfortunately this version is not included in the bonus pack because it requires Borland's TASM assembler. However you can download everything you need from the 'Net. The 32-bit compiler is on the XPL0 website at: and a free version of TASM is in at:

When you have TASM and TLINK set up on your computer, you can compile and run the Hello program like this:

        C:\CXPL>xpx hello

Note that the batch file xpx.bat is used instead of x.bat. You'll need to type "hello.exe" instead of just plain "hello" to run the newly compiled version if is in the current directory. That's because DOS gives first priority to running .com files.


Now let's start taking advantage of this new compiler. Here's Polaris's cross-fade demo.

include c:\cxpl\codes;          \intrinsic 'code' declarations for 32-bit XPL0

int     CpuReg,                 \array containing CPU hardware registers
        DataSeg,                \our data segment address
        I;                      \index for Main
real    XFade;                  \cross-fade; 0.=image1, 1.=image2
char    Image1(320*200),        \image from .bmp file
        Pal1(256*3),            \palette from the .bmp file
        ImageCombo(320*200),    \combined images

proc    VSync;                  \Wait for vertical retrace to begin
while port($3DA) & $08 do [];   \wait for vertical retrace signal to go away
repeat until port($3DA) & $08;  \wait for vertical retrace
end;    \VSync

proc    LoadBmp(Name, Image, Pal);      \Load 320x200x8 .bmp file
char    Name,           \file name
        Image,          \array to store image into
        Pal;            \palette array
int     Hand, I, T,
        X, Y;           \coordinates
Hand:= FOpen(Name, 0);                  \open file for input
FSet(Hand, ^I);                         \set device 3 to handle
for I:= 0, 53 do T:= ChIn(3);           \skip unused header info
for I:= 0, 255 do
        Pal(I*3+2):= ChIn(3)>>2;        \blue
        Pal(I*3+1):= ChIn(3)>>2;        \green
        Pal(I*3+0):= ChIn(3)>>2;        \red
        T:= ChIn(3);                    \unused
for Y:= -(200-1), 0 do                  \.bmp files are upside down!
    for X:= 0, 320-1 do
        Image(X-Y*320):= ChIn(3);
end;    \LoadBmp

proc    GenPal(Degree); \Vary PalCombo by interpolating between Pal1 and Pal2
real    Degree;         \varies from 0. to 1.
int     I, J;
\When Degree = 0. then PalCombo is at full intensity for Pal1.
\When Degree = 1. then PalCombo is at full intensity for Pal2.
for I:= 0, 256-1 do                     \for all the color registers...
    for J:= 0, 3-1 do                   \for red, green, blue...
        PalCombo(I*3+J):= Pal1((I>>4)*3+J) +
                fix(Degree*float( Pal2((I&$0F)*3+J) - Pal1((I>>4)*3+J) ));
end;    \GenPal

begin   \Main
CpuReg:= GetReg;                        \access copy of CPU (H/W) registers
DataSeg:= CpuReg(12);                   \our data segment address is in DS reg.

LoadBmp("IMAGE1.BMP", Image1, Pal1);
LoadBmp("IMAGE2.BMP", Image2, Pal2);

\Create a combined bitmap from the two 16-color bitmaps
\Image1 gets the upper nibble, and Image2 gets the lower nibble
for I:= 0, 320*200-1 do
        ImageCombo(I):= Image1(I)<<4 ! (Image2(I)&$0F);

SetVid($13);                                    \320x200x8-bit color graphics
Blit(DataSeg, ImageCombo, $A000, 0, 320*200);   \copy bitmap to screen

XFade:= 0.0;                                    \start by showing Image2
repeat  XFade:= XFade + 0.005;
        GenPal(abs(1.0 - Mod(XFade, 2.0)) );
                \varies between 1 and 0, then from 0 back to 1 (sawtooth)
        VSync;                                  \regulate speed
        port($3C8):= 0;                         \write color registers
        for I:= 0, 256*3-1 do
                port($3C9):= PalCombo(I);
until   ChkKey;

SetVid($03);                                    \restore normal text mode
end;    \Main

XFade is not only the name of this program, but it's also declared as the name of a 'real' variable. In 32-bit XPL0 integer variables have a range of -2147483648 to 2147483647, whereas 'real' variables range between +/-2.23E-308 and +/-1.79E+308 with 16 decimal digits (53 bits) of precision. This is plenty for what we're doing here.

Notice that the array declarations are taking up much more than 60K. In fact three of them take 64000 bytes apiece. 32-bit XPL0 uses extended memory, thus arrays can be megabytes in size.

Another little nicety available with this version of XPL0 is the 'port' command. It's used in the VSync procedure. This is a more efficient and intuitive way to access I/O ports. Contrary to what you might expect, directly accessing the hardware is a more consistent and reliable interface than calling BIOS routines to do these kinds of operations.

Similarly, there are three other command words used in this 32-bit code that are not available in the 16-bit, interpreted version of XPL0 (there you must use less efficient intrinsic calls to do the same operations):

'fix' converts (rounds) a real value to its closest integer.

'float' converts an integer to a real.

'abs' takes the absolute value of a real or an integer.

The Blit intrinsic is used to quickly copy a block of memory from one place to another. In this case the target location is video memory (segment address $A000). Since DOS (or Windows) decides where to put our data in memory, we must ask it where that is. GetReg provides access to an array that contains copies of the CPU's hardware registers. Among these is the data segment (DS) register which tells us where our data is located. In the program this information is stored into DataSeg, which is then passed on to Blit.

The Mod intrinsic takes the modulo of real numbers. It's similar to the remainder (Rem) intrinsic that is used for integers. For example, Mod(11.3, 2.0) = 1.3.

Here's a screen shot of what this program looks like, but you really need to see it run to appreciate the effect.


Here's Polaris's Lens example. This version takes advantage of another feature of 32-bit XPL0 by using 640x480 VESA graphics instead of the original 320x200 VGA.

include c:\cxpl\codes;          \run-time library routines (intrinsics)

def     Radius = 150,           \lens radius (pixels)
        Radius2 = Radius*Radius,
        Zoom = 3;               \magnification factor

int     CpuReg;                 \address of CPU register array (from GetReg)
char    Image(640*480);         \image from .bmp file

func    CallInt(Int, AX, BX, CX, DX, BP, DS, ES); \Call software interrupt
int     Int, AX, BX, CX, DX, BP, DS, ES; \(unused arguments need not be passed)
CpuReg:= GetReg;
CpuReg(0):= AX;
CpuReg(1):= BX;
CpuReg(2):= CX;
CpuReg(3):= DX;
CpuReg(6):= BP;
CpuReg(9):= DS;
CpuReg(11):= ES;
return CpuReg(0) & $FFFF;       \return contents of AX register
end;    \CallInt

func    OpenMouse;              \Initializes mouse; returns 'false' if it fails
begin                           \Pointer is at center of screen but hidden
CallInt($21, $3533);            \Make sure vector ($33) points to something
if ((CpuReg(1) ! CpuReg(11)) & $FFFF) = 0 then return false;
return if CallInt($33, $0000) then true else false; \reset mouse & return status
end;    \OpenMouse

func    GetMousePosition(N); \Return position of specified mouse coordinate
int     N;      \0 = X coordinate; 1 = Y coordinate
\For video modes $0-$E and $13 the maximum coordinates are 639x199, minus
\ the size of the pointer. For modes $F-$12 the coordinates are the same as
\ the pixels. For 80-column text modes divide the mouse coordinates by 8 to
\ get the character cursor position.
CallInt($33, $0003);
return (if N then CpuReg(3) else CpuReg(2)) & $FFFF;
end;    \GetMousePosition

proc    LoadBmp(Name);  \Load 640x480x8 .bmp file into Image and set color regs
char    Name;           \file name
int     Hand, I, T,
        X, Y,           \coordinates
        R, G, B;        \red, green, blue
Hand:= FOpen(Name, 0);                  \open file for input
FSet(Hand, ^I);                         \set device 3 to handle

for I:= 0, 53 do T:= ChIn(3);           \skip unused header info
port($3C8):= 0;                         \set color registers
for I:= 0, 255 do
        B:= ChIn(3)>>2;
        G:= ChIn(3)>>2;
        R:= ChIn(3)>>2;
        T:= ChIn(3);
        port($3C9):= R;
        port($3C9):= G;
        port($3C9):= B;
for Y:= -(480-1), 0 do                  \.bmp files are upside down!
    for X:= 0, 640-1 do
        Image(X-Y*640):= ChIn(3);

end;    \LoadBmp

proc    ExamineImage;   \Run magnifier until a key is struck
int     MX, MY,         \mouse coordinates
        X, Y,           \screen coordinates
        C;              \color of pixel to plot
repeat  MX:= GetMousePosition(0);
        MY:= GetMousePosition(1);
        for Y:= 0, 480-1 do             \constantly scan entire Image
            for X:= 0, 640-1 do
                if (MX-X)*(MX-X) + (MY-Y)*(MY-Y) <= Radius2 then \inside lens
                     C:= Image((X+(Zoom-1)*MX)/Zoom + (Y+(Zoom-1)*MY)/Zoom*640)
                else C:= Image(X+Y*640);
                Point(X, Y, C);
until   ChkKey;                         \loop until a key is struck
end;    \ExamineImage

begin   \Main
if not OpenMouse then [Text(0, "A mouse is required");   exit];

SetVid($12);                            \fool old mouse drivers
SetVid($101);                           \640x480x8-bit color


SetVid($03);                            \restore normal text mode
end;    \Main

The CallInt function uses the SoftInt intrinsic to access the hundreds of DOS and BIOS interrupt routines available in every PC. Note that the command 'func' is used instead of 'proc' to indicate that this subroutine returns a value. Also note the command 'return' at the end that specifies the value that is to be returned.

One thing to keep in mind when calling DOS and BIOS routines is that they live in a (humble) 16-bit world. The "& $FFFF" is used to mask off any junk that might be lurking in the high 16 bits of our 32-bit world.

An unusual characteristic of CallInt is that it allows a variable number of arguments to be passed to it. Only the register values (and place holders) that are actually used need to be passed.

Among the many BIOS interrupt routines are routines for using the mouse. These are made a little more programmer-friendly by the functions OpenMouse and GetMousePosition.

The meat of this program is in the procedure called ExamineImage. Everything else is either support or initialization code.

The Main routine at the bottom contains a couple new items. Note the 'exit' command. This exits the program, in this case, when a mouse is not detected. A value can optionally follow 'exit' and be used to return an error code to DOS. This works similar to the 'return' command, but 'exit' terminates the program.

Also note that brackets [] are another way to write 'begin' and 'end'. (XPL0 honchos prefer to use these sparingly. C programmers are welcome to go hog-wild.)

Here's what the screen looks like when Lens.exe is run, but you really need to move the lens around with the mouse to appreciate it.


Here's Polaris's Scroller example.

string 0;			\use the zero-terminated string convention
include c:\cxpl\codes;          \run-time library routines (intrinsics)

def     ScrWidth = 320,         \screen dimensions (pixels)
        ScrHeight = 200;
def     FontWidth = 32,         \font character dimensions (pixels)
        FontHeight = 64;
def     FontImageWidth = 1536,
        FontImageHeight = 64;

int     CpuReg,                 \array containing copy of CPU hardware registers
        DataSeg,                \segment address where our data got loaded

char    Message,                \message string to be displayed
        Background(ScrWidth*ScrHeight), \background screen buffer
        FontImage(FontImageWidth*FontImageHeight),      \image from the .bmp file
        ScrBuffer(ScrWidth*ScrHeight);  \screen buffer

func    StrLen(Str);            \Returns the number of characters in a string
char    Str;
int     I;
I:= 0;
while Str(I) \#0\ do I:= I+1;
return I;
end;	\StrLen

proc    LoadBmp(Name, Image);
\Load .bmp file into Image and set up color registers
char    Name,           \file name
        Image;          \array to store image into
int     Hand, I, T,
        X, Y,           \coordinates
        W, H,           \width and height
        R, G, B;        \red, green, blue
Hand:= FOpen(Name, 0);                  \open file for input
FSet(Hand, ^I);                         \set device 3 to handle
for I:= 0, 17 do T:= ChIn(3);           \skip unused header info
W:= ChIn(3) + ChIn(3)<<8;               \18 width
T:= ChIn(3) + ChIn(3)<<8;               \20 unused
H:= ChIn(3) + ChIn(3)<<8;               \22 height
for I:= 24, 53 do T:= ChIn(3);          \skip unused header info

port($3C8):= 0;                         \set up color registers
for I:= 0, 255 do
        B:= ChIn(3)>>2;                 \order is backwards
        G:= ChIn(3)>>2;
        R:= ChIn(3)>>2;
        T:= ChIn(3);
        port($3C9):= R;
        port($3C9):= G;
        port($3C9):= B;

for Y:= -(H-1), 0 do                    \.bmp files are upside down!
    for X:= 0, W-1 do
        Image(X-Y*W):= ChIn(3);
end;    \LoadBmp

proc    MaskedBlit(ImSrc, ImDst, Xs, Ys, Xd, Yd, W, H);
\Copy rectangular area of image
char    ImSrc, ImDst;   \image array source and destination
int     Xs, Ys, Xd, Yd, \coordinates in source and destination
        W, H;           \width and height of area to copy
int     X, Y, C, Is, Id;
for Y:= 0, H-1 do
        Is:= (Y+Ys)*FontImageWidth + Xs;
        Id:= (Y+Yd)*ScrWidth + Xd;
        for X:= 0, W-1 do
                C:= ImSrc(X+Is);
                if C then
                    if X+Xd>=0 & X+Xd=^A & Ch<=^Z then Ch:= Ch + ^a - ^A;

\Do a linear search
loop    begin
        for I:= 0, StrLen(ChMap)-1 do
                if ChMap(I) = Ch then quit; \(I = index into ChMap)
        quit;                               \in case Ch isn't found (I = StrLen)

\Copy character from FontImage to ScrBuffer (if we found a valid one)
if I < StrLen(ChMap) then
    if X+FontWidth>0 & X<ScrWidth then \verify that the X position is visible
        MaskedBlit(FontImage, ScrBuffer,
                I*FontWidth, 0,         \coordinates of letter in FontImage
                X, Y,                   \coordinates to put letter in ScrBuffer
                FontWidth, FontHeight); \font character dimensions
end;    \DrawChar

proc    DrawString(X, Y, Msg);
int     X, Y;
char    Msg;
int     I;
for I:= 0, StrLen(Msg)-1 do
        DrawChar(X+I*FontWidth, Y, Msg(I)&$7F);
end;    \DrawString

begin   \Main
CpuReg:= GetReg;
DataSeg:= CpuReg(12);

SetVid($13);                                    \320x200x8-bit color
LoadBmp("BIGFONT.BMP", FontImage);
LoadBmp("BACKGND.BMP", Background);             \use palette from Backgnd

Message:= "Greetz to all you XPL0 coders! ... Have a great time!! - Boreal";
FrameCount:= 0;
repeat  Blit(DataSeg, Background, DataSeg, ScrBuffer, ScrWidth*ScrHeight);
        DrawString(ScrWidth-FrameCount, 68, Message);
        if StrLen(Message)*FontWidth - FrameCount + ScrWidth <= 0 then
                FrameCount:= 0;
        FrameCount:= FrameCount+1;
        Blit(DataSeg, ScrBuffer, $A000, 0, ScrWidth*ScrHeight);
until   ChkKey;

SetVid($03);                                    \restore normal text mode
end;    \Main

XPL0 normally terminates strings by setting the most significant bit on the last character. However this convention can be changed with the "string 0" command so that strings are terminated with a zero byte. This makes XPL0 strings compatible with the rest of the world.

About the only other new item in this program is the 'loop' command, used in the DrawChar procedure. XPL0 has four kinds of looping constructs: 'for', 'while', 'repeat' and 'loop'.

The 'loop' command works with the 'quit' command to provide a general way of handling loops. In this instance the 'for' loop scans the character map (ChMap) for a matching character (Ch); and if one is found, the 'quit' command terminates both the 'for' loop and the 'loop' loop. (XPL0 does not have the much-abused 'goto' command.)

Here's a screen shot of the scroller in action:


That pretty well wraps it up for this crash course in writing demos with XPL0. In Hugi 31 Polaris also provided a 3D wireframe demo. I'll save that one to launch a separate article.

Please visit my webpage for more examples and to download the 32-bit version of XPL0. It's probably a good idea to also download the 16-bit version, which contains the 127-page manual and smoothes the way toward understanding the 32-bit version.

Thanks for reading all of this, and thanks for your interest in XPL0! (It needs all the users it can get.)

-Boreal (aka: Loren Blaney)