Earxtutchap4
Jump to navigation
Jump to search
.------<------<------<-----. v CHAPTER 4 : MAKING LOOPS ^ '------>------>------>-----' This chapter really is different in many ways to the last two. It is not aimed at getting sound, music or interaction directly, but it shows you the basics on how to make a fast and effecient loop for all of your routines. You want to plot someting like 80 pixels on your Falcon true color screen. You know that every pixel is one word(16-bits). You can do either this: repeat the command that moves a word 80 times in your code (this is called "unrolling" or "hardcoding"), OR this: You can use a loop... Let's take a look at falcon-pixel plotting routines: moveq #0,d0 * Prepare d0 for being a counter. loop: move.w #$ffff,(a0)+ * Do one pixel and move to the next. addq.w #1,d0 * Increase the counter. cmpi.w #80,d0 * If the counter isn't 80 > again. bne.s loop As you can see every loop-structure consists of an initialising part, a processing part and a part to do loop-household things. Well, this is reasonable. At least it is smaller than repeating the command every time in your code.. But it's slower. And if it's one thing we don't want in assembly it's slow code! The first step to faster code is: moveq #80-1,d7 * initialize counter loop: move.w #$ffff,(a0)+ * do one pixel and move to the next dbra d7,loop * Subtract 1 from d7.w and loop * until d7=-1 There you have it. If you're not using the counter for other purposes, you can just as well use a dbra loop. It's simply much faster! There are many ways to get this loop even faster, but you'll read more about that in the next chapter. Nested loops are a bit more complicated then this and I can hear you asking what 'nested' actualy means. A 'nested' loop means 'a loop in a loop'! Wow! That sounds GrOoVy! Like true industrial, man!! A good example of a nested loop would be something like clearing the left half of the screen. Again this situation goes for Falcon true color, but the same can easily be adapted to the ST-low resolution. move.l #screenaddress,a0 * Screenaddress in a0. move.w #200-1,d7 * Initialize for bigloop. bigloop: move.w #160-1,d6 * Initialize for loop. loop: clr.w (a0)+ * Clear one pixel and move to the next. dbra d6,loop * Loop 160 times. adda.l #160*2,a0 * Move to next line. dbra d7,bigloop * Loop 200 times. Note that the screen is 320 pixels wide so the half is 160 pixels wide. When you've cleared those 160 pixels you need to adjust a0 by adding the length in bytes of the 160 pixels. This brings you to the beginning of the next line. As you can see d7 is now reserved for 'bigloop' and d6 is reserved for 'loop'. This automatically means you can never have more than 7 nested loops because you only have 8 data registers. It's ofcourse possible to backup the registers and restore them again, but the more memory accesses.. the slower it will get. Ofcourse the power of loops isn't only repeating the same operations over and over again without using up much space. They can also be used to make code more flexible. A flexible loop can for instance allow copying/drawing differently sized blocks or maybe show a starfield from whatever angle you want simply by putting some different values and addresses into registers before running them. We're now gonna go up a level to see how you could construct very big loops. If you have a game you need to rebuild your paying-screen every so often. For this you need a really complicated loopstructure. If you checked out my last book, you might know that I explained something about game-loops. I'm gonna do more or less the same, but now in assembler. Here's the situation: * We want our background refreshed everytime. * We want our enemy sprites moved accordingly to their programming and they can react to the main-player too. * We want the sprites displayed with animation and some FX like explosions too. * We want our main sprite drawn in the same way. * We want a nice panel at the side of the screen that shows realtime statistics. * We want to check for joystick input and read it and check if the spacebar is pressed (spacebar exits the game) The loop will look somthing like this: mainloop: bsr HANDLE_INPUT * Read stick+keys and update variables. bsr HANDLE_BACKGROUND * Initialize position+masks+animation. bsr HANDLE_MAINSPRITE * Handle collision+masks+speed+weapons. bsr HANDLE_ENEMIES * Do same for all the enemies. bsr PLOT_BACKGROUND * Put the background in screenbuffer. bsr PLOT_MAINSPRITE * Put the main sprite in screenbuffer. bsr PLOT_ENEMIES * Put the enemies in screenbuffer. bsr WAIT_VBL * Wait for the VBL (no flicker). bsr SWAP_SCREENS * Swap screens (no flicker). tst.b spacepress * Test for space. beq.s mainloop * if no spacepress > again As you can see you divide all the hard work into subroutines. The subroutines theirselves aren't in here, because it's irrelevant and it would be far too much work. About the order of subroutines.. The checking for input from hardware decives MUST always be seperated in big loops. If you don't you're bound to get crashes all over the place! If you simply let your program read joystick input everywhere, you mess the code up so hard that even yourself won't know what you did. Also note that you musn't put the call to HANDLE_INPUT inbetween other routine-calls. If you do that you'll mess up the position of your main-sprite. The handling routines come in second. You should also keep these together. They only calculate the frames, position and what FX are necesary. After that, the plotting routines do the rest. Then there is some waiting for the VBL. You should now what that is from chapter 2. Also the screenbuffers are swapped, which also has to do with flickerless animation. I'll explain that later on. It's absolutely essential that you keep these together in the right order and do them AFTER THE PLOTTING! Finally a byte is tested to see if the looping can continue or not. This byte is updated by the HANDLE_INPUT routine. BTW for those of you that don't know how subroutines work I'd like to explain it. It's quite important for the understanding of structures. OK, when you use a bsr or 'branch to subroutine' the 680x0 remembers the position of the following instruction. Then it jumps to a label and executes all the instructions untill it reaches a 'rts' (return from subroutine). Then it jumps back to the saved location. Just look at the picture: Step 1: |Step 2: =/\====-------------------+=/\====------------------ ....... | ....... .... | .... move.w #1,d0 | move.w #1,d0 -> bsr routine | bsr routine rol.l #1,d0 | rol.l #1,d0 ..... | ..... ... | ... . | . . | . . | . routine: | routine: ..... | -> ..... ... | ... ..... | ..... rts | rts Step 3: |Step 4: =/\====-------------------+=/\====------------------ ....... | ....... .... | .... move.w #1,d0 | move.w #1,d0 bsr routine | bsr routine rol.l #1,d0 | -> rol.l #1,d0 ..... | ..... ... | ... . | . . | . . | . routine: | routine: ..... | ..... ... | ... ..... | ..... -> rts | rts (Yeh! Now I'm real proud of myself that I made cool ASCII art again!) The little arrow is the current postion where the 680x0 is executing the instructions. Using this bsr/rts combination is very common in most programs and it is damn handy. It has the following advantages: * Allows repetitive use of same piece of code without having to copy it. * Allows the code to be called from different positions in the code. * Makes loops more readable. Ofcourse using this technique is only handy in places where not much speed is required. In the 'mainloop'-example this is the case! But beware when calling from within the innermost nested loops! A situation like the following will drasticly decrease the execution speed of a loop structure: movea.l screen_address,a0 * Set a0 to the first pixel on screen. move.w #200-1,d7 * Prepare for 200 outer loops. yloop: bsr DRAW_LINE * Call routine to draw a screenline. adda.w #160,a0 * Set a0 to next screenline. dbra d7,yloop * INPUT: a0: startaddress of screenline DRAW_LINE: move.w #320-1,d7 * Prepare to loop 320 times. xloop: bsr PLOT_PIXEL * Go to next pixel here.. dbra d7,xloop rts * INPUT: a0: address of actual pixel PLOT_PIXEL: * Code goes in here.. rts This piece of code is well readable, but sadly it lacks speed. A bsr/rts combination everytime a pixel is drawn is a bad idea. It causes enormous overhead. So only use them in outer loops. This makes the global structure of the program look somewhat better and easier to modify at high level. The innerloops should best be kept without bsr/rts, saving/restoring of registers (d0 to a6) and other costly instructions. So, when you start coding on loopstructures you always come across the well- known two tradeoffs: speed and readability/adaptability. Coder's opinions on those two differ most of the time. Some people like their code completely readable, some optimise every byte, some make a mix of the two. Whether you do or don't make everything optimised, you should always consider this way of working with optimisation in loops. 1) First of all, lay out your loop-structure from the highest level. Get your code running and please don't think about speed yet. 2) Check out where the bottleneck in the loop is. This is mostly (always :)) the loop nested deepest. 3) Then only optimise these innerloops. Remove unneeded branches/subroutine jumps, replace costly instructions with cheaper ones (or combinations of cheaper ones), reduce flexibility by using simpler logic and instructions, etc, etc. 4) If you're a perfectionist you can also optimise other pieces of code besides the most inner loop. This often is the step where the code becomes unreadable and it's wise to only do this when you want to release your final product (i.e. game, program or demo). Using this method you keep a good overall view AND get the speed where it is needed most. Let's conclude this chapter with a practical example. A loop that reads a table with sprite-positions and plots sprites on screen accordingly. To begin with we setup our initial sluggish, but readable code. Please note that the sprite- routine is for Falcon highcolor, just to keep it simple. *============================================================================== * :STep _/I\_ Laying out the STructure: ******** CODE MEMORY SECTION ******** TEXT * Routine that draws all sprites in the spritetable. DRAW_SPRITETABLE: lea sprite_table,a0 * Get the spritetable. move.w number_of_sprites,d0 * Get the number of sprites. moveq #0,d1 * Initialize loopcounter. draw_sprite_loop: movem.l d0-d1/a0,-(sp) * Save used registers. move.w (a0)+,d0 * Get X center of sprite. move.w (a0)+,d1 * Get Y center of sprite. bsr.s DRAW_SPRITE * Jump to the spriteroutine. movem.l (sp)+,d0-d1/a0 * Restore used registers. addq #4,a0 * Goto next sprite. addq.w #1,d1 * Increase the loopcounter. cmp.w d0,d1 * If not all sprites are done: bne.s draw_sprite_loop * then loop once again. rts * Routine that draws a 16*16 highcolor sprite on screen at a given position. * INPUT: d0.w: center X coordinate of sprite * d1.w: center Y coordinate of sprite DRAW_SPRITE: movea.l #screen,a0 * Get address of the screen. movea.l #sprite,a1 * Get address of the sprite. subq.w #16/2,d0 * Get left position of sprite. subq.w #16/2,d1 * Get right position of sprite. add.l d0,d0 * / Calculate sprite's mulu.w #320*2,d1 * | offset on add.l d0,d1 * \ the screen. adda.l d1,a0 * Add offset to the screenaddy. move.w #16-1,d7 * Setup Y loopcounter. yloop: move.w #16-1,d6 * Setup X loopcounter. xloop: move.w (a1)+,(a0)+ * Plot one pixel and goto next. dbra d6,xloop adda.w #(320-16)*2,a0 * Goto to next screenline. dbra d7,yloop rts ******** DATA MEMORY SECTION ******** DATA number_of_sprites: DC.W 4 * four sprites in our table sprite_table: DC.W 167,89 * X and Y centers of sprite 1 DC.W 23,156 * X and Y centers of sprite 2 DC.W 230,53 * X and Y centers of sprite 3 DC.W 97,170 * X and Y centers of sprite 4 sprite: INCBIN SPRITE.SPR * Include 16*16 binary sprite. ******** RESERVED MEMORY SECTION ******** BSS screen: DS.W 320*200 * Reserve a 320*200 HC screen. *============================================================================== *============================================================================== * :STep -=< II >=- Finding the bottleneck: Well... You found the innermost loop yet? Ofcourse, it's the "xloop" inside the "yloop" inside "DRAW_SPRITE". If you look closely you'll notice there are 4 sprites to draw and the sprites are 16*16=256 pixels to draw. This equals a total of 4*256 = 1024 pixels to means 1024 (!) times the xloop is executed!! (I the sound of that word "executed" :-]) If you check out the relevance on the other loops, you'll see that an equal amount is done in "yloop" and quite alot more is done in "draw_sprite_loop". This might be true, but "yloop" is done only 4*16 = 64 times and "draw_sprite_loop" is done a mere 4 times. A close study of how much the each loop costs gives the following results: xloop: approx. 12 clockcycles on 68030 (without cachehit) yloop: approx. 10 clockcycles draw_sprite_loop: approx. 70 clockcycles Then multiply this by the number of times each loop is done. xloop: 12 * 1024 = 12288 cycles yloop: 10 * 64 = 640 cycles draw_sprite_loop: 70 * 4 = 280 cycles It's very clear that xloop is the bottleneck here and hell, you needn't even do the calculations in this case, it's all quite evident. As soon as you know the innerloop, it's settled. *============================================================================== * :STep -=> III <=- Optimising the innerloop: How to some perform some serious clockcycle hackin' procedures on tha xloop's ass?!?! Well.. You could always unroll the loop. yloop: xloop: move.w (a1)+,(a0)+ move.w (a1)+,(a0)+ move.w (a1)+,(a0)+ move.w (a1)+,(a0)+ move.w (a1)+,(a0)+ move.w (a1)+,(a0)+ move.w (a1)+,(a0)+ move.w (a1)+,(a0)+ move.w (a1)+,(a0)+ move.w (a1)+,(a0)+ move.w (a1)+,(a0)+ move.w (a1)+,(a0)+ move.w (a1)+,(a0)+ move.w (a1)+,(a0)+ move.w (a1)+,(a0)+ move.w (a1)+,(a0)+ adda.w (320-16)*2,a0 dbra d7,yloop Yep... this eliminates the move.w #16-1,d6 as well as the dbra 16,xloop. This reduces the number of cycles for a pixel from 12 to 10. Very smart, but this code looks kinda chunky. AND... There is still a way to reduce the size of the code and speed it up even more! yloop: xloop: movem.l (a1)+,d0-d6/a2 * Move 16 pixels into regs and * goto next spriteline. movem.l d0-d6/a2,(a0) * Move 16 pixels onto screen. adda.w #320*2,a0 dbra d7,yloop The movem.l instruction is specialized for moving large amount of LONGs in/out of the registers. In this example we move 8 LONGs (d0,d1,d2,d3,d4,d5,d6 and a2) and every LONG is two highcolor pixels. So 8 * 2 = 16 pixels. But how fast is this really? Well, according to the literature about cyclefucking on the Falcon, this should be about 140 cycles for the pair, so 140 / 16 = a bit less than 9 cycles for a pixel. And hey, that's just a bit faster. Let's check out the score so far: Old number of cycles: 12288 + 320 + 280 = 12888 Now for the new timings.. The xloop doesn't exists anymore.. All that's left is a pair of movem's. The total time for the yloop is something like 140+8 = 148. New number of cycles: 148*64 + 280 = 9752 This means a ((12888/9752) - 1) * 100% = 32 % speed increase! *============================================================================== * :STep /|\ IV /|\ Perfection: Yes kids, grandpa Earx sure met a few freaks in his lifetime. People who won't give up till they killed every redundant bit of code and counted every single cycle (Hi Defjam, Llama and mr. Ni!). =) I know some coders that don't give a damn about excessive optimisation, but still it might be nice to optimise this example a bit further, eventhough there isn't more than a few percent in speed to gain. Let's start with the most inner loop again. This now is "yloop". As you can see you could unroll this loop also, but you've now already seen the principle of this, so we'll focus on something else.. The adda.w #320*2,a0 is quite slow, because the immediate data in the instruction (the number 320*2) needs to be fetched every time this instruction is done by the CPU. A better option is to put the number in a register and add with this register everytime. This should save maybe 3 or 4 cycles every loop (shock, horror!). Also, you could optimise "draw_sprite_loop" quite a bit more by keeping track of which registers are used in "DRAW_SPRITE" so you don't use overlapping registers and hence needn't do the register-(re)storing. Furthermore the addq.w/cmp.w/bne.s combination can be transformed into a simple dbra which is more efficient. Phew.. That's it. Ok, hope you learned something from this looping bussiness. The summary: Clockcycle: A single tick generated by the CPU-clockcrystal. An instruction takes up a number of these ticks. Some instructions take less cycles than others. Dbra instruction: Nothing to do with certain female bodyparts, but actually an instruction often used to keep count of the number of loops done and decide whether to reloop or terminate the loop. Hardcoding: A term often used to describe optimising a piece of code completely and only to one specific situation. This mostly leads to exceedingly speedy, but also very unreadable and chunky code. The word is often used too instead of "unrolling". Loop: A piece of code that is reran by the CPU. Mainloop: A term mostly used for the most important loop in a program. Nested loop: A loop within another loop. Overhead: The time wasted when executing a certain piece of code in a specific case. Highly adaptable code mostly suffers from huge amounts of overhead. Highly specialised code has minimal overhead. Subroutine: A block of code terminated with a "rts" instruction. Yes, basicly that's all there is to it.. :)) But you should always jump to a subroutine with "bsr" or "jsr". Unrolling: Write out the number of loops you want to do in your code, by repeating the looped instructions everytime. Mostly leads to fast code, but can get very hugy and maybe to large to fit into the 68K cache.
Back to ASM_Tutorial