Issue #3
April 7th, 2001

Download the related file.

Introduction: Some History, a Problem, and a Solution

QuickBasic has always had a severe problem when it comes to memory. DOS itself can only address 1 MB of memory in real mode. This is the base (conventional) memory and the upper memory area (UMA). Base memory is the first 640 K of memory. The upper memory area is not actually physical memory. Other devices, such as the video card and BIOS, map their memory to this area.

Well, the QuickBasic IDE eats up memory pretty quick, and so does all your program code. Add a couple of sprites, and before you know it you are seeing "Out of Memory" error messages left and right. Now you've got a problem. You still have to add sound, and you don't even have space for all graphics!

There are a couple of solutions to this problem. The first solution is to work outside the QuickBasic IDE, using the command line to compile and test your program. Not only is this a pain, but this only gives you about 300 K more memory to work with.

Figure 1-1: A typical PC memory map

The second solution is to somehow access memory above 1 MB. There are three basic ways to do this: Use extended memory (XMS), use expanded memory (EMS), or use protected mode. Protected mode is extremely fast, but it's a huge pain and is extremely complicated. XMS and EMS are a bit easy to use, if a little clunky. Data stored with XMS or EMS must be swapped (or mapped) in and out of the lower 1 MB memory region so that your program can access it.

On the left is a "memory map" of a typical computer. As you can see, the page frame is located at either segment D000h (if not in use) or segment E000h. There is not actually memory located at the page frame; it is just an area that is addressable by the CPU. Using EMS, however, we can map memory above 1 MB to the page frame, allowing the CPU to directly access it.

Well, now you're asking yourself, "How do I use XMS or EMS?" MS-DOS provides two drivers, HIMEM.SYS and EMM386.EXE, which provide access to XMS and EMS, respectively. It is important to note that EMM386 requires the HIMEM driver because it uses the XMS memory provided by HIMEM to simulate EMS memory.

I have chosen to use EMS memory for a couple of reasons. First of all, EMM386 has a much easier interface that can be accessed using interrupts, which can be done using pure QB code. Secondly, EMS provides a 64K page frame in the UMA (See figure 1 above). This means that you don't have to use a buffer in base memory to swap data in and out of (as XMS requires).

"So, are there any disadvantages to using EMS?" Yes, I'm afraid there are two. EMS is a tad slower than XMS, although it is still very fast. The second disadvantage is that only 386 computers can use the EMM386 driver, because it only works on 80386+ processors. (Note: 286s can still use EMS, however, they require physical expanded memory modules or third-party EMS emulators.)

Overview: The Basic Concepts of Using EMS

The technique I am going to describe here is supported by the LIM EMS 4.0 standard. LIM stands for Lotus/Intel/Microsoft, which were the three companies that came up with this standard in 1987 for using EMS.

First of all, expanded memory is divided into 16K segments called logical pages. These pages must be allocated to a handle to be used. EMM386 by default provides 64 handles when running in MS-DOS mode, and 256 handles when running under Windows 95/98. The operating system uses one handle, so that leaves your program with 63 or 255 handles to use.

There are two ways to access the data in the logical pages. The first way is to map the pages to the EMS page frame. The page frame is 64K (4 physical pages), allowing you to map up to four logical pages to it. Any data written to the page frame is actually written to the logical pages mapped to it. This method is useful when playing sounds from EMS, because no base memory is required.

The second way to access the data is to use EMM386 to copy the data directly from expanded memory to a location in base memory. Using this method, you can copy the exact number of bytes you need to any segment and offset. This method is useful when dealing with sprites and data arrays.

Using EMS: Let's Do It!

Before I go any further, let me just say that all code in this article will be pure QuickBasic. I am doing this for a couple of reasons. One, I believe that QB code is a lot easier to understand than assembly, even if it is slower. Two, anyone who wants the code is assembly can easily convert it, but converting assembly to QuickBasic is much more difficult.

Let me also say that I am not aiming to build a super-fast lightning-speed EMS library. I am explaining how EMS works and am providing some solid examples. If you want an EMS library already made and don't care how it works, then you should use DirectQB or the Future Library. That said, let's get started!

Note: All the code in this article, including the sample program, can be found in the ems1.zip file.

