Using EMS in QuickBASIC: Part 3
by Plasma (Jon Ρetrosky)
It’s been a loooooong time
Sorry part 3 took so long to finish...I had a lot of other stuff to do and it sort of got
pushed to the side, especially when the QB Chronicles got discontinued. Anyway here it is!
Review
In part one, we set up a basic EMS interface with QuickBasic. We also created a sample
program, which did nothing more than test the EMS routines and shuffle some memory around. In
part two, we created a simple system to load small wave files and play them back directly from
EMS using the Sound Blaster DMA.
Note: If you missed part one, you should read it before continuing. It's
available in the tutorials section at Phat Code.
It's also published in issue #3 of the QB Chronicles.
Now I’ll show you some other neat things that can be accomplished with EMS: Creating huge
arrays and using EMS as a graphics buffer.
First Up: Arrays
If you’ve ever tried to create an array with more than 32767 elements, you know that it
doesn’t work. This is one of the many limitations of QuickBasic. Another limitation is that
arrays cannot be larger than 64K, unless QuickBasic is started with the /AH switch. However,
using the /AH switch slows down array access speed drastically. And you still can’t create an
array larger than 64K with integer elements, because this would require more than 32767
elements!
But what if you need to create a huge array? One with a million elements? (This question was
recently posted on the board by Qbmaniac.) Well, it can be done with EMS. :)
Array Tracking
There are essentially two pieces of information that are required to keep track of an array:
The first is the element length (how many bytes each is element is). Integer arrays use 2
bytes for each element, while long integer arrays use 4 bytes for each element. String arrays
are a bit different; the elements can be different lengths. To keep things simple, we’ll just
allow arrays with a fixed element length. (You can create string arrays, but the elements will
have to be a fixed length.)
The second is the number of elements. This seems simple, but what about arrays with multiple
dimensions? (i.e., Array(32, 64) ) And if an array is dimensioned with 10 elements, those
elements can be numbered from 0 to 9, 1 to 10, or even something like -4 to 5. To solve these
problems, we’ll just allow single dimension arrays (although multiple dimension arrays can be
simulated, more on this later). Also, the element numbering will start at 1. So in this case, if
there were 10 elements, they would be numbered from 1 to 10.
Since it would be nice to be able have more than one array in EMS, we’ll create a type called
ArrayEMS to hold information about each array. Each array we created will be of this type.
Note: You can find the complete source code for this tutorial in ems3.zip.
DEFINT A-Z
TYPE ArrayEMS 'EMS array information
Handle AS INTEGER 'Handle EMS pages for the array are allocated to
ElementLen AS INTEGER 'Length of each element in bytes (2=int, 4=long, etc.)
NumElements AS LONG 'Number of elements in the array (numbered starting at 1)
END TYPE
Creating the Arrays
To create an array in EMS, you should first define a variable as ArrayEMS type, and then
set the element length and the number of elements that you want in the array.
DIM TestArray AS ArrayEMS 'set up an EMS array called TestArray
TestArray.ElementLen = 2 '2 = integer
TestArray.NumElements = 1000000 'element range = 1 to 1,000,000
This is just the setup; it won't actually create an array in EMS. We need to make a function
to do this ourselves. The easiest way to create an array in EMS is to calculate the needed
memory based on the element length and the number of elements, and then allocate this
amount to a new handle. The disadvantage of this is that each array requires a free handle.
However, this shouldn’t be that big a problem, because I don’t think you’ll be needing 60+ arrays
that are larger than 64K.
To calculate the memory needed, just take the number of elements and multiply it by the
element length. This will give you the amount in bytes:
BytesNeeded& = NumElements * ElementLen
The problem here is that EMS allocates memory in 16 KB pages, not by bytes. So we need to
convert this to pages, which can be done by simply taking the bytes and dividing by
16384. (16 KB = 16384 bytes)
PagesNeeded = BytesNeeded& / 16384 + .5
Now that we have the number of pages needed, we can allocate this memory to an EMS handle.
This is all that is needed to set up the array. Below is a function that does all this:
DECLARE FUNCTION EMS.Array.Dim (Array AS ArrayEMS)
FUNCTION EMS.Array.Dim (Array AS ArrayEMS)
'Allocates the required number of EMS pages for an EMS array. The
'array must be of ArrayEMS type, and the number of elements and
'element length must be set before this function is called.
'
'Array = EMS array to allocate EMS pages for
'
'Returns -1 if successful, or 0 if there was an error.
IF EMS.FreeHandles = 0 THEN
EMS.Array.Dim = 0 'No free handles left!
EXIT FUNCTION
END IF
'Calculate the number of 16K pages needed based on the
'element length and the number of elements.
BytesNeeded& = Array.NumElements * Array.ElementLen
PagesNeeded = BytesNeeded& / 16384 + .5
IF EMS.FreePages < PagesNeeded THEN
EMS.Array.Dim = 0 'Not enough free pages!
EXIT FUNCTION
END IF
Array.Handle = EMS.AllocPages(PagesNeeded) 'Allocate pages for the array
' and save the handle
EMS.Array.Dim = -1 'Return the success code
END FUNCTION
To use this function, just pass your variable of ArrayEMS type:
IF NOT EMS.Array.Dim(TestArray) THEN 'allocate EMS memory for the array
PRINT "Error allocating EMS pages for array."
ELSE
PRINT "EMS pages allocated for array successfully."
END IF
Now the array is ready to go!
Accessing the Arrays
Now that we have an array set up, it would be nice if we could set and get the values of
different elements. After all, that is the purpose of an array... :)
Setting an element is a little tricky, because we have to specify the memory location in the
form of a page, and then an offset. But it’s really not that much different from using segments
and offsets in base memory.
First, we’ll find the absolute location of the element. This is done by subtracting one from
the element number (because we need it to start at 0) and multiplying it by the length of each
element:
Offset& = (Element& - 1) * ElementLen
To find the page, we just divide the absolute offset by 16384, since each page is 16 KB:
Page = Offset& \ 16384
And the offset from that page can be found by multiplying the page by 16384 and subtracting
that number from the absolute offset:
PageOffset = Offset& - (Page * 16384&)
Now that we have the page and the page offset, we can use the EMS.CopyMem routine from part 1
to copy the data from base memory to the location in EMS.
Below is the EMS.Array.Set sub, which sets an element in an EMS array:
DECLARE SUB EMS.Array.Set (Array AS ArrayEMS, Element&, SrcSegment, SrcOffset)
SUB EMS.Array.Set (Array AS ArrayEMS, Element&, SrcSegment, SrcOffset)
'Sets an element in an EMS array. The element number must be in range
'from 1 to the total number of elements in the array. The element value
'will be read from base memory at SrcSegment:SrcOffset.
'
'Array = EMS array to use
'Element& = Element number
'SrcSegment = Segment of buffer in base memory containing element value
'SrcOffset = Offset of buffer in base memory containing element value
IF Element& < 1 OR Element& > Array.NumElements THEN
'Element out of range
EXIT SUB
END IF
'Find the absolute offset of the element
Offset& = (Element& - 1) * Array.ElementLen
'Find which EMS page the element lies on
Page = Offset& \ 16384
'Find the page offset of the element
PageOffset = Offset& - (Page * 16384&)
'Copy the element from base memory to EMS
EMS.CopyMem CLNG(Array.ElementLen), 0, SrcSegment, SrcOffset, Array.Handle, Page, PageOffset
END SUB
You'll notice that this sub uses SrcSegment and SrcOffset instead of a single variable for
the source. The reason for this is that we can't be sure what type the array is, and by
using the segment and offset of the variable, our sub can use any type of variable.
To use the EMS.Array.Set sub, you first put the value you want to store in your array in a
temporary variable. Then you call EMS.Array.Set, using the segment and offset of that temporary
variable as the source:
' put the value of 'a' into element #3542 of the EMS array TestArray
a = 12345
EMS.Array.Set TestArray, 3542, VARSEG(a), VARPTR(a)
Once we’ve set an element, we’d probably like to be able to read it back again. The same
approach can be used as setting the element; all that is needed is to copy the data from EMS to
base memory, instead of from base memory to EMS:
DECLARE SUB EMS.Array.Get (Array AS ArrayEMS, Element&, DstSegment, DstOffset)
SUB EMS.Array.Get (Array AS ArrayEMS, Element&, DstSegment, DstOffset)
'Gets an element stored in an EMS array. The element number must be in
'range from 1 to the total number of elements in the array. The element
'value will be placed into base memory at DstSegment:DstOffset.
'
'Array = EMS array to use
'Element& = Element number
'DstSegment = Segment of buffer in base memory to hold returned element value
'DstOffset = Offset of buffer in base memory to hold returned element value
IF Element& < 1 OR Element& > Array.NumElements THEN
'Element out of range
EXIT SUB
END IF
'Find the absolute offset of the element
Offset& = (Element& - 1) * Array.ElementLen
'Find which EMS page the element lies on
Page = Offset& \ 16384
'Find the page offset of the element
PageOffset = Offset& - (Page * 16384&)
'Copy the element from EMS to base memory
EMS.CopyMem CLNG(Array.ElementLen), Array.Handle, Page, PageOffset, 0, DstSegment, DstOffset
END SUB
Reading back an element is similar to setting an element:
' get the value element #3542 of the EMS array TestArray and store it in 'a'
a = 0 'this line isn't necessary; it's only here to prove that the array is actually storing data ;)
EMS.Array.Get TestArray, 3542, VARSEG(a), VARPTR(a)
And if you want to swap two elements, that can easily be achieved also, with the EMS.ExchMem
routine:
DECLARE SUB EMS.Array.Swap (Array AS ArrayEMS, Element1&, Element2&)
SUB EMS.Array.Swap (Array AS ArrayEMS, Element1&, Element2&)
'Swaps two elements in an EMS array. The element number must be in range
'from 1 to the total number of elements in the array. The elements must
'be in the same array.
'
'Array = EMS array to use
'Element1& = First element number
'Element2& = Second element number
IF Element1& < 1 OR Element2& < 1 OR Element1& > Array.NumElements OR Element2& > Array.NumElements THEN
'Element out of range
EXIT SUB
END IF
'Find the absolute offset of element #1
Offset1& = (Element1& - 1) * Array.ElementLen
'Find which EMS page element #1 lies on
Page1 = Offset1& \ 16384
'Find the page offset of element #1
Page1Offset = Offset1& - (Page1 * 16384&)
'Find the absolute offset of element #2
Offset2& = (Element2& - 1) * Array.ElementLen
'Find which EMS page element #2 lies on
Page2 = Offset2& \ 16384
'Find the page offset of element #2
Page2Offset = Offset2& - (Page2 * 16384&)
'Swap the elements
EMS.ExchMem CLNG(Array.ElementLen), Array.Handle, Page1, Page1Offset, Array.Handle, Page2, Page2Offset
END SUB
Swapping two elements is straightforward, as well:
' Swap element #3542 with element #78293 in the EMS array TestArray
EMS.Array.Swap TestArray, 3542, 78293
Erasing Arrays
All that's left now is to erase the array when you’re finished with it, to release the EMS
memory it is using:
DECLARE SUB EMS.Array.Erase (Array AS ArrayEMS)
SUB EMS.Array.Erase (Array AS ArrayEMS)
'Deallocates EMS pages used by the specified EMS array.
'
'Array = EMS array to use
EMS.DeallocPages Array.Handle
END SUB
Again, using this should be a no-brainer :)
EMS.Array.Erase TestArray
Simulating Multidimensional Arrays
The array routines we have developed work fine for arrays with a single dimension, but what
if you wanted an array with 2 or more dimensions? A good example of this is a map for a tile-based
game. Most likely this type of array will have 2 dimensions, something like Map(x, y).
To simulate this type of array, you first have to decide how many elements each dimension
has. Using our map example, we'll give the first dimension (x) 400 elements, and we'll give the
second dimension (y) 600 elements. When you dimension your array, use the number of elements
in the first dimension times the number of elements in your second dimension:
DIM MultiArray AS ArrayEMS 'set up an EMS array called MultiArray
MultiArray.ElementLen = 2 '2 = integer
MultiArray.NumElements = 400& * 600 '(1 to 400, 1 to 600)
Status = EMS.Array.Dim(MultiArray) 'allocate EMS memory for the array
Next, you use this general formula to find the "real" element to pass to the EMS array routines:
RealElement& = TotalElementsFirstDimension& * (ElementSecondDimension - 1) + ElementFirstDimension
Now you're probably saying "what?!", so I'll give an example...here is how you would access
Map(274, 421):
' get the element at (274, 421)
RealElement& = 400& * (421 - 1) + 274
EMS.Array.Get MultiArray, RealElement&, VARSEG(variable), VARPTR(variable)
PRINT variable
That's all there is to it! Now that we've tackled the challenge of storing huge arrays in EMS,
we'll move onto some "fun stuff"...
Next: Graphics
It seems that the most widely used screen mode in QuickBasic is screen 13, and for a good
reason. It is the only QuickBasic screen mode with 256 colors. (Although ModeX and SVGA
modes can also provide 256 colors, they are not as easy to program for.)
|
Figure 3-1: A typical PC memory map |
Screen 13's 320x200 resolution is low, but there’s a nice bonus to this: The amount
of memory required for one screen is exactly 64000 bytes, which fits nicely into 64K.
(hint-hint...the EMS page frame is 64K...hint-hint)
The downside to screen 13 is that it provides only one graphics page, which makes it
very difficult to do flickerless animation. I know what you’re thinking, and you’re
right...we can get around this with EMS! :)
Referring to the PC memory map from part one, we can see that the VGA buffer is
located at segment A000, offset 0000, and is 64K. The EMS page frame is also 64K, which
means that it can easily be used as double buffer. Instead of writing to the VGA buffer,
we write to the page frame instead. Then we can copy the contents of the page frame over
to the VGA buffer, to produce flickerless animation.
There has to be a catch, right? Well, unfortunately, there is. In screen 13,
QuickBasic will only use A000 as the video segment, and provides no easy way to change
this.* This means that none of QB’s graphics statements can be used to draw to the
double buffer, and we’ll have to write our own routines. Luckily, it’s not that hard.
* It is possible to change the active drawing segment so that QB draws to a different
location in memory, but the method is a bit unorthodox...I think I'll save this approach
for another tutorial... ;)
We Don't Need No Stinking PSET
As many of you probably already know, you can set pixels in screen 13 by using POKE instead
of PSET. The advantage of this is that it is faster than PSET, and you can specify the segment
that you want to use with DEF SEG. In our case, we can set the segment to the EMS page frame and
draw the pixels directly to EMS.
If you've always used PSET and don't know what I'm talking about, I'll explain... In screen
13, the screen is arranged in a linear bitmap. This means that any pixel can be set by just
setting the segment to A000, and then using POKE to set the pixel:
POKE y * 320& + x, pixel
Reading a pixel is exactly the same, except PEEK is used:
pixel = PEEK(y * 320& + x)
In the above example, if the segment was set to A000, the pixel read/writes would have taken
place in the video buffer (what you see on the screen). However, if the segment was set to the
EMS page frame, the pixel read/writes would have taken place in a buffer in EMS, invisible to the
user.
Buffers Here, Buffers There
The only problem now is buffer management. It would be nice if we could have multiple
graphics buffers in EMS, but we need a way to keep track of them. We can do this with a few
shared variables:
DEFINT A-Z
DIM SHARED Gfx.Handle 'EMS handle allocated to pages for the graphics buffers
DIM SHARED Gfx.NumBuffers 'Total number of graphics buffers allocated
DIM SHARED Gfx.PageFrame 'EMS page frame segment (for quick access)
DIM SHARED Gfx.LastMapped 'First page of the four pages that were last
' mapped to the page frame
DIM SHARED Gfx.TextSeg 'Segment of 8x8 ROM font
DIM SHARED Gfx.TextOff 'Offset of 8x8 ROM font
Gfx.Handle holds the EMS handle the pages for the buffers are allocated to, and Gfx.NumBuffers
is the number of graphics buffers that have been allocated. Gfx.PageFrame holds the segment of
the EMS page frame. We are storing this in a variable instead of using the EMS.PageFrame
function because it will be much faster than asking for the page frame each time. Gfx.LastMapped
is the first page of the four pages that were last mapped to the page frame. Again, this is used
for speed, so we don’t needlessly remap pages that are already mapped. Finally, Gfx.TextSeg and
Gfx.TextOff are the segment and offset of the 8x8 ROM font in memory. We will use this font to
draw text. (Because PRINT won’t work with our EMS graphics buffers either!)
Setup
We’ll need a way for the programmer to set up these screen buffers. This can be done with one
function. All this function has to do is find the number of EMS pages needed, allocate them,
and then save the handle and the number of buffers available.
The number of EMS pages needed can be found by taking the number of buffers requested and
multiplying it by 4:
PagesNeeded = NumBuffers * 4
(Each buffer needs to be 64 K, and each page is 16 K.)
This function will also take care of a few initialization tasks: getting the page frame
address and storing it for later use, and finding the 8x8 ROM font location via interrupt 10h.
DECLARE FUNCTION Gfx.Alloc (NumBuffers)
FUNCTION Gfx.Alloc (NumBuffers)
'Allocates the required number of EMS pages for the number of graphics
'buffers specified. Also stores the location of the EMS page frame and the
'8x8 ROM font for later use.
'
'NumBuffers = Number of graphics buffers to allocate EMS pages for
'
'Returns -1 if successful, or 0 if there was an error.
PagesNeeded = NumBuffers * 4 'Each buffer needs 64K, and pages are 16K each
IF EMS.FreePages < PagesNeeded THEN
'Not enough pages
Gfx.Alloc = 0
EXIT FUNCTION
END IF
Gfx.Handle = EMS.AllocPages(PagesNeeded) 'Allocate pages and save the handle
Gfx.NumBuffers = NumBuffers 'Save the number of buffers to prevent
' a non-existent buffer from being used
Gfx.PageFrame = EMS.PageFrame 'Save the page frame segment for
' quicker access
Gfx.LastMapped = -1 'Assume no pages have been mapped
Regs.ax = &H1130 'Get the segment and offset of the 8x8
Regs.bx = &H300 ' ROM font and store it for later use
InterruptX &H10, Regs, Regs
Gfx.TextSeg = Regs.es
Gfx.TextOff = Regs.bp
FOR Buffer = 1 TO NumBuffers 'Clear the newly allocated buffers to
Gfx.Cls Buffer ' get rid of any "garbage"
NEXT
Gfx.Alloc = -1 'Return success status code
END FUNCTION
You’ll notice that the function also clears each buffer. (Using a sub that we haven’t written
yet...I must be psychic :) ). This is needed because EMS pages aren’t necessarily "blank" after
they have been allocated; they can contain data from previous programs that have used them. So
the function automatically clears them off to get rid of any junk that may be there.
I guess we’d better create the Gfx.Cls routine now, since we already used it...
This routine is a little tricky, because there is no EMS function to "clear" pages, and QB
can only write to memory 1 byte at a time. (Clearing the pages this way would be way too slow!)
We can get around this by creating a blank 16K array (an integer array with 8192 elements, 2
bytes each), and then using our EMS.CopyMem sub to copy this "blankness" to each of the 4 EMS
pages in the buffer that we want to clear. ("Blankness"? And this was supposed to be a technical
tutorial...)
DECLARE SUB Gfx.Cls (Buffer)
SUB Gfx.Cls (Buffer)
'Clears the specified graphics buffer.
'
'Buffer = Graphics buffer to clear (Use 0 for the video buffer)
IF Buffer = 0 THEN
CLS 'That was easy ;)
ELSE
Page = (Buffer - 1) * 4 'Find the starting EMS page of the buffer
DIM Blank(8192) 'Create a null 16K buffer
'Copy this 16K buffer to each of the 4 pages in the graphics buffer
EMS.CopyMem 16384, 0, VARSEG(Blank(0)), VARPTR(Blank(0)), Gfx.Handle, Page, 0
EMS.CopyMem 16384, 0, VARSEG(Blank(0)), VARPTR(Blank(0)), Gfx.Handle, Page + 1, 0
EMS.CopyMem 16384, 0, VARSEG(Blank(0)), VARPTR(Blank(0)), Gfx.Handle, Page + 2, 0
EMS.CopyMem 16384, 0, VARSEG(Blank(0)), VARPTR(Blank(0)), Gfx.Handle, Page + 3, 0
ERASE Blank 'Nuke the 16K buffer
END IF
END SUB
Draw Them Pixels
Next we’ll create the basic Gfx.Pset and Gfx.Point subs. These are based on the general
method of using PEEK and POKE to set pixels, as described previously. The only interesting
part about these subs is probably setting the segment.
If the programmer specifies 0 as the graphics buffer to use, we'll assume that they want to
use the video buffer and not an EMS graphics buffer. If they specify a graphics buffer, we
need to first check and see if the pages for this buffer are already mapped. If so, we can save
some time by not mapping them again. If not, then they must be mapped. No matter which EMS
graphics buffer is being used, the segment is always that of the page frame.
DECLARE SUB Gfx.Pset (Buffer, x, y, Colr)
DECLARE FUNCTION Gfx.Point (Buffer, x, y)
SUB Gfx.Pset (Buffer, x, y, Colr)
'Sets the pixel at (x, y) on the specified EMS graphics buffer to the
'specified color.
'
'Buffer = Graphics buffer to set pixel on (Use 0 for the video buffer)
'x = X coordinate (0-319)
'y = Y coordinate (0-199)
'Colr = Pixel color (0-255)
IF Buffer = 0 THEN 'Is the graphics buffer the video buffer?
DEF SEG = &HA000 'If so, use A000 for the segment
ELSE
DEF SEG = Gfx.PageFrame 'If not, use the EMS page frame as the segment
Page = (Buffer - 1) * 4 'Find the starting page that needs to be
' mapped to the page frame.
IF Gfx.LastMapped <> Page THEN 'If this page isn't already
EMS.MapXPages 0, Page, 4, Gfx.Handle ' mapped, then assume we have
' map all the pages for the
' specified buffer.
Gfx.LastMapped = Page 'Save the newly mapped page
END IF
END IF
POKE y * 320& + x, Colr 'Set the pixel at (x, y) to Colr
END SUB
FUNCTION Gfx.Point (Buffer, x, y)
'Returns the color of the pixel at (x, y) on the specified EMS graphics
'buffer.
'
'Buffer = Graphics buffer to read pixel from (Use 0 for the video buffer)
'x = X coordinate (0-319)
'y = Y coordinate (0-199)
'
'Returns: Pixel color (0-255)
IF Buffer = 0 THEN 'Is the graphics buffer the video buffer?
DEF SEG = &HA000 'If so, use A000 for the segment
ELSE
DEF SEG = Gfx.PageFrame 'If not, use the EMS page frame as the segment
Page = (Buffer - 1) * 4 'Find the starting page that needs to be
' mapped to the page frame.
IF Gfx.LastMapped <> Page THEN 'If this page isn't already
EMS.MapXPages 0, Page, 4, Gfx.Handle ' mapped, then assume we have
' map all the pages for the
' specified buffer.
Gfx.LastMapped = Page 'Save the newly mapped page
END IF
END IF
Gfx.Point = PEEK(y * 320& + x) 'Read the pixel at (x, y)
END FUNCTION
With the methods used in these two subs, you should be able to create any type of graphics
routines you want...lines, circles, get/put, etc. I’m not going to explain them in detail here,
however. (Oh sure, just leave you hanging, right? :) Ok, ok, the source code included with this
tutorial has extra graphics routines, if you want to use those instead of creating your own. But
after all, this is an EMS tutorial, not a graphics primitives tutorial...)
Note: If you would like to add clipping to any of the included graphics routines, all you
have to do is check to make sure that each pixel drawn will be within the limits of the screen:
(0, 0)-(319, 199). However, this slows it down a little, so right now the only routine that uses
clipping is my Gfx.Put sub.
You should also be aware that none of the graphics routines have error checking; you could
easily specify a non-existent graphics buffer. If you would like to implement error checking,
all that is needed is to check to make sure the buffer specified is not greater than Gfx.NumBuffer.
Print "Hello World"
There is one more graphics routine I’ll walk you through: printing text on a graphics buffer.
(Reading the ROM font from memory can be a challenge if you’ve never done it before. Everybody
who *has* done it can stop laughing at the n00bies now.)
The first step is to get the segment and offset of the ROM font, which we already did in
our Gfx.Alloc sub. This location in memory contains font data that can be used to draw 8x8
characters. To save space, each pixel in the font is represented by one bit. Since there are
8 bits in one byte, only one byte is needed for a row of pixels in a character. However, since
there are 8 rows, a total of 8 bytes are needed for each character. There are 256 characters
total, arranged in order from 0 to 255.
To "print" a string using the ROM font, we need to step through the string, one character
at a time. For each character in the string, we find the offset of the font character, and then
read the font one row at a time, expanding the bits into separate pixels, which are placed into
the graphics buffer. After all the rows of the character have been drawn, we move on to the
next character in the string, and repeat this process until every character has been drawn.
DECLARE SUB Gfx.Print (Buffer, x, y, Text$, Colr)
SUB Gfx.Print (Buffer, x, y, Text$, Colr)
'"Prints" text on the EMS graphics buffer, starting at position (x, y) and
'using the specified color.
'
'Buffer = Graphics buffer to print text on (Use 0 for the video buffer)
'x = X coordinate (0-319)
'y = Y coordinate (0-199)
'Text$ = Text string to print
'Colr = Text foreground color (0-255)
'
'Note: The X and Y coordinates are true pixel coordinates, not "rows" and
' "columns" like QB uses. Also, clipping is not supported, so take
' care not to exceed the bounds of the screen.
IF Buffer = 0 THEN 'Is the graphics buffer the video buffer?
BufferSeg = &HA000 'If so, use A000 for the segment
ELSE
BufferSeg = Gfx.PageFrame 'If not, use the EMS page frame as the segment
Page = (Buffer - 1) * 4 'Find the starting page that needs to be
' mapped to the page frame.
IF Gfx.LastMapped <> Page THEN 'If this page isn't already
EMS.MapXPages 0, Page, 4, Gfx.Handle ' mapped, then assume we have
' map all the pages for the
' specified buffer.
Gfx.LastMapped = Page 'Save the newly mapped page
END IF
END IF
StartX = x 'Save the starting X and Y coordinates
StartY = y ' because they will be modified
FOR i = 1 TO LEN(Text$) 'Step through the string, one character at a time
Char = ASC(MID$(Text$, i, 1)) 'Get the ASCII code of the current char
FOR j = Gfx.TextOff + Char * 8 TO Gfx.TextOff + 7 + Char * 8
'Get the font character data, one row at a time
DEF SEG = Gfx.TextSeg
Bits = PEEK(j)
'Draw the font character onto the graphics buffer
DEF SEG = BufferSeg
'Find the starting offset of this row in the graphics buffer
Offset& = StartY * 320& + StartX
'Check to see which bits are set, and only draw pixels whose
' corresponding bits are set
IF Bits AND 128 THEN POKE Offset&, Colr
IF Bits AND 64 THEN POKE Offset& + 1, Colr
IF Bits AND 32 THEN POKE Offset& + 2, Colr
IF Bits AND 16 THEN POKE Offset& + 3, Colr
IF Bits AND 8 THEN POKE Offset& + 4, Colr
IF Bits AND 4 THEN POKE Offset& + 5, Colr
IF Bits AND 2 THEN POKE Offset& + 6, Colr
IF Bits AND 1 THEN POKE Offset& + 7, Colr
StartY = StartY + 1 'Move to the next row
NEXT
StartX = StartX + 8 'Move to the next character
StartY = y 'Reset the row
NEXT
END SUB
Copying Buffers
Drawing to hidden buffers in EMS is pretty useless if you can't ever see what's on the
buffer...at some time, you'll probably want to copy a buffer from EMS to the video buffer (or
even another EMS buffer). Using EMS.CopyMem, making a sub to do this is a piece of cake:
DECLARE SUB Gfx.Pcopy (FromBuffer, ToBuffer)
SUB Gfx.Pcopy (FromBuffer, ToBuffer)
'Copies one EMS graphics buffer to another EMS graphics buffer.
'
'FromBuffer = Graphics buffer to copy from (Use 0 for the video buffer)
'ToBuffer = Graphics buffer to copy to (Use 0 for the video buffer)
IF FromBuffer = 0 THEN 'Is the source graphics buffer the video buffer?
SrcHandle = 0 'If so, use 0 for the handle
SrcSegment = &HA000 ' and A000 for the segment
ELSE
SrcHandle = Gfx.Handle 'If not, use the EMS handle
SrcSegment = (FromBuffer - 1) * 4 ' and the starting page
END IF
IF ToBuffer = 0 THEN 'Is the source graphics buffer the video buffer?
DstHandle = 0 'If so, use 0 for the handle
DstSegment = &HA000 ' and A000 for the segment
ELSE
DstHandle = Gfx.Handle 'If not, use the EMS handle
DstSegment = (ToBuffer - 1) * 4 ' and the starting page
END IF
'Copy 64000 bytes, from the source buffer to the destination buffer
EMS.CopyMem 64000, SrcHandle, SrcSegment, 0, DstHandle, DstSegment, 0
END SUB
Deallocating the Graphics Buffers
Finally, before the program ends, we need to deallocate the graphics buffers to release the
EMS pages. This can be done with a single call to EMS.DeallocPages, passing the graphics handle.
DECLARE SUB Gfx.Dealloc ()
SUB Gfx.Dealloc
'Deallocates EMS pages used by the graphics buffers.
EMS.DeallocPages Gfx.Handle
END SUB
Yet Another Sample Program
Below is the final sample program of the EMS tutorial series. It demonstrates using our EMS
array routines to create and access a huge 1,000,000 element integer array. (which takes almost 2 MB!)
It also uses our new graphics routines to animate some balls on the screen, using EMS as
a double buffer to eliminate flicker.
RANDOMIZE TIMER
CLS
IF NOT EMS.Init THEN
PRINT "No EMM detected."
END
END IF
COLOR 14, 1
PRINT SPACE$(22); "Using EMS in QuickBasic: Part 3 of 3"; SPACE$(22)
COLOR 15, 0
PRINT STRING$(31, 196); " EMS Information "; STRING$(32, 196)
COLOR 7
PRINT "EMM Version: "; EMS.Version$
IF EMS.Version$ < "4.0" THEN
PRINT
PRINT "EMM 4.0 or later must be present to use some of the EMS functions."
END
END IF
PRINT "Page frame at: "; HEX$(EMS.PageFrame); "h"
PRINT "Free handles:"; EMS.FreeHandles
IF EMS.FreeHandles = 0 THEN
PRINT
PRINT "You need at least one free handle to run this demo."
END
END IF
PRINT "Total EMS:"; EMS.TotalPages; "pages /"; EMS.TotalPages * 16&; "KB /"; EMS.TotalPages \ 64; "MB"
PRINT "Free EMS:"; EMS.FreePages; "pages /"; EMS.FreePages * 16&; "KB /"; EMS.FreePages \ 64; "MB"
IF EMS.FreePages < 128 THEN
PRINT
PRINT "You need at least 128 pages (2 MB) free EMS to run this demo."
END
END IF
PRINT
COLOR 15
PRINT STRING$(32, 196); " EMS Array Test "; STRING$(32, 196)
COLOR 7
PRINT "Creating an integer EMS array with 1,000,000 elements...";
DIM TestArray AS ArrayEMS
TestArray.ElementLen = 2
TestArray.NumElements = 1000000
IF NOT EMS.Array.Dim(TestArray) THEN
PRINT
PRINT "Error allocating EMS pages for the array."
END
END IF
PRINT "ok!"
FOR Test = 1 TO 6
Element& = INT(RND(1) * 1000000) + 1
Value = INT(RND(1) * 32768)
PRINT "Setting element #"; Element&; "to"; Value; "... ";
EMS.Array.Set TestArray, Element&, VARSEG(Value), VARPTR(Value)
PRINT "ok!"
Value = 0
PRINT "Getting element #"; Element&; "... ";
EMS.Array.Get TestArray, Element&, VARSEG(Value), VARPTR(Value)
PRINT "returned:"; Value
NEXT
PRINT "Erasing EMS array...";
EMS.Array.Erase TestArray
PRINT "ok!"
LOCATE 25, 19
COLOR 31
PRINT "Press any key to test EMS graphics functions";
KeyPress$ = INPUT$(1)
IF NOT Gfx.Alloc(3) THEN
CLS
PRINT "Error allocating graphics buffers."
END
END IF
DIM BallPos(1000, 3)
FOR i = 1 TO 1000
BallPos(i, 1) = INT(RND(1) * 320)
BallPos(i, 2) = INT(RND(1) * 200)
BallPos(i, 3) = INT(RND(1) * 4)
NEXT
SCREEN 13
FOR y = 0 TO 15
FOR x = 0 TO 15
READ Pixel
Gfx.Pset 1, x, y, Pixel
NEXT
NEXT
DIM BallImage(129)
Gfx.Get 1, 0, 0, 15, 15, VARSEG(BallImage(0)), VARPTR(BallImage(0))
FOR x = 0 TO 319
Gfx.Line 2, x, 0, x, 199, x MOD 255
NEXT
FOR x = 0 TO 319
Gfx.Line 3, x, 0, x, 199, (255 - x) MOD 255
NEXT
FOR Buffer = 2 TO 3
FOR ShiftY = -1 TO 1
FOR ShiftX = -1 TO 1
Gfx.Print Buffer, 5 + ShiftX, 169 + ShiftY, "Press + to add balls, - to remove balls", 0
Gfx.Print Buffer, 5 + ShiftX, 179 + ShiftY, "Press ENTER to toggle backgrounds", 0
Gfx.Print Buffer, 5 + ShiftX, 189 + ShiftY, "Press SPACE to toggle Vsync, ESC quits", 0
NEXT
NEXT
Gfx.Print Buffer, 5, 169, "Press + to add balls, - to remove balls", 15
Gfx.Print Buffer, 5, 179, "Press ENTER to toggle backgrounds", 15
Gfx.Print Buffer, 5, 189, "Press SPACE to toggle Vsync, ESC quits", 15
NEXT
NumBalls = 10
Background = 2
Vsync = 0
DO
KeyPress$ = INKEY$
IF KeyPress$ <> "" THEN
SELECT CASE KeyPress$
CASE CHR$(27)
EXIT DO
CASE CHR$(13)
IF Background = 2 THEN
Background = 3
ELSE
Background = 2
END IF
CASE " "
IF Vsync THEN
Vsync = 0
ELSE
Vsync = 1
END IF
CASE "+"
IF NumBalls < 1000 THEN
NumBalls = NumBalls + 1
END IF
CASE "-"
IF NumBalls > 1 THEN
NumBalls = NumBalls - 1
END IF
END SELECT
END IF
Gfx.Pcopy Background, 1
FOR i = 1 TO NumBalls
PosX = BallPos(i, 1)
PosY = BallPos(i, 2)
Direction = BallPos(i, 3)
Gfx.Put 1, PosX, PosY, 0, VARSEG(BallImage(0)), VARPTR(BallImage(0))
NewDir = Direction
SELECT CASE Direction
CASE 0 ' up and to the left
PosX = PosX - 1
IF PosX < -16 THEN NewDir = 1
PosY = PosY - 1
IF PosY < -16 THEN NewDir = 3
CASE 1 ' up and to the right
PosX = PosX + 1
IF PosX > 320 THEN NewDir = 0
PosY = PosY - 1
IF PosY < -16 THEN NewDir = 2
CASE 2 ' down and to the right
PosX = PosX + 1
IF PosX > 320 THEN NewDir = 3
PosY = PosY + 1
IF PosY > 200 THEN NewDir = 1
CASE 3 ' down and to the left
PosX = PosX - 1
IF PosX < -16 THEN NewDir = 2
PosY = PosY + 1
IF PosY > 200 THEN NewDir = 0
END SELECT
BallPos(i, 1) = PosX
BallPos(i, 2) = PosY
IF NewDir <> Direction THEN
BallPos(i, 3) = NewDir
END IF
NEXT
IF Vsync THEN
WAIT &H3DA, 8, 8
WAIT &H3DA, 8
END IF
Gfx.Pcopy 1, 0
LOOP
Gfx.Dealloc
SCREEN 0
WIDTH 80, 25
END
' Sprite image for the ball
DATA 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
DATA 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
DATA 0, 0, 0, 0, 0, 0, 4, 4, 4, 4, 4, 0, 0, 0, 0, 0
DATA 0, 0, 0, 0, 4, 4, 4, 4, 4, 4, 4, 4, 4, 0, 0, 0
DATA 0, 0, 0, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 0, 0
DATA 0, 0, 4, 4, 4, 4, 4, 4, 4,40,40,40, 4, 4, 4, 0
DATA 0, 4, 4, 4, 4, 4, 4, 4,40,40,40,40,40, 4, 4, 4
DATA 0, 4, 4, 4, 4, 4, 4, 4,40,40,40,40,40, 4, 4, 4
DATA 0, 4, 4, 4, 4, 4, 4, 4,40,40,40,40,40, 4, 4, 4
DATA 0, 4, 4, 4, 4, 4, 4, 4, 4,40,40,40, 4, 4, 4, 4
DATA 0, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4
DATA 0, 0, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 0
DATA 0, 0, 0, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 0, 0
DATA 0, 0, 0, 0, 4, 4, 4, 4, 4, 4, 4, 4, 4, 0, 0, 0
DATA 0, 0, 0, 0, 0, 0, 4, 4, 4, 4, 4, 0, 0, 0, 0, 0
DATA 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
Thus ends the EMS tutorial series...
There are plenty of other uses for EMS; I’ve just touched the surface to show some examples
of what can be done. It’s really not that hard to use now that you know the basic concepts, and
you should be able to use it for just about anything you want!
Keep the QB scene alive...
-Plasma
If you have any questions or comments about this article, or found an error, please
email me or post a message on the Phat Code forum.
(Thanks to Joakim for pointing out my /AH and long int=4 errors!)
|