ELEMENTARY GRAPHICS
part 3 of 3
ZX Computing, September 1986

Toni Baker rounds off her machine code graphics series.


This is the third and final part of my Elementary Graphics series. In
this article I intend to go through all of the BASIC commands and
functions which have anything to do with graphics, and tell you how to
do them in machine code. This is actually extremely easy, since the
majority of commands etc. can be performed simply by calling a machine
code subroutine. In many ways, though, machine code is more powerful
than BASIC, simply because it can do more. I shall therefore show you
how to perform variations on BASIC commands, which you cannot achieve
in BASIC.


CLS

The first task we'll look at is clearing the screen. In BASIC this is
achieved by making use of the CLS command. In machine code we have
quite a few options, the easiest and simplest way being to simply call
the subroutine CLS at address 0D6B. This works exactly like the BASIC
CLS. It is also possible to clear the lower part of the screen only -
normally this will be the bottom two lines, but you could make it
more. Calling CLS_LOWER at address 0D6E will achieve this. Don't
forget that the lower screen will be cleared in the border colour, not
the screen colours.

We have an even more useful option available to us, however. There is
a subroutine in the ROM called CL_LINE at address 0E44. To use this
subroutine the B register must contain a number between 01 and 18h.
The action of the subroutine is to clear the bottom 'B' lines of the
screen. For instance, to clear the lowest ten lines of the screen it
is only necessary to load B with 0Ah and call CL_LINE. You can use
CL_LINE in one of two ways. If bit zero of the system variable
(TVFLAG) is reset then the screen colours will be used, otherwise the
border colour will be used. It is therefore possible to clear the
entire screen (i.e. including the bottom two lines) in the screen
colours - this would not have been possible using CLS! You should
note, however, that CLS will restore the print position to the start
of the screen, whereas CL_LINE will not.


Scrolling the screen

When you attempt to print something beyond the last available print
position, then the message "scroll?" appears at the bottom of the
screen and you are expected to press "y" or "n" (although in practice
any key will do). Pressing "n" gives you an error message, whereas "y"
will allow the screen to scroll. Subsequent attempts to print beyond
the end of the screen will cause the screen to scroll automatically
until everything on the screen is new (i.e. until everything which was
displayed at the last "scroll?" has disappeared off the top of the
screen). This type of scrolling is built in, and will occur in both
BASIC and machine code. We may therefore refer to it as "automatic"
scrolling.

But "manual" scrolling is also possible. Calling the subroutine SCROLL
[Actually called CL_SC_ALL in the ROM disassembly. JimG] at address
0DFE will scroll the entire screen, but with some unexpected effects:
firstly the current print position will be unchanged - you must deal
with this by hand: secondly, [if] the first line of the lower screen
is not blank, or is a different colour to the permanent upper screen
colours, then the result may not be what you desired.

The second way of producing manual scrolling is even more impressive.
The subroutine CL_SCROLL at address 0E00 is designed to scroll only
part of the screen. The B register must contain the number of lines to
be moved by the scroll (i.e, one less than the number of lines
affected). A minimum of two lines must be scrolled - if only the
minimum is used then B must contain 01 - the effect will be to
transfer the bottom line to the penultimate line, and then to blank
the bottom line. With B greater than one, any number of lines may be
scrolled, up to and including the full screen (for which B must equal
17h). The general effect of CL_SCROLL is therefore to transfer the
bottom 'B' lines of the screen upward one line, and then to blank the
bottom line. B must be in the range 01 to 17h.


Dumping the screen to a printer

COPY is a rather strange command on the Spectrum. On the 16K or 48K
version of the Spectrum it will dump a copy of the screen onto the ZX
Printer, if one is present. This will also be the case for the 128K
version in 48K mode. The Spectrum 128 in 128K mode, however, will dump
a copy of the screen onto any Epson compatible printer via the
built-in RS232 interface. How can we cope with both of these
possibilities in machine code?

The subroutine COPY at address 0EAC will dump the screen onto the ZX
printer. Note that this subroutine must not be used with the Spectrum
128 in 128K mode, since to do so would invariably cause a crash.