EMS is controlled through interrupt 67h. The nice thing about this interrupt is that nearly every service returns a status code in AH after the interrupt returns. This makes it easy to write an error-reporting routine; all we have to do is save the value of AH in a shared variable after each interrupt. We can check if an error occurred by just checking the variable at any time. We can also make a function which returns an error description based on the error code, so we don't have to memorize what all the error codes mean!

Below is the start of our EMS program. In order for the EMS routines to work, you must start QuickBasic with the /L switch. This will load the QB.QLB quicklibrary, which allows us to use interrupts. Notice that we're using integers by default and dynamic arrays. We also need to include the QB.BI file because we're using InterruptX.

DEFINT A-Z
'$DYNAMIC
'$INCLUDE: 'QB.BI'
DIM SHARED Regs AS RegTypeX
DIM SHARED EMS.Error		'Holds the error code of the last operation

 

DECLARE FUNCTION EMS.ErrorMsg$ ()

 

FUNCTION EMS.ErrorMsg$

 

  'Returns a text string describing the error code in EMS.Error.

 

  SELECT CASE EMS.Error
    CASE &H0: Msg$ = "successful"
    CASE &H80: Msg$ = "internal error"
    CASE &H81: Msg$ = "hardware malfunction"
    CASE &H82: Msg$ = "busy -- retry later"
    CASE &H83: Msg$ = "invalid handle"
    CASE &H84: Msg$ = "undefined function requested by application"
    CASE &H85: Msg$ = "no more handles available"
    CASE &H86: Msg$ = "error in save or restore of mapping context"
    CASE &H87: Msg$ = "insufficient memory pages in system"
    CASE &H88: Msg$ = "insufficient memory pages available"
    CASE &H89: Msg$ = "zero pages requested"
    CASE &H8A: Msg$ = "invalid logical page number encountered"
    CASE &H8B: Msg$ = "invalid physical page number encountered"
    CASE &H8C: Msg$ = "page-mapping hardware state save area is full"
    CASE &H8D: Msg$ = "save of mapping context failed"
    CASE &H8E: Msg$ = "restore of mapping context failed"
    CASE &H8F: Msg$ = "undefined subfunction"
    CASE &H90: Msg$ = "undefined attribute type"
    CASE &H91: Msg$ = "feature not supported"
    CASE &H92: Msg$ = "successful, but a portion of the source region has been overwritten"
    CASE &H93: Msg$ = "length of source or destination region exceeds length of region allocated to either source or destination handle"
    CASE &H94: Msg$ = "conventional and expanded memory regions overlap"
    CASE &H95: Msg$ = "offset within logical page exceeds size of logical page"
    CASE &H96: Msg$ = "region length exceeds 1 MB"
    CASE &H97: Msg$ = "source and destination EMS regions have same handle and overlap"
    CASE &H98: Msg$ = "memory source or destination type undefined"
    CASE &H9A: Msg$ = "specified alternate map register or DMA register set not supported"
    CASE &H9B: Msg$ = "all alternate map register or DMA register sets currently allocated"
    CASE &H9C: Msg$ = "alternate map register or DMA register sets not supported"
    CASE &H9D: Msg$ = "undefined or unallocated alternate map register or DMA register set"
    CASE &H9E: Msg$ = "dedicated DMA channels not supported"
    CASE &H9F: Msg$ = "specified dedicated DMA channel not supported"
    CASE &HA0: Msg$ = "no such handle name"
    CASE &HA1: Msg$ = "a handle found had no name, or duplicate handle name"
    CASE &HA2: Msg$ = "attempted to wrap around 1M conventional address space"
    CASE &HA3: Msg$ = "source array corrupted"
    CASE &HA4: Msg$ = "operating system denied access"
    CASE ELSE: Msg$ = HEX$(EMS.Error) '"undefined error"
  END SELECT

  EMS.ErrorMsg$ = Msg$

 

END FUNCTION

Now that we have an error checking routine, it will be easier to diagnose any problems that we may have when creating our EMS routines or using them in a program.

Before we start using EMS, our program should first check and see whether an expanded memory manager (EMM) is installed. This is accomplished by getting the interrupt vector address for interrupt 67h, which is the EMM interrupt. DOS provides an interrupt routine to return this information:

