phat code If you hear an onion ring, answer it.
Main

Projects

Downloads

Articles

Links

Articles

Memory

search for in   Tutorials / References
 

Using EMS in QuickBASIC: Part 3

AuthorPlasma (Jon Ρetrosky)
Emailplasma@phatcode.net
Websitehttp://www.phatcode.net/
ReleasedJul 18 2002
PlatformDOS
LanguageQuickBASIC
Summary

This tutorial looks at how EMS can be used to store huge arrays, and how EMS can be used as a double buffer for smooth graphics.

 

Article Text

Printable Version / Download Article

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!)