DENTHOR/ASPHYXIA'S VGA TRAINERS FLAME EFFECT
This trainer is on assembler. For those people who already know assembler quite well, this tutorial is also on the flame effect.
DENTHOR, coder for ... _____ _____ ____ __ __ ___ ___ ___ ___ __ _____ / _ \ / ___> | _ \ | |_| | \ \/ / \ \/ / | | / _ \ | _ | \___ \ | __/ | _ | \ / > < | | | _ | \_/ \_/ <_____/ |__| |__| |__| |__| /__/\__\ |__| \_/ \_/ email@example.com The great South African Demo Team! Contact us for info/code exchange!
Grant Smith, alias Denthor of Asphyxia, wrote up several articles on the creation of demo effects in the 90s. I reproduce them here, as they offer so much insight into the demo scene of the time.
These articles apply some formatting to Denthor's original ASCII files, plus a few typo fixes.
Assembler - the short version
Okay, there are many assembler trainers out there, many of which are probably better than this one. I will focus on the areas of assembler that I find important… if you want more, go buy a book (go for the Michael Abrash ones), or scour the ‘net for others.
First, let us start off with the basic set up of an assembler program.
This tells your assembler program to order your segments in the same manner that high level languages do.
<MODEL> can be:
Enable 286 instructions … can be
Set the stack.
<size> will be the size of your stack. I usually use
Tells the program that the data is about to follow. (Everything after this will be placed in the data segment):
Tells the program that the code is about to follow. (Everything after this will be placed in the code segment)
Tells the program that this is where the code begins:
Tells the program that this is where the code ends:
To compile and run an assembler file, we run:
I personally use
tasm; you will have to find out how your assembler works.
Now, if we ran the above file as follows:
You would think that is would just exit to DOS immediately, right? Wrong. You have to specifically give DOS back control, by doing the following:
Now if you compiled it, it would run and do nothing.
Okay, let us kick off with registers.
Firstly: A bit is a value that is either 1 or 0.
This is obviously quite limited, but if we start counting in them, we can get larger numbers. Counting with ones and zeros is known as binary, and we call it base 2. Counting in normal decimal is known as base 10, and counting in hexadecimal is known as base 16.
As you can see, you need four bits to count up to 15, and we call this a nibble. With eight bits, we can count up to 255, and we call this a byte. With sixteen bits, we can count up to 65535, and we call this a word. With thirty-two bits, we can count up to lots, and we call this a double word.
A quick note: Converting from binary to hexadecimal is actually quite easy. You break up the binary into groups of four bits, starting on the right, and convert these groups of four to hex.
Converting to decimal is a bit more difficult. What you do, is you multiply each number by its base to the power of its index…
The same system can be used for binary.
To convert from decimal to another base, you divide the decimal value by the desired base, keeping a note of the remainders, and then read the results backwards.
Read the remainders backwards, our number is
A2F1 hex. Again, the same method can be used for binary.
The reason why hex is popular is obvious: using bits, it is impossible to get a reasonable base 10 (decimal) system going, and binary gets unwieldly at high values. Don’t worry too much though: most assemblers (like
tasm) will convert all your decimal values to hex for you.
You have four general purpose registers:
DX. Think of them as variables that you will always have. On a 286, these registers are 16 bytes long, or one word.
As you know, a word consists of two bytes, and in assembler you can access these bytes individually. They are separated into high bytes and low bytes per word.
The method of access is easy. The high byte of
AH, and the low byte is
AL. You can also access
A 386 has extended registers:
EDX… you can access the lower word normally (as
AX, with bytes
AL), but you cannot access the high word directly … you must
ror EAX,16 (rotate the binary value through 16 bits), after which the high word and low word swap … do it again to return them. Acessing
EAX as a whole is no problem:
mov eax, 10; add eax,ebx … these are all valid.
Next come segments. As you have probably heard, computer memory is divided into various 64k segments (note: 64k = 65,536 bytes, sound familiar?) A segment register points to which segment you are looking at. An offset register points to how far into that segment you are looking. One way of looking at it is like looking at a 2D array… the segments are your columns and your offsets are your rows. Segments and offsets are displayed as Segment:Offset … so
$a000:50 would mean the fiftieth byte in segment
The segment registers are
CS. A 386 also has
GS. These values are words (0-65,535), and you cannot access the high or low bytes separately.
CS points to your code segment, and usually if you touch this your program will explode.
SS points to your stack segment, again, this baby is dangerous.
DS points to your data segment, and can be altered, if you put it back after you use it, and don’t use any global variables while it is altered.
ES is your extra segment, and you can do what you want with it.
The offset registers are
BP. Offset registers are generally associated with specific segment registers, as follows:
SS:SP … On a 286,
BX can be used instead of the above offset registers, and on a 386, any register may be used.
DS:BX is therefore valid.
If you create a global variable (let’s say
bob), when you access that variable, the compiler will actually look for it in the data segment. This means that the statement:
A quick note: A value may be signed or unsigned. An unsigned word has a range from 0 to 65,535. A signed word is called an integer and has a range -32,768 to 32,767. With a signed value, if the leftmost bit is equal to 1, the value is in the negative.
Next, let us have a look at the stack. Let us say that you want to save the value in
ax to do other things, then restore it to its origional value afterwards. This is done by utilizing the stack. Have a look at the following code:
At this point,
ax is equal to 50.
Remember we defined the stack to be
200h further up? This is part of the reason we have it. When you push a value onto the stack, that value is recorded on the stack heap (referenced by
SP is incremented) When you pop a value off the stack, the value is placed into the variable you are popping it back in to,
SP is decremented and so forth. Note that the computer does not care what you pop the value back into.
This would set the values of both
50. (There are faster ways of doing this, pushing and popping are fairly fast though).
This would swap the values of
bx. As you can see, to pop the values back in to the original variables, you must pop them back in the opposite direction to which you pushed them.
would result in no change for any of the registers.
When a procedure is called, all the parameters for that procedure are pushed onto the stack. These can actually be read right off the stack, if you want to.
As you have already seen, the
mov command moves a value…
source must be the same number of bits long.
would not work, and neither would
are all valid.
shl I have explained before, it is where all the bits in a register are shifted one to the left and a zero added on to the right. This is the equivalent of multiplying the value by two.
shr works in the opposite direction.
rol does the same, except that the bit that is removed from the left is replaced on the right hand side.
ror works in the opposite direction.
div <value> divides the value in
ax by value and returns the result in
al if value is a byte, placing the remainder in
ah. If value is a word, the double word
DX:AX is divided by value, the result being placed in
ax and the remainder in
dx. Note that this only works for unsigned values.
idiv <value> does the same as above, but for signed variables.
mul <value> If value is a byte,
al is multiplied by value and the result is stored in
ax. If value is a word,
ax is multiplied by value and the result is stored in the double word
imul <value> does the same as above, but for signed variables.
j* commands are fairly simple: if a condition is met, jump to a certain label.
and so forth.
Procedures are declared as follows:
Variables are also easy:
creates a variable
bob, a byte, with an initial value of 50.
creates a variable
bob2, a word, with an initial value of 50.
bob3, an array of 7 bytes.
bob4, an array of 100 bytes, with no starting value.
Go back and look at Part 7 for a whole lot more assembler commands, and get some sort of reference guide to help you out with others. I personally use the Norton Guides help file to program assembler.
To demonstrate how to write an assembler program, we will write a fire routine in 100% assembler. The theory is simple.
Set the palette to go from white to yellow to red to blue to black. Create a 2D array representing the screen on the computer. Place high values at the bottom of the array (screen) for each element, do the following:
- Take the average of the four elements under it:
- Get the average of the four elements, and place the result in the current element.
Easy, no? I first saw a fire routine in the Iguana demo, and I just had to do one ;) … it looks very effective.
With the sample file, I have created a batch file,
make.bat. It basically says:
So to build and run the fire program, type:
The source file is commented quite well, so there shouldn’t be any problems.
As you can see, the sample program is in 100% assembler. For the next tutorial I will return to Pascal, and hopefully your newfound assembler skills will help you there too.