Interrupt 21h Service 35h - Get Interrupt Vector

AH = 35h
AL = Interrupt number

After INT 21h Returns:

ES = Segment of ISR

Once we have the segment of interrupt 67h, we can check to see if an EMM is installed. This is done by changing the segment to ES and looking at the memory location at offset Ah (10). If an EMM is installed, we should find "EMMXXXX0" here. Below is a function that will do this:

DECLARE FUNCTION EMS.Init ()

 

FUNCTION EMS.Init

 

  'Returns true (-1) if an EMM is installed
  'or false (0) if an EMM is not installed.

 

  Regs.ax = &H3567                        'Get the interrupt vector for int 67h
  InterruptX &H21, Regs, Regs
  DEF SEG = Regs.es                       'Point to the interrupt segment
  FOR x = 10 to 17                        'Store the 8 bytes at ES:0A in EMM$
    EMM$ = EMM$ + CHR$(PEEK(x))
  NEXT
  IF EMM$ <> "EMMXXXX0" THEN
    EMS.Init = 0              'EMM not installed
  ELSE
    EMS.Init = -1             'EMM installed
  END IF

 

END FUNCTION

We now have a simple function that tells us whether or not an EMM is installed. It would be nice to know what version is installed, because if it is below 4.0, some of our routines won't work! Interrupt 67h Service 46h does this:

Interrupt 67h Service 46h - Get EMM Version

AH = 46h

After INT 67h Returns:

AH = Status
AL = EMM version number, with the major version stored in the 4 MSB and the minor version stored in the 4 LSB.

 

DECLARE FUNCTION EMS.Version$ ()

 

FUNCTION EMS.Version$

 

  'Returns a string containing the EMM version.
  '(Must be "4.0" or greater to use our routines.)

 

  Regs.ax = &H4600                           'Get the EMM version
  InterruptX &H67, Regs, Regs
  EMS.Error = (Regs.ax AND &HFF00&) \ &H100  'Save the status code

 

  Version = Regs.ax AND &HFF                 'Split the version number into
  Major = (Version AND &HF0) \ &H10          'its major and minor counterparts
  Minor = Version AND &HF
  EMS.Version$ = LTRIM$(STR$(Major)) + "." + LTRIM$(STR$(Minor))

 

END FUNCTION

Also, before we allocate any expanded memory, it would be nice to know where the page frame is. This way, we know where to find the pages that we map to the page frame. Service 41h does this.

Interrupt 67h Service 41h - Get Page Frame Segment

AH = 41h

After INT 67h Returns:

AH = Status
BX = Segment of page frame

 

DECLARE FUNCTION EMS.PageFrame ()

 

FUNCTION EMS.PageFrame

 

  'Returns the segment of the EMS page frame

 

  Regs.ax = &H4100                           'Get the segment of the page frame
  InterruptX &H67, Regs, Regs
  EMS.Error = (Regs.ax AND &HFF00&) \ &H100  'Save the status code
  EMS.PageFrame = Regs.bx

 

END FUNCTION

Well, we're still not ready to allocate any memory. We need to check and make sure that the EMM has enough handles. The number of available (remaining) EMS handles can be found by subtracting service 4Bh (handles in use) from service 54h subfunction 02h (total handles)

Interrupt 67h Service 4Bh - Get Number of EMM Handles

AH = 4Bh

After INT 67h Returns:

AH = Status
BX = Number of EMM handles in use

 

Interrupt 67h Service 54h Subfunction 02h -

Get Total Number of Handles

AH = 54h
AL = 02h

After INT 67h Returns:

AH = Status
BX = Total number of EMM handles

 

DECLARE FUNCTION EMS.FreeHandles ()
FUNCTION EMS.FreeHandles

 

  'Returns the number of free (available) EMS handles.

 

  Regs.ax = &H4B00                             'Get the # of handles in use
  InterruptX &H67, Regs, Regs
  UsedHandles = Regs.bx

 

  Regs.ax = &H5402                             'Get the total # of handles
  InterruptX &H67, Regs, Regs
  EMS.Error = (Regs.ax AND &HFF00&) \ &H100    'Store the status code
  TotalHandles = Regs.bx

 

  EMS.FreeHandles = TotalHandles - UsedHandles 'Subtract to get the # of free handles

 

END FUNCTION

As stated previously, the number of free handles will usually be 63 when running in pure DOS or DOS mode, or 255 when running under Windows 95/98.

We're almost ready to allocate some memory! We just need to know one more thing: how much memory is available to allocate?! Service 42h returns both the total number of EMS logical pages and the free number of EMS logical pages. Since a page is 16K, we can find the amount of memory for each in KB by multiplying by 16.

Note: It is possible to allocate all available pages when running in pure DOS or DOS mode. When running under Windows 95/98, however, you need to leave 40 pages (640 KB) free.

Interrupt 67h Service 42h - Get Number of Pages

AH = 42h

After INT 67h Returns:

AH = Status
BX = Number of free EMS pages
DX = Total number of EMS pages

 

DECLARE FUNCTION EMS.FreePages ()
DECLARE FUNCTION EMS.TotalPages ()

 

FUNCTION EMS.FreePages

 

  'Returns the number of free (available) EMS pages
  '(Multiply by 16 to get the amount free EMS in KB.)

 

  Regs.ax = &H4200                           'Get the # of free pages
  InterruptX &H67, Regs, Regs
  EMS.Error = (Regs.ax AND &HFF00&) \ &H100  'Store the status code
  EMS.FreePages = Regs.bx

 

END FUNCTION

 

FUNCTION EMS.TotalPages

 

  'Returns the total number of EMS pages
  '(Multiply by 16 to get the total amount of EMS in KB.)

 

  Regs.ax = &H4200                           'Get the # of total pages
  InterruptX &H67, Regs, Regs
  EMS.Error = (Regs.ax AND &HFF00&) \ &H100  'Store the status code
  EMS.TotalPages = Regs.dx

 

END FUNCTION

Finally! We're ready to allocate some pages to a handle! Service 43h allocates the number of pages in BX and returns the EMS handle the memory is allocated to. You must save the value of this handle! You will need it when using other EMS functions, similar to the way you use file numbers in QuickBasic.

Interrupt 67h Service 43h - Get Handle and Allocate Memory

AH = 43h
BX= Number of logical pages to allocate

After INT 67h Returns:

AH =

Status

DX = Handle

 

DECLARE FUNCTION EMS.AllocPages (NumPages)

 

FUNCTION EMS.AllocPages (NumPages)

 

  'Allocates the number of pages in [NumPages] and
  'returns the EMS handle the memory is allocated to.

 

  Regs.ax = &H4300                           'Allocate [NumPages] pages of EMS
  Regs.bx = NumPages
  InterruptX &H67, Regs, Regs
  EMS.Error = (Regs.ax AND &HFF00&) \ &H100  'Store the status code

 

  EMS.AllocPages = Regs.dx                   'Return the handle

 

END FUNCTION

 

Figure 1-2: 8 pages allocated to handle #1, and 10 pages allocated to handle #2

Every time you call this function, the memory is allocated with a different handle. The pages in each handle are numbered starting with 0. On the left is a visual representation of 8 pages allocated to handle #1 and 10 pages allocated to handle #2.

You will notice that there are two pages numbered 0. These are different pages! One page 0 is allocated to handle #1, and the other page 0 is allocated to handle #2! That is why it is important to specify the correct handle when using EMS, otherwise EMM386 won't know which page you mean.

One very important thing to remember when allocating EMS is that you must deallocate the memory at the end of your program! If you don't, the memory will be "lost" until your computer is restarted. Service 45h releases the pages allocated to a handle, as well as freeing the handle for later use.

Note: If you're running under Windows, you can get the memory back by closing the DOS box and opening another one. You should still deallocate the memory, however.

 

Interrupt 67h Service 45h - Release Handle and Memory

AH = 45h
DX= EMM Handle

After INT 67h Returns:

AH = Status

 

DECLARE SUB EMS.DeallocPages (Handle)

 

SUB EMS.DeallocPages (Handle)

 

  'Deallocates the EMS pages allocated the EMS handle [Handle].
  'You MUST remember to call the sub before your program ends
  'if you allocate any memory!

 

  Regs.ax = &H4500                           'Release the pages allocated to [Handle]
  Regs.dx = Handle
  InterruptX &H67, Regs, Regs
  EMS.Error = (Regs.ax AND &HFF00&) \ &H100  'Store the status code

 

