+-----------------------------------------------+ | File: doublbuf.txt | | Author: Zip | | Email: zippy_@hotmail.com | | Homepage: http://www.angelfire.com/co/zippy15 | | Date: 10-14-99 | | Description: Tutorial on double buffering in | | screen mode 13h | +-----------------------------------------------+ This document may be freely distributed provided that I am given credit, and that it is unchanged (including this message). Now read! :) +-------+ | Intro | +-------+ If you've been working on a program that requires a lot of colors, but don't want to use SVGA, then you probably use screen mode 13h (320x200x256). It has a half-way decent resolution, plenty of colors, and is fairly easy to program. However, it has one major drawback: it only has one video page. What this means is that you can't use double-buffering in screen mode 13h, at least not with PCOPY. This may not be a problem, until you (or someone using your program) notice that sprites oftenflicker when moved to the upper left corner of the screen, even when you wait for vertical retrace (WAIT &H3DA, 8). Double-buffering is the best way to get rid of this flickering. If you don't know what double-buffering is, it's the process of drawing to an 'invisible' (active) page, then copying its content to the visual page, thereby preventing the user from seeing each object being drawn separately. Although you can't use more than one video page, there is still a way to get around it: use a virtual screen. All this is, is a buffer the size of the screen that you read and write to instead of reading and writing to the screen. Then, when you're ready to draw it all to the screen (like PCOPY, except with our buffer), just draw the entire buffer at once. There is one problem, though, if you try to draw the contents of the buffer one pixel at a time, because it's way too slow to be practical. Therefore, we use a GET/PUT buffer, so we can use QB's 'PUT' command to draw the buffer on screen very quickly. +--------------------+ | Referencing Memory | +--------------------+ Before we actually get started with this, you need to understand something very important: memory. I don't mean just knowing that it's where you store stuff. What I mean is, you need to know how to reference a particular location in memory. Memory is referenced by two numbers, a segment and an offset, written as 'segment:offset'. Think of the address as a street address. The segment would be the street you live on, and the offset would be the house number. For example, if your address is '123 blah st.', then the memory location would be 'blah:123'. Of course, memory is referenced only with numbers, so you wouldn't put 'blah'. Each offset references a single byte of memory, and each segment is 16 offsets, or 16 bytes. A 16 byte block of memory is also sometimes called a 'paragraph'. In this way, you can think of the memory as a book, where the segment is the paragraph, and the offset is the line number. Note that by using this method of addressing memory, there are many ways to reference the same location in memory. For example, 0:26 is the same as 1:10, assuming they're written in decimal notation. Memory addresses, however, are usually written in hexadecimal ('hex'; base 16), which uses characters 0-9 & A-F to represent a number (16 characters to represent a number, hence the name 'base 16'). This way, we can represent a number up to 255 (the maximum value of one byte) using only 2 digits, as opposed to 3 in decimal notation. Base conversion of numbers is beyond the scope of this tutorial, though I may write a tutorial on this subject if there is enough interest. In QB, numbers in hex are preceeded by '&H' to denote hex notation. For example, the number 1A would be represented as &H1A in QB. Note that the '&H' part of the number is usually omitted when talking about a particular memory address so it will be easier to read, but you should remember that it's in hex instead of decimal. Now we can represent the address 40:21 as 41:11 or 42:01. To hopefully help make things a little clearer, I'll use a simple illustration of the first few segments in memory: Segment: 0 1 2 3 Offset: 0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF ^ ^ ^ ^ 0:5 0:13 0:27 0:39 1:03 1:17 1:29 2:07 2:19 3:09 The point of this illustration is to show that there's more than one way to address the same location in memory, and using hex makes it a lot easier to figure out, too, since 10 in hex is 16 in decimal. +-----------------------+ | Setting up the buffer | +-----------------------+ We'll need a buffer the size of the screen (320*200), but since we DIM it as an array of integers, we can dimension it half the size of the screen, because one integer is two bytes. That's (320*200)/2, or 32000. We also need to add 2 bytes for the GET/PUT header, stored at offsets 0 and 1, which contains the width and height of the sprite. And since we'll need this buffer throughout the program, we declare it as SHARED. DIM SHARED BUFFER(32002) AS INTEGER Now we can't just start reading & writing to this array yet, because it still doesn't have the right header. Therefore, we set the right screen mode (so it'll make a valid header for the screen mode we're working with), and use GET to copy the screen (currently blank) to our buffer, which will give us the right header. Note that putting SCREEN 13 in qb automatically clears the screen, so we don't need to use CLS here. SCREEN 13 GET (0, 0)-(319, 199), BUFFER Our buffer is now ready to use. +-----------------------+ | Drawing to the buffer | +-----------------------+ From here on, we won't use any of QB's built-in graphics functions, except PUT, which will copy the contents of our buffer to the screen (like PCOPY in screen mode 7 or 9). The most basic graphics function (and most important), is a pixel-plotting one (like PSET). To keep the syntax simple and similar to QB's built-in function, we'll name our sub BPSET. It will take the same parameters as QB's sub PSET (X, Y, COLOUR). Before we make this sub, however, we have to know how to properly poke to the buffer. You must understand that our buffer, just like any other variable, is just part of memory, and therefore can be referenced with a segment and an offset. Of course, our buffer has 64002 offsets, so we'll have to figure out which one to write to. When poking to the screen in SCREEN 13 without using a buffer, you multiply the Y coordinate by the width of the screen (X resolution), and add the X coordinate. Then you poke the color value to that offset in segment &HA000. OFFSET& = Y * 320& + X DEF SEG = &HA000 POKE OFFSET&, COLOUR DEF SEG We'll use the same concept while poking to our buffer, except that it's in a different segment. Also, the width of the GET/PUT buffer is stored in the first 2 bytes of the buffer as the number of bits across, so we'll have to divide that value by 8 before determining the offset to write to. We get the segment of the buffer using VARSEG(BUFFER(0)). Note that it's important to specify the first element of the array, instead of putting VARSEG(BUFFER), because the buffer will overlap many segments, and we only want to know where it starts. Also, we need to know the offset of where the buffer starts, and we find that using VARPTR(BUFFER(0)). You can specify any element of the array for VARPTR, because we can reference them all in the same segment (because the segments overlap). W& = BUFFER(0) / 8 O& = VARPTR(BUFFER(2)) OFFSET& = (W& * Y& + X&) + O& Here, we use LONGs, because the offset is likely to be greater than the maximum value for an integer (32767), and that would cause an overflow error. In this code, W& is the width of the buffer, O& is the beginning offset of the actual image data (right after the header), and X& & Y& are parameters to the function (the coordinates to draw at). One thing I forgt to mention about overlapping segments and such: an offset can be no larger than 64K, so you'll have to change the segment then instead. Because of this, we won't use only VARSEG(BUFFER(0)) for the segment. Instead, we'll calculate the exact segment to poke to. DEF SEG = VARSEG(BUFFER(0)) + INT(OFFSET& / 16) POKE OFFSET& MOD 16, COLOUR% DEF SEG Here, COLOUR% is a parameter to the function, and what it is should be obvious. I think this section of code is quite simple to figure out, but if not, then here's what it does: INT(OFFSET& / 16) finds the number of paragraphs in the offset, and rounds the number down in order to get the correct segment, and adds it to the segment of the beginning of the buffer (VARSEG(BUFFER(0))). The OFFSET& MOD 16 divides OFFSET& by 16, then returns the remainder, which will be a number from 0 to 15. Though this may be relatively slow, it helps us avoid any overflow errors, which is more important right now (what good is a super-fast program if it messes up a lot?). You can work on speeding it up later. And here's our finished BPSET sub: SUB BPSET (X&, Y&, COLOUR%) W& = BUFFER(0) / 8 O& = VARPTR(BUFFER(2)) OFFSET& = (W& * Y& + X&) + O& DEF SEG = VARSEG(BUFFER(0)) + INT(OFFSET& / 16) POKE OFFSET& MOD 16, COLOUR% DEF SEG END SUB +-------------------------+ | Reading from the buffer | +-------------------------+ Using the same concepts presented above for the BPSET sub, we can make a function to return the color value at a given set of coordinates in our buffer (like QB's POINT function). FUNCTION BPOINT% (X&, Y&) W& = BUFFER(0) / 8 O& = VARPTR(BUFFER(2)) OFFSET& = (W& * Y& + X&) + O& DEF SEG = VARSEG(BUFFER(0)) + INT(OFFSET& / 16) BPOINT% = PEEK(OFFSET& MOD 16) DEF SEG END FUNCTION The only difference here is that we PEEK from the location in the buffer inste ad of POKEing to it. +------------------------------+ | Copying the buffer to screen | +------------------------------+ So far, we can set up and write to our buffer, but one thing is still missing: we can't view the contents of the buffer! This is where QB's PUT command comes in. PUT (0, 0), BUFFER, PSET Of course, that's quite a lot to type every time you want to copy the buffer to screen, so we can make a sub that does this, and resembles QB's PCOPY command. SUB BCOPY PUT (0, 0), BUFFER, PSET END SUB And that's all there is to it. +---------------------+ | Clearing the buffer | +---------------------+ If we couldn't clear the buffer, then it probably wouldn't be of much use to us, because it would still have all the old stuff we drew. To clear the buffer, simply give each element of BUFFER the value 0, then PUT it on the screen. We'll name our sub BCLS to keep it similar to qb's syntax. SUB BCLS FOR A& = 2 TO 32002 BUFFER(A&) = 0 NEXT A& PUT (0, 0), BUFFER, PSET END SUB +------------------------------+ | Making your own line routine | +------------------------------+ If all we could do with our buffer is draw a single pixel at a time, then it wouldn't really be worth using. Therefore, we use our BPSET sub to make more advanced off-screen graphics functions. Another basic graphics function is the line. SUB BLINE (X1%, Y1%, X2%, Y2%, C%) '(X1, Y1) and (X2, Y2) are the coordinates, 'and C% is the color DIM DX AS INTEGER, DY AS INTEGER DIM CX AS INTEGER, CY AS INTEGER, NP AS INTEGER 'DX = delta X (change in X) 'DY = delta Y (change in Y) 'CX = absolute value of DX 'CY = absolute value of DY 'NP = number of pixels between the two points DIM MX AS SINGLE, MY AS SINGLE DIM AX AS SINGLE, AY AS SINGLE 'MX = distance to move on X axis for every pixel 'MY = distance to move on Y axis for every pixel '(-1 <= MX <= 1 and -1 <= MY <= 1) 'AX = actual X position when drawing line 'AY = actual Y position when drawing line DX = X2% - X1% DY = Y2% - Y1% CX = ABS(DX) CY = ABS(DY) IF CX > CY THEN NP = CX ELSE NP = CY END IF MX = DX / NP MY = DY / NP AX = X1% AY = Y1% FOR A = 0 TO NP XX& = AX YY& = AY BPSET XX&, YY&, C% AX = AX + MX AY = AY + MY NEXT A END SUB +------------+ | Conclusion | +------------+ With some thinking, you can make your own GET & PUT functions using this buffer and the basic functions we've already made for using the buffer. You can also make your own CIRCLE routine (quite simple, actually), and who knows what all else. If you know assembly language, then you may speed these functions up even more, but that's beyond the scope of this tutorial. This is just meant to teach you the concepts, and to get you started. The rest is up to you. Below is the complete program made throughout this tutorial, in case you would like to copy it and run it in QB.You are free to use and modify this program as much as you want, provided that I'm given some credit. DECLARE FUNCTION BPOINT% (X&, Y&) DECLARE SUB BLINE (X1%, Y1%, X2%, Y2%, C%) DECLARE SUB BCOPY () DECLARE SUB BPSET (X&, Y&, COLOUR%) DIM SHARED BUFFER(32002) AS INTEGER SCREEN 13 GET (0, 0)-(319, 199), BUFFER BLINE 0, 0, 319, 199, 15 BCOPY SUB BCOPY PUT (0, 0), BUFFER, PSET END SUB SUB BLINE (X1%, Y1%, X2%, Y2%, C%) '(X1, Y1) and (X2, Y2) are the coordinates, 'and C% is the color DIM DX AS INTEGER, DY AS INTEGER DIM CX AS INTEGER, CY AS INTEGER, NP AS INTEGER 'DX = delta X (change in X) 'DY = delta Y (change in Y) 'CX = absolute value of DX 'CY = absolute value of DY 'NP = number of pixels between the two points DIM MX AS SINGLE, MY AS SINGLE DIM AX AS SINGLE, AY AS SINGLE 'MX = distance to move on X axis for every pixel 'MY = distance to move on Y axis for every pixel '(-1 <= MX <= 1 and -1 <= MY <= 1) 'AX = actual X position when drawing line 'AY = actual Y position when drawing line DX = X2% - X1% DY = Y2% - Y1% CX = ABS(DX) CY = ABS(DY) IF CX > CY THEN NP = CX ELSE NP = CY END IF MX = DX / NP MY = DY / NP AX = X1% AY = Y1% 'Note: If you're using screen mode 13h, you can speed this part up if you put 'DEF SEG = &HA000 'then use 'POKE AY * 320 + AX, C% 'instead of using PSET. I only used PSET here so it would work in any graphics 'mode. FOR A = 0 TO NP XX& = AX YY& = AY BPSET XX&, YY&, C% AX = AX + MX AY = AY + MY NEXT A END SUB FUNCTION BPOINT% (X&, Y&) W& = BUFFER(0) / 8 O& = VARPTR(BUFFER(2)) OFFSET& = (W& * Y& + X&) + O& DEF SEG = VARSEG(BUFFER(0)) + INT(OFFSET& / 16) BPOINT% = PEEK(OFFSET& MOD 16) DEF SEG END FUNCTION SUB BPSET (X&, Y&, COLOUR%) W& = BUFFER(0) / 8 O& = VARPTR(BUFFER(2)) OFFSET& = (W& * Y& + X&) + O& DEF SEG = VARSEG(BUFFER(0)) + INT(OFFSET& / 16) POKE OFFSET& MOD 16, COLOUR% DEF SEG END SUB +--------------+ | Contact Info | +--------------+ And that's it for this tutorial. Hopefully this will help someone reduce flicker in their program and perhaps even learn something. :) If you have any questions, comments, etc., you can contact me via email at zippy_@hotmail.com, or on icq at 48620353.