128 ANIMATION from ZX Computing, December 1986 Toni Baker shows how the 128's extra memory can be used for super smooth animation. As I sit here at my TV screen I am looking at an amazing effect which I would not have thought possible only a few months ago. A multi-faceted spiked shape is rotating before my eyes, taking about five seconds for each revolution. The shape - which can only be adequately described as a first-stellated dodecahedron - is drawn as a framework, so that I can see the back of the shape as well as the front. It's rather incredible - but beautiful. But, what is most astonishing of all is that the effect is being produced by none other than the ZX Spectrum! The secret lies in the vast untapped resources of memory on the Spectrum 128, making It possible to store whole screens, and recall them in sequence. Full screen high resolution animation is at last possible. Even more astonishing is the knowledge that the BASIC program which drew the individual frames was knocked up in only a few hours. The skeletal appearance of the rotating figure is a consequence of the simplicity of the BASIC program. Had I spent a few days playing with an art studio program I could have achieved full colour shading with light glinting off each facet as it passes the screen - but that kind of modification I shall leave to you. There are many methods of drawing screen pictures, but the animation effect was produced by a little piece of machine code of my own. Memory map To understand how the program works it is necessary to know all about the organisation of the memory on the Spectrum 128. This is very simple, so I shall now explain it. Memory on the 128 is arranged in PAGES. There are eight such pages, numbered from zero to seven, and each page of memory contains 16K. Hence we have 16 * 8 = 128K of memory altogether. In addition there are two ROMs, but these aren't important as far as this program is concerned. Addresses in paged memory begin at C000 and end at FFFF. Thus for each page of RAM the addresses overlap. Address CDEF on page zero is not the same thing as address CDEF on page one. They are different locations, despite having the same address. How then is it possible to access all of this memory if it's all superimposed on top of itself with the same address referring to any one of eight pages? The answer is a technique called PAGING. Only one 16K page may be "paged in" at a time. "Paged in" means that a page may be accessed - it may be PEEKed or POKEd or used in the normal way. If a page is NOT paged in then it may not be accessed at all (with just two exceptions - which we'll come to later). Because the addresses overlap, only one page may be paged in at a time. At all times, one of the pages will be paged in. When you use the computer normally, either in BASIC or machine code then page zero will be paged in, and it is important for any machine code program to restore page zero before returning to BASIC. As traditional machine- codeists will know, memory on the Spectrum appears to start at 4000h and go continuously all the way up to FFFF, and then stop. Below 4000h is the ROM, or at least one of the ROMs! But appears is the operative word. Memory is organised in eight 16K pages as I've explained, and the appearance of a continuous stretch of RAM from 4000h to FFFF is a 48K illusion, and it's all brought about by hardware, not software. Pages The first chunk of memory runs from 4000h to 7FFF. This chunk is in fact RAM page five! The second chunk runs from 8000h to BFFF - this is RAM page two! The last chunk, which runs from C000 to FFFF is RAM page zero. In practice this means that If you page in RAM page five then addresses C000 to FFFF will access precisely the same memory locations as addresses 4000 to 7FFF. POKEing an address in the range C000 to FFFF will actually POKE the corresponding address in the range 4000 to 7FFF. In a similar fashion, if page two is paged in then PEEKing an address in the range C000 to FFFF will PEEK the corresponding address in the range 8000 to BFFF. Thus the appearance of continuity is maintained. For the machine code programmer it means that pages two and five are special. They appear to be fixed in memory, and have fixed addresses less than C000. Pages two and five are, in fact, always paged in, and this is an advantage. Page five is special in another way too - a more familiar way. It stores the screen. POKEing an address in the range 4000 to 57FF (or an address in the range C000 to D7FF when page five is paged in) will directly POKE the screen. This you know, but the Spectrum 128 has not one but TWO memory mapped screens. Let us for a while explore this concept. Screens There are two screens, but only one of them may be visible on your TV at any one time. Normally this is screen zero. Screen zero is said to be ACTIVE whenever its contents appear on the TV, and similarly screen one is said to be ACTIVE whenever ITS contents appear on the TV. Screen one is stored in page seven, and this is a hardware manifestation, not a software one, so you cannot change the location of screen one. In this way page seven is special too. Locations C000 to D7FF store the screen bytes, while locations D800 to DAFF store the attributes, but remember these addresses refer to RAM page seven, not to RAM page zero, so it is not sufficient to POKE the addresses. Page seven must be paged in first. As far as the screens are concerned, it doesn't make any difference which page is paged in. Screen zero or screen one may be active regardless of whichever page of RAM is paged in. Only one screen may be active at a time (fairly obviously), and it is impossible to deactivate both screens at once - so either one or the other will always be showing on the TV. This means that it is possible to create flicker free animation! If you draw on screen one whilst screen zero is active then only screen zero will appear on the screen. Once the drawing is complete then you can activate screen one and the TV picture will change INSTANTLY!!! Now, with screen one active, you can draw on screen zero - only screen one (the previously completed drawing) will be showing on the TV. When this drawing is complete you can reactivate screen zero. Once again the TV image will change INSTANTLY with no flicker whatsoever. This, then, is the principle of my program. When we write Spectrum 128 addresses down it is conventional to use a five digit hexadecimal number, rather than a four digit number. The first digit refers to the RAM page number. In this way I could uniquely refer to address BEAD on page four as address 48EAD. This would be distinct from, say, 6BEAD, which refers to address BEAD on RAM page six. Whilst the machine code instruction set makes it impossible to refer to such locations directly (eg LD A,(4C000) is impossible) it is nonetheless a useful notation for we human beings. In this notation we could say that screen one occupies addresses 7C000 to 7DAFF inclusive (including the attribute bytes). This notation has its disadvantages too. Because RAM page five is permanently mapped in at 4000 to 7FFF then we can describe the position of screen zero in one of two different ways - either as 4000 to 5AFF, or as 50000 to 5DAFF. Both of these descriptions refer to the same chunk of memory. For completeness, I should add that there are also two 16K ROMs, although, as has already been stated, the ROMs aren't really relevant to this program. The two ROMs each occupy addresses 0000 to 3FFF, so, as with the RAM pages, only one ROM may be paged in at a time. Using the same convention as for the RAM pages we can uniquely specify a ROM address as a five digit hexadecimal number, so that 01234 refers to address 1234 in ROM zero, whereas 11234 refers to address 1234 in ROM one. Surprisingly, the ROM which appears to be paged in normally (which you can PEEK either from BASIC or machine code) is actually ROM page ONE, not zero. This ROM is the same as the old 16K ROM which was present on 16K and 48K Spectrums, with just a couple of changes. Enough of ROMs - let's get back to RAM pages and screens. Exactly HOW do you page them? The answer is the OUT instruction, and a new system variable called BANK_M. Its address is 5B5C (or 5DB5C to keep harping on about the same point over and over again). Figure one will explain exactly what each of its bits is for. In machine code it is possible to change RAM page, or to change which screen is active, by the simple procedure of loading BC with 7FFD, loading the A register with a value constructed from Figure one, and then performing two steps - in this order: (i) Store the value from the A register in the system variable (BANK_M); (ii) then use the machine code instruction OUT (C),A. The order of the last two instructions is important. If you put these the wrong way round then the system will go wrong if an interrupt occurs between the two instructions - this way round it's quite safe. It is also possible to change ROM page by this method, but interrupts must be disabled whilst the last two instructions are carried out in this case. -- Figure 1. The meaning of the bits of the system variable BANK_M at address 5B5C. When output to port 7FFD will change pages as follows: --- --- --- --- --- --- --- --- | | | | | | | | | --- --- --- --- --- --- --- --- \___ ___/ | | | \_____ _____/ | | | | | | | | | | | | | | \-------- Bits 2,1,0 = RAM page currently paged in | | | \---------------- Bit 3 = Screen number currently active | | \-------------------- Bit 4 = ROM number currently paged in | \------------------------ Bit 5 = Must be reset always \------------------------------ Bits 7,6 = Not used -- M/C The program makes use of all of these techniques. The machine code program is extraordinarily simple, provided you understand the screen and paging system that I have just described. I have achieved animation by storing twelve complete screen images throughout the vast spread of memory available. The attribute bytes in this case are not saved since they are the same for each frame, but you could adapt the program to store the attribute bytes as well with no difficulty. I have stored two complete frames on RAM pages zero, one, two, three, four and six. I have not used page five because page five contains screen zero and the BASIC program, along with the system variables, the machine stack (following the BASIC CLEAR instruction) and so on. I have not used page seven because page seven contains screen one and a whole host of new system variables and stuff. Even numbered frames are stored at address C000 on the relevant page, whIle odd numbered frames are stored at address D800 on the same page. Unfortunately it isn't possible to use the machine code LDIR instruction to transfer bytes from one page of memory to another if both pages have the same addresses. For instance suppose there were a frame stored at address 4C000, and I wished to transfer it to address 7C000. The LDIR instruction is not possible. It is possible to load one byte at a time, provided you change pages between the fetch and the store, and then back again afterwards, but this takes a phenomenally long time in machine code terms. To get round the problem I have made use of a temporary buffer at address 6800 (that is 5E800). The above example would be solved by paging in page four, using LDIR to transfer the frame from 4C000 down to 6800, and then paging in page seven and using LDIR once more - this time to transfer from the buffer at 6800 up to 7C000. The machine code is in three parts. It is stored at address B000, which corresponds to address 2F000. Note that although page two is in fact used to store screens, these screens occupy locations 2C000 to 2EFFF only. Locations above this are free for machine code and will not be overwritten by the various frames. Page A The first part of the code is called PAGE_A (address B001). It pages in the required RAM page and activates the required screen, as specified by the A register, but without changing the current ROM. This is quite boringly simple. The second piece of code is STORE_FRAME (address B011) and is called from BASIC to transfer the image currently on the screen (screen zero that is - the normal screen used by BASIC) into its specific place in memory. The last piece of code is called ANIMATE (address B03D) and it is this program which animates the twelve stored frames at a rate of twelve frames per second. This rate is completely flicker free and gives one second of continuous full screen free flowing movement. If the twelve frames are designed to repeat in a cycle, as in my example, then you have a cycle of continuous movement which goes on forever. Fortunately my program does allow you to break out by pressing BREAK. The BASIC which I have included is an example of how to use the ANIMATE routine. The outer FOR/NEXT loop, FOR I = 0 TO 11, will draw a quite pretty geometric figure from twelve different angles. Line 320 will call the machine code STORE_FRAME routine which will store each picture in memory once it is drawn. Finally, line 340 will call the ANIMATE routine to set the picture moving. You can adapt, or even change the BASIC program altogether if you like. The most impressive thing you could do would be to create twelve full screen pictures using an art studio type program, and save these on tape once they are drawn. Then you can rewrite my BASIC program to simply load screen images from tape and store them in memory one at a time as they are loaded. This program is quite interesting from a machine code point of view, and quite impressive from a visual point of view. It is my offering for the Winter Solstice - a present to you all (or at least those of you who've got a Spectrum 128 - the Plus Twos out now and you never know - you might get one as a present this season). Happy Solstice everyone. #B000 00 FRAME_NO DEFB #00 #B001 4F PAGE_A LD C,A ;C:= required page/screen number #B002 3A5C5B LD A,(BANK_M) ;A:= current page/screen number #B005 E6F0 AND #F0 #B007 B1 OR C #B008 01FD7F LD BC,#7FFD ;BC:= port reqd to change page/scr #B00B 325C5B LD (BANK_M),A ;Store new page/screen #B00E ED79 OUT (C),A #B010 C9 RET #B011 1100C0 STORE_FRAME LD DE,#C000 ;DE:= addr of even numbered frames #B014 3A00B0 LD A,(FRAME_NO) ;A:= frame number to store #B017 F5 PUSH AF #B018 CB3F SRL A ;Divide by two #B01A 3002 JR NC,ST_FR_2 #B01C 16D8 LD D,#D8 ;DE:= addr of odd numbered frames #B01E FE05 ST_FR_2 CP #05 #B020 2001 JR NZ,ST_FR_3 #B022 3C INC A ;Note that page 5 must be skipped #B023 CD01B0 ST_FR_3 CALL PAGE_A ;Select RAM page A/Screen zero #B026 210040 LD HL,#4000 ;HL: points to screen zero #B029 010018 LD BC,#1800 #B02C EDB0 LDIR ;Store screen in memory #B02E AF XOR A ;A:= 00 #B02F CD01B0 CALL PAGE_A ;Restore RAM page zero/Screen zero #B032 F1 POP AF ;A:= frame number #B033 3C INC A ;A:= next frame number #B034 FE0C CP #0C #B036 2001 JR NZ,ST_FR_4 ;Jump unless all frames stored #B038 AF XOR A ;In which case start again #B039 3200B0 ST_FR_4 LD (FRAME_NO),A ;Store new frame number #B03C C9 RET ;Return #B03D 3E07 ANIMATE LD A,#07 #B03F CD01B0 CALL PAGE_A ;Store RAM page 7/Screen zero #B042 210058 LD HL,#5800 ;HL: points to attribute file #B045 1100D8 LD DE,#D800 ;DE: points to screen 1 attributes #B048 010003 LD BC,#0300 #B04B EDB0 LDIR ;Copy attributes into screen 1 #B04D 76 ANIM_LOOP HALT ;Wait till next frame #B04E 2100C0 LD HL,#C000 ;HL:= addr of even numbered frames #B051 3A00B0 LD A,(FRAME_NO) ;A:= frame number to display #B054 F5 PUSH AF #B055 17 RLA #B056 17 RLA #B057 17 RLA #B058 E608 AND #08 #B05A 4F LD C,A ;C:= 00 (even frames) or 08 (odd) #B05B F1 POP AF #B05C F5 PUSH AF #B05D CB3F SRL A ;A:= page number on which this frame is stored #B05F 3002 JR NC,ANIM_2 #B061 26D8 LD H,#D8 ;HL:= addr of odd numbered frames #B063 FE05 ANIM_2 CP #05 #B065 2001 JR NZ,ANIM_3 #B067 3C INC A ;Note that page 5 must be skipped #B068 B1 ANIM_3 OR C ;Incorporate screen bit #B069 C5 PUSH BC #B06A CD01B0 CALL PAGE_A ;Select RAM page on which frame is ;stored, and either screen 0 (even ;frames or screen 1 (odd frames) #B06D 110068 LD DE,#6800 #B070 010018 LD BC,#1800 #B073 EDB0 LDIR ;Copy frame into buffer #B075 C1 POP BC #B076 79 LD A,C ;A:= 00 (evens) or 08 (odds) #B077 0F RRCA #B078 0F RRCA #B079 EE07 XOR #07 #B07B B1 OR C ;A:= 07 (evens) or 0D (odds) #B07C CD01B0 CALL PAGE_A ;For even frames select screen 0 ;and RAM page 7; for odd frames ;select screen 1 and RAM page 5 #B07F 210068 LD HL,#6800 ;HL: points to copy of frame to ;display #B082 1100C0 LD DE,#C000 ;DE:= address of screen (Note that ;address C000 on RAM page 5 is the ;same as address 4000 normally) #B085 010018 LD BC,#1800 #B088 EDB0 LDIR ;Copy the frame into the screen ;not being displayed #B08A F1 POP AF ;A:= frame number #B08B 3C INC A ;A:= next frame number #B08C FE0C CP #0C #B08E 2001 JR NZ,ANIM_4 ;Jump unless all frames displayed #B090 AF XOR A ;In which case start again #B091 3200B0 ANIM_4 LD (FRAME_NO),A ;Store new frame number #B094 CD541F CALL BREAK_KEY ;Is BREAK key pressed? #B097 38B4 JR C,ANIM_LOOP ;Loop back to display next frame ;unless BREAK pressed #B099 AF XOR A #B09A CD01B0 CALL PAGE_A ;Restore RAM page 0/Screen 0 #B09D CF RST #08 ;Generate error report #B09E 0C DEFB #0C ;"D BREAK - CONT repeats"