END SUB

Now that we have the ability to allocate memory to a handle, we should be able to do something with it! As I said before, there are two ways to access EMS: mapping up to 4 pages to the page frame, and working with the data on the pages from there, or copying the data from EMS to a specified location base memory.

I'll start with the page frame approach first, since it's the simpler of the two. Service 44h maps a single logical page in EMS to one of four physical pages in the page frame.

Interrupt 67h Service 44h - Map Memory

AH = 44h
AL = Physical page number (0-3)
BX = Logical page number
DX = Handle

After INT 67h Returns:

AH = Status

 

DECLARE SUB EMS.MapPage (Physical, Logical, Handle)

 

SUB EMS.MapPage (Physical, Logical, Handle)

 

  'Maps the logical EMS page [Logical] (allocated to the handle [Handle])
  'to the physical page [Physical] in the EMS page frame.

 

  Regs.ax = &H4400 + Physical                'Map the logical page [Logical]
  Regs.bx = Logical                          'to the physical page [Physical]
  Regs.dx = Handle
  InterruptX &H67, Regs, Regs
  EMS.Error = (Regs.ax AND &HFF00&) \ &H100  'Store the status code

 

END SUB

If we want to map more than one page, service 50h will do this, but it's a bit more complicated. We must first set up a buffer with a list of logical pages and the physical pages to map them to. We then pass the segment and offset of this buffer to the interrupt.

Interrupt 67h Service 50h Buffer Format

Offset Length Type Description
0 2 INT Logical page number
2 2 INT Physical page to map to logical page
Buffer continues with entries alternating logical/physical for up to 4 pages. Size of buffer will range from 4 bytes (1 page) to 16 bytes (4 pages).

 

Interrupt 67h Service 50h - Map Multiple Pages

AX = 5000h
CX = Number of pages to map
DX = Handle
DS = Segment of buffer described above
SI = Offset of buffer described above

After INT 67h Returns:

AH = Status

 

DECLARE SUB EMS.MapXPages (PhysicalStart, LogicalStart, NumPages, Handle)

 

SUB EMS.MapXPages (PhysicalStart, LogicalStart, NumPages, Handle)

 

  'Maps up to 4 logical EMS pages to physical pages in the page frame, where:
  '
  'PhysicalStart = Physical page first logical page is mapped to
  'LogicalStart  = First logical page to map
  'NumPages      = Number of pages to map (1 to 4)
  'Handle        = EMS handle logical pages are allocated to

 

  'Create a buffer containing the page information
  FOR x = 0 to NumPages - 1
    MapInfo$ = MapInfo$ + MKI$(LogicalStart + x) + MKI$(PhysicalStart + x)
  NEXT

 

  Regs.ax = &H5000                           'Map the pages in the buffer
  Regs.cx = NumPages                         'to the pageframe
  Regs.dx = Handle
  Regs.ds = VARSEG(MapInfo$)
  Regs.si = SADD(MapInfo$)
  InterruptX &H67, Regs, Regs
  EMS.Error = (Regs.ax AND &HFF00&) \ &H100  'Store the status code

 

END SUB

The LIM 4.0 standard introduced another way to access EMS besides the page frame: direct copying! Service 57h copies memory from any location in EMS or base memory to any location in EMS or base memory. (Thanks to zChip for pointing this out on the NeoBasic board.)

This allows four kinds of copying: EMS to EMS, EMS to base, base to EMS, and even base to base! This service also requires a buffer so it knows exactly how to copy the memory.

Interrupt 67h Service 57h Buffer Format

Offset Length Type Description
0 4 LONG Length of memory to copy
4 1 BYTE Source memory type: CHR$(0) for base, CHR$(1) for EMS
5 2 INT Source memory handle (Use 0 if source is base memory)
7 2 INT Source memory offset
9 2 INT Source memory segment (Use page number if source is EMS)
11 1 BYTE Destination memory type: CHR$(0) for base, CHR$(1) for EMS
12 2 INT Destination memory handle (Use 0 if destination is base memory)
14 2 INT Destination memory offset
16 2 INT Destination memory segment (Use page number if destination is EMS)
Size of buffer will always be 18 bytes.

 

