ELEMENTARY GRAPHICS part 1 of 3 ZX Computing, July 1986 Toni Baker begins a three part series on mastering machine code graphics. When we use the word "Graphics" we usually think of producing complex, and possibly moving or 3D effect pictures on the screen - but graphics doesn't necessarily mean that. Something as simple as PRINT "*", is utilising graphics - and in machine code if you don't understand how to do the simple things then the more intricate designs will remain beyond you. By "Graphics" I mean the process of altering the image on the TV screen in any way at all. This three part series is intended to cover the bare necessities of machine code graphics. In future series I may go on to the cleverer stuff, but for now this is a beginner's course - an apprentice course. The first things we need to know about are streams and channels. A "Stream" is computer jargon for "A small river", whereas a "Channel" is what we computer experts call "A narrow sea". These nautical references are, I confess, difficult concepts to master when viewed in the context of the ZX Spectrum, but the words are taken from the conventional English words of the same spelling: a stream (a number between zero and fifteen to index input and output) and channel (a device such as a printer or a TV screen). I hope I've made that clear. We need to know about them because although you can forget them altogether in BASIC if you want to, in machine code they are of paramount importance. Because: INPUT "HELLO";A$ is the same thing as PRINT #0;"HELLO";: INPUT A$ PRINT "HELLO" is the same thing as PRINT #2;"HELLO" LPRINT "HELLO" is the same thing as PRINT #3;"HELLO" The point is that all printing - whether to the main screen, the lower screen (ie. the bottom two or more lines normally used for INPUT items) or to a printer are - as far as the machine code programmer is concerned - identical. In the BASIC statements above, the number after the "#" symbol is the stream number. Since we usually only ever need to print to the screen or the printer, or to the lower screen (when creating an INPUT prompt) the stream number (in BASIC) does not normally need to be included - hence we usually use the format on the left. In machine code all is different. The stream number is essential, and must be specified: Stream #0 refers to channel "K" - the lower screen Stream #1 refers to channel "K" - the lower screen also Stream #2 refers to channel "S" - the main screen Stream #3 refers to channel "P" - the printer Although the channels which these stream numbers refer to may be altered by the OPEN # command, to do so is not very usual. I shall therefore assume they have not been altered, and shall continue to use #0 for the lower screen, #2 for the main screen, and #3 for the printer. Now - as you know - a computer can only ever do one thing at a time. This means it can only ever print one thing at a time. It can't print both to the screen and to a printer simultaneously. For this reason it can only ever need to use one channel, and hence one stream, at a time. The stream it is using at any particular moment is called the current stream, and likewise the channel being used is the current channel. When we call a machine code subroutine the current channel is unchanged. This means that the current channel remains as it was the last time any printing or editing was done. Usually we find this is channel "K" (the lower screen), but it may not be. Assuming that your program hasn't used the printer (or a microdrive or network, etc.) since the last time you entered a command (eg. RUN) or input something, then the current channel will still be either "K" or "P". I'm going to show you an ingenious little trick to switch between the two. You see the only difference between these two channels is bit zero of the system variable TVFLAG. This bit is reset for channel "S" and set for channel "K". This means that we can switch between channels "S" and "K" just by changing this bit. To demonstrate, try running the two programs of Figure One to see exactly what difference the first instruction makes. (Incidentally the values of the remaining bits of TVFLAG are irrelevant at this point.) Another trick is to switch between channels "K" and "S" and channel "P". Again only one bit is needed to tell the difference. Bit one of the system variable FLAGS is reset for channels "S" and "K" but set for the ZX Printer. The program of Figure Two will print to the ZX Printer using this simple trick. (But WARNING - The SPECTRUM 128 does not allow the use of the ZX Printer when in 128K mode. The program (Figure Two) must not be run on a Spectrum 128 in 128K mode or it will cause a crash!) But now for the general case - and this will work on a Spectrum 128 in 128K mode. We may change the current channel by selecting a new stream number. All we have to do is to load the A register with the desired stream number, and call a simple ROM subroutine. Figure Three shows this being done to select the screen as the current channel. Remember that this will always work, irrespective of whichever was previously the current channel. Users of the Spectrum 128 should note that when they are in 48K mode channel "P" is the ZX Printer, but when in 128K mode channel "P" is the built-in RS232 interface at the left hand side of the Spectrum case. It is safe to select channel "P" (ie. to select stream number three) in either case by this method. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - FIGURE ONE (Program a) FD360200 DEMO_S LD (TVFLAG),00 ;Reset bit zero of TVFLAG 3E2A LOOP LD A,"*" ;A contains the ASCII code for "*" D7 RST 10 ;Print CHR$(A); to current channel 18FB JR LOOP (Program b) FD360201 DEMO_K LD (TVFLAG),01 ;Set bit zero of TVFLAG 3E2A LOOP LD A,"*" D7 RST 10 ;Print an asterisk 18FB JR LOOP FIGURE TWO FDCB01CE DEMO_P SET 1,(FLAGS) 0600 LD B,00 ;Prepare for 256d asterisks 3E2A LOOP LD A,"*" D7 RST 10 ;Print an asterisk 10FB DJNZ LOOP ;Repeat until finished C9 RET FIGURE THREE 3E02 GEN_S LD A,02 ;A contains desired stream number CD0116 CALL CHAN_OPEN ;Select this as the current stream 3E2A LOOP LD A,"*" D7 RST 10 18FB JR LOOP - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - All of this is essential to know because Spectrum printing is achieved with the machine code instruction RST 10, and RST 10 will only print to the current channel. It is vital that you tell the computer where and how you want to print before you tell it what to print. RST 10 You can use the machine code instruction RST 10 to print any single character, be it an ASCII character, a graphics character, or a token. Simply load the A register with the code of the character and then execute RST 10. Note that ordinary characters when printed occupy only one print position, but keyword characters between A5 and FF (between A3 and FF on the Spectrum 128 in 128K mode) are expanded and printed in full, using as many print positions as there are letters in the keyword. In all cases RST 10 leaves all registers (except BC', DE' and possibly A) unchanged. PRINTING NUMBERS Single digits between 0 and 9 To print a number between 0 and 9 either load A with the number and CALL OUT_CODE (at address 15EF) (NOTE: this corrupts the E register), or load A with the ASCII code for the digit and use RST 10. Note that ADD A,30h / RST 10 uses exactly as many bytes as CALL OUT_CODE, but does not corrupt the E register. Integers between 0 and 9999 To print a positive number less than 10000d (or zero) simply load the number into the BC register pair and CALL OUT_NUM_1 (at address 1A1B). Integers between 0 and 65535 Two subroutine calls are required this time, once the integer has been placed in the BC register pair. First CALL STACK_BC (address 2D2B), then call PRINT_FP (address 2DE3). STACK_BC pushes the number onto the calculator stack, and PRINT_FP is a general purpose routine to print any number. Negative Integers Print a minus sign (by loading A with 2D and using RST 10) then print ABS of the integer using one of the methods described above All floating point numbers Since it is not possible to store a full floating point number in a single register or register pair it follows that a floating point number can only arise as a result of using the calculator (see separate series in this magazine). In such a case, the number to be printed should be left at the top of the calculator stack, and then CALL PRINT_FP (address 2DE3). In addition to printing the number the PRINT_FP subroutine will also delete the floating point number from the calculator stack. (Warning: If the number is strictly between -1 and +1, zero excluded, then a bug in the ROM causes an unwanted zero to be left on the calculator stack at this point). PRINTING STRINGS There are (at least) three different ways to print a string. In each case the text of the string must be stored at a fixed location in the Spectrum's memory. The first method of printing strings is also the simplest. DE must point to the first character of the string, and BC must contain the length of the string. Then simply CALL PR_STRING (at address 203C). Secondly, note that a string can arise as a result of using the calculator, in which case the string can be made to appear at the top of the calculator stack. In this case it is possible to print it straight from the stack by the procedure CP A followed by CALL PR_STR_1 (address 2036). Thirdly it is possible to print the (A+1)th string in a table of strings. DE must point to the byte before the first character of the first string - the byte pointed to must be between 80 and FF. Neither graphics characters nor token keywords are allowed in such a string. Each string in the table must be non-empty and must be terminated by the last character of the string having bit seven set. Assuming such a table has been constructed and registers A and DE assigned accordingly, the required string may be printed by CALL PO_MSG (at address 0C0A). Things to watch out for Note that if, at any stage, one of the predefined graphics characters (whose code is between 80 and 8F) is printed, then system variable addresses 5C92 to 5C99 will be corrupted. This corrupts calculator memories zero and one (see calculator series). This is totally unimportant unless you are either using the calculator or storing information in these variables. Also - if the subroutine PRINT_FP is called then all six of the calculator's memories will be corrupted - that is the whole of the system variable MEMBOT (addresses 5C92 to 5CAF). Again this is totally unimportant unless you are using the calculator memories or storing information in this area. Finally, note that the system variable MEM normally contains the address of MEMBOT. If the value of MEM has been altered (this is highly unlikely but I include it for sake of completeness) then PRINT_FP will not work correctly. The control characters In the Spectrum character set codes 20 to 7F are ASCII characters and codes 80 to FF are graphics characters and keyword tokens This leaves the codes 00 to 1F. These are control characters, and can be used to perform PRINT AT and stuff like that. Control 16h is the AT control (not to be confused with the keyword AT which has code ACh). Control codes are operated by loading the code into the A register and using RST 10. This means that they may be "printed" just like any other character, and may be included in strings. AT needs two parameters, and the control code is no exception. The two parameters must be specified in the right order, and these too must be "printed" with RST 10. Figure 4(a) shows the at control code being used to simulate PRINT AT 5,4;. Nor does it matter how many machine code instructions are executed between the three occurrences of RST 10. The Spectrum will always "remember" that the next two RST 10s will be AT parameters. The use of the AT control character plays its most useful role when we want to PRINT AT a variable location. Figure 4(b) will simulate PRINT AT D,E;. The next control code we meet is TAB, which is control 17h. This control code also requires two parameters: the low byte, followed by the high byte, of the required TAB column number. Of course, on the screen (and the ZX Printer) there are only thirty-two columns - any number higher than thirty-one is reduced to a number between zero and thirty-one by continually subtracting thirty-two from the number. This means that, effectively, the high byte is irrelevant (since 256d is a multiple of 32d). This is not necessarily true when you print to a printer other than the ZX (ie. via the RS232 interface). For this reason, the second TAB parameter may only be considered arbitrary if it is known in advance that the output will be sent to the screen or the ZX Printer. Figure Five shows the tab control code in action. Here I have considered the second byte to be important (just in case) and so have assigned it with a correct value of zero. Normally, however, the XOR A instruction could have been omitted. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - FIGURE FOUR (Program a) 3E16 AT_5_4 LD A,16 D7 RST 10 ;PRINT AT ... 3E05 LD A,05 D7 RST 10 ;5, ... 3E04 LD A,04 D7 RST 10 ;4; (Program b) 3E16 AT_D_E LD A,16 D7 RST 10 ;PRINT AT ... 7A LD A,D D7 RST 10 ;D, ... 7B LD A,E D7 RST 10 ;E; FIGURE FIVE 3E17 TAB_E LD A,17 D7 RST 10 ;PRINT TAB ... 7B LD A,E D7 RST 10 ;E ... AF XOR A D7 RST 10 ;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - The COMMA control (code 06) may be used to simulate PRINT , (ie. TAB to column 0 or column 16d) - it requires no parameters at all - simply load the A register with 06 and use RST 10. The ENTER control (code 0D) may be used to simulate PRINT ' (ie. print a new line, which has the effect of moving the print position to the start of the next line). Again, all we need do is load the A register with 0D and use RST 10. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - FIGURE SIX 3E10 DEMO LD A,10 D7 RST 10 ;PRINT INK ... 3E02 LD A,02 D7 RST 10 ;2; ... 3E11 LD A,11 D7 RST 10 ;PAPER ... 3E06 LD A,06 D7 RST 10 ;6; ... 3E2A LD A,"*" D7 RST 10 ;"*"; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - RST 10 is a very versatile instruction. Not only will it do all I have described above, but it can also be used to change the PAPER and INK colours, and so on, just like in a BASIC PRINT statement. Suppose we wanted to print a red asterisk on a yellow back ground. The program of Figure Six will do just the job. Note that once the print colours have been selected by this means they remain in force until they are changed, or until the end of the BASIC statement which called the machine code. Figure Seven shows all the control codes and what they do, and what parameters they need. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - FIGURE SEVEN --------------------------------------------- CONTROL CODE | PARAMETERS NEEDED, IF ANY --------------------------------------------- 06 comma | 08 backspace | 0D enter | --------------------------------------------- 10 ink | 00 black 11 paper | 01 blue | 02 red | 03 magenta | 04 green | 05 cyan | 06 yellow | 07 white | 08 transparent | 09 contrast --------------------------------------------- 12 flash | 00 off 13 bright | 01 on | 08 transparent --------------------------------------------- 14 inverse | 00 off 15 over | 01 on --------------------------------------------- 16 at | line number, column number --------------------------------------------- 17 tab | low byte, high byte --------------------------------------------- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Alternatives For channels "K", "S" and "P" (the ZX Printer) it is possible to achieve PRINT AT / PRINT TAB / PRINT , without using control codes at all. Here's how: PRINT AT B,C; can be achieved by CALL AT_B_C (address 0A9B) PRINT TAB A; can be achieved by CALL TAB_A (address 0AC3) [Actually called PO_FILL in the ROM disassembly. JimG] PRINT, can be achieved by CALL PO_COMMA (address 0A5F) There is also a rather easy way of changing both the paper and ink colours at the same time. In fact this method will also specify the current bright status and flash status while you're at it. All you have to do is to change one system variable - it's called ATTR_T. Simply construct an attribute byte with your required combination of colours and load it into this variable. For instance, the single instruction LD (ATTR_T),07 (FD365507) will change the colours to paper black / ink white / bright off / flash off. To construct such an attribute byte simply calculate (in decimal) 128*F + 64*B + 8*P + I (where F=flash status, B=bright status, P=paper colour, and I=ink colour). It is also easy to select "transparent" paper / ink / bright / flash, as you can in BASIC (ie. PRINT PAPER 8;). You can do it either with control codes, or by this method: Decide which of paper / ink / bright / flash you want to be transparent, add up the numbers from the list below, and load the result into the system variable MASK_T. For INK transparent: 07 For PAPER transparent: 38 For BRIGHT transparent: 40 For FLASH transparent: 80 For instance, the single instruction LD (MASK_T),3F (FD36563F) will assign both paper and ink to be transparent. Well, that's all from me for this month. Next month I'll continue the series by talking about the process of POKEing and manipulating the screen directly, without involving the use of PRINTing at all. See you then. ELEMENTARY GRAPHICS part 2 of 3 ZX Computing, August 1986 Toni Baker continues her graphics series with a look at the layout of the screen display. In this article I want to talk aboUt the possibilities of creating graphics by directly POKEing the screen, instead of "printing". To master this art we must first understand how the screen works. The mysterious layout of the TV screen is therefore today's topic. Let us proceed. The screen display The area of memory which is normally used for the TV screen lies between addresses 4000 and 5AFF. Those between 4000 and 57FF store the "black and white" version of the picture (ie. with all the colour taken out), whereas addresses 5800 to 5AFF store the colours. Those of you without a 128K Spectrum are restricted to using these addresses only - the screen area cannot be moved. Those of you with a Spectrum 128 will find that there is a second area of memory which may be used to store a screen image (addresses 7C000 to 7DAFF) and we'll be looking at that possibility later on in the article. For now though, we'll just take the simple case of the 48K machine. The first (hex) address for the screen is 4000. In decimal this is 16384. Type CLS followed by POKE 16384,255 (Spectrum 128 owners would be advised to use the Screen option from the menu to move any program to the bottom of the screen first, if they are operating in 128K mode). Watch what happens - you will see a little bar appear in the top left hand corner of the screen. Now type BORDER 5 so that you can see where the middle part of the screen begins and ends - you should notice that the little bar you've just printed is in the very top left hand corner. Now try typing POKE 16384,15 - this will give you a little bar half the length of the first, and not quite in the corner. What you have to do to understand this is to think of the numbers in binary. If we imagine that the zeroes are blank squares, and the ones are filled- in squares, then we can easily see the picture given by Figure One [ELEMG2_1.GIF]. According to this Figure One, the instruction POKE 16384,85 should give a speckled bar in the top left hand corner (if it doesn't then try tuning your TV in a bit better). In other words, each bit of the screen memory corresponds to one pixel on the TV. Have a look at Figure Two [see "part2" in ELEMGRPH.TAP] - this contains two machine code programs (very short), and a BASIC program (also very short), which I'd like you to try. The first of these prints a symbol on the screen, but without using RST 10; the second prints a lower case letter "a". The third program in the set is in BASIC, and this too manages to get a whole character-sized symbol on the screen - and all without using PRINT. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Program One 211000 START LD HL,0010 09 ADD HL,BC ;HL points to data given 110040 LD DE,4000 ;DE points to first square on screen 0608 LD B,08 7E LOOP LD A,(HL) ;A= next byte of data 23 INC HL 12 LD (DE),A ;Poke into screen 14 INC D 10FA DJNZ LOOP ;Repeat for all eight bytes C9 RET 00 38 54 92 BA D6 54 38 ;Data to print Program Two 210040 START LD HL,4000 ;HL points to first square on screen 11083F LD DE,3F08 ;DE points to expansion of "A" 0608 LD B,08 1A LOOP LD A,(DE) ;A= next byte of data 77 LD (HL),A ;Poke into screen 24 INC H 13 INC DE 10FA DJNZ LOOP C9 RET Program Three 10 FOR i=0 TO 7 20 INPUT a$ 30 LET x=FN h(a$)-16*FN h(a$(2)) 40 POKE 16384+256*i,x 50 NEXT i 60 DEF FN h(x$)=CODE x$-7*(x$>":")-48 RUN this then input (for example): "FF","81","BD","A5","A5","BD","81","FF" Figure 2. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Graphics in ROM All of the keyboard characters are stored in pixel expansion form in the ROM. To find the pixel expansion for any given character just multiply its character code by eight and add 3C00. For instance, "a" has character code 61h, and 8*61 + 3C00 = 3F08. This is the address we gave to DE in the program in Figure Two. You may have noticed that the machine code programs used INC D (and INC H) to locate the next row of pixels on the screen. Now this is very interesting because INC H increases the value of H by one, and therefore increases the value of HL by 0100h. Also the BASIC program used an increment of 256 (note 256d = 0100h). This tells us something about the layout of the screen. If HL contains the address of the first (topmost) pixel-row of a character square then the other seven pixel-row addresses can all be obtained by an appropriate number of INC H instructions (see Figure Three [ELEMG2_3.GIF]). So now we know the effect of adding 100h to the address, what about adding one? Try typing in the following BASIC program and running it to see what happens: 10 LET x=16384 20 FOR i= 0 TO 7 30 POKE x,255 40 LET x=x+1 50 NEXT i You should see a thin bar lining out across the top of the screen. Changing line 20 to FOR i=0 TO 31 extends the line right across the full width. So far it seems simple, but is there more to it than that? Change line 20 again to read FOR i=0 TO 255 (after all, we already know what happens if we add 256). Try it and see. All well and good - the first eight rows of characters are lined out. But - surely there's something wrong here? After all, we've added 255 to the original address to get the last address, and yet we know that if we added instead 256 (ie. if we added one more) then we'd be one pixel-row down from our original position. You can try it if you like (just change line 20 again). Common sense, on the other hand, would lead us to expect that the next pixels would be at the left of the screen, one more character square down. Common sense, unfortunately, doesn't mean much in the world of the Spectrum. Change line 20 to read FOR i=0 TO 2047 and see what happens. If you can imagine that the first eight lines of character squares are a completely isolated and separate part of the screen, having no relation to the rest of it, then the problem disappears and it all seems rather boring and sensible once more. Adding one moves you one character square to the right (skipping down to the left hand edge of the next line of character squares if you go off the right hand edge), and adding 256 (100h) moves you down by one pixel-row (provided you don't try to move down below the bottom of a character square). Figure Four [ELEMG2_4.GIF] shows that if we write the address of any screen position within these top eight rows in binary, and assign such a number to HL then it looks very sensible indeed, with H recording the row number within a character square, and L recording which character square. If you split the value in L into two parts, as I have done in the diagram, then we actually come up with the PRINT AT coordinates of the character square in question. This is just one way of looking at the correlation between what you see on the screen, and what the Spectrum sees in its memory. Screen lines Figure Four also shows us a second picture. You see - so far we've only looked at the first eight lines of the screen. We now need to look at the other 16. The first eight lines use up all the addresses from 4000 to 47FF. The next eight lines are organised in exactly the same way! They occupy addresses 4800 to 4FFF. Finally, the third eight-line segment occupies addresses 5000 to 57FF, and once again is organised in exactly the same way. What this means in practice is that we have to envisage the screen as being divided up into three "thirds" or "segments", with segment zero being the top eight lines, segment one being the middle eight lines, and segment two being the bottom eight lines. Thus - although in general adding one to a screen address will move you one square to the right (or onto the left hand edge of the next line) the procedure will not work if you try to cross from one segment to the next. For instance, the last square in segment zero has address 40FF whereas the first square in segment one has address 4800. Confusing though this may seem it is still very straightforward if we look at the address in binary. The second diagram in Figure Four gives the general picture - the address of any pixel-row on the screen. I use the terms "square", "row", "line", etc with precise meaning. Figure Five [ELEMG2_5.GIF] shows you the best way to visualise this breakdown. Now this way of representing screen addresses is fine for computers, since computers work in binary, but it's not all that good for humans. The diagram in Figure Six [ELEMG2_6.GIF] looks at screen addresses directly in hex. All you have to do is read off the first three hex digits from the left or right edge of the screen (whichever is closer) and the fourth digit from the top. Thus the square on the diagram which is filled in has address 509A. I hope you can see how to read this from the picture. This means that the eight addresses which together comprise this square are 509A, 519A, 529A, 539A, 549A, 559A, 569A and 579A. And now, a quick little subroutine to turn PRINT AT coordinates into screen addresses. This is the subroutine in Figure Seven [see "part2" in ELEMGRPH.TAP], which assumes that B contains the screen's y coordinate, and C contains the x. The final address is left in HL. Attribute bytes Now that we've looked at the screen in black and white, we need to consider the colour aspect of it. Consider the following BASIC program: 10 PAPER 6: INK 0: BORDER 6: CLS 20 FOR i=1 TO 22 30 PRINT ,"SYNCHRONICITY" 40 NEXT i 50 FOR i=22528 TO 23295 60 POKE i,15 70 NEXT i Watch what happens when you run it. First of all some text is printed on the screen (in black on yellow), and then, one square at a time, the screen changes colours (to white on blue) without altering the text. To understand exactly why the program works it is much easier to think in hex. The area of memory between 5800 and 5AFF is called the attributes file (as opposed to the display file which is the screen area). The contents of the attributes file determine the colours on the screen. It's very simple. Every character square on the screen has one attribute byte all to itself. The contents of such an attribute byte determine the colours of the corresponding square. This means that if you POKE an address in the attributes file then you will change the colours of one character square. We discussed attribute bytes in last month's article, so I won't go over them here in too much detail, but just a reminder: an attribute byte stores the FLASH status (off or on), the BRIGHT status (off or on), the PAPER colour (0 to 7), and the INK colour (0 to 7). The byte value is 128*F + 64*B + 8*P + I, or in binary: F B P P P I I I. In machine code, of course, it is useful to know precisely which byte in the attributes file corresponds to which square on the screen. You can work out the address of any individual attribute byte in BASIC by the formula 22528 + 32*Y + X (where X and Y are the PRINT AT coordinates of the corresponding square). You see, unlike the main screen, the attributes file is laid out completely sensibly - left to right, top to bottom. In fact - if you go back to Figure Six I'll show you an easy way to visualise it. Pick a square (for instance the one marked), and read off the first three digits from the left or right of the screen (whichever is closer), but use the figures given in brackets! Finally read the fourth digit from the top. This gives the full address in hex - for instance the attribute byte for the square marked has address 5A9A. Got it? Changing the BORDER colour There are essentially two steps involved in changing the colour of the screen border in machine code. This is because the ROM uses the border colour in two different ways. The actual border colour is the colour you see on the screen, right now, with your very eyes. The recorded border colour is a separate record kept by the Spectrum amongst the system variables. Every time you press a key whilst in command mode the actual border colour is changed to that of the recorded border colour. The problem for the programmer is that changing the actual border colour does not alter the ROM's permanent record - so you may find that the border colour changes back to what it was before, the next time you press a key. Alternatively, simply changing the permanent record will not affect the actual colour on the screen (not immediately anyway). You must change both of these. To change the actual border colour on screen you may use either the BASIC instruction OUT 254,colour or the machine code OUT (FE),colour. These instructions look identical - in fact they are. To change the permanent record you must POKE the system variable BORDCR (address 5C48) with an attribute byte for the border. It is the PAPER colour of this attribute which will be used for the border. The rest of the attribute byte is used to specify the colours of the lower part of the screen used for INPUT etc. There is, as always, an easy way of doing both jobs at once: the machine code instruction CALL BORDER_A at address 2297 (in hex CD9722). This will change both the actual border colour and the recorded border colour to whatever colour you desire - the choice of colour must first be loaded into the A register. Additional Information for Spectrum 128 owners The Spectrum 128 has not one, but two areas of memory which may be used to store a screen image. Ordinarily addresses 4000 to 5AFF are used, but the second possible location is addresses C000 to DAFF on RAM page seven. The first region is called screen zero and the second region is called screen one. Obviously only one of these screens may appear on the television at any one time. The addresses of individual bytes within screen one are obtained by calculating the address for the corresponding byte within screen zero and then "setting" bit fifteen (ie. change the initial 4 into a C, or the 5 into a D). Remember though that this screen area resides in RAM page seven, not RAM page zero (the normal page). Note that it is possible for screen one to be active irrespective of whichever RAM page is paged in - in other words, it is not necessary for page seven to be paged in for screen one to work. Changing from screen zero to screen one (or vice versa) may be achieved by calling either label SCR_0 or SCR_1 from the machine code program of Figure Eight [see "part2" in ELEMGRPH.TAP]. Note that the ROM is only designed to print onto screen zero, not screen one. Neither PRINT nor RST 10 will work on screen one, nor will PLOT or DRAW, neither will the automatic listing, and neither will command editing or INPUT. You must select screen zero before the end of a program. Another word of warning. The Spectrum, being a machine of very many bugs, not only displays a complete lack of ROM software to use screen one (not even in the new ROM), but worse - the software it does contain clashes directly with any possibility of using screen one. The problem is this: whenever you SAVE! something into RAMdisc using the new SAVE! command, the files saved are stacked one above the other. The first file saved will begin at address 1C000, and the stack will build upwards through consecutive memory areas: 1C000 to 1FFFF, 3C000 to 3FFFF, 4C000 to 4FFFF, 6C000 to 6FFFF, then 7C000 upwards. At the same time a second stack is built, beginning at address 7EBFF and growing downwards - this stack stores the NAMES of the files saved (as used by CAT!) and other information about the files. These two stacks are not allowed to meet - if there is any danger of this happening then error report "4 Out of memory" is given. Furthermore, the directory stack is not allowed to grow downward beyond address 7C000 (this is why it is impossible to SAVE! more than 562 files, even if they don't add up to 69K). It turns out that even though these stacks are not allowed to meet, they are allowed overwrite screen one. In fact, screen one may be overwritten by either the file stack, or the directory stack. Conversely, if enough programs are SAVE!d in RAMdisc then it is equally possible that POKEing into screen one may corrupt some of the files - a far more serious prospect. Machine code programmers should beware! Screen one may only be used if you don't save too much into RAMdisc. You can safely save 64K or 216 files, whichever comes first and still make use of screen one. Well that's it for this month. Next month I shall conclude this short mini-series by telling you how anything BASIC can do, machine code can do better - and faster. Happy Lughnasadh - see you next month. 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -