Spell presents: ////==\\\\ || || //======== |\ /| //\\ //======\ || || || || ||\ /|| // \\ // || || || || || \ / || // \\ || || ||======|| ||==== || \/ || || || || || || || || || || ||====|| || ==\\ || || || || || || || || \\ || || || || \\======== || || || || \\====// Issue 12 24-2-97 -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x- Index: 1. Introduction 1.1. About the magazine 1.2. About the author 1.3. Distribution 1.4. Subscribing 1.5. Contribuitions 1.6. Hellos and greets 2. Mailroom 3. Designing a text adventure - Part V 4. Isn't it nicer with textures ?! 4.1. A word on interpolation 4.2. Afine texture mapping 4.3. Lambert lighting with textures 4.4. Mip-mapping 4.5. Arbitrary-sided polys 5. Into the Realm of Sound - Part I 5.1. DIGITAL SOUND?! 5.2. The Creative Labs Sound Blaster 5.3. Programming the beast 6. To float or not to float 6.1. Introduction 6.2. Basic Concepts 6.3. Fixed-Point Math Theory 6.4. Closing Coments 7. A simple introduction to C - Part I 7.1. Functions 7.2. Including files and comments 7.3. Datatypes 7.4. Defining of the variables 7.5. Calculations 7.6. Arrays 7.7. A function called printf 7.8. Scanf 7.9. Pointers 7.10. Allocating and freeing memory 7.11. Conditions 7.12. ASCII in C 7.13. Assembly and Pascal 7.14. Graphics 8. Sprites Part V - Colision detection 8.1. Box collision 8.2. Pixel perfect colision 9. Graphics Part XI - Introduction to unchained modes 9.1. What ?! 9.2. Planes 9.3. How ? 9.4. Putting down a pixel 9.5. Getting a pixel 9.6. Changing the viewport 9.7. The example program 10. Cool ASM and Pascal tricks 10.1. Why ?! 10.2. Full ASM functions 10.3. Using 32 bit registers 10.4. Using 32 bit instructions 10.5. 32 bit parameters 11. Hints and tips 12. Points of view 13. The adventures of Spellcaster, part 12 -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x- 1. Introduction 1.1. About the magazine Ok, here it is, issue 12 of 'The Mag'... This issue is packed with good stuff, so I won't bother you with a great introduction... I just hope I can finish this issue faster than I did last one... See the index above for contents... :) This issue may be late by the time I've finished, because I'm working on my demo for the Simple'97 party in Portugal !! The first party in Portugal, and I hope to get a good placing in the democompo with my 18 bit color demo... :))) I dream all the time... There aren't any prizes or anything, but it's just for the fun... I'm a very competitive person... :) When you read this magazine, I'll assume some things. First, I assume you have Borland's Turbo Pascal, version 6 and upwards (and TASM for the assembly tutorials). I'll also think you have a 80386 (or 386 for short; a 486 would be even better), a load of patience and a sense of humor. This last is almost essencial, because I don't receive any money for doing this, so I must have fun doing it. I will also take for certain you have the 9th grade (or equivelent). Finally, I will assume that you have the last issues of 'The Mag', and that you have grasped the concepts I tried to transmit. If you don't have the issues, you can get them by mail, writing to one of the adresses shown below (Snail mail and Email). Almost everything I know was learnt from painfull experience. If you re-re-re-read the article, and still can't understand it, just drop a line, by mail, or just plain forget it. Most of the things I try to teach here aren't linked to each other (unless I say so), so if you don't understand something, skip it and go back to it some weeks later. It should be clearer for you then. Likewise, if you see any terms or words you don't understand, follow the same measures as before. Ok, as I'm earing the Net gurus and other god-like creatures talking already, I'm just going to explain why I use Pascal. For starters, Pascal is a very good language, ideal for the beginner, like BASIC (yech!), but it's powerfull enough to make top-notch programms. Also, I'll will be using assembly language in later issues, and Pascal makes it so EASY to use. Finally, if you don't like my choice of language, you can stop whining. The teory behind each article is very simple, and common with any of the main languages (C, C++, Assembly - Yes, that's true... BASIC isn't a decent language). Just one last thing... The final part of the magazine is a little story made up by my distorted mind. It's just a little humor I like to write, and it hasn't got nothing to do with programming (well, it has a little), but, as I said before, I just like to write it. 1.2. About the author Ok, so I'm a little egocentric, but tell me... If you had the trouble of writing hundreds of lines, wouldn't you like someone to know you, even by name ? My name is Diogo de Andrade, alias Spellcaster, and I'm the creator, editor and writer of this magazine. I live in a small town called Setubal, just near Lisbon, the capital of Portugal... If you don't know where it is, get an encyclopedia, and look for Europe. Then, look for Spain. Next to it, there's Portugal, and Setubal is in the middle. I'm 19 years old, and I'm in the second year in the university (if you do want to know, I'm in the Technical Institute of Lisbon, Portugal), so I'm not a God-Like creature, with dozens of years of practice (I only program by eight years now, and I started in a Spectrum, progressing later to an Amiga. I only program in the PC for two years or so), with a mega-computer (I own a 386SX, 16 Mhz), that wear glasses with lens that look like the bottom of a bottle (I use glasses, but only sometimes), that has his head bigger than a pumpkin (I have a normal sized head) and with an IQ of over 220 (mine is actually something like -220 :) ). I can program in C, C++, Pascal, Assembly, Prolog, CaML, Visual C++, lots of idiot languages and even BASIC (yech!)... And due to school, this list is increasing... :))) So, if I am a normal person, why do I spend time writing this ? Well, because I have the insane urge to write thousands of words every now and then, and while I'm at it, I may do something productive, like teaching someone. Just one more thing, if you ever program anything, please send to me... I would love to see some work you got, maybe I could learn something with it. Also, give me a greet in your program/game/demo... I love seeing my name. 1.3. Distribution I don't really know when can I do another issue, so, there isn't a fixed space of time between two issues. General rule, I will try to do one every month, maybe more, probably less (Eheheheh). This is getting to an issue every two months, so, I'll think I'll change the above text... :) 'The Mag' is available by the following means: - Snail Mail : My address is below, in the Contributions seccion... Just send me a disk and tell me what issues you want, and I will send you them... - E-Mail : If you E-mail me and ask me for some issues, I will Email you back with the relevant issues attached. - Internet : You can access the Spellcaster page and take the issues out of there in: http://alfa.ist.utl.pt/~l42686 Follow the docs link... - Anonymous ftp : I've put this issue of 'The Mag' on the ftp.cdrom.com site, in the pub/demos/incoming/code... It will probably be moved to pub/demos/code or something like that. - BBS's : You can check out the BBS's list that will carry this and all the other issues of 'The Mag' in the file SPELL.DOC. 1.4. Subscribing If you want, I'm starting "The Mag"'s subscription list... To get 'The Mag' by Email every month, you just need to mail me and tell me so... Then, you will receive it every time a new issue is made... 1.5. Contributions I as I stated before, I'm not a God... I do make mistakes, and I don't have (always) the best way of doing things. So, if you think you've spotted an error, or you have thought of a better way of doing things, let me know. I'll be happy to receive anything, even if it is just mail saying 'Keep it up'. As all human beings, I need incentive. Also, if you do like to write, please do... Send in articles, they will be welcome, and you will have the chance to see your names up in lights. They can be about anything, for a review of a book or program that can help a programmer, to a point of view or a moan. If anyone out there has a question or wants to see an article about something in particular, feel free to write... All letters will be answered, provided you give me your address. If you have a BBS and you want it to include this magazine, feel free to write me... I don't have a modem, so I can only send you 'The Mag' by Email. You can also contact me personally, if you study on the IST (if you don't know what the IST is, you don't study there). I'm the freshman with the black hair and dark-brown eyes... Yes, the one doesn't put a foot in a class for ages ! My adress is: Praceta Carlos Manito Torres, n§4/6§C 2900 Set£bal Portugal Email: spellcaster@bigfoot.com spellcaster@cryogen.com dgan@camoes.rnl.ist.utl.pt l42686@alfa.ist.utl.pt And if you want to contact me in real time, get into the Dragon Mud To do that telnet to: Alfa.Ist.Utl.Pt : Port 5000 I'm sometimes there... My handle there is Olorin (Ehehe, Tolkien rules !). 1.6. Hellos and greets I'll say hellos and thanks to all my friend, especially for those who put up with my not-so-constant whining. Special greets go to Scorpio, Denthor from Asphyxia (for the excelent VGA trainers), Draeden from VLA (for assembly tutorials) and all those coders out there who release source code and tutorials, Garfield (for the 'The Mag' logo and general suport) and all the demo groups out there. Oh, and I almost forgot all the people at Infinity Network ! I will also send greets to everybody that responded to my mag... Thank you very much ! -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x- 2. Mailroom This issue's mailroom features only one email I got from Viriato, concerning LOTS of errors and inaccuracies I made in issue 11... As the mail was in portuguese, and all, I will just say what he thought it was wrong, maybe I'll delve a bit into some subjects and all... Let's start: a) There is a part in my mag that I make an ASCII like this: 1____2 | | | | 3____4 Well, vertice 3 and 4 are to be exchanges (as most of you migth have understood). b) Me and Viriato had a 'long' talk (we exchange about 6 mails about it) because of the Z-Buffer and BSP tree sorting methods. The conclusions were simple enough: (i) It is irrelevant if the camera is looking along the Z axis or so... The camera is a relative thing, and as that, any way of implementing diferent viewing angles can be reduced to thus. So, the arguments against the praticabilty of the other algorithms facing this is neglectable. (ii) The Z-Buffer method has several advantages that I haven't mencioned. In fact, after my talk with Viriato, I came to the conclusion that Z-Buffer is the best algorithm there is... I'll do an article about it soon... Well, the advantages of Z-Buffer are: - It is a perfect algo for the determination of visible polys. - It's the faster algo for worlds with LOTS of objects. - It doesn't need face sorting, that is, it doesn't have to order the polys in any particular order - It enables us to render any geometric form, as long as we can determin it's color and Z coord. - We don't have to worry with face intersection - It is relatively easy to implement In some tests that were made, the conclusion was that the time needed to render a scene with Z-Buffer tends to remain constant while the number of objects goes up, while in the other sorting algos, the time increases. Contrary to what I said, the Z-Buffer algo works with any kind of camera, fixed or moving... You'll understand why when I make an article on camera (probably next issue) The D-Buffer method I spoke of, aparently doesn't exist... What there is is a method called S-Buffer, which suposedly increases the speed of of Z-Buffer, but I don't know what it is about really. (iii) BSP trees solve the polygon overlapping/instersection problem, but so does the Z-Buffer. The BSP tree algo is slow, but only when calculating the tree (that is constant if the objects are not moving), so it is the fastest way to draw constant scenes (I think) c) Now, correcting what I said about backface removal. If I'm not mistaken, I said that the method only could apply when the object was directly in front of us... This isn't true... The method can be applied in any way. The theory behind it is simple: You check if the dot product between the view vector and the face normal is negative... If it is, then you draw the poly. The view vector is the vector that connecting the center of the object and the camera... In our examples, the camera is at (0,0,0), so the view vector for polygon with center in (Xc,Yc,Zc) will be (Xc,Yc,Zc). You can see an example in the Draw3dPoly in the TMAP1.PAS program. d) In the article about Lambert Shading, I say: "This works because of the proprieties of the cosine (it is never larger than 1). We must negate the color because the cosines of the displayable polygons are always negative." Well, this is more of an optimization... If you use an anticlockwise vertice numbering, instead of the normal clockwise one, you don't need to the negation. Of course, you have to invert everything respectingly the backface culling (=backface removal), for example. e) Shr doesn't work with signed numbers in Pascal !! The guys at Borland screwed up. Pascal should recognize if the number he is working with is a signed or unsigned number. If it is unsigned, he uses the normal Shr, if it signed, he should use the Sar (Shift Arithmetic Right). But, Pascal doesn't do that ! :( It does that selection with the Shl, though... It uses Shl for unsigned and Sal for signed... Just take care. This is particulary true when dealing with fixed point maths... f) I said that the signed numbers are stored "as eight bits, the leftmost one storing the sign (0=positive, 1=negative), and the remaining seven bits store the numbers"... Well, this is wrong... The signed numbers are stored using the "two's complement"... You obtain the two's complement by getting the logical Not of the number and adding one. For example: -1 = Not(1) + 1 = 11111111 The property of numbers stored this way is that it is easier to do subtractions... Aditions work in the same way as usual (you do a binary sum), and subtractions work by adding the simetrical number. This only works in this way, because if you used the sign bit storing format, you would get a wrong result. g) I say in the Mode 13h article that the palette data goes to video memory... Well, this isn't true... The palette data goes to an array of register, the RAMDAC h) I said that it was 4 times faster putting a word in video memory than putting a byte... I was wrong (surprise, surprise)... It is only 2 times faster. i) My assembler HLine routine is wrong. Why ? Because if the starting and ending points of the line are equal, it will not draw nothing, while it should draw one dot. To avoid this, add 1 to Cx after it calculates the diference between the starting and ending points... There's a great optimization to do in it also, but check out hints and tips... The rest of Viriato's comments make the greater part of this issue's hints and tips section, since they are not errors, just optimizations I forgot to explain... Well, as you can see, I make LOTS of mistakes... You shouldn't take every- thing you read here as some sort of Bible...It is just my (limited) knowledge. I think I'm a fairly good teacher, and that is why I write 'The Mag'. It isn't to show off, because I don't have that much to show off... Most of the errors I make is due to lack of testing, since I learn most of what I teach a week or so before writing the article... But, if I spent more time learning things the better, I would take longer to write 'The Mag'. And we don't want that, do we ? So, you should read the stuff here, and if something interests or puzzles you, don't think "If he says, he must know it!", but think "This guy's an ass !!! He doesn't know shit about this... Still, he gave me some ideas"... :) Well, I finished for now... Thanks, Viriato, for spotting the errors and beeing such a sour-ass... :))) -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x- 3. Designing a text adventure - Part V This issue's tutorial will lead us to the manipullation of the gamescape. What is the gamescape ? Well, it is all that it isn't monsters or objects. For example, the painting in on the dining room, or the stove in the kitchen of Fanglore... This is fairly easy to achieve, but I got to cover it anyway... :) So, what can we manipulate in the Fanglore enviroment ? If I remember correctly (it has been sometime since I've worked on this...), we can open and close the oven in the kitchen, and we can push a bookshelf in the library. Each of these things have diferent reactions. Let's first do the examination manipulation: { The specified parameter refers a gamescape object } Flag:=False; If Param='PICTURE' Then Begin Flag:=True; WriteLn; TextColor(Yellow); WriteLn('It is a painting of naked men and women doing...'); WriteLn('OH MY GOD ! What are they doing with that sheep !!!'); WriteLn; End; If Param='BOOKSHELF' Then Begin Flag:=True; WriteLn; TextColor(Yellow); WriteLn('This bookshelf has an assorted array of books...'); WriteLn('Authors include Tolkien, Stephen King and Stephen'); WriteLn('Donaldson... Nice choice...'); WriteLn; End; If Param='OVEN' Then Begin Flag:=True; WriteLn; TextColor(Yellow); WriteLn('It is a gas-heated oven...'); If Oven Then WriteLn('It is open...') Else WriteLn('It is closed...'); WriteLn; End; See where to put this in line 440 of the FangLore.Pas program... The Oven variable is a variable that tells if the oven is opened or close... It is initialize to FALSE (closed). Now, let's manipulate the enviroment... First let's open and close the oven ! :) If Parsed[1]='OPEN' Then Begin Valid:=True; TextColor(LightRed); If Parsed[2]='OVEN' Then Begin If CurrentRoom=7 Then Begin If Oven Then WriteLn('It''s already open !') Else Begin Oven:=True; WriteLn('You open the oven...'); If Objects[2].Pos=255 Then Begin WriteLn('You see a gas mask inside it...'); Objects[2].Pos:=7; End Else WriteLn('It is empty...'); End; End Else WriteLn('I can''t see no oven...'); End Else WriteLn('Sorry, I can''t open a ',Parsed[2],'...'); End; If Parsed[1]='CLOSE' Then Begin Valid:=True; TextColor(LightRed); If Parsed[2]='OVEN' Then Begin If CurrentRoom=7 Then Begin If Not Oven Then WriteLn('It''s already closed !') Else Begin Oven:=False; WriteLn('You close the oven...'); If Objects[2].Pos=7 Then Objects[2].Pos:=255; End; End Else WriteLn('I can''t see no oven...'); End Else WriteLn('Sorry, I can''t close a ',Parsed[2],'...'); End; This code should be put in the Play procedure... It is VERY simple. It's just a bunch of 'if then else''s strum together... Talk about sloppy code. Now, let's push the bookshelf... :) If Parsed[1]='PUSH' Then Begin Valid:=True; TextColor(Yellow); If Parsed[2]='BOOKSHELF' Then Begin If CurrentRoom=8 Then Begin If Book Then WriteLn('I can''t push it further...') Else Begin Book:=True; Write('You push the bookshelf, revealing '); WriteLn('a secret passage !'); Rooms[8].North:=11; Rooms[8].Desc[4]:='There is a secret passage to the '; Rooms[8].Desc[4]:=Rooms[8].Desc[4]+'north !'; Rooms[8].Desc[5]:='*'; End; End Else WriteLn('I can''t see a bookshelf...'); End Else WriteLn('Sorry, I can''t push a ',Parsed[2],'...'); End; Again this code is to be put in the Play procedure... The Book variable is a boolean similar to the Oven variable... Notice that we've not only created a new passage in the game, but we also changed a bit the description of the room !!!! So, we're done for this article... Very simple one, but I'm fed up with this issue, so this is all you'll get ! :) Next issue we'll discuss reduncy and loading and saving the position in the game... -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x- 4. Isn't it nicer with textures ?! Let's start with the beggining... What is texture mapping ? Well, nowadays, this is quite a stupid question, since every 3d game nowadays uses texture mapping... But, anyway, texture mapping is the act of getting a 2d bitmap and stick it to a 3d object, giving it a sense of texture (if well done). Initially, this article was going into various texture mapping considerations, including lighting, perspective correct texture mapping and mip-mapping, but due to problems with time and other things, I've not included the perspective correct texture mapping. I'll include it in the next issue. Before we start the texture mapping part, I'll just do one thing. I'll change the FPoly procedure. If you don't remember, the FPoly proceedure draws a filled polygon. We'll change it because it eases up our work in programming the texture mapping procedure from it. What we are going to do is to divide the polygon drawing into two phases: 1) Calculate the maximmum/minimum x's. Let's call this the calcultation phase. 2) Draw the poly. Let's call this the draw phase. Before, we calc'ed the maximmum and minimum x coords for each y and draw it. Now, we find all the coords and then in the end draw it... Procedure FPoly(X1,Y1,X2,Y2,X3,Y3,X4,Y4:Integer;Color:Byte;Where:Word); Var MnY,MxY:Integer; A:Integer; Procedure Side(X1,Y1,X2,Y2:Integer); { This routine updates the Poly variable, based on the side it is } Var Temp:Integer; X,XInc:Integer; A:Integer; Begin If Y1=Y2 Then Exit; { Exchange (X1,Y1) with (X2,Y2), in case Y2 is bigger than Y1. It is better than doing a routine for each case, and in the case of normal poly, drawing from (X1,Y1) to (X2,Y2) is the same as drawing from (X2,Y2) to (X1,Y1) } If Y2Poly[A].MaxX) Then Poly[A].MaxX:=X Shr 7; X:=X+XInc; End; End; Begin { Reset the Poly variable... Do this in assembler if you want a good speed up } For A:=0 To 199 Do Begin Poly[A].MinX:=MaxInt; Poly[A].MaxX:=-MaxInt; End; { Set the maximmum and minimum Y coordinate } MnY:=Y1; MxY:=Y1; If MnY>Y2 Then MnY:=Y2; If MnY>Y3 Then MnY:=Y3; If MnY>Y4 Then MnY:=Y4; If MxY199 Then MxY:=199; { Calc the sides } Side(X1,Y1,X2,Y2); Side(X2,Y2,X3,Y3); Side(X3,Y3,X4,Y4); Side(X4,Y4,X1,Y1); { Draw the poly } For A:=MnY to MxY Do HLine(A,Poly[A].MinX,Poly[A].MaxX,Color,Where); End; The Poly variable is the variable that stores the minimum and maximum Y values for each scanline on the screen. It is defined like this: Var Poly:Array[0..199] Of Record MinX,MaxX:Integer; End; This should be easy to understand... It's the same procedure as before, but more modulated and divided in two parts... The hardest part is probably figuring out the fixed point part, but that's easy... Read the article about it in this issue... 4.1. A word on interpolation In the course of this article (and other, like Gourad Shading,etc), you'll read lot's of time 'interpolate this' and 'interpolate that', and some of you might wonder: 'What the hell is interpolation ???'... Well, interpolation is the process of finding out some value of a function, given only some points of that function... In most functions, this will give an aproximate function, but by using the correct interpolation method, you can get a very acurate aproximation function... There are dozens of interpolation methods, including linear, quadric and cubic splines, nth splines, linear interpolation, bi-quadric interpolation, etc... The method we will talk when we refer to interpolation is linear interpolation... This is quite easy... Given two points A=(Ax,Ay) and B=(Bx,By), we will find the points that lie between them both, using a straight line to find it. As you might know, in this case, the line is defined by: (Y-Ay)=M*(X-Ax) where: M=(By-Ay)/(Bx-By) <- This is the gradient of the line So, the interpolated line would be something like this: (By-Ay) Y= ------- * (X-Ax) + Ay (Bx-Ax) So, if you wanted to know the position the point C=(Cx,Cy), just had to substitute the coordinates in the equation, like this: (By-Ay) Cy= ------- * (Cx-Ax) + Ay (Bx-Ax) As I said, this is quite simple. But know, let's go to the thing that interests us most... This is what I call 'discrete interpolation', because it gives us the coordinates of discrete points, not any point. Example: In the above equation, you could get the coordinate of the point which haves an X coordinate equal to 2.5, then the point with the X equal to 2.6, or even 2.435362351... With 'discrete interpolation', you have a step value (let's say, for example, 1), and you can only find out the coordinates of the points with the X equal to (for example) 0,1,2, etc... In the case of most of the computer aplications, this is sufficient (with a smaller or bigger step value). But why is this more usefull ? Because this way, if the distance between the points we want to find out is constant in any axis (X or Y), there will be a fixed ammount to displace the coordinate correspondent to the other axis (Y or X), and that will lead to that you only need one addition for point, instead of using a divide and a multiply !!! So, given points A and B as above, you can interpolate it like this: X0=Ax Y0=Ay Repeat X0=X0+XInc Y0=Y0+YInc Until All Points We Want In the middle of the loop, (X0,Y0) have the interpolated point according to step. XInc and YInc are the step variables. We'll see later how do we calculate them. This is particular usefull if you want to get from a point A (in texture space, for example) to another point B in a number of steps N. You just have to set (X0,Y0) to the coordinates A, and then XInc and YInc to: XInc=(Bx-Ax)/N YInc=(By-Ay)/N We will use this quite extensivelly in the texture mapping procedure and in gourad shading... I don't know if I explained myself well, or not... This is a bit hard for me to explain, though it is simple to understand if the teaching was right. So, experiment with this, because this is an essential part of texture mapping... 4.2. Afine texture mapping This is the first of the texture mapping processes I'm going to teach. This is not the best nor the fastest, bla bla, but I figure it out alone, so I'm not sure if this is the way everybody does... But it works !! And that's that count. The theory behind texture mapping is pretty easy. For example, you have a texture like this: ABCA Each letter corresponds to a color. This is a 4x4 texture. BBAC If we'll try to map it to the polygon shown below: ACEF BBAA P1 ____ P2 | | | | P4 ---- P3 We'll get something like this: ABCA See ? We establish a relation between the points and the edge BBAC of the texture. Now, let's rotate the poly around the Y axis, ACEF like this: BBAA P2 ____ P1 | | | | P3 ---- P4 If we texture map it now, we'll get: ACBA Still the relationship holds !!! Now, let's imagine we rotate it CABB around the Z axis, like this: FECA AABB . P1 / \ / \ P4 . . P2 \ / \ / . P3 We would get: A C C <- The holes are just an ASCII imperfection. F A B I'm just drawing this examples in 2d, but A E B A 3d texture mapping obeys the same principle. A C B B A B When I was drawin the ASCII above (the last 'texture mapped' poly), I first drawed the edges, like this: A C C Then, I interpolated, that is, I filled in the blanks, F B in a linear (=afine) manner... That is why this kind of A A texture mapping is called afine texture mapping. A B B A B The ideia is this. We find the textel (this is the texture's pixels) of the vertices of the polygon (Fig 1), then we interpolate to calculate the edges of the polygon (Fig 2), and then we interpolate between the edges to get the polygon's area (Fig 3). A A A C C C C F B F A B A A A A A E B A A B A C B B A B A B B B Fig.1 Fig.2 Fig.3 This is basically the same we do when we are drawing a normal filled poly. When we draw a normal polygon, we first have the vertices, then we interpolate the edges, and then the area within. Putting this in steps: 1) The vertices are given 2) Interpolate the edges 3) Interpolate the interior With a normal poly, step 1 is given, step 2 is when we find the max and min X coord, and step 3 is when we draw the horizontal line joining them. So, we just add to the code of the normal poly the calculation of the texture coordinates. This is where the 'problems' start... But even this is simple: We know from the start to which textel the vertice of the polygons correspond. From there, in the calc phase, we calculate (using linear interpolation) the textel corresponding to the edge pixel and get for (MinX,Y) and (MaxX,Y) two textels, that we call (U1,V1) and (U2,V2). I use the letters U and V to represent vectors (points) in the texture because that's some kind of convention. Then, in the draw phase, we traverse the texture from (U1,V1) To (U2,V2) in (MaxX-MinX) steps, getting the colors for the texture !!! This is easy, yet it can be pretty confusing... I'll present now the code for the texture mapping procedure. Just two thing before that. 1) The Poly array changed to include the texture information for each scanline. Var Poly:Array[0..199] Of Record MinX,MaxX:Integer; U1,V1:Integer; U2,V2:Integer; End; 2) The texture mapping procedure accepts now another parameter, that is a pointer to the texture map, stored in the same way as the sprites in my tutorials are (first word is X size, second word is Y size, and then the bitmap data). So, let's see the texture mapping in phases... First, we set clear the Poly array: For A:=0 To 199 Do Begin Poly[A].MinX:=MaxInt; Poly[A].MaxX:=-MaxInt; Poly[A].U1:=0; Poly[A].V1:=0; Poly[A].U2:=0; Poly[A].V2:=0; End; We can get a good speedup by coding this part in assembler... It is easy, but I'll leave that for you... :) Then, we get the size of the texture map... The texture is stored in the same way as the images for the sprites tutorial, that is, first word is X size, second word is Y size and then comes the bitmap data... Segm:=Seg(Texture^); Offs:=Ofs(Texture^); Move(Mem[Segm:Offs],XSize,2); Move(Mem[Segm:Offs+2],YSize,2); Offs:=Offs+4; We add 4 to the offset so that Segm:Offs point to the start of the bitmap, since we won't need the Xsize and Ysize data anymore... Then, we set the Y maximmum and minimmum, like we do in the normal poly procedure: MnY:=Y1; MxY:=Y1; If MnY>Y2 Then MnY:=Y2; If MnY>Y3 Then MnY:=Y3; If MnY>Y4 Then MnY:=Y4; If MxY199 Then MxY:=199; Then we go the first stage interpolation... The edge interpolation. This is done via a procedure which gets the X and Y coordinates of the two points of the poly that define the side we want to interpolate... The side is also passed as a parameter, so that we know which side are we talking about. Side 1 is the top side, and the order goes clockwise. We must check if Y1 and Y2 are equal, or else we risk a divide by zero operation... :( The interpolation is always done according the Y axis, that is, the number of steps for the interpolation depends on the 'height', that is equal to Y2-Y1. Then, we set up the interpolation, according to the side, setting up the (U,V) coordinates on texture space, and the texture steps: If Side=1 Then Begin Us:=0; Vs:=0; Ue:=XSize; Ve:=0; TextIncX:=(XSize Shl 7) Div Height; TextIncY:=0; End Else If Side=2 Then Begin Us:=XSize; Vs:=0; Ue:=XSize; Ve:=YSize; TextIncX:=0; TextIncY:=(YSize Shl 7) Div Height; End Else If Side=3 Then Begin Us:=XSize; Vs:=YSize; Ue:=0; Ve:=YSize; TextIncX:=(-XSize Shl 7) Div Height; TextIncY:=0; End Else If Side=4 Then Begin Us:=0; Vs:=YSize; Ue:=0; Ve:=0; TextIncX:=0; TextIncY:=(-YSize Shl 7) Div Height; End; Then, we must do two rotines, in case the Y2 is above Y1 or not. They are basically equal, with just some diferences concerning some signs and starting texture coordinates... You should be able to understand the diference if you do a scheme... I'll just explain one of the routines, when Y2 is smaller than Y1 (that is Y2 is above Y1)... First we calculate the X step (for the edges of the polygon) and then we set the starting (U,V) coordinates. Then, we traverse the polygon, updating the Poly array: For A:=Y2 To Y1 Do Begin { Calc the minimum and maximum X coordinates in this scanline } If (X Shr 7Poly[A].MaxX) Then Begin Poly[A].MaxX:=X Shr 7; Poly[A].U2:=U Shr 7; Poly[A].V2:=V Shr 7; End; X:=X+XInc; U:=U+TextIncX; V:=V+TextIncY; End; Then, after finding the edges in texture and screen space for the polygon, we interpolate the inside of the polygon, interpolating the coordinates in texture space... This is another rotine you should convert to assembler, since it is easy and should be very fast: For A:=MnY to MxY Do Begin DeltaX:=Poly[A].MaxX-Poly[A].MinX; If DeltaX<>0 Then Begin TextIncX:=((Poly[A].U2-Poly[A].U1) Shl 7) Div DeltaX; TextIncY:=((Poly[A].V2-Poly[A].V1) Shl 7) Div DeltaX; U:=Poly[A].U1 Shl 7; V:=Poly[A].V1 Shl 7; For B:=Poly[A].MinX To Poly[A].MaxX Do Begin { Get the textel } Temp:=((V Shr 7)*XSize)+(U Shr 7); C:=Mem[Segm:Offs+Temp]; PutPixel(B,A,C,Where); U:=U+TextIncX; V:=V+TextIncY; End; End; End; End; So, this is it !! As the matter of fact, there are lots of improvements you could do, but the best of them all is converting the whole texture mapping procedure to assembler, since it is fairly easy to do... Even I can do it ! :) Well, it is example time everyone !!! :) The example is the standart 'rotating cube with textures on the sides'... I've used backface removal in this example (a modified way... See the mailroom department to understand it) to avoid using face sorting... I use two diferent sized textures to show you the potencialities, and five diferent textures... The fringes of the object are a bit mangled, don't know why is this yet, but I'm sure it is due to errors in the interpolation... I think that if you use a more accurate fixed point, you will not get this error. The file is TMAP1.PAS (It's 400 lines long!)... Check it out ! :) Afine texture mapping is pretty good when it comes to polygons that don't vary too much in the Z axis. If they do, and if the poly is too close to the camera, the texture will become distorted. This is due to the fact that this kind of texture mapping doesn't take in account the geometry of the object, it just shrinks and expands an image to fit a polygon on screen. This is sorted out by using smaller polys, or polys that are away from the camera, or using perspective correct texture mapping. I'll delve into it in next issue. For now, I'll just move on to 4.3. Lambert lighting with textures There are several ways to do this. A really good one is the shademaps method, that requires some precalculation, but it is very fast and gives out pretty good results. What are shademaps ? Well, imagine that you had a pixel with colour 32, and you wanted to know what colour what it would be if you dimmed that pixel to 25% of his intensity. Using shademaps, you just needed to access position 32 of an array called something like Shade25, that gives out the shaded colours correspondent to some other colour. In our example, it could be something like 54. So, if you wanted to dim all pixels with colour 32 25%, you just needed to replace them with colour 54. Now, the 'though' part is calculating the shademap, but even that is pretty simple. Let's create a new structure, called shademap: Type ShadeMap=Array[0..255] Of Byte; Now, let's do a nice procedure: Procedure GenTint(Pal:RgbList;Var Map:ShadeMap;R,G,B:Byte); So, for example, to create the Shade25 array in the above example, you would type in Var Shade25:ShadeMap; Pal:RgbList; ................................ GetPalette(Pal); GenTint(Pal,Shade25,25,25,25); This would create a shademap where all the colours would be put with it's red, green and blue components at 25% of their intensity. In the same way, you could call GenTint(Shade2,200,200,200), that would double the intensity of the colours. But let's do now the real procedure. First we must convert the percentages given in the procedure call into factors that really matter: FactR:=R/100; FactG:=G/100; FactB:=B/100; Then, for every colour in the palette, we must find out the colour that we would get in a true colour display corresponding to the shading indicated: RealR:=Round(Pal[A].R*FactR); If RealR>63 Then RealR:=63; RealG:=Round(Pal[A].G*FactG); If RealG>63 Then RealG:=63; RealB:=Round(Pal[A].B*FactB); If RealB>63 Then RealB:=63; We need to make sure that the colour factor does not exceed 63, for obvious reasons. We could do that faster with an AND, but speed isn't important, because all of this is precalculated, before the game/demo/whatever really starts to run. Finally, we must find out what colour that exists in the palette is closer to that true color: MinC:=0; MinD:=MaxInt; For C:=0 To 255 Do Begin { Find distance of colour C to the real colour } Dist:=Abs(RealR-Pal[C].R)+ Abs(RealG-Pal[C].G)+ Abs(RealB-Pal[C].B); { If the distance is the smallest so far... } If Dist63 Then RealR:=63; RealG:=Round(Pal[A].G*FactG); If RealG>63 Then RealG:=63; RealB:=Round(Pal[A].B*FactB); If RealB>63 Then RealB:=63; { Find now the most approximate colour in the palette } MinC:=0; MinD:=MaxInt; For C:=0 To 255 Do Begin { Find distance of colour C to the real colour } Dist:=Sqrt(Sqr(RealR-Pal[C].R)+ Sqr(RealG-Pal[C].G)+ Sqr(RealB-Pal[C].B)); { If the distance is the smallest so far... } If DistMxY Then MxY:=P2.Verts[A].Y; If P2.Verts[A].Y199 Then MxY:=199; Now, the call to the side procedure is done n times (where n is the number of sides). It now includes thr (U,V) coordinates of the start and end points, instead of the number of the side we used before to find out the (U,V)'s... First it is called the Side procedure for the side that runs from the 1st to the 2nd vertice, and then from the 2nd to the 3rd and so on until from the (n-1)th to the nth vertice, and finally from the nth to the 1st again. { Calc the sides } For B:=1 To P2.NSides-1 Do Side(P2.Verts[B].X,P2.Verts[B].Y, P2.Verts[B+1].X,P2.Verts[B+1].Y, P2.Verts[B].U,P2.Verts[B].V, P2.Verts[B+1].U2,P.Verts[B+1].V); Side(P2.Verts[B+1].X,P2.Verts[B+1].Y, P2.Verts[1].X,P2.Verts[1].Y, P2.Verts[B+1].U,P2.Verts[B+1].V, P2.Verts[1].U,P2.Verts[1].V); Until know the diferences are structural, since the data structure changed, The Side procedure also changed little: Procedure Side(X1,Y1,X2,Y2,Us,Vs,Ue,Ve:Integer); { This routine updates the Poly variable, based on the side it is. The sides are numbered clockwise. Side 1 is the topmost (when the poly has suffered no rotation } Var Temp:Integer; X,XInc:Integer; A:Integer; Begin { Set the start and end coordinates of the texture map } If Y1=Y2 Then Exit; Height:=Y2-Y1; TextIncX:=Round(((Ue-Us)/Height)*128); TextIncY:=Round(((Ve-Vs)/Height)*128); ............................................ ............................................ The rest is equal. Basically we took of the if's in the beginning, since the (U,V) coordiantes are now specified in the calling of the procedure. I use real arithmetics and then I round it to fixed point in the two last lines to spare from an if to know if we ought to negate or no the result, as we used to do. So this is it... See the example program TMAP4.PAS for a good example of this code... Don't forget the usefullness of this procedures... It is usefull even if you use 4 sided polys, since you can specify the (U,V) pairs. "Why the hell do I want that for ?", you are asking. Well, for example, imagine you have a face texture, an image of a face. And you want to map it to a 3d face. Don't forget that a good 3d face is made up of various polys, to form the nose and eyebrows, lips, etc, so it is good, for example, to map one of the eyes to the eye poly... Use your imagination... In the next issue's 3d articles, I'll delve into perspective correct texture mapping, Z-buffering, and 3d clipping... See you then... -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x- 5. Into the Realm of Sound - Part I I'm a Spell's friend (I think :|) and I'm in the same course and the same [ Of course you are !!! :))) ] school Spell is. You may not believe, but we have never seen each other. We met on the News, if I recall. My 'war name' is Viriato, the name of a lusitan chief warrior, who is closely linked to Portugal's history. Well, I've heard that Spell would like to have same article(s) about programing the Sound Blaster, for his special 'The Mag' issue 12, and I decided to write one, an introductory one, but about only the digital sound capabilitys of the Sound Blaster (mono). I though that it would be nice to include a bit (very basic and informal) of theory about digital sound. After all, that's what this is all about. Troughout the article I will build a small Sound Blaster Digital Sound Unit (SBDSU). I will do all my programming in Pascal. I hear you laugh. Don't. Pascal is a language understandable by almost everyone. My intention is not to make the fastest and smaller driver ever, but only to give some basics. If I wanted speed, I would have done it in ASM, but then no one would understand a damn thing. Well, I'm not an expert so, if you note some 'barbarity' in the text, just let me know (if you don't, then I must be a god-like creature, :):)). And if you didn't understand something, even after the re-re-reading that Spell him self advices, then drop me a note at l41324@alfa.ist.utl.pt or nmasj@camoes.rnl.ist.utl.pt. The first email address is preferable. But lets get started... 5.1. DIGITAL SOUND?! Sound can be considered as a complicated wave, or (maybe more precisely) as a mixture of many different simple waves (sinusoids). Then, if sound is a wave, we can represent it graphically. We can also use math to transform or 'join'(mix) waves. Lets start by drawing (oh my God! the man is about to use Ascii art!) a simple wave: The T axis represents time. The I axis I ^ represents intensity. Time is always time, | but intensity can be air pressure (sound | __ wave, the thing we are interested about), | _- -_ luminosity (a light wave) or even voltage, | / \ when we use electricity to 'represent' a |/--------\--------/--> wave, like in your hi-fi system. A pure | \ / T wave (sound, light or electric) is continuous | -_ _- each means exactly the same as when you talk | -- about a continuous function in math. The wave | in the figure is a continuous (well, you must try to imagine the draw as a continuous one) sine wave. A long time ago, some one finded out that we can 'transform' sound in electricity and vice-versa, with a microphone and a speaker. The intensity of electricity trough a wire can represent a sound, as she varies in time (in the draw, it would be measured in Volts, for example). And how can we record a sound? For example in a tape (cassette). What a tape does is to record the intensity (using a microphone) of the sound in her strip as time goes by. Then if we revert the process (read the intensity of sound from the strip as time goes by), we can reproduce the recorded sound (using a speaker). The problem that arises when we think of using a computer to record sound is that sound is a continuous quantity and a computer can only keep and treat discrete quantitys. So what can we do? We must quantitize sound. Instead of record the In (I will use In instead of I for intensity from now on) value at *every* T value (impossible, because T is continuous, real valued), we record it only at every Ti seconds. And the In value we record will be an approximated In value. For example, we would record a In value (this act is what we call 'take a sample'; and the act of repeatedly take a sample we call 'sampling') every second, and the value would take values in {..., -1, -0.5, 0, 0.5, 1, 1.5, ...}. Here is an example of a pure sine wave and its sampled version: ^ In ^ In 3 | 3 | | __-__ | === 2 | _- -_ 2 | === === | / \ | 1 | / \ 1 | === === | / \ T | T 0 |/-----------------\---------> 0 |==---------------===--------> 0 1 2 3 4 5 6 \8 10 12 0 1 2 3 4 5 6 8 10 12 -1 | \ -1 | === | \ | -2 | -_ -2 | === | --_ | = -3 | -3 | Pure Sine Wave Sampled Sine Wave As you can see, some information is lost on the sampled wave. Note that the sampled value at T = n is considered the value for time T = [n; n + Ti], where Ti is the interval between two reads of In (one second, in our case). The number of times we take a sample in a second is called the sampling rate, or sampling frequency. In the example, if we had a higher sampling rate, we would have less information loss. The sampling rate we used was 1 Hz, or 'one sample/second'. On the other hand, we would have less losses if we had used a In 'step' smaller than 0.5, the step we used. We can think of each possible In value (sampled value) as an integer number, wich we can call sample value. Then we have the table In real value | In sampled value (In = intensity) | - 3 | 0 - 2.5 | 1 - 2 | 2 - 1.5 | 3 - 1 | 4 - 0.5 | 5 0 | 6 0.5 | 7 1 | 8 1.5 | 9 2 | 10 2.5 | 11 3 | 12 Since In of our example wave is in [-3; 3], we don't need to go further on the sampled value. We used 13 values (0-12), so it can be stored using (minimum) 4 bit words. If you have ever heard that CD sound is recorded at 44.1 Khz and 16 bits, now you know what that means. Each sample is a 16 bit word (has 65535 possible values, or intensity possible values) and samples are taken 44100 times in a second. "Big" parameters, to minimize sampling errors (the loss of information). At 16 bit and 44.1Khz sampled sound, sampling errors are too small to be perceptible by us. When someone choose these values, he/they just didn't play with a coin. There is a theorem, the Nyquist theorem (also so called the sampling theorem) that gives us a way to calculate the minimum sampling frequency, and the choice of 16bits as to do with a thing called something like signal-noise relationship. But this is out of our scope... Notice that I used unsigned numbers for the sampled value; I could have used signed numbers. There is no special reason for use one or another 'type' of numbers, in general. Well, in some calculations, one type is better than the other, as for volume control: it is easier to do it with signed samples, because it is enough to multiply each sample by a constant; if the samples are unsigned, it's not that simple. Typicality, in the world of PC unsigned numbers are used for digitised sound (in fact, it seems that from SB16 up the two types are supported, but normally unsigned samples are used). In the world of Amiga, signed numbers are used. 5.2. The Creative Labs Sound Blaster A way of using a computer to record sound it would be to have a card that would transform electric intensity in a (integer) number. Then we could just plug in a microphone and keep reading the value of electric intensity from the "card's Input port", storing it in an array. If the card could also transform numbers in electric intensitys, we could pick up the array values and send them to the "card's Output port", reproducing the recorded sound. This is basically what a Sound Blaster can do, concerning digital sound, or 'quantitised' sound. The entity who converts electric In (intensity) in numbers is called ADC, short for Analogic To Digital Converter. Analogic is the name given to real valued quantities. The entity who does the reverse thing (numbers -> electric In) is called DAC, short for Digital to Analog Converter. These two entitys are electronic devices, electronic chips. Actually, we can go out to an electronics store and bye such devices. An SB has a heart, much like a PC has one (microprocessor), and it is called DSP, Digital Signal Processor. A DSP is a processor especiallized in treating digital signals. The SB's DSP controls all the operations that are made by the card, related to digital sound (and MIDI hardware). Lets make a small blocks diagram representing an SB (unless stated contrarily, I speak only about the mono SBs, versions 1.x and 2.x; however, everything is appliable to all SBs, because they are downwards compatible) To PC (ISA bus) ______________________________ / \ +-----+ / Mic | ADC | <- | +----------------------+ +-----+ <- +-----+ \ Line In | FM synthesizer (OPL) | | DSP | +----------------------+ +-----+ -> +-----+ | | DAC | | +-----+ | +-----------------+ | +------> | Sound Amplifier | <---------+ +-----------------+ | | | +-----> Speakers +--------> Phones As you can see, SBs have two practically distinct parts: the synthesized sound part (FM synthesizer) and the digital sound part (DSP/DAC/ADC). Here I will talk only about the digital sound part. The DSP chip manages all the actions. Anything you want to do, you will do giving commands to the DSP (I'm only speaking about digital sound, remember that). SB is able to sample sound and to reproduce it. We can program it (the DSP, more precisely) to give/reproduce one sample whenever we want (direct mode), or to give/reproduce a 'sound block', this is, an array of samples (DMA mode). That array can be stored/retrieved directly into/from the PC's memory, trough DMA. This last mode is much powerfull, because the processor can do something else while the SB treats the sound, in background. If you don't know much (or nothing) about DMA, don't cry: we will talk about it when needed. [ And there's an article on DMA in this issue... ] The DSP is responsible for the compressed sound (ADPCM) also. ADPCM is a compression technique used (optimized) for sound. Here is a port address map for the SB (digital part) 2x6h DSP - Reset 2xAh DSP - Data In (in to PC, DSP -> PC) 2xCh DSP - Data Out, commands and WriteReady bit (bit 7) 2xEh DSP - Data Available (ReadReady bit (bit 7)) The 'x' in the addresses depends on the base port address of the SB. The base address can be from 210h to 260h. If your SB's base address is 220h (very common) then the DSP - Reset port address is 226h. Well, now that we know a bit more about SB and SB's internals, we can start with real the funny thing: program it. 5.3. Programming the beast The first command you should (must) do is to reset the DSP, so he gets ready to receive commands, and to 'clean up' any mess another program migth have done. The algorithm is as follows: 1. Write a 1 to port DSP - Reset (2x6h) 2. Wait at least 3.3 microseconds (for DSP reaction) 3. Write a 0 to port DSP - Reset (2x6h) 4. Wait at least 100 microseconds for DSP data available (2xEh) 5. Read from DSP Data In (2xAh) The value read in step 5. must be AAh. If it's not, then there is no DSP and therefore no SB (at the tested base port), or the DSP reset was unsuccessful (damage DSP?). We can now make a function that performs a DSP reset assuming that the SB is at a given base port, and returns TRUE or FALSE accordingly. If there is an SB, but the DSP reset was unsuccessful we will assume no SB on the system. Function DSPReset (Base_Port:Word):Boolean; { Performs a DSP reset, returning TRUE only if successful } Begin Port[Base_Port+$6]:=1; Delay(1); { 1/1000 second is more than enough } Port[Base_Port+$6]:=0; Delay(1); { It's enough time for ReadReady bit in port } { DSP - DataAvailable (2xEh) go to 1 } DSPReset:=(Port[Base_Port+$A]=$AA) End; { DSPReset } The intention of step 4 is to wait for the DSP to have data available for us to read. We could do it by cycling until bit 7 of port DSP - Data Available be 1, indicating available data to be read from the DSP. But if there weren't no DSP at the specified port, the wait loop would be for ever. So, we just give it more than enough time to the DSP to respond, if there are any (minimum: the 100 microseconds stated on step 4.). From this function, we can make another one to detect if there is an SB present in the system. At the same time, we will detect the SB base port and keep it. To keep this information, and future SB information, like the base port and DSP version, we will use a global variable record. Sb:Record Base_Port:Word; { SB's base port address } Irq_Number:Word; { SB's irq number } Dma_Channel:Word; { SB's DMA channel } Dsp_Major_Version:Byte; { DSP version: } Dsp_Minor_Version:Byte { 1.x - SB1.x; 2.x - SB2.x } End; This variable 'is seen' by all the routines in the unit. The function to detect if an SB is present in the system and its base port is rather simple. All we need to do is to test (try to perform a DSP reset) for a DSP on every possible SB's (base) ports (about 6: 210h,220h, 230h, 240h, 250h and 260h). Lets call this function SBPresent. Function SBPresent:Boolean; { Returns TRUE only if an SB has been detected and keeps its base port } Begin Sb.Base_Port:=$210; { Look for a DSP in the possible ports } While (Sb.Base_Port<$270) And Not(DSPReset(Sb.Base_Port)) Do Inc(Sb.Base_Port,$10); { Try next possible port } { Return TRUE if Sb.Base_Port in [210h..260h] or FALSE otherwise } SBPresent:=(Sb.Base_Port>=$210) And (Sb.Base_Port<=$260); End; { SBPresent } Well, we are done with the initialization thing (for now... it's still missing the dma and irq, but we will do it later). Now, lets talk about giving commands to the DSP. We send commands to the DSP trough port DSP - Data Out (2xCh). Commands are a one byte 'op code' followed by zero or more arguments, depending on the operation. First of all, we must ask "hey DSP, are you ready to obey my commands or receive data?". This is necessary because SBs work *much* slowly than PC's microprocessors. Algorithm: 1. Loop until bit 7 of port DSP - Data Out (2xCh) is 0 2. Send command or data byte to DSP trough port DSP - Data Out (2xCh) When we want to read data from the DSP, we will also have to wait, until data is available: 1. Loop until bit 7 of port DSP - Data Available (2xEh) is 1 2. Read data byte from DSP trough port DSP - Data In (2xAh) Lets do two routines to help us with DSP data in and out: Procedure DSPWrite(Value:Byte); { Writes a value (command or data) into the DSP } Begin While (Port[Sb.Base_Port+$C] And $80)=0 Do; { Wait for bit 7 } Port[Sb.Base_Port+$C]:=Value End; { DSPWrite } Function DSPRead:Byte; { Reads a data value from the DSP } Begin While (Port[Sb.Base_Port+$E] And $80)=0 Do; DSPRead := Port[Sb.Base_Port+$A] END; { DSPRead } Here is a list of some simple DSP commands with its form of use, for a smoooooth start: 10h Direct 8 bit DAC: 1. Write command 10h (into DSP) 2. Write the 8 bit sample (into DSP) 20h Direct 8 bit ADC: 1. Write command 20h (...) 2. Read the 8 bit sample (from DSP) D1h Enable Speaker: 1. Write command D1h D3h Disable Speaker: 1. Write command D3h D8h Speaker Status: 1. Write command D8h 2. Read speaker status (0 - OFF; FFh - ON) E1h DSP Version: 1. Write command E1h 2. Read major version number 3. Read minor version number Commands 10h and 20h, Direct 8-bit DAC and Direct 8 bit ADC respectively, allows us to do exactly what I said in the beginning of the last part of this article. With command 10h we can 'send' an 8 bit sample to the card's output (speakers), and with command 20h we can take an 8 bit sample from the card's input (mic or line in), thus reproduce or record a sound, if we do it repeatedly. The card deals with unsigned samples. In 8 bit samples, the central point, intensity 0, is represented by value 07Fh (127). Keep this in mind, when you play around with the card. Commands D1h, D3h and D8h respectively turn the speaker on, off, and return the speaker status. When programming your SB, you will always find the speaker disabled, because a reset disables the speaker. So, before you 'send' any sound to the card you must turn the speaker ON. Don't do like I did. I almost punched the computer's face before I realized the only problem with my routines was that I forgot to turn the speaker on. Frustrating... The speaker status command tells you if the speaker is on or off. It's a good programing practice to, on the way out, leave everything as you have found, so, I advise you to turn the speaker off before your program terminates. As an example, here is a routine that gets the DSP version and puts it in the variable Sb: Function GetDSPVersion:Byte; { Gets the DSP version of an SB's DSP and keeps it in the variable Sb. } { It also returns the major version number (useful for easy SB id). } Begin DSPWrite($E1); Sb.Dsp_Major_Version:=DSPRead; Sb.Dsp_Minor_Version:=DSPRead; GetDSPVersion:=Sb.Dsp_Major_Version End; Sometimes it's important to know each SB model (SB 1.0, SB PRO, ...) we have available on the system, because some models do more than others. The DSP version of an SB can identify the SB model, as follows: DSP Version SB Model 1.xx Sound Blaster 1.x 2.xx Sound Blaster 2.0 3.xx Sound Blaster Pro 4.0x Sound Blaster 16 4.12+ Sound Blaster AWE32 Now that you are a bit more familiarized with command sending to the DSP, lets do something more interesting: a graphics mode input scope, or graphics mode oscilloscope. A scope shows us the waveform of the sound being inputted to the SB, trough a microphone or a sound source connected to "line In". It is very easy to implement. All we have to do is to continuously read a sample from the DSP and plot a graph on the screen. The graph will have time on the horizontal axis and intensity on the vertical axis. Remember that a sample represents an intensity. We choose a number of pixels for the horizontal axis. Then, we take a sample and at each pixel's column we calculate the y value according to the sample, and put a pixel on the screen at that location. Next we move no next pixel's column and re-take a sample, put a pixel at right location, and so on. When we reach the last pixel of the horizontal axis we go back to the first pixel. It's more easy to see than is to explain: For X_Cord:=1 To 100 Do { I choose a 100 pixels large scope } Begin { Take a sample } DSPWrite($20); { Direct 8 bit ADC command } Sample:=DSPRead; { Read a sample } { Calculate the y coordinate of pixel and plot it } Y_Cord:=YRes-Sample; { YRes is the vertical resolution } Putpixel(X_Cord, Y_Cord, Color, VGA) End; As simple as this. Well, to work fine, you now only have to repeatedly execute this FOR and erase the older pixels before you plot the new ones. YRes is the vertical resolution. This subtract (YRes - sample) is needed because the Y axis grow down on the screen (if you read all other mags, you probably know this already). Look at the entire program that comes with the mag, SCOPE.PAS. Lets move on to another example of what we can do with simple SB commands. Lets make a parrot. - Uouuu! Wait a minute! I though this was a SB programming paper! - don't get nervous, a parrot is a program that repeats everything you say, man! We will use the Direct ADC command to record the sound inputting to SB for a while, and then we will use the Direct DAC command to playback the recorded sound. If we do it repeatedly we have... a parrot. First, to record the sound. We will have to take a sample every t seconds, (t is much smaller than one second!) so that the sound goes uniformly sampled. The samples are kept in a buffer (array). We will use the timer to control the timing. After the buffer is filled, we start playingback, until the entire buffer is playedback. Then we restart the recording. We will have to install a IRQ handler for the timer, which will be responsible for taking a sample (when recording), and playingback a sample (when playingback). Therefore, the handler will have two distinct parts, or, lets say two operating modes, record and playback. The variable Playing controls which part of the handler is active in which moment. Procedure TimerHandler; Interrupt; Begin { If playing, play samples until will have played the entirebuffer } { Else, take a sample, until we have filled the entire buffer. } If Playing Then Begin DSPWrite($10); { Send Direct DAC command } DSPWrite(Buffer[Sam_Num]); { Send sample to be played } End Else { If it's not playing, it's recording } Begin DSPWrite($20); { Send Direct ADC command } Buffer[Sam_Num]:=DSPRead; { Read (take) a sample } End; { 'Go to' next sample } Inc(Sam_Num); { If the end of the buffer is reached, swap the 'mode' and } { reset sam_num. } If Sam_Num>Buffersize Then Begin Playing:=Not Playing; Sam_Num:=1 End; { If it is the time, update the BIOS clock } Dec(BIOSClockCounter); If BIOSClockCounter = 0 Then Begin BIOSClockCounter:=iBIOSClockCounter; Inc(BIOSClock) { Update the BIOS clock } End; { Warn the Master PIC about the ending interrupt } Port[$20]:=$20 End; At the beginning of the program, we reprogram the timer to 'tick' faster, this is, to invoke Int 8 lots of times a second, at a determined frequency. The rest of the code is in the example program, PARROT.PAS. I just wanto talk a bit about the BIOS clock. The normal int 8 handler is called ~18.2 times a second. Among other things, he increments a longint variable in the BIOS data area, the clock. The clock is used for many things, such as for the files time stamp, to control the time which the floppy motor keeps running and, of course, for timing operations. Thus, it is important to maintain the clock running at the normal speed, whenever we take control of the handler. Imagine we reprogram the timer to trigger int 8 at 72 Hz (int 8 is called 72 times a second). From the new handler, our handler, we need to update the clock every 72 / 18.2 = ~4 interrupts. To do it, get a variable counter, and start it with 4. Decrement it every int 8. When it reaches 0, update the clock and reload the variable with 4. In the TimerHandler procedure above, iBIOSClockCounter keeps the 4 and BIOSClockCounter is the variable counter. In PARROT.PAS, I choose a default sampling frequency of 10KHz ('practical frequencys go from about 5KHz to 44.1KHz; below 5KHz, the sound is just too bad and above 44.1KHz the sound won't sound 'even better'). The sound quality won't be great, but with Direct DAC/ADC commands we can't go much further. The time needed by DSPWrite and DSPRead operations limit the frequency. Feel free to make experiments with this frequency. Well, I could give some more examples of what we can do with this two simple SB commands, DirectDAC and DirectADC, but I've write a lot already, and I yet have a lot to write, and Spellcaster must be nervous by now, thinking of the space area my article will occupy... this mag will end up with more lines written by me, hehe... Anyway, I think now you have a lot to experiment with... you could try to do a program that triggers something whenever a loud sound is made... or a program to display a VU (a power measurer)... or you could do a simple *.SAM/*.SMP player (SAMs and SMPs are Amiga files, so, the samples are signed; therefor, before you play them, you must transform them into unsigned samples: Sample_Unsigned = Sample_Signed+2^(Number_Bits_Sample-1) example for 8bit samples: Unsigned = Signed+2^(8-1)= Signed+128 or... whatever you can get out of your creative mind (you are creative dudes, aren't you?). Remember this paper is an introductory one, so as the examples. Everything in the examples can be improved, and believe me, there is a lot to improve. And to finish this chapter, I would like to tell you how to mix two or more sounds (sampled sounds, of course). When you listen someone talking, and a big fuzzy and noisy car passes near you, you listen the two sounds, together, mixed. What happens to the waves? They are summed, and the resulting wave has the two sounds. Well, then it's easy to mix two sampled sounds, you just add one to the other, sample by sample! Yes, it's almost right. There's only one detail to take in consideration: if you have 8 bit samples, adding two of them may result in a sample that doesn't fit in 8 bit, like (255 + 255). The solution is then to make an arithmetic medium, like (sample1[n] + sample2[n]) div 2 (the samples are in two arrays, sample1 and sample2). If you have 3 sampled sounds to mix, then Mixed_Sample[N] = (Sample1[N]+Sample2[N] + Sample3[N]) Div 3 and so on. An improvement you can do to this formulae, for S sampled sounds is Mixed_Sample[N] = (Sample1[N]+...+SampleS[N] + S Div 2) Div S Why the term '+ S div 2'? This term will make the result be symmetrically round. It is equivalent to summing 0.5. This will result in better sound quality, because the calculations are made in a more precise way. Note that, as you increase the number of mixed sounds, you decrease the volume of each one. There are other mixing technics, some of them better than this one. -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x- 6. To float or not to float [ Note: The writer of this article is a C lover, and a Pascal hater... Because of that, the article had some C code. I was going to take it off, but I've decided to keep it... ] 6.1. Introduction Up until the Pentium processor series,floating point math was a true pain; just watch: * FPU units (a.k.a. co-pros) were not standard equipment, so coders couldn't count on them. * Floating Point Math was waaaaay too slow for a lot of realtime applications (the kewl ones at least =). * High-Level languages had FPU emulators, but these were even slower than using the FPU. * The highest percentage of asm coders to commit suicide was trying to code floating point routines with FPU emulation. Given the above, it's darn easy to understand the virtues of integer math: * Integer Math is available in every CPU. :> * It's waaaaaaay much faster than floating point math. * Asm coders can now commit suicide for a refreshingly whole universe of reasons. :] 6.2. Basic Concepts So, what the hell is this integer math stuff? Well, it's mainly the simulation of floating point operations using only integers. The main idea is to "reserve" i bits for the integer part of the number and f bits for the fractional part; the integer variable then looks like this: b b b b n i i-1 0 | <-i-> | . | <-f-> | As you could see if I could do ascii :P, the decimal point is fixed so the range suported by a certain fixpoint representation is: i f 16 16 2 -1.2 -1, or for the case of a 16.16 representation, 2 -1.2 -1, or in plain English, 65535.65535 (assuming unsigned data). * Precision: One fact you should keep in mind is that fixed-point math is nowhere near as precise as floating point; as a matter of fact, 16.16 fixed-point numbers have a maximum of 5 digits of precision, or in other words, an error of +-5e-5 (that's about +-0.00005). Of course, if your needs of fractional precision are more demanding and you don't need to represent big integer numbers, you can always allocate more bits for the fractional part; let's say you are working with a normalized coordinate system...the range of numbers will be [-1..1], which means you will only need 2 bits for the integer part: 1 for sign and 1 for the integer part [0,1], which leaves you with 30 fractional bits. Now, 2.30 fixed-point numbers have a maximum error of +-5e-10 (about +-0.0000000005), which is more than enough;so the key idea here is: DON'T STICK TO A FORMAT! Fixed-Point math can pay if you use it wisely, so, before getting some source and sticking it into your code, think twice. :P Try to understand the principles and then adapt it to your specific needs, you'll be pleased with the results... Now, away with the bullshit and let's start working: * Getting Started First thing you want to do is to convert between data representations since fixed-point data is not widely available anywhere as it's not a native format of our beloved CPUs... Note that I'm assuming you code for 32-bits...if you're still stuck in the 16-bit nightmare, keep reading, what i'll say here applies to both, only the examples will be 32-bit code... * Base data type If you're a high-level language coder, you need a base data type to hold your precious fixed-point numbers: In C: typedef int FixFloat; typedef unsigned FixFloat; In (argh!) Pascal: Type FixFloat=Word; (i'm not sure about this...SpellCaster?-) [ You got it wrong, but I corrected it, as I will do througout this article ] * Integer -> Fixed-Point The conversion from integer to Fixed-Point format is as easy as shifting...you just SHIFT the integer into its proper place, past the fractional area: In C: A=[Number]<<[f]; In 2.30: A=[Number]<<30; In (puke!) Pascal: A=[Number] Shl [f]; In 2.30: A=[Number] Shl 30; * Floating Point -> Fixed-Point The conversion is as easy, only you can't use shifts; instead, you figure out how much is 2 to the power of [f], say N, and multiply: In C or (=O) Pascal: A=[Number]*N; In 16.16: A=[Number]*65536.0; * Fixed-Point -> Integer/Floating Point Basically you just do the inverse operations, either by shifting right F bits to get the integer part, or by dividing by N to get a floating point value back... :) 6.3. Fixed-Point Math Theory So now you have your cute fixed-point numbers waiting to be used, let's do it then: * Basic Operations * Adding and Subtracting These are stupidly simple operations: In C/Pascal: C=A+B; * Multiplication Now comes the fun part... *g* Remember your first years of school? They taught us to multiply/divide BY HAND! What are computers for anyway? :-) Well, at least that's gonna help you a lot in understanding this crap: d.f x d.f ----- dd.ff Imagine you're multiplying two fractional numbers; to do so, you just ignore the decimal point and multiply them just like integers counting the decimal digits afterwards. In fixed-point multiplication you do exactly the same way, only that you know exactly how many fractional digits you'll have since the decimal point is fixed. :) So, to start, you just mul the numbers together; the kind CPU will return a 64-bit number(for 32-bit mul) as a result, with 2*f fractional digits; so, given that you want a result in d.f format, u just shift away the least significant f fractional digits (bits) so as to convert dd.ff into d.f; easy no? :-) After you recover from my last explanation, take a look at the code below, assuming 2.30 fixed point numbers and of course, 32-bit code: [ 1st number is in EAX, 2nd is in EDX ] imul edx ; EDX:EAX=EAX*EDX (64-bit) [R] add eax, 20000000h ; EDX:EAX+=0.5 (rounding) [R] adc edx, 0 ; Add overflow to EDX shrd eax, edx, 30 ; get rid of extra frac. ; numbers and get the de- ; cimal part in EAX. [ Jesus, you are going to scare off everybody... Well, I'll clear it up later, if you don't... :) ] The rounding instructions (marked with [R]) aren't necessary, but can help with the precision; once again, the choice is yours... 0x20000000h is 0.5 in 2.30 fixed-point format or 1<<(f-1), 1<<29. The adc line just sums the overflow from the add to the high dword of the result. The last line just gets rid of the lowest significant fractional digits to make room for the integer component. =) * Division Given the above, it's easy to understand the division operation, it's just the inverse situation: dd.ff | d.f ----- d.f Given a 64-bit number and a 32-bit one, it's just a matter of actually dividing, yep it's that easy! :) [ 1st numb in EDX, 2nd in EBX ] sub eax, eax ; EAX=0 shrd eax, edx, 2 ; 2 least significant bits (lsb) ; from EDX go to EAX sar edx, 2 ; loose the 2 lsb keeping ; the sign information idiv ebx ; result now in EAX, in 2.30 ; format, courtesy of your ; friendly CPU. :> Easy or what? Now, this might confuse you a bit, so let's try to make it clear since it really got me confused when i started learning it... :) In the beginning you have a 32-bit number in EDX which will be divided by the 32-bitter in EBX; but before you divide you've got to get your 1st number in 64-bits, or else you can kiss your precision goodbye. To do so, you just have to recall the format given above for multiplication results: dd.ff! (Just remember that dd represents *4* bits and ff *60*...) Now, you have d.f wich means 2 integer digits plus 30 fractional digits, and you want to get it into 4 decimals plus 60 fractional, so how can we do that? Loose 2 digits from EDX into EAX and you have your 4 decimal digits! So that's what SHRD does, it copies the 2 least significant fractional digits from EDX into the 2 most significant digits of EAX. Now we can safely adjust EDX, loosing the least 2 digits wich are now safely kept in EAX; SAR does the job, preserving the sign information as a bonus... After the above operations, EDX will contain 4 decimal digits + 28 fractional digits and EAX will have 2 fractional digits + 30 fractional zeros... Once you have your kewl 64-bitter, just divide, and the number will automatically be in 32 bits, 2.30 format and all...Of course the same applies to other formats... Note that no rounding is performed, and it usually isn't needed; if you're a decadent sort of person, you could implement it, but that's unnecessary overhead in a slow operation by nature. It's a good idea in multiplication though and the overhead is minimum... * Transcendental (wow!) functions and the pizza monster By now i hear you cry: "What about functions? I need my sine doses!" and others: "Why the f*k did I start reading this sh*t?". Calm down, the first group can keep reading; as for the second one, there are a lot of free mailbombers on the net, get the best! *g* The best way to implement functions is by the use of lookup tables... for functions like sine/co-sine/etc. It's as easy as making a choice of resolution; how many diferent values will you have? Well, it's up to you actually; 256 is a good number, it's not much more than 1 degree resolution and it may be well worthy...on the other hand, if you're kinky and want to have a full 360 values table, well...go ahead and see if I care :) Basically, you just have to code a separate proggie to calculate the M values for your tables, convert each one to the fixed point format of your choice, and output them to a file if you don't need to generate it realtime... :) For those functions which can't be hardcoded on a lookup table, you don't have many choices: * 1. Kick your teacher's head until he gives you a good and inexpensive approximation function. * 2. Derive your own approximation, Taylor Series, whatever... * 3. Ask someone who has done it. Menace to kill his/her pet if it won't let you have it. Once that we are at it, there's a really nice article by Kiwidog on Imphobia #12 about Fixed-Point Square Roots using lookup tables... Read it! 6.4. Closing Coments This is my first damn doc/tutorial/whatever, so.... :} Before leaving the few of you that got this far, i have a few last words, well, one word actually: OPTIMISE! What i mean by optimisation is not the obvious code-it-in-asm talk, just remember that even if you code those basic fixed-point operations as inline asm funcs (as you should:), there's always a small amount of overhead like for example, in the division by getting the number to 64-bits, so if you have a certain formula that gets called often enough to justify it, code it inline instead of calling the basic ones separately! As an example, suppose you have some formula like: a * b ----- (does it look familiar? :>>) c if you calc it as Fdiv(Fmul(a,b),c)), you'll get something like: /* Assuming a,b,c already in eax,edx and ebx */ imul edx add eax, 20000000h adc edx, 0 shrd eax, edx, 30 ; Redundant, we're converting to 32-bits sub eax, eax ; and will now convert again to 64-bits after loosing shrd eax, edx, 2 ; 30 bits of fractional precision, while we could have sar edx, 2 ; used the 64-bit result directly from the mul and get idiv ebx ; a more precise result and a speed gain... :O while if you coded it as a separate func: /* a in eax, b in edx, c in ebx */ imul edx add eax, 20000000h adc edx, 0 idiv ebx As you can guess, it gets worse quickly as you increase the number of operations... Weird? No Way! Life's that kind of a *itch! Now, suppose you're using this example func loads of times inside an inner loop...guess what? :> The main point is: try to UNDERSTAND the principles, if not from what I wrote, from the wealth of other documents on this subject, then use that power for your own requirements...it all depends on the needs of your application. Also, if you're coding a nifty proggie and spent nights optimising it for Pentium, DON'T USE FIXED-POINT! It doesn't pay to use it on P5's...use the FPU instead. Also, i'm not sure about the speed advantages of fixpoint on the 486 either, but then i'm too lazy to code some benchmarks and Pentium is becoming a standard anyway and... ;) I'm sorry if you're disappointed, but there's not much more to say about the subject, now it's a matter of getting your hands dirty and code the basics, then some tables, then some test proggies, then some often used expressions, then... :) As we say in my country: "If you don't like it, don't eat it!" :P After reading this awfull doc, you'll be surely pleased to read some others (there's a nice one by Rage Technologies or something...ftp1_rti.zip if i recall) on: ftp://ftp.cdrom.com or (not sure...they're both kewl:) ftp://x2ftp.oulu.fi If you still have problems, don't hesitate to mail me: i930339@dei.isep.ipp.pt or find me on irc on Undernet #coders as Gh8 or Gate... :) Hope i could be of some help to someone; happy coding! Spellcaster's back !!! That's, Gh8, I'll take it from here... Now it's my turn !! Ahaha !! :) Well, you Pascal freaks out there are probably wondering how can you put the stuff that was just taught in practice... God knows I was ! :)) Now, to implement 8.8 fixed point maths, it is easy... You just have to use Pascal code... No fancy ASM needed... As the matter of fact, for any kind of maths that just need 16 bits can be made to work nicely enough with Pascal code without the need to Asm... To prove it, I made a unit called Fixed16 that enables to specify the number of the bits for the integer and fraccional part (you just specify the fracional part, the unit calcs the rest)... The code is in Fixed16.Pas. Now, let's try to do a 32 bit fixed point unit... This is trickier, as you will see... So, first of all, we need to use assembler to implement this stuff... I know some of you hate assembler, but I don't know any other way of doing this. If you have read the previous issues of 'The Mag', you probably know how to use assembler with Pascal (if not, read those issues about it... I don't recall which ones are... Find out yourself...). The problem with Pascal's inline assembler is that it can't do 32/64 bit intructions !!! And that's a serious problem, since we need to handle 64 bit numbers (if there was a data structure in Pascal that could handle 64 bit numbers, we would use it and wouldn't need assembler... But there isn't...). So, using a series of tricks which I describe in article 10 (or something like that... The one on ASM tricks...), I could code a similar set of operations that I did in the Fixed16.Pas unit... The code is in Fixed32.Pas. But remember, don't just copy and paste to your program... The code can be slightly improved, and I think it is really a great source of info on getting your hands really dirty in Turbo Pascal ! :) And this is the end of this article... Spellcaster out ! :) -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x- 7. A simple introduction to C - Part I This article was written by a Finish friend of mine, Matti Junkkarinen, and it is dedicated to all you 'traitors' out there ! :) I changed a bit the original text, since it was an adaptation of my own tutorials, but it is almost the same... So let's start: Well, I, Matti Junkkarinen from Finland, translated Spellcaster's MAG to C. I'm not good at English (though I have 9 at English and grading here Finland goes from 4 to 10), so try to understand my ridiculous sentences...if you have anything to ask you can send e-mail to me at address: matti.junkkarinen@mbnet.fi. I started this dirty work by ask of Spellcaster...or Diogo...or whatever he like to himself be called. I program at Borland C++ 3.0, but I program C-code. I hate C++. I can also code in C++, but I just hate it. Well, try to understand my translations...my manner to program is little different than others, but it is so with any C-programmer. This is not very good introduction...there are many good tutorials to C in the Net...seek them if you want to learn...or buy a good book. 7.1. Functions Well, now it's time for introduction to C...Let's start with functions... If you know Pascal you know there are procedures and functions, but in C you have only functions...Or if they don't return any value they will still be called functions. In every program you have to have a function called main... It's the main function and OS, which run your program will run it. Your programs will always start at the start of main and usually they end at the end of main. A sample main function is like this... void main(void) { } It does nothing...Good start I think :) Commands are places inside { and }. Void before main means that that function returns no values...It's like a procedure in Pascal. Void between ( and ) means that it won't get any arguments. NOTE: C is a case sensitive... So it's not same to write void or Void or VOId etc... All keywords in C are in lower case. Well, let's write a program what prints legendary Hello world to the screen... #include void main(void) { printf("Hello world! \n"); } So, printf is a function, which is declarated at file called stdio.h. You can call any function by this way... function_name(argument_1, arg_2, arg_3, ...); You have to put ; to end of all C sentences...(exclude function starts like main there... And all conditions and loops). "Hello world! \n" is a string, which is sent to function printf as it's argument. Anything between " and " is a string...that \n means line changing. 7.2. Including files and comments You can include files...Like stdio.h or test.c like this way... #include - if file is at include-directory #include "name" - if file is somewhere else An example... #include #include "test.c" - This is at the same directory #include "e:\c\code\misc\test\mystic.c" Hey, comment-characters are in C /* and */...everything between them is a comment...If you use C++ compiler you can use // comment too, which declares only one line to a comment... Well, here's an example about functions, their declarations and calls... #include /* Including stdio.h...it contains declaration */ /* for printf... */ void Test(void); /* Declarations of Test and Test2 functions. */ void Test2(void); /* You have to put ; to end of the.. */ void main(void) { printf("This is main\n"); /* Printing a message */ Test(); /* Calls function Test...no arguments */ Test2(); /* Calls function Test2...no arguments */ printf("Main again...program will end soon...\n"); } void Test(void) /* Start of Test...don't put ; here! */ { printf("This is Test\n"); /* Prints a message... */ } /* Test ends here...running goes back to the program which called it...in this case it's main...so it'll run the next command of main below call of the Test */ void Test2(void) { printf("This is Test2\n"); } 7.3. Datatypes Well, here are datatypes of C... Type Description --------------------------------------------------------------------------- char A character...it can be signed or unsigned (that belongs settings of compiler). It's like a one character... It's size is one byte. signed char This is a signed character...it can have a number between -128 to 127. It's size is one byte. unsigned char Unsigned charaster...it can have a number from 0 to 255. Size if one byte. int/short/ Signed integer. This variable type can hold numbers signed int between -32768 and 32767. It's size is two bytes. unsigned int/ Unsigned integer. Range is from 0 to 65536. It's size unsigned is two bytes. long/long int Signed long number. It can contain numbers between -2147483648 and 2147483647. It's size is four bytes. unsigned long/ Unsigned long integer. It can hold number between unsigned long int 0 and 4294967296. It's size is four bytes. There are still floats and doubles, but I'll descripe them later. 7.4. Defining of the variables You can define variables like this... int an_integer; char very_long_name_for_one_character; Just put datatype first (you can make also your own datatypes, but about them later...), then name of the variable and then ;. You can declare variables also like this... int var, var2, var3, var4, var5, var6; That declares 6 variables, which are all type of signed int. You can put values to variables when you declare them...like this... unsigned long test_variable = 2436437, test_variable2 = 23712868; char a = 25; And in program you can put values to variables like this... int a; a = -2345; Variables can be defined anywhere in the program, contrary to highly tipified languages like Pascal, that have a data definition area... You can define a variable when you are about to use it, like this int a=function_that_returns_an_integer(); 7.5. Calculations You can make calculations between variables... int a, b, c; a = 10; b = - 200; c = 40; a = b + c; b = a; c = a * (a - b); b = c / (b * a + c); a = c + 24; b = 20 * a - b / 2; You can use +, -, * and / to add, substract, multiply and division your variables and numbers. 7.6. Arrays You can define an array of any datatype like this.. data_type array_name[size_off_array]; ie. int array[200]; unsigned char array2[10]; long array3[4]; You can put values to your array at same time you declare it... char a[5] = {26, 24, 12, 34, -2}; ...put numbers in { and } and separate them with ,. Here's how you can use your arrays... unsigned int mystique[4]; int a; mystique[0] = 2324; a = 2 * mystique[0]; mystique[1] = a + 32; mystique[3] = mystique[1] - mystique[0]; Just remember that, the first number of array is 0...not 1...so the last number is the size of array - 1. You can define text strings like this... char str[10] = {'H', 'e', 'l', 'l', 'o', '!'}; NOTE: By character between ' and ' you can get it's ASCII value. char t = 'A'; /* Now t is 65 */ You can also define....hmmm...I don't know that name... int array[10][10]; That will define ten arrays size of ten. You can access them as the same way like normal arrays... array[2][4] = 2376; You can also use variables as indexes to arrays... int a; char array[10]; a = 4; array[a] = 22; /* And now the fourth member of that array will be 22 */ Well, I'll write a little about printf and scanf...So lets move on.... You can print your variables and text with printf.... 7.7. A function called printf int a; unsigned b; char str[10] = {'H', 'e', 'l', 'l', 'o', '!'}; a = 30; b = 60000; printf("%d\n", a); printf("%u\n", b); printf("Value of variable a is %d and b is %u\n", a, b); printf("String is: %s\n", str); /* Put here just str..not str[10].... You have to place %? between " and " and then place your variables at same order which they was inside of " and ". %u and %d are printf format specifiers. There are some of them.... %d and %i = signed integer %u = unsigned integer %c = character %l = long %s = string 7.8. Scanf Well, that's about printf...now I'll show you scanf. You can get values to your variables by using scanf... int t; printf("Type a number: "); scanf("%d", &t); As you can see there's & before the variable name...That means you send to function a pointer to variable... Not copy of variable as it will be done if you use only t... Then scanf can't change value of t... It just changes value of it's copy and your original variable won't change. So you have to put that & there. Format specifiers are same as they are in printf. You can get more than one value at the same time by doing like this... int t, t2, t3; printf("Please type three numbers separated by space: "); scanf("%d %d %d", &t, &t2, &t3); If you want to get a string... char str[20]; printf("Write something: "); scanf("%s", str); Notice that now you don't place that &. That's because only str (not str[]) is a pointer to the first member of that array. Or you can do it like this way, but it's exactly same... scanf("%s", &str[20]); If you want to define how long string will be what will be get... scanf("%20s", str); ...just put number between % and s. Well, there are pointers and memory allocation left, if I explain same things in C that Spellcaster explained in Pascal. And I think I have to do so, because otherwise at the next MAG there could be something that uses them and then I have to explain them then...I'll explain them now, so I don't have to care about them later... 7.9. Pointers I just can explain them in English...I'm not good enough. Well in Finnish I could, but... PLEASE WRITE THIS START AGAIN, SPELLCASTER... I'll just show how to define and use them.... You can define a pointer like this... char *a; That * means that a is a pointer. Like this you can point something by using a pointer... char *p, t; p = &t; Now variable p points to t. Remember & means variables address...so p contains address of variable t. You can change pointed data... char *p, t; p = &t; *p = 20; Now t = 20 and p still points to t. You have to put * before p...That means it changes pointed data value. I won't now explain pointers of pointers or pointers of pointers of pointers...You can guess why :) 7.10. Allocating and freeing memory You can't define too big arrays, like Spellcaster wrote above at end of his introduction to Pascal. There are several functions to allocate memory in standard libraries but now I'll explain only malloc. Include file stdlib.h, because malloc and free are defined there. Here's a small program that should explain how to alloc, use and free memory.... #include /* Including stdlib.h and stdio.h */ #include void main(void) { char *pointer; /* defining a pointer */ pointer = malloc(64000); /* allocating 64000 bytes memory and pointer points at start of it */ if (pointer == NULL) /* if pointer is NULL (0) then allocating */ exit(1); /* failured and program ends...I haven't told */ /* anything about conditions yet so don't */ /* amaze...I'll explain them at Part 2 like */ /* Spellcaster */ printf("Memory allocation succeed!"); pointer[0] = 23; /* Setting first byte of allocated memory to */ pointer[1] = 44; /* 23 and second to 44 */ free(pointer); /* Freeing memory...don't forget to do this! */ } That was it...I commented it so you can understand it much better...well, now I have writed this part of introduction to C... 7.11. Conditions Conditions in C are similar to Pascal. They don't require the THEN clause and are a bit more flexible. For example, to find if a number is larger than another, you do this: int a = 10; int b = 15; if (a > b) printf("A is biggest"); The sintax for conditions in C is as follows: if (condition) { command } NOTE: If you have only one command to run if condition is true then you can leave { and } off... The condition is anything that returns a logic result (TRUE or FALSE). While in Pascal there is a type BOOLEAN that is TRUE or FALSE, in C there is no such thing. Any numeric type can be used in a condition. If that number is 0, that's the same as being false. If it is other number, then it is true. Example: int a; a = 10; if (a) printf("This gets printed\n"); a = 0; if (a) printf("This never gets printed\n"); The available operators are as follows: ÚÄÄÄÄÄÄÄÄÄÄÂÄÄÄÄÄÄÄÄÄÄÄÄÄÄÂÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ Operator ³ Desciption ³ Example ³ ÃÄÄÄÄÄÄÄÄÄÄÅÄÄÄÄÄÄÄÄÄÄÄÄÄÄÅÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ³ == ³ Equal to ³ A==B : Returns TRUE if A equals B ³ ³ ³ ³ ³ ³ != ³ Not Equal to ³ A!=B : Returns TRUE if A is diferent to B ³ ³ ³ ³ ³ ³ < ³ Less than ³ A ³ Greater than ³ A>B : Returns TRUE if A is greater than B ³ ³ ³ ³ ³ ³ <= ³ Less than or ³ A<=B : Returns TRUE if A is less or equal ³ ³ ³ Equal to ³ to B ³ ³ ³ ³ ³ ³ >= ³ Greater than ³ A>=B : returns TRUE is A is greater or ³ ³ ³ or Equal to ³ equal to B ³ ÀÄÄÄÄÄÄÄÄÄÄÁÄÄÄÄÄÄÄÄÄÄÄÄÄÄÁÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ Let's see a complete example: #include void main(void) { long a, b; printf("Write number 1:"); scanf("%d", &a); printf("Write number 2:"); scanf("%d", &b); if (a == b) printf("The numbers are equal"); if (a < b) printf("a is smaller than b"); if (a > b) printf("a is bigger than b"); getch(); } getch() is a function that returns a character that is pressed. It is very common to be used to make a pause in the code, since it waits for a key to be pressed if there aren't any in the buffer. In C, contrary to Pascal, you can't compare strings like variables...you can compare them by functions...I remember that one is strcmp, because I use my own string functions...I'm not sure about strcmp's syntax, but I think it goes like this... int strcmp(char *str1, char *str2); So that strcmp will return a value...if strings are same it'll return 0. If first is bigger then second one it will return > 0. And if second string is biggert than first string it will return < 0. So, you can use it like this... char string1[10] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}; char string2[10] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '5'}; int cond; cond = strcmp(string1, string2); if (cond == 0) printf("Strings are equal."); if (cond > 0) printf("First string is bigger."); if (cond < 0) printf("Second string is bigger."); Hey, the prototype of strcmp is in file string.h...so, you have to include it at the start of program....like this... #include In C { is like Begin and } is like End in Pascal, so you aren't limited to just one command per if. 7.12. ASCII in C While in Pascal you had to make something like A=Ord('A') to obtain the ASCII code of A, in C it is easier. You just do: A='A' -> Gives you the ASCII code of the character A. In C you don't need to have a function like that ORD, beause in C char is exactly same as byte...I didn't know before they are separated in Pascal. Well, if you want to print variables ASCII code or variable itself you just choose which you want to print... Maybe this example helps... #include void main(void) { char a; a = 'a'; printf("The ASCII code of %c is %d", a, a); /* You choose character or ASCII code to print by selecting %d or %c. */ a = 66; printf("The char with ASCII code of %d is %c", a, a); /* You can see that it's much easier in C... */ } 7.13. Assembly and Pascal To use Assembly in C (in Borland C++....other compilers have usually different ways...look them for help or books...) ... printf("Anything..."); ..... .... asm { mov ax, 10h mov bx, 15h shr ax } ..... ..... The assembly part is equal to Pascal, or any other compiler (with the exception of the compilers that use the AT&T syntax, which I ain't gonna discuss here). Now, to end this tutorial, I'll delve into graphics. C suffers (or suffered) from the same problem as Pascal when it came to graphics. BGI's SUCK ! So, we use a bit of assembler and other stuff to get more speed and flexibility. The theory behind this is explained in issue 2. Here you are only going to find the direct translations of the procedures in that issue's graphic tutorial: 7.14. Graphics void Initgraph(void) { asm { mov ah, 0 mov al, 13h int 10h } } void Closegraph(void) { asm { mov ah, 0 mov al, 03h int 10h } } void BIOSPutpixel(unsigned x, unsigned y, unsigned char col) // This is very slow { asm { mov ah, 0ch mov al, ss:col /* You can use too mov al, [col], but variables are in stack...thata ss:? is better I think, but you can use which one you want... */ mov cx, ss:x mov dx, ss:y mov bx, 1 int 10h } } While in Pascal we could access any memory position by using the MEM array, in C it isn't that easy. We must use the poke and pokeb functions: poke(segment, offset, value); pokeb(segment, offset, value); poke puts a word VALUE in the memory position SEGMENT:OFFSET, while pokeb puts only a byte. So, to put down a pixel, ultra fast: void Putpixel(unsigned x, unsigned y, unsigned char col) { pokeb(0xa000, 320 * y + x, col); } Well, if I haven't tell it to you...In C you can use hexadecimal numbers by putting 0x before it... Like this... Assembly Pascal C ------------------------------------------------- 10h $10 0x10 324837h $324837 0x324837 200h $200 0x200 It's very simple... In inline assembler you can use ?h numbers too.... Now, let's see a full example. This was lots to translate, but this is just nothing when you look next MAGs... I won't comment this...OK, I will comment hardest parts like Spellcaster wrote to me... #include #include /* This is included because this program uses random command and it's prototype is in stdlib.h */ #define VGA = 0xa000 /* Well, you can define constants like this in C... I think I haven't tell it to you yet... just but that #define first and then constant's name and then = and then value of the constant...don't put that ; after #defines.... */ void Initgraph(void); /* These are prototypes of those functions...if you */ void Closegraph(void); /* don't put then before main you have to protype */ void BIOSputpixel(unsigned x, unsigned y, unsigned char col); /* like this.*/ void MEMputpixel(unsigned x, unsigned y, unsigned char col); void Cls(unsigned char col); void main(void) { unsigned a, b; printf("This program tests two putpixels.\n"); printf("First it test the BIOS put pixel, then it tests MEM put pixel.\n"); printf("It was written by Spellcaster and translated to C by Mazar.\n"); printf("Press return to start"); getch(); Initgraph(); for (a = 0; a <= 199; a ++) for (b = 0; <= 319; b ++) BIOSputpixel(b, a, random(256); getch(); Cls(0); for (a = 0; a <= 199; a ++) for (b = 0; <= 319; b ++) MEMputpixel(b, a, random(256); getch(); Closegraph(); printf("That's all folks..."); } void Initgraph(void) { asm { mov ah, 0 mov al, 13h int 10h } } void Closegraph(void) { asm { mov ah, 0 mov al, 03h int 10h } } void BIOSPutpixel(unsigned x, unsigned y, unsigned char col) { asm { mov ah, 0ch mov al, ss:col /* You can use too mov al, [col], but variables are in stack...thata ss:? is better I think, but you can use which one you want... */ mov cx, ss:x mov dx, ss:y mov bx, 1 int 10h } } void MEMPutpixel(unsigned x, unsigned y, unsigned char col) { pokeb(0xa000, 320 * y + x, col); } void Cls(unsigned char col) { /* Well, I don't know is there any function to set mem in C...I can't re- member only one...I use my own functions...I'll write a function in Assembly for you...maybe in future MAGs you will know how it works... */ asm { mov al, ss:col mov ah, al mov cx, 32000 xor di, di mov es, 0xa000 rep stosw } /* That function is fast....I think it's so fast it can be....or it can be speeded up by using 32-bit registers, but not at this time... */ } So, this is the end of this introduction to C... Hope you enjoyed it. I will try to get Matti to write some more for future issues... Now, let's get back to a real language :) TURBO PASCAL ! :) -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x- 8. Sprites Part V - Colision detection I think I don't need to explain what collision detection, but to avoid confusions, I better do... Collision detection occurs to check if two sprites are colliding with each other... Simple, isn't it ? There are two important types of collision detection: box and pixel perfect. A good program should use a combination of the two to achieve a good and fast collision detector... 8.1. Box collision Box collision detection is a type of collision that for some objects it gives excelent results, while with others terrible results ! :) For the sake of simplicity and explanation, imagine that the sprites we are trying to detect if they are colliding are of rectangle-like shape, like a tank or something like that... I arrived to the conclusions I'm going to explain by grabbing a pen and a sheet of paper and thinking for two minutes... Ah, notice that I've changed the Sprite structure a bit, to include the X and Y size, since they come in handy in this, and it is more pratical to have it in a variable than searching the memory for the correct image and getting the size from there... Now, what are we going to check out ? Well, we're are going to check out if the rectangles that are each of the sprites overlap ! That's simple to do it... I divided the checking in two parts: the X overlapping, and the Y overlapping. To keep this short: ÚÄ¿ ÚÄ¿ ÚÄ¿ ÚÄÄ¿ ÀÄÙ ÀÄÙ ÀÄÙ ÚÄ¿ ³ ÚÁ¿ ÚÄ¿ ÚÄ¿ ÀÄÙ ÀÄÁÄÙ ÀÄÙ ÀÄÙ A B C D In situation A there is no overlapping, in B there is X overlapping and no Y overlapping, in C there is Y overlapping, and X overlapping, and in D there is X and Y overlapping. Only in situation D the objects are colliding, so we come to the conclusion that there must be X and Y overlapping to have a collision. So, how do we check for X and Y overlapping ? They are equal (you just need to change the X's for Y's). The way I do it is by checking the displacement of one sprite, relatively to the other. So Delta:=2.X-1.X Where 2.X and 1.X represent the X coordinates of sprite number 2 and number 1, respectively. Now, if Delta is larger than 0, it means that sprite 2 is to the left of sprite 1, and if that Delta is smaller than the size of sprite 1, there is X overlapping. On the other hand, if Delta is smaller than 0, it means that sprite 2 is to the right of sprite 1, and if the module (the absolute value) of Delta is smaller than the size of sprite 2, there is overlapping in X... If this sounds confusing, draw simple schemes and you'll see that it is easy ! :) The code of the collision detection will look something like this: Function Col(Spr1,Spr2:Sprite):Boolean; Var XCol,YCol:Boolean; Delta,CompSize:Integer; Begin Delta:=Spr2.X-Spr1.X; If Delta<0 Then If Abs(Delta)MinX Then DeltaX:=MinX; Sx1:=0; End Else Begin DeltaX:=Spr1.X+Spr1.XSize-Spr2.X; If DeltaX>MinX Then DeltaX:=MinX; Sx2:=0; End; Sy1:=Spr2.Y-Spr1.Y; Sy2:=Spr1.Y-Spr2.Y; If Sy1<0 Then Begin DeltaY:=Spr2.Y+Spr2.YSize-Spr1.Y; If DeltaY>MinY Then DeltaY:=MinY; Sy1:=0; End Else Begin DeltaY:=Spr1.Y+Spr1.YSize-Spr2.Y; If DeltaY>MinY Then DeltaY:=MinY; Sy2:=0; End; { Check the intersection } Seg1:=Seg(Spr1.Img^); Ofs1:=Ofs(Spr1.Img^)+4; Seg2:=Seg(Spr2.Img^); Ofs2:=Ofs(Spr2.Img^)+4; For X:=0 To DeltaX-1 Do Begin For Y:=0 To DeltaY-1 Do Begin C1:=Mem[Seg1:(Ofs1+Spr1.XSize*(Y+Sy1)+(Sx1+X))]; C2:=Mem[Seg2:(Ofs2+Spr2.XSize*(Y+Sy2)+(Sx2+X))]; If (C1<>0) And (C2<>0) Then Break; End; If (C1<>0) And (C2<>0) Then Break; End; If (C1<>0) And (C2<>0) Then PPCol:=True Else PPCol:=False; End; A good optimization of this code can be made if we think that every checking of a line is linear, so instead of doing a mult by pixel, you do a mult by line ! Check the example code Col2.Pas to see this implemented... Well, this is what I remembered to teach you about collision detection. Not too complicated, but this is something that should be optimized to the max, since it will be routines that should be used a lot in an action game, for example... On next issue, if there aren't any requests on sprite related subjects, I'll start to make a games using the stuff we learned here... Cya... -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x- 9. Graphics Part XI - Introduction to unchained modes 9.1. What ?! What is ModeX ? Well, to explain this, I'll start with why did it appear... Have you ever wondered that even VERY old videocards had 256Kb of memory, and that a normal mode 13h screen only uses 64Kb ? So, where are the other 192Kb ? Well, trapped inside the limitations of segmentation... Remember when I told you that the limit for a segment was 64 Kb ? Well, this is why you can't normally use the 256Kb memory... But, don't despair... There are ways around this... One of them is to use VESA, which I'm not an expert, but someday I'll delve into... The other is to use unchained modes. What are unchained modes ? Well, they are one way to use the whole 256Kb memory of a video card... But it has it's drawbacks... You loose linearity ! You no longer can use those nice linear putpixels... :( But that's a small price you pay for the versatility of unchained modes. With unchained modes, you can have a page with 640x400x256, or four pages with 320x200x256, and you can switch from one to another in a fraction of the time it would take you to copy a virtual screen to the physical screen. There are all sorts of weird modes you can enter, like 320x600x256, and with unchained modes, you can do all sorts of cool effects (for instance, my demo works in unchained mode, to achieve the transparency effect). In this series of tutorials, we will study several modes that are more used. In this article, we will go into 320x200x256 unchained, with four pages. This mode is also known as the Chain-4 mode. This tutorial will first tell you what we want to achieve and how we do it, then I'll explain... 9.2. Planes No, no, I ain't gonna talk about those marvels of modern engineering that fly around... Well, from the above explanation, you might be wondering how we are going to access 256Kb of memory, since a segment can only have 64Kb. The answer is planes... In unchained modes, we will still do "Mem[$A000:Pos]:=C" to set a pixel. But now Pos will have a diferent meaning... It will address 4 pixels, instead of one !! Confused ? Don't be... It's simple... For example Mem[$A000:0000] is the position for points (0,0),(1,0),(2,0) and (3,0). But this doesn't mean that we can only draw 4 pixels at a time (although we can... More on this later)... To set individual pixels, we have to set a special register to tell which point are we talking about, or more accuratly, what plane are we talking about. In the above example, point (0,0) is on plane 0, point (1,0) is on plane 1 and so on... Another example is point (4,0). It is access by activing the writing of plane 0, and placing a byte at position $A000:0001. We will soon see how to find out the plane and how to find out the position in memory of any point... 9.3. How ? To achieve an unchained mode, we must program several registers of the video card. So, I'll start. To enter the mode 320x200x256, you just have to enter Mode 13h and then reprogram some registers. I admit that I don't know why do we need to set some of them (if someone knows, I would appreciate an explanation), but we need to... In most of the register setting operation, we need to first tell the video card what index of the register we are going to change. We do that with a "Port[register]:=index" instruction. This is because we have some registers that perform more than one function. In most cases, we will not set new values in the registers, but alter the existing ones. So, the InitChain4 procedure is something like this: Procedure InitChain4; Assembler; Asm 1 Mov Ax,0013h 2 Int 10h 3 4 Mov Dx,03c4h 5 Mov Al,4 6 Out Dx,Al 7 Inc Dx 8 In Al,Dx 9 And Al,11110111b 10 Or Al,00000100b 11 Out Dx,Al 12 Mov Dx,3ceh 13 Mov Al,5 14 Out Dx,Al 15 Inc Dx 16 In Al,Dx 17 And Al,Not 10h 18 Out Dx,Al 19 Dec Dx 20 Mov Al,6 21 Out Dx,Al 22 Inc Dx 23 In Al,Dx 24 And Al,Not 02h 25 Out Dx,Al 26 Mov Dx,3d4h 27 Mov Al,14h 28 Out Dx,Al 29 Inc Dx 30 In Al,Dx 31 And Al,Not 40h 32 Out Dx,Al 33 Dec Dx 34 Mov Al,17h 35 Out Dx,Al 36 Inc Dx 37 In Al,Dx 38 Or Al,40h 39 Out Dx,Al 40 Mov Dx,3d4h 41 Mov Al,13h 42 Out Dx,Al 43 Inc Dx 44 Mov Al,[Size] 45 Out Dx,Al End; "ARGH !! ASSEMBLER !!!", I hear you cry... Why ? Well, because I'll lazy, and I didn't wanted to convert the code from ASM to Pascal... But it is easy to understand the code after I explain it (that's why the code has numbers in the lines)... First, in lines 1/2 we enter in ordinary mode 13h... Then, we proceed into setting the various registers. Between lines 4 and 11 we change the contents of register 03c4h, index 4. I look into some magic book (in my case it is Norton Guides) and I find out the utility of register 03c4h (the sequencer register), index 4: -------------------------------------------------------------------------- Register: 03c4h (Sequencer) Index: 4 (Memory Mode Register) Bit 0: Set if in alphanumeric mode, clear if in graphics mode 1: Set if more video card has more than 64Kb 2: If set, then the Odd/Even addressing is on. This means that all odd bytes will be put in planes 1 and 3, while the even bytes will be put on planes 0 and 2. 3: If bit 1 is set, then it selects video memory planes (256 colours) rather than the Map Mask and Read Map Select registers... -------------------------------------------------------------------------- So, what we are going to do is to reset bit 3 (don't know why... That's line 9 in the code) and we're going to set bit 2 (line 10)... It is clear why do we want Odd/Even addressing, regarding what we know about planes... So, next we are going to tamper with register 03ceh, index 5: -------------------------------------------------------------------------- Register: 03ceh (Graphics) Index: 5 (Mode Register) Bit 0/1: Write Mode: Controls how data from the CPU is transformed before being written to display memory: 0: Mode 0 works as a Read-Modify-Write operation. First a read access loads the data latches of the EGA/VGA with the value in video memory at the addressed location. Then a write access will provide the destination address and the CPU data byte. The data written is modified by the function code in the Data Rotate register (3CEh index 3) a function of the CPU data and the latches, then data is rotated as specified by the same register. 1: Mode 1 is used for video to video transfers. A read access will load the data latches with the content of the addressed byte of video memory. A write access will write the contents of the latches to the addressed byte. Thus a single MOVSB instruction can copy all pixels in the source address byte to the destination address. 2: Mode 2 writes a color to all pixels in the addressed byte of video memory. Bit 0 of the CPU data is written to plane etc. Individual bits can be enabled or disabled through the Bit Mask register (3CEh index 8). 3: Doesn't exist 2: (EGA only): Forces all outputs to a high impedance state if set. 3: Read Mode: 0: Data is read from one of 4 bit planes depending on the Read Map Select Register (3CEh index 4). 1: Data returned is a comparison between the 8 pixels occupying the read byte and the color in the Color Compare Register (3CEh index 2). A bit is set if the color of the corresponding pixel matches the register. 4: Enables Odd/Even mode if set (See 3C4h index 4 bit 2). 5: Enables CGA style 4 color pixels using even/odd bit pairs if set. 6: (VGA only) Enables 256 color mode if set. -------------------------------------------------------------------------- In line 17, we clear bit 4 (we disable Odd/Even mode, don't now why again. Now that I think of it, I don't know why we do most of the stuff... :) But I know I have to do it !). Now, let's continue to tweak register 03ceh, but this time, index 6: -------------------------------------------------------------------------- Register: 03ceh (Graphics) Index: 6 (Miscellaneous Register) Bit 0: Indicates graphics mode if set, alphanumeric mode otherwise. 1: Enables Odd/Even mode if set 2/3: Memory mapping: 0: use A000h-BFFFh 1: use A000h-AFFFh 2: use B000h-B7FFh 3: use B800h-BFFFh -------------------------------------------------------------------------- Notice that for you to shut down Odd/Even mode, you have to change various registers (I wonder why...). So, in line 24, we clear bit 1... We now move on to register 03d4h, index 14h: -------------------------------------------------------------------------- Register: 03d4h (CRTC) Index: 14h (Underline Location Register) Bit 0-4: Position of underline within Character cell. 5: (VGA only) If set memory address is only changed every fourth character clock. 6 (VGA only) Double Word mode addressing if set. -------------------------------------------------------------------------- In line 31 we clear 6, disabling Double Word addressing... Why ? Don't know... I know, I'm a poor teacher... But what can I do ! :) I don't have that much info... Following next comes index 17 of the same register: -------------------------------------------------------------------------- Register: 03d4h (CRTC) Index: 17h (Mode Control Register) Bit 0: If clear use CGA compatible memory addressing system by swapping address bit 0 and 13. Creates 2 banks for even and odd scan lines. 1: If clear use Hercules compatible memory addressing system by swapping address bit 1 and 14. 2: If set increase scan line counter only every second line. 3: If set increase memory address counter only every other character clock. 4: (EGA only) If set disable the EGA output drivers. This bit is used for other purposes in some Super VGA chips. 5: When in Word Mode bit 15 is rotated to bit 0 if this bit is set else bit 13 is rotated into bit 0. 6: If clear system is in word mode. Addresses are rotated 1 position up bringing either bit 13 or 15 into bit 0. 7: Clearing this bit will reset the display system until the bit is set again. -------------------------------------------------------------------------- Line 38 sets bit 6, so the system is not in word mode (I take it is in byte mode, since we have disabled double word mode). Again, I don't know why... "So many question, so few answears... :)"... What a shitty Chain4 tutorial... Anyway, let's move on to one I do know how to move around: -------------------------------------------------------------------------- Register: 03d4h (CRTC) Index: 13h (Offset Register) Bit 0-7: Number of bytes per scanline / K, where K is 2 in double word mode, 4 in word mode and 8 in byte mode. -------------------------------------------------------------------------- In line 44 we are going to ajust the width of the virtual screen. The physical has always (in this unchained mode) 320 pixels. So, if we set the width to be 640, we'll have 2 screen of width per 2 screen of height. If we set the width to be 320, we get 1 screen width and 4 screens height. If we set 1280, we get 4 screens width and 1 screen height. We can also set stuff like 1000, which would get you 3.125 screens width per 1.28 screens of height... So, you must choose the the size... You'll need the size for almost every dealing with unchained modes, so it is pratical to put it in a constant. In the examples, I want a 640x400 virtual screen, so Size is equal to 640/8=80. So now the mode is set... To shut it down, you just have to return to text mode (the procedure is equal to the CloseGraph procedure in the Mode13h unit). Let's move on to... 9.4. Putting down a pixel This operation is slightly slower and more dificult than putting down a pixel in mode 13h, because we lost the linearity of the memory. In mode 13h we had (the number are adresses in memory): 0 319 --------------------- .............. ----------- 0 |0000|0001|0002|0003| .............. |013E|013F| --------------------- .............. ----------- 1 |0140|0141|0142|0143| .............. |027E|027F| --------------------- .............. ----------- ................................................ --------------------- .............. ----------- 199 |F8C0|F8C1|F8C2|F8C3| .............. |F9FE|F9FF| --------------------- .............. ----------- In unchained mode Chain4, we will have something like: 0 1 2 3 4 638 639 -------------------------- .............. ----------- 0 |0000|0000|0000|0000|0001| .............. |009F|009F| -------------------------- .............. ----------- 1 |00A0|00A0|00A0|00A0|00A1| .............. |013F|013F| -------------------------- .............. ----------- ..................................................... -------------------------- .............. ----------- 199 |7C60|7C60|7C60|7C60|7C61| .............. |7CFF|7CFF| -------------------------- .............. ----------- ..................................................... -------------------------- .............. ----------- 399 |F960|F960|F960|F960|F961| .............. |F9AF|F9AF| -------------------------- .............. ----------- See ?! It's a bit more complicated... :) But do not despair. You only need to find out two things: the address in memory on which to write the the plane to which you want to write. Let's start with the plane. As you can easily see, there are groups of four pixels that share the same address. Thinking a bit, you come to the conclusion that Plane:=X Mod 4; The Mod function returns the remainder of the integer division. Now, we must tell the computer to write to that plane. We do that by using register 03c4h, index 2: -------------------------------------------------------------------------- Register: 03c4h (Sequencer) Index: 2h (Map Mask Register) Bit 0: Enables writes to plane 0 if set 1: Enables writes to plane 1 if set 2: Enables writes to plane 2 if set 3: Enables writes to plane 3 if set -------------------------------------------------------------------------- We in the example programs use the PortW array, instead of the Port array. The PortW array is equal to the Port array, with the diference that it puts a word in the register, instead of a byte. So: Port[Register]:=A; Port[Register+1]:=B; will be equal to: PortW[Register]:=(B Shl 8)+A; So, the code to select the plane can be: Plane:=X Mod 4; If Plane=0 Then PortW[$3c4]:=1+2; If Plane=1 Then PortW[$3c4]:=2+2; If Plane=2 Then PortW[$3c4]:=4+2; If Plane=3 Then PortW[$3c4]:=8+2; But this code would be very slow (comparisons are always slow), so it comes the time for clever thinking. Look that the plane mask (1,2,4 and 8) corresponds to the numbers 0,1,2 and 3. So the plane mask is equal to: BitMask:=2^Plane; Or, in computer terms: BitMask:=1 Shl Plane; Nice isn't it ? Shl's are very fast, so it's a good improvement. Now, let's move on to memory calculation... In mode 13h, we used the formula Address:=320*Y+X; We need to find a similar formula for unchained modes. I remember that this stuff is identical in all unchained modes (with one or two structural diferences). Now, we know that we have N groups of four bytes that share the same address per scanline (N=XSize Div 4=Size*8 Div 4=Size*2), so after some minutes of thinking (I'll save you the trouble) we come across the formula: Address:=Y*(Size*2)+(X Div 4); And voila, here we have it... I've read a bunch of unchained modes tutorials when I was preparing this article, and lots of them didn't know where the *2 came from !!! It comes from the fact that Size is the size of the screen dividing by 8, but we have groups of 4 bytes with the same address, so we have to multiply Size by 2 to have the exact number of groups of addresses. The complete putpixel procedure (after some slight optimization): Procedure PutPixel(X,Y:Integer; Color:Byte); Var Pos:Word; BitMask:Byte; Begin Pos:=Y*(Size Shl 1)+(X Div 4); BitMask:=1 Shl (X Mod 4); PortW[$3c4]:=(BitMask Shl 8)+2; Mem[$A000:Pos]:=Color; End; Not too hard, is it ?! :) Now let's move to something that is sometimes required... 9.5. Getting a pixel I hear you say "Well, instead of writing to memory, I read from there !" But it is not like that... Why ? Although the address is the same, the plane selection register is diferent: -------------------------------------------------------------------------- Register: 03ceh (Graphics) Index: 4h (Read Map Select Register) Bit 0-1: Selects plane from where to read -------------------------------------------------------------------------- So, the GetPixel function will be: Function GetPixel(X,Y:Integer):Byte; Var Pos:Word; BitMask:Byte; Begin Pos:=Y*(Size Shl 1)+(X Div 4); BitMask:=X Mod 4; PortW[$3ce]:=(BitMask Shl 8)+4; GetPixel:=Mem[$A000:Pos}; End; Ok... Moving on... 9.6. Changing the viewport The viewport is the part of the unchained screen that you see... Changing the viewport only requires you to write to two registers, and the operation takes only 1/60th of a second !!! It is the fastest page flip there can be ! -------------------------------------------------------------------------- Register: 03d4h (CRTC) Index: Ch (Start Address High Register) Bit 0-7: Upper 8 bits of the start address of the display buffer -------------------------------------------------------------------------- Register: 03d4h (CRTC) Index: Dh (Start Address Low Register) Bit 0-7: Lower 8 bits of the start address of the display buffer -------------------------------------------------------------------------- In simple words, this two registers must have the address of the upper-left corner pixel of the screen... So, if you want the image to be displayed from any pixel of the screen, you must get it's address and put it into this register: Procedure ChangeViewPort(X,Y:Word); Var A:Word; Begin A:=Y*(Size Shl 1)+X; PortW[$3d4]:=(Hi(A) Shl 8)+$C; PortW[$3d4]:=(Lo(A) Shl 8)+$D; End; So, this is it for the basics... You should check out the Chain4-2.Pas program... It has routines to read a 640x400 PCX and scroll around. I'm going to explain the Chain4-1.Pas program... 9.7. The example program First of all, as you may have noticed already, if you set the Map Mask Register to 15, you are enabling the write for ALL planes... The result ? Well, if you haven't tried it already (and you should have :) ) is that in fact you write to the four planes, placing four pixels at a time !!!! This can be great, specially when rendering solid color polys, where when you are making an effect that spends lots of the processor. Doing an effect in a 80x50 buffer and then mapping it to the Chain4 screen is faster than making the a 320x200 effect... It can look worse, but it is many times faster... Now, we come to the a good point... How do we map a 80x50 buffer on the 320x200 screen... Well, after enabling the write to all planes, you just copy the first line of the buffer (80 bytes) to the first, second, third and forth lines of the screen. Notice now that a 320x200 screen has 80x200 bytes of memory allocated to it, so you have to copy four times in the Y axis, to get the correct aspect ratio... We do this in the example program... Now, let's go to plasmas... What is a plasma ? A plasma is... well... er... Check out the example program... That's a plasma... A realtime one (there are other ones)... Cool, isn't it ? This will be shortest plasma tutorial of all time... Maybe one day I'll make a better one... :) But for now, this is enough. To make a Chain4 plasma, you just have to follow some basic steps: 1) Setup a nice palette, that makes good transitions from one colour to other... This is not a rule, but if you don't do this, you'll loose the mellow look, and get a more hardcore effect (check out Colour Blind... There is a plasma there (a not-realtime plasma, in fact) that makes you want to puke because of the setting of colours... :) ) 2) Make the 80x50 buffer (in the example program I used a 80x55 buffer, can't remember why... But it doesn't matter ! :) ) 3) Make and move the plasma 4) Copy the buffer to the screen 5) Goto step 3 'till you puke ! :) Now, doing the plasma is quite simple... A plasma is (by definition) a fractal... Fractals are mathematical entities... When used in the computer sense, a fractal is an image created from an equation... A plasma usually (this isn't a rule) is the sum of sine and cosine waves. In the example program, there are four, with diferent wavelengths, etc, which make the distorted effect you see... Check out the source code and see how it works... It's rather simple, after a couple of hours of code-snooping... I'm fed up with this issue, and it is already about 6 months late, and I've got to finish 2 articles still ! :( -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x- 10. Cool ASM and Pascal tricks I decided to make this article so that you can understand what the strange and criptic code in the Fixed32.Pas unit is all about. This article is mainly on how to use 32 bit code in our old 16 bit Pascal. But first things first. 10.1. Why ?! Well, we need to use tricks to use 32 bit code because the guys at Borland decided that they should dump the greatest language on Earth: Borland Pascal. Now, people who love Pascal above everything else (like me) can do three things: a) Be a traitor and change to a nice 32 bit flat mode C compiler and get into the flow... b) Get a copy of a 32 bit flat mode Pascal compiler. There is one, in shareware, called TMT Pascal. Version 1.0 was recently released and it is available for download in www.tmt.com. This compiler is very promising... Lots of friends of mine are using it. If there are any requests, I'm sure I can make some articles on it, with the help of the number 1 die hard fan of TMT Pascal (don't pass out, Brain Power... :)) c) Learn a bunch of Borland Pascal tricks that really don't cut into it as a dedicated 32 bit Pascal would, but they do most things with a bit more work... Option (b) is the best (for me at least)... But this article is really about (c)... So, onward. 10.2. Full ASM functions You probably have something like this in ye old good Pascal: Function DoStupidStuff(A:Byte):Byte; Begin DoStupidStuff:=A*2+5; End; and you want to convert it to super-duper-fast ASM code... This is very simple. Just know this: (a) When the result of the function is a byte (or shorting), the value that AL stores in the end of the function code will be the result of the function. (b) When the result of the function is a word (or integer), the value that AX stores in the end of the function code will be the result of the function. (c) When the result of the function is a longint, the value that DX:AX stores in the end of the function code will be the result of the function. DX:AX means that the high part of the result must be stored in DX and the low part in AX... In a good 32 bit compiler, this is EAX, instead of DX:AX (I think most 32 bit compilers do it this way). So, the above function would be something like: Function DoStupidStuff(A:Byte):Byte; Assembler; Asm Mov Al,[A] { AL = A } Shl Al,1 { AL = AL*2 } Add Al,5 { AL = AL+5... The result will be in AL, as we wanted } End; This is 32 bit trick number one... Now for number two: 10.3. Using 32 bit registers This one if also fairly easy. This trick enables you to use the 32 bit registers present in the 386 (I think they aren't available on the 286, but I'm not sure), EAX, EBX, ECX and EDX (and it probably enables you to use EDI and ESI, but I never tried it... Never needed them... :). So, how do you do this. Imagine you wanted to do: Mov EAX,5 The code that you need is: DB 66h; Mov AX,5 What is this ? Well, the DB statement makes Pascal's inline assembler compiler (henceforth PIAC) put down the value that follow, in this case 66h. For what ? Well, the designers of the 386 designed the 66h opcode as being the opcode that changed the type of argument acordingly to the memory model the computer was operating in. So, when the computer is operating in 16 bit mode (like Pascal always does), every command (every opcode) that is preceded with the 66h opcode is a 32 bit command, while if the computer operates in 32 bit, the 66h opcode makes the command a 16 bit one. The DB trick can be used for other stuff beside using the extended registers. One of the possible uses is to make a segment override that uses the 386's new segment registers. Example: Db 65h; Mov Ax,[Di] is equal to Mov Ax,Gs:[Di] Db 64h; Mov Ax,[Di] is equal to Mov Ax,Fs:[Di] You can use combinations of the opcodes: Db 64h; Db 66h; Mov Ax,[Di] is equal to Mov Eax,Fs:[Di] But the Db 66h only works for 32 bit commands that have a 16 bit counter- -part... So, we come to another trick, probably the hardest one to describe: 10.4. Using 32 bit instructions One instruction that doesn't have a 16 bit counterpart is Shrd dest,src,count which shifts the register dest, [count] bits to the right. The blank positions in dest are filled with the [count] most significant bits of src. So, imagine you wanted to do the instruction: Shrd EAX,EDX,5 You might think of doing this like: Db 66h; Shr AX,DX,5 But the PIAC will give you an error, because Shr's syntax doesn't allow you to specify where you should get the bits for the blank positions. That really sucks, and it took me LOTS of time to get this working. The solution is to change Shrd EAX,EDX,5 into: Db 66h; Db 0Fh; Db 0ACh; Db 0D0h; Db 05h; "WHAT IS THIS ?", you will probably scream out loud. Simple: it is the translation that any decent 32 bit assembler will do of the Shrd EAX,EDX,5 command. What we do is to find out exactly what sequence of bytes implement the code we want and can't get into Pascal. Then we put it there using the DB statement, that make the PIAC put the code there. This is a very nice trick, yet can be a pain the ass. I recomend Turbo Debug to find out the opcodes you need... So, this is the end of trick 3... Now to the last one, trick 4: 10.5. 32 bit parameters Now, imagine you wanted to do something like this: Function Stuff(A:LongInt):LongInt; Begin Stuff:=A*2; End; You would do something like this in Assembler: Function Stuff(A:LongInt):LongInt; Assembler; Asm Db 66h; Mov Ax,[A] { Mov EAX,[A] } Db 66h; Shl Ax,1 { Shl EAX,1 } { The following code translates something in EAX to something in DX:AX, to export 32 bit data } Db 66h; Mov Dx,Ax { Mov EDX,EAX } Db 66h; Shr Dx,16 { Shrd EDX,8 } End; If you tried to run this, you would get an error... Why ? Because the stupid PIAC doesn't know that the DB 66h statement is converting the AX in the Mov Ax,[A] instruction into EAX. So he thinks that we are trying to put a Longint (32 bit value) into AX (16 bit value), which obviously enough is a no-no... :( So, we have to use a very confusing trick to get around this. We must get the parameters in a diferent way. We'll use the stack. Pascal (as most of languages) push the parameters into the stack. But where in the stack will we get them ? When Pascal (or any other language, including full ASM) call a procedure (or function), IP (and CS if the procedure is defined as far) are pushed into the stack. Then, Pascal pushes into the stack the contents of DI and of DS (or ES... I can never remember which... Try it yourselfs). Finally Pascal pushes the parameters, and this is where we can get them. Notice that Pascal in the end will pop the values back, including the parameters (that's why a variable can't be altered without the VAR clause), so we can't just POP them out ourselfs... We'll have to be cunning, and use the SP and SS registers, which point to the stack. The look of the stack will be something like this (I'll do the scheme for the following procedure): Function Mult(A,B:LongInt):LongInt; [A] 10/11/12/13 [B] 6/ 7/ 8/ 9 DS 4/ 5 DI 2/ 3 SP -> IP 0/ 1 The numbers on the right are the offsets. So, if we wanted DS, we would do Mov Ax,Word Ptr [SS:SP+4]; Just one word of caution. We can't use SP+4 as the offset in the example above. We should use Mov BX,SP in the beggining and then use BX+4, since BX is the only register that enables this kind of addressing. So, the first procedure would look like: Function Stuff(A:LongInt):LongInt; Assembler; Asm Mov Bx,Sp Db 66h; Mov Ax,Word Ptr [SS:Bx+6] { Mov EAX,DWord Ptr [SS:Bx+6] } Db 66h; Shl Ax,1 { Shl EAX,1 } { The following code translates something in EAX to something in DX:AX, to export 32 bit data } Db 66h; Mov Dx,Ax { Mov EDX,EAX } Db 66h; Shr Dx,16 { Shrd EDX,8 } End; This is a very nifty trick, and can lead to endless crashes if not handled properly... But it is very usefull... Well, this is the end of this article. These concepts are a bit confusing in the beggining, but with a little use they become very simple (with the exception of the last, which requires tons of paper so that you know where the hell in the stack are those parameters you need... :) ). If you are serious in 32 bit programming, you should get TMT Pascal... I'll try to really review it in next issue or so. -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x- 11. Hints and Tips * - Begginners tip ** - Medium tip *** - Advanced tip - Incremental calculation method (*) In the previous issue, I told you to exchange the piece of code: For A:=1 To 50 Do Begin For B:=1 To 100 Do Begin C:=A*10+B; WriteLn(C); End; End; for the piece: For A:=1 To 50 Do Begin D:=A*10; For B:=1 To 100 Do Begin C:=D+B; WriteLn(C); End; End; Well, in this case, Viriato reminded me that I could use one of the best optimizations: the incremental calculation method: 0 D:=1; For A:=1 To 50 Do Begin Inc(D,10); For B:=D to D+99 Do Writeln(B) End The ideia of this method is using the last result to calculate the next. For example, imagine you have a sucession of numbers defined like this: U(n)=10*n This defines a sucession like this: 0 10 20 30 40 50 60 70 80 .... Now, you could define this sucession using the anterior result. Look: U(3)= 30 = 20+10 = U(2)+10 U(2)= 20 = 10+10 = U(1)+10 U(1)= 10 = 0+10 = U(0)+10 U(0)= 0 So, you could define the sucession as: | U(0)= 0 | U(n)= U(n-1)+10 And you don't need to use a costly mult... This is used a lot in graphic programming... I use it in the texture mapping article, combining with the interpolation stuff... I called it (at the time) 'discrete interpolation'... - Fast PutPixel (**) Viriato told me a way to optimize the PutPixel rotine even further. I told you that you could do it like this: Procedure PutPixel(X,Y:Word;C:Byte;Too:Word); Assembler; Asm Mov Ax,[Too] Mov Es,Ax Mov Bx,[X] Mov Dx,[Y] Mov Di,Bx Mov Bx,Dx Shl Dx,8 Shl Bx,6 Add Dx,Bx Add Di,Dx Mov Al,[C] Stosb End; The base for Viriato's optimization is the fact that StosB is VERY slow for putting just one pixel, because it takes some time to get itself ready, and after putting down the pixel it increases Di, which for one pixel only, we don't need. Viriato's routine has other minor optimizations, regarding mostly the movement of memory and saving registers: Procedure PutPixel(X,Y:Integer;Color:Byte;Too:Word);Assembler; Asm Mov Es,Too { I thought that I couldn't load Es directly } Mov Bx,Y Mov Di,Bx Shl Bx,6 Shl Di,8 Add Di,X Mov Al,Color Mov Es:[Di+Bx],Al { This is faster than STOSB } End; If anyone has a faster putpixel, tell me all about it ! :)) - Fast Horizontal Line (**) Well, I told you in the previous issue that you could optimize this routine if you could put two pixels at a time, but that would only work with a line that had an even number of pixels. Well, I forgot to mention that you could do this anyway, if you drew the pixels you could two at a time, and then putting down an extra pixel if the number of pixels was odd... But, I didn't say because I thought that the test would take longer than just placing the pixels one at a time (since jumps are so slow). Well, Viriato (who else ?!) told me of a VERY clever way of doing this without needing to jump: Procedure HLine(Y,X1,X2:Word;Color:Byte;Where:Word); Assembler; Asm { Set the starting coordinate and the color both in the high and low part of Ax, since we may draw two pixels at a time } Mov Es,Where Mov Bx,Y Mov Di,Bx Shl Bx,6 Shl Di,8 Add Di,Bx Add Di,X1 Mov Al,Color Mov Ah,Al Mov Cx,X2 Sub Cx,x1 Inc Cx { Cx now holds the number of pixels to draw } Mov Bx,Cx { Save the Cx register } And Cx,1 { If Cx was odd, Cx will be 1, else it will be 0 } Rep StosB { If Cx=0 (that is, Cx was even),then this doesn't do anything } Mov Cx,Bx { Restore Cx } Shr Cx,1 { Divide Cx by 2 } Rep StosW { If Cx=0 (that is, Cx was odd), then this doesn't do anything also. But if it has something, this places two pixels in a row ! } End; This is a very good routine for solid polys... :) Thanks again, Viriato. -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x- 12. Points of View Well, this is the end of another issue of 'The Mag'... Hope you enjoyed this issue, even though it came VERY LATE. But, I had good reasons for the delay... Number one, school... Number two, vacaciones... Number three, my demo Lost Love... And number four, lazyness ! :) Be sure to check out my demo Lost Love... It features mellow effects and music, with the aid of a 21 bit mode to smooth things up... Similar (some stuff is a authentic rippoff) to Luminati, by Tran. It doesn't run as good as I wanted to make it, but some stuff is nice altogether... Moving on, since I'm fed up 'till the tip of my hairs of this issue. Next issue will feature more text adventure tutorial, an article on object oriented programming, an article on Z-buffer and 3d clipping (including the code and explanation of perpective correct texture mapping), an article on how you go about writing your 3d engine (some ideias, related to the object oriented stuff). On the sprite side, we'll have the beggining of the making of a full action game, on the unchained mode tutorials I'm gonna teach you ModeX, ModeY and other neat stuff. Also the followup of the sound article (whatever it's me or Viriato that write it) and an article on mouse control !! I think it will be a nice issue, isn't it ? So, cya in some months ! :))) -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x 13. The adventures of Spellcaster, the rebel programmer of year 2018. Episode 12 - Beyond enemy lines - Well, I guess this is it, Karl... - I said, while I looked towards the monitor... We were getting ready to crack The Gate... Karl already tried it, but he was caught... But he didn't had a massive HexaMind computer, loaded with the most sofisticated program on Earth: a human mind... - Ok, crank it up, Spell... - the cold and electronic voice of my old friend and partner said. I connected the computer to the city's comm system and seconds later, the welcome screen of CompTel appeared in the screen. I've activated the log option of my computer to record everything that happened (well, I've also recorded the voice part of this event, so some stuff weren't actually inserted in the computer... That stuff is in square brackets): Comptel Computer: Hello... I'm Billy 1237... What can I do to help you, sir ? DeathRipper: Access NeuroFrame's main control system CC: Please type in password... [ Spellcaster: Karl, do you know the password ?!?! DR: Yep... I got it from some guy Comptel fired, without knowing that he knew the password... Eheheheh... ] DR: ************** CC: Access granted... Rerouting data to Neuroframe... NeuroFrame: What is your option ? DR: Format NeuroFrame: /U /V:FUCK_YOU /F:0000.1 NF: Ilegal command... What is your option ? DR: FDisk NeuroFrame: NF: Ilegal command... What is your option ? DR: Del *.* /s NF: Ilegal command... You have run out of chances... Central command will be notified of this ilegal access... Tracing call... [ DR: Spell, shut us down, NOW !!!! SC: Sure... ] I pulled the plug of the small terminal and I waited a few seconds before plugging it in again... - Shit, that was close... - DeathRipper said, wipping it's virtual brow. - Yep... Could he have traced use in such short notice ? - No... It was too fast for him... I don't know what happened... They must have upgraded or changed the system from the last time I've been there. Fuck !!! We must found out how to break it... - But how ?! - We must go in there... - WHAT ?!!!! How do you want to do that ?! - Well, you and Excalibur could desguise yourselfs and get a job at Comptel. After some months, tops, you should be able to know what the hell can we do against that fucking beast of a computer ! - No way !!!! That would be suicide... - It's the only way, Spell ! - I won't go there ! - Come on... We've come so far ! - No, no, no and no... - Ok, ok... Talking about Kristie, where's she ? - Er... Well... - Spit it out, Diogo... - the electronic voice said... I swear I could have sensed a bit of curiosity in the voice... - Well, she sort of run away yesterday... - Sort of ?! - I don't know where the hell she is... We've talked and she ran off... - Talked ?! - Well, I kissed her... - Go on... - Karl said, with an amusing sound in his voice. - And she punched me hard... When I got up, she was no longer there... - Well... I probably know were she is... - Karl said, with a sly voice. - Well ?! What are you waiting for telling me ? - I won't tell you. - WHAT ?! WHY ?! - I should I bother telling you if you don't bother doing me a favour ?! - WHAT ?! YOU BLACKMALLING SON OF A BITCH, YOU BIG FAT BUCKET OF SCREWS !!! - Now, now, insults won't get you anywhere... - Fine... Don't tell me... I don't need to know... She doesn't care about me, so I don't care about her ! - Ok... If you say so... Silence fell between us two... I just looked everywhere. I don't know if I was searching for Kristie or if I was looking for something to distract me of it... Finally, I gave up when I remembered that Karl had an infinite ammount of pacience, since he was half machine now. - Ok... You win. Tell me where she is and I'll go to Comptel... - I said with a disgraced voice. - She's in an old windmill outside of town, in the Arabda Natural Park. She always went there when something went wrong... - Ok... - I said, absently, as I turned and climbed into our jetcar. I accelerated in the airway, heading North, towards Arabda Park... See you in the next issue Diogo "SpellCaster" Andrade