Interrupt 67h Service 57h - Copy Memory Region

AX = 5700h
DS = Segment of buffer described above
SI = Offset of buffer described above

After INT 67h Returns:

AH = Status

 

DECLARE SUB EMS.CopyMem (Length&, SrcHandle, SrcSegment, SrcOffset, DstHandle, DstSegment, DstOffset)

 

SUB EMS.CopyMem (Length&, SrcHandle, SrcSegment, SrcOffset, DstHandle, DstSegment, DstOffset)

 

  'Copies memory from EMS or base memory to EMS or base memory, where:
  '
  'Length&    = Length of memory to copy in bytes
  'SrcHandle  = EMS handle of source memory (use 0 if source is base memory)
  'SrcSegment = Segment of source memory (or page number if source is EMS)
  'SrcOffset  = Offset of source memory
  'DstHandle  = EMS handle of destination memory (use 0 if destination is base memory)
  'DstSegment = Segment of destination memory (or page number if destination is EMS)
  'DstOffset  = Offset of destination memory

 

  'Determine the source and destination memory types by checking the handles
  IF SrcHandle = 0 THEN SrcType$ = CHR$(0) ELSE SrcType$ = CHR$(1)
  IF DstHandle = 0 THEN DstType$ = CHR$(0) ELSE DstTYpe$ = CHR$(1)

 

  'Create a buffer containing the copy information
  CopyInfo$ = MKL$(Length&) + SrcType$ + MKI$(SrcHandle) + MKI$(SrcOffset) + MKI$(SrcSegment) + DstType$ + MKI$(DstHandle) + MKI$(DstOffset) + MKI$(DstSegment)

 

  Regs.ax = &H5700                           'Copy the memory region
  Regs.ds = VARSEG(CopyInfo$)                'described in the buffer
  Regs.si = SADD(CopyInfo$)
  InterruptX &H67, Regs, Regs
  EMS.Error = (Regs.ax AND &HFF00&) \ &H100  'Store the status code

 

END SUB

Service 57h subfunction 01h is another useful service. It works almost exactly like service 57h, except it exchanges the memory instead of copying it. The format for the buffer is the same.

Interrupt 67h Service 57h Subfunction 01h -

Exhange Memory Region

AH = 57h
AL = 01h
DS = Segment of buffer described above
SI = Offset of buffer described above

After INT 67h Returns:

AH = Status

 

DECLARE SUB EMS.ExchMem (Length&, SrcHandle, SrcSegment, SrcOffset, DstHandle, DstSegment, DstOffset)

 

SUB EMS.ExchMem (Length&, SrcHandle, SrcSegment, SrcOffset, DstHandle, DstSegment, DstOffset)

 

  'Exhanges memory from EMS or base memory to EMS or base memory, where:
  '
  'Length&    = Length of memory to exchange in bytes
  'SrcHandle  = EMS handle of source memory (use 0 if source is base memory)
  'SrcSegment = Segment of source memory (or page number if source is EMS)
  'SrcOffset  = Offset of source memory
  'DstHandle  = EMS handle of destination memory (use 0 if destination is base memory)
  'DstSegment = Segment of destination memory (or page number if destination is EMS)
  'DstOffset  = Offset of destination memory

 

  'Determine the source and destination memory types by checking the handles
  IF SrcHandle = 0 THEN SrcType$ = CHR$(0) ELSE SrcType$ = CHR$(1)
  IF DstHandle = 0 THEN DstType$ = CHR$(0) ELSE DstTYpe$ = CHR$(1)

 

  'Create a buffer containing the copy information
  ExchInfo$ = MKL$(Length&) + SrcType$ + MKI$(SrcHandle) + MKI$(SrcOffset) + MKI$(SrcSegment) + DstType$ + MKI$(DstHandle) + MKI$(DstOffset) + MKI$(DstSegment)

 

  Regs.ax = &H5701                           'Exchange the memory region
  Regs.ds = VARSEG(ExchInfo$)                'described in the buffer
  Regs.si = SADD(ExchInfo$)
  InterruptX &H67, Regs, Regs
  EMS.Error = (Regs.ax AND &HFF00&) \ &H100  'Store the status code

 