It is possible, however, to write a machine code program to COPY the
screen onto the ZX printer which will work on the Spectrum 128 in 128K
mode. The trick is to avoid erasing the printer buffer, as it is this
which causes the crash. Such a program is Included as Figure One - it
might even save you money because now you don't have to go out and buy
a "proper" printer- the ZX one can be used again!

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
;FIGURE ONE
;Program to dump the screen onto a ZX printer; will work on 128K Spectrum.

EA60 06B0     128COPY   LD   B,#B0         ;B= number of pixel rows to be copied
EA62 210040             LD   HL,#4000      ;HL= address of first row to copy
EA65 F3       128COPY_1 DI
EA66 C5       C_LOOP_1  PUSH BC
EA67 E5                 PUSH HL
EA68 78                 LD   A,B           ;A= number of rows remaining
EA69 FE03               CP   #03
EA6B 9F                 SBC  A,A
EA6C E602               AND  #02           ;Set bit one for the last two rows
EA6E D3FB               OUT  (#FB),A       ;Slow printer motor for last two rows
EA70 57                 LD   D,A           ;D contains "last two rows" flag
EA71 CD541F   C_LOOP_2  CALL BREAK_KEY
EA74 3807               JR   C,C_CONT_1    ;Jump unless BREAK pressed
EA76 3E04               LD   A,#04
EA78 D3FB               OUT  (#FB),A       ;Switch off printer
EA7A FB                 EI                 ;Re-enable interrupts
EA7B CF                 RST  #08           ;Exit with report
EA7C 0C                 DEFB #0C           ;"D, BREAK - CONT repeats"
EA7D DBFB     C_CONT_1  IN   A,(#FB)
EA7F CB77               BIT  6,A
EA81 2006               JR   NZ,C_CONT_2   ;Jump if printer not connected
EA83 17                 RLA
EA84 30EB               JR   NC,C_LOOP_2   ;Wait until printer is ready
EA86 CD120F             CALL COPY_ROW      ;Copy next row to printer
EA89 E1       C_CONT_2  POP  HL
EA8A C1                 POP  BC
EA8B 3E07               LD   A,#07
EA8D 24                 INC  H
EA8E A4                 AND  H
EA8F 200A               JR   NZ,C_CONT_3   ;Jump if within same character line
EA91 7D                 LD   A,L
EA92 C620               ADD  A,#20
EA94 6F                 LD   L,A
EA95 3804               JR   C,C_CONT_3    ;Jump if new screen segment reached
EA97 7C                 LD   A,H
EA98 D608               SUB  #08
EA9A 67                 LD   H,A
EA9B 10C9     C_CONT_3  DJNZ C_LOOP_1      ;HL= addr of next row.Loop until done
EA9D 3E04               LD   A,#04
EA9F D3FB               OUT  (#FB),A       ;Switch off printer
EAA1 FB                 EI
EAA2 C9                 RET
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Spectrum 128 owners might also be interested to hear that their own
particular version of COPY (ie. to dump the screen onto an Epson
compatible printer via the RS232) is also available from machine code.
The program in Figure Two will achieve this. Notice its extreme
simplicity! It will work on all Spectrum 128s, even if Uncle Clive
decides to change the ROM, because COPY_VECTOR is a vector address
which will be available on all versions of the 128 ROM.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
;FIGURE TWO
;Program to dump the screen to Epson printer; will only work on 128K Spectrum.

EAA3 CD005B   COPY_E    CALL SWAP          ;Page in new ROM
EAA6 CD2A01             CALL COPY_VECTOR   ;Call new ROM subroutine
EAA9 C3005B             JP   SWAP          ;Page in old ROM once more
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

It is also possible to COPY just one part of the screen to the ZX
printer. You can COPY any chunk of screen, from the bottom, the top,
or anywhere out of the middle. Furthermore you are not even restricted
to using whole character squares - the section to be copied can both
begin and end in the middle of a character square if you wish! Your
only restriction is that you must copy the full width of the screen,
even though you don't have to copy the full height. To achieve this,
HL must point to the first screen byte to be copied (ie. the byte in
the top left-hand corner of the chunk to be copied, which must be at
the left edge of the screen). In addition, B must contain the height
of the block to be copied, when measured in pixels (ie. B must contain
eight times the number of character squares in the height of the
chunk). Then either (1) disable interrupts and then call COPY_1 at
address 0EB2 - note that interrupts will be enabled upon return (16K
or 48K Spectrum, or Spectrum 128 in 48K mode), or (2) call the label
128COPY_1 in the program in Figure One.


Pause

The BASIC PAUSE statement may easily be simulated in machine code. All
you really have to do is to load BC with the number of frames for
which you wish to pause (there are fifty frames every second) and then
simply call PAUSE_1 at address 1F3D. Note that if BC is instead loaded
with zero then you simulate PAUSE 0, which will remain paused forever
(or until a key is pressed). This could be quite useful in graphics.

It is also possible to PAUSE for a precise number of frames without
the possibility of the PAUSE being terminated by pressing a key;
Figure Three shows a program for just this purpose. Simply load BC
with the required number of frames before calling the subroutine.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
;FIGURE THREE
;Program to PAUSE for BC frames, without terminating on key depression.

EAAC 78       PAUSE_X   LD   A,B
EAAD B1                 OR   C
EAAE C8                 RET  Z             ;Return if pause completed
EAAF 0B                 DEC  BC            ;Decrement count
EAB0 76                 HALT               ;Pause for one frame
EAB1 18F9               JR   PAUSE_X       ;Jump back for next test
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -


Plotting points

There are two ways to PLOT points in machine code. The first, and
simplest, method Is to load C with the x-coordinate, and B with the
y-coordinate, and then call the subroutine PLOT_SUB at address 22E5.
There is, however, a second method, which might be more suited to
calculator enthusiasts. In this method the two coordinates must be
placed at the top of the calculator stack in the order x,y. This being
done you simply call the subroutine PLOT at address 22DC. The
coordinates will be removed from the stack and the point will be
plotted.


Drawing a straight line

In machine code there are essentially two ways of drawing a straight
line, but before we look at these it is worth reminding ourselves of
the DRAW statement itself. You see, the syntax of the BASIC DRAW
statement is DRAW X,Y: but X and Y are not screen coordinates -
instead they are displacements from the last point plotted. It is
these displacements, rather than absolute screen coordinates, which we
must use in order to draw a line.

First the simple(ish) method. The X displacement must be stored
between registers C and E, with C containing ABS(X), and E containing
01 if X is positive or zero, FF if it's negative. In a similar way the
Y displacement must be split between B and D, with B containing ABS(Y)
and D containing 01 if Y is positive or zero, FF if it's negative.
Once the registers are so arranged, the subroutine DRAW_3 may be
called, at address 24BA. This will draw the line, but it will also
corrupt HL'. HL' must be reloaded with 2758h before returning to
BASIC, or else you'll get a crash.

A second way of drawing a straight line is to have the X and Y
displacements at the top of the calculator stack, in the order X,Y.
You then merely have to call DRAW_LINE (address 24B7). This will
remove X and Y from the calculator stack and then draw the line. Note
that this too will corrupt HL', and so you must reload It with 2758h
before returning to BASIC


Drawing an arc

In BASIC, the statement DRAW X,Y,A will draw an arc from the last
point PLOTted (x1,y1,say) to (X+x1,Y+y1), such that the arc is drawn
anti-clockwise, and such that the angle subtended by the arc is 'A'
radians. To perform this task in machine code the three parameters, X,
Y and A must be placed, in that order, at the top of the calculator
stack. The subroutine DRAW_ARC may then be called, at address 2394,
which will draw the arc on the screen as required. Note that HL' is
corrupted by this routine, and must be restored to 2758h before you
return to BASIC.


Drawing a circle

To draw a circle, the X and Y coordinates of the centre of the
proposed circle must be placed at the top of the calculator stack,
followed by the radius. You must then call DRAW_CIRCLE, which is at
address 232D, and the circle will be drawn. This routine also corrupts
HL', so it is important to restore HL' to 2758h before returning to
BASIC.


Testing a point on the screen

The function POINT(X,Y) can be fairly easily simulated in machine code
either with X and Y initially on the calculator stack (in which case
you must call POINT_SUB at 22CB), or with C containing the X
coordinate and B containing the Y coordinate (in which case you should
call POINT_1 at address 22CE). In either case the result will be left
at the top of the calculator stack, and will be zero if the point was
PAPER, one if the point was INK. Calling the subroutine FP_TO_A at
address 2DD5 after having called the point subroutine will transfer
the result into the A register.


Testing the colours of a Character Square

The function ATTR(Y,X) can also be easily simulated in machine code.
If Y and X are initially on the calculator stack then call S_ATTR_S
(address 2580), otherwise C must contain the line number, and B the
column number, than call ATTR_1 (address 2583). In either case the
result will be left at the top of the calculator stack. Calling
FP_TO_A at address 2DD5 after having called the ATTR subroutine will
transfer the result to the A register. The result will be the
attribute byte for the given character square.


Testing the contents of a Character Square

The function SCREEN$(Y,X) is used in BASIC to determine which of the
ASCII characters (if any) is printed at character square (Y,X). It
will not, however, detect either user defined graphics or the built-in
block graphics. Furthermore, the Spectrum, being a machine of very
many bugs, makes a complete mess of the calculator stack whenever
SCREEN$ is used in BASIC, with the result that the BASIC statement IF
SCREEN$(0,0) = SCREEN$(0,1) THEN PRINT "TRUE" will always yield TRUE
irrespective of whether or not it's supposed to. Fortunately this bug
is not present in machine code!

To use the SCREEN$ function you must call either S_SCRN$_S (address
2535) if the two coordinates are at the top of the calculator stack in
the order Y,X; or you must call SCREEN$_1 (address 2538) if C contains
the line number and B contains the column number. In either case the
string result will be left at the top of the calculator stack. If you
then call STK_FETCH (address 2BF1) then BC will contain zero if no
match was found, or one if a match was found, in which case A will
contain the character code of the character found, and DE will point
to a second copy of A.

Figure Four is an improved version of the SCREEN$ function, which will
also detect both user- defined graphics and built-in block graphics.

Well, that concludes this article, and also this series. I have
covered the elementary aspects of Spectrum graphics; the basic
essentials which you'll need to know before you can progress to more
complicated graphics. There will of course be other graphics series in
the future, but not straight away. See you next month. May the force
be with you ...

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
;FIGURE FOUR
;Improved SCREEN$ function to detect all types of character.
;Call from label SCR_FP if coords are at top of calc stack in order Y,X.
;Result on stack.
;Call from label SCR_FN if used with user-defined functions FN S and FN S$.
;DEF FN S(y,x)=USR SCR_FN
;DEF FN S$(y,x)=CHR$ USR SCR_FN AND USR SCR_FN
;Call from label SCR_1  if C = y coordinate; B = x coordinate. Result left in
;A reg with zero flag set for successful search, reset otherwise.

EAB4 CD0723   SCR_FP    CALL STK_TO_BC     ;C= y coord, B= x coord
EAB7 CDDAEA             CALL SCR_1         ;A= Screen$(y,x)
EABA 010000             LD   BC,#0000
EABD 2003               JR   NZ,SCR_STR    ;Jump if no match found
EABF 03                 INC  BC            ;BC= length of string
EAC0 F7                 RST  #30           ;Create space for string
EAC1 12                 LD   (DE),A        ;Assign string with character found
EAC2 C3B22A   SCR_STR   JP   STK_STO_$     ;Push onto calc stack and return

EAC5 2A0B5C   SCR_FN    LD   HL,(DEFADD)   ;HL points to function args
EAC8 110400             LD   DE,#0004
EACB 19                 ADD  HL,DE
EACC 4E                 LD   C,(HL)        ;C= y coordinate
EACD 19                 ADD  HL,DE
EACE 19                 ADD  HL,DE
EACF 46                 LD   B,(HL)        ;B= x coordinate
EAD0 CDDAEA             CALL SCR_1         ;A= Screen$(y,x)
EAD3 2801               JR   Z,SCRSINGLE   ;Jump if character found
EAD5 AF                 XOR  A             ;Otherwise use zero
EAD6 4F       SCRSINGLE LD   C,A
EAD7 0600               LD   B,#00         ;BC= character found
EAD9 C9                 RET

EADA 79       SCR_1     LD   A,C
EADB 0F                 RRCA
EADC 0F                 RRCA
EADD 0F                 RRCA
EADE E6E0               AND  #E0
EAE0 A8                 XOR  B
EAE1 5F                 LD   E,A
EAE2 79                 LD   A,C
EAE3 E618               AND  #18
EAE5 EE40               XOR  #40
EAE7 57                 LD   D,A           ;DE= address of square to search
EAE8 D5                 PUSH DE            ;Stack search address
EAE9 2A365C             LD   HL,(CHARS)
EAEC 24                 INC  H             ;HL points to character set
EAED CD4D25             CALL S_SCRN_LP-2   ;Test for ASCII character
EAF0 CDF12B             CALL STK_FETCH     ;A= character found, if any
EAF3 D1                 POP  DE            ;DE= address of square to search
EAF4 0D                 DEC  C
EAF5 C8                 RET  Z             ;Return if search successful
EAF6 D5                 PUSH DE
EAF7 2A7B5C             LD   HL,(UDG)      ;HL points to UDG character set
EAFA 0615               LD   B,#15         ;B= number of UDGs
EAFC CD4F25             CALL S_SCRN_LP     ;Test for UDG character
EAFF CDF12B             CALL STK_FETCH     ;A= result - 25h
EB02 C625               ADD  A,#25         ;A= character found, if any
EB04 E1                 POP  HL            ;HL= address of square to search
EB05 0D                 DEC  C
EB06 C8                 RET  Z             ;Return if search successful
EB07 CD17EB             CALL TEST_HALF     ;Test for 1st half of block graphic
EB0A C0                 RET  NZ            ;Return if search unsuccessful
EB0B 4F                 LD   C,A           ;C= first half of graphic
EB0C CD17EB             CALL TEST_HALF     ;Test for 2nd half of block graphic
EB0F C0                 RET  NZ            ;Return if search unsuccessful
EB10 87                 ADD  A,A
EB11 87                 ADD  A,A
EB12 81                 ADD  A,C
EB13 C680               ADD  A,#80         ;A= character code of block graphic
EB15 BF                 CP   A             ;Set the zero flag
EB16 C9                 RET

EB17 CD27EB   TEST_HALF CALL TEST_ROW      ;Test for 1st row of block graphic
EB1A C0                 RET  NZ            ;Return if search unsuccessful
EB1B 57                 LD   D,A           ;D= code for first row
EB1C 0603               LD   B,#03
EB1E CD27EB   TH_LOOP   CALL TEST_ROW      ;Test for remaining rows
EB21 C0                 RET  NZ            ;Return if search unsuccessful
EB22 BA                 CP   D
EB23 C0                 RET  NZ            ;Return of pixel row  different
EB24 10F8               DJNZ TH_LOOP       ;Repeat for remaining rows
EB26 C9                 RET

EB27 7E       TEST_ROW  LD   A,(HL)        ;A= pixel row from screen
EB28 24                 INC  H             ;HL points to next row
EB29 1E00               LD   E,#00
EB2B CD39EB             CALL TEST_NIBB     ;Test high nibble
EB2E C0                 RET  NZ            ;Return if test fails
EB2F CB03               RLC  E             ;Bit 0 of E assigned as required
EB31 CD39EB             CALL TEST_NIBB     ;Test low nibble
EB34 C0                 RET  NZ            ;Return if test fails
EB35 7B                 LD   A,E
EB36 07                 RLCA               ;A= result
EB37 BF                 CP   A             ;Set the zero flag
EB38 C9                 RET

EB39 C5       TEST_NIBB PUSH BC
EB3A CB13               RL   E
EB3C 17                 RLA
EB3D CB1B               RR   E             ;Assign bit 7 of E from A
EB3F 0603               LD   B,#03
EB41 AB       TN_LOOP   XOR  E
EB42 17                 RLA
EB43 3805               JR   C,TNABANDON   ;Abandon test if any bit different
EB45 10FA               DJNZ TN_LOOP       :Repeat test for all bits
EB47 BF                 CP   A             ;Set the zero flag
EB48 1802               JR   TN_EXIT
EB4A F6FF     TNABANDON OR   #FF           ;Reset the zero flag
EB4C C1       TN_EXIT   POP  BC
EB4D C9                 RET
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