END SUB

A Sample Program

Well, we now have all the routines needed to fully utilize EMS in our programs! I know that you're itching to see EMS in action, so below is a sample program that tests all of our EMS routines, as well as timing the page map speed and EMS copy speed.

CLS

IF NOT EMS.Init THEN
  PRINT "No EMM detected."
  END
END IF

COLOR 14, 1
PRINT SPACE$(22); "Using EMS in QuickBasic: Part 1 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 < 64 THEN
  PRINT
  PRINT "You need at least 64 pages (1 MB) free EMS to run this demo."
  END
END IF

PRINT
COLOR 15, 0
PRINT STRING$(31, 196); " Allocation Test "; STRING$(32, 196)
COLOR 7
PRINT "Allocating 64 pages (1 MB) of EMS...";


Handle = EMS.AllocPages(64)
IF EMS.Error THEN
  PRINT "error!"
  PRINT EMS.ErrorMsg$
  END
ELSE
  PRINT "ok!"
END IF

PRINT "Pages allocated to handle"; Handle
PRINT
COLOR 15, 0
PRINT STRING$(30, 196); " Page Map/Copy Test "; STRING$(30, 196)
COLOR 7
PRINT "Mapping logical page 0 to physical page 0...";

EMS.MapPage 0, 0, Handle
IF EMS.Error THEN
  PRINT "error!"
  PRINT EMS.ErrorMsg$
  END
ELSE
  PRINT "ok!"
END IF

PRINT "Mapping logical pages 0-3 to physical pages 0-3...";

EMS.MapXPages 0, 0, 4, Handle
IF EMS.Error THEN
  PRINT "error!"
  PRINT EMS.ErrorMsg$
  END
ELSE
  PRINT "ok!"
END IF

PRINT "Copying logical pages 0-31 to logical pages 32-63...";

EMS.CopyMem 512288, Handle, 0, 0, Handle, 32, 0
IF EMS.Error THEN
  PRINT "error!"
  PRINT EMS.ErrorMsg$
  END
ELSE
  PRINT "ok!"
END IF

PRINT "Exchanging logical pages 0-31 with logical pages 32-63...";

EMS.ExchMem 512288, Handle, 0, 0, Handle, 32, 0
IF EMS.Error THEN
  PRINT "error!"
  PRINT EMS.ErrorMsg$
  END
ELSE
  PRINT "ok!"
END IF

PRINT
COLOR 15, 0
PRINT STRING$(22, 196); " 10-Second Speed Test (Please Wait) "; STRING$(22, 196)
COLOR 7

Mapped& = 0
StartTime! = TIMER
DO UNTIL StartTime! + 5 <= TIMER
  EMS.MapXPages 0, 0, 4, Handle
  Mapped& = Mapped& + 4
LOOP

Copied& = 0
StartTime! = TIMER
DO UNTIL StartTime! + 5 <= TIMER
  EMS.CopyMem 512288, Handle, 0, 0, Handle, 32, 0
  Copied& = Copied& + 1
LOOP

PRINT "Pages Mapped/Sec:"; Mapped& \ 5
PRINT "Bytes copied/Sec:"; (Copied& * 512288) \ 5
PRINT
COLOR 15, 0
PRINT STRING$(30, 196); " Deallocation Test "; STRING$(31, 196)
COLOR 7
PRINT "Deallocating 64 pages...";

EMS.DeallocPages (Handle)
IF EMS.Error THEN
  PRINT "error!"
  PRINT EMS.ErrorMsg$
  END
ELSE
  PRINT "ok!";
END IF

KeyPress$ = INPUT$(1)
CLS
END

Until Next Time...

Now that we have the fundamentals of using EMS down, we can get to the fun stuff! In part two (coming in issue #4), I'll show you how to use the EMS routines we developed to store multiple sounds and play them back via DMA. In part three, I'll show you how to store sprites and huge data arrays in EMS.

If you have any questions or comments concerning this article, or (gasp!) found an error, please e-mail me.

This article was written by: Plasma357

All site content is © Copyright 2001, HyperRealistic Games. This excludes content submitted by other people.