Using assembly language to create dynamic memory for Your QB programs

Charles Johnson
charles@cts.com


Hi! My name is Charles Johnson. I've been watching this web site for a while now, and I really like what I see. I started programming in Quick BASIC years ago. Although I'm now a proffesional Windows programmer, I still love to write QB code for DOS.

I've been a BASIC and assembly programmer for many years, and I've written many proffesional QB programs. Now my skills as a DOS programmer are going to waste, so I pass them on to you.

This is what I hope to be the first in a series of articles to help people write better games with Quick BASIC. Future articles I have planned include:

Fast transparency blitting (much faster than get and put),
color cycling,
sound blaster,
isometric games (Like Diablo).

For any programming help, tips tricks etc.. send me some e-mail, and I'll try write back quickly

First, a word about assemblers. The assembler I use is called A86. It is available as shareware from Eric Isaacson. I don't know of any URLs off hand, as I got it years ago from Compuserve. I recomend A86, because it's small and fast, and it doesn't mire you down in a lot of required directives, so it makes it easy to program with. It also comes with a huge manual that is very helpful in writing assembly routines.

Second, a word about BASIC compilers (note: BASIC is always capitalized, as it is an acronym). The compiler I use is VBDOS, I don't have a copy of Quick BASIC any more. All the code I write should be compatible or modifiable for ANY version of BASIC (I've even made it work with a 1980 version of QB 1.0!). It can also be modified to work with the QBASIC interpreter, but this would require arrays and Call Absolute which I'll cover in another article. If you have any problems making it work with your version of BASIC, drop me a line, and I'll help you make it work.

Now, on with the show...

I see alot of unfinished works on this site, and the main problem I see people having is Quick BASIC's limited memory.

With the code in these modules, you should be able to allocate, deallocate and quickly move memory from one place to another, giving you the freedom of using all of your 640k. And in a furture article, I'll show you how to use the dynamic memory to store your graphics and fast blit them to the screen.

When a Quick BASIC program runs, it requests all available memory from DOS, then it wastes that memory by giving you access to 64k of data space. Fortunately, DOS provides services for freely allocating memory through interrupt 21h, function 48h. Unfortunately, because BASIC has requested all free memory, none of that memory is left for us to allocate. Microsoft in their infinite wisdom knew that they painted us into a corner, so they provided us with SETMEM, to help us release some memory.

The first function we will discuss is AllocMem, this function is used to ask DOS for memory. For input we pass the amount of memory we need (Size%) and for output we get back a segment that contains addresses from 0 to the Size%-1. This function would be written in BASIC as:

DIM R AS REGTYPEX

PRINT SETMEM(-128000)

R.AX = &H4800
R.BX = SizeInBytes% / 16

CALL INTERRUPTX(&H21, R, R)

IF R.FLAGS AND 1 = 0 THEN Segment% = R.AX



While this code is much easier to write in BASIC, We have to do our memory moving in assembler, also because size and speed are the two most important issues in any BASIC program, we'll write as much as we can easily write in assembler:

ALLOCMEM: PUSH BP
PUSH BX

MOV BP,SP

MOV BX,[BP+8] ;Put size in BX

Function 48H requires the memory size in 16 byte paragraphs, so we divide by 16 by shifting the bits right 4 places

SHR BX,4
MOV AH,048H
INT 021H

JNC NOALLOCERROR

To return an integer value as a functin to BASIC, you must set ax to the value to be returned, since this code will only be run in the event of an error, ax is set to 0 to indicate we were unable to allocate memory

XOR AX,AX

NOALLOCERROR:

POP BX
POP BP

RETF 2


Since we must always free any memory we allocate...
FREEMEM:
PUSH BP
PUSH AX
PUSH BX
PUSH ES

MOV BP,SP

MOV ES,[BP+12] ;The segment to be freed is placed in ES

MOV AH,049h :Function 49h frees allocated memory
INT 021h

POP ES
POP BX
POP AX
POP BP

RETF 2

Before discussing the other assembler routines let's write a test program in BASIC to allocate memory.

DEFINT A-Z 'always

DECLARE FUNCTION AllocMem% (BYVAL Size%)
DECLARE SUB FreeMem (BYVAL Segment%)

PRINT SETMEM(0) 'Shows how much memory BASIC has allocated
PRINT SETMEM(-128000) 'Releases 128k, and prints the remaining

Segment% = AllocMem%(&HFA00) 'Allocate 64000 bytes, enough for
'a screen 13 off screen buffer!

IF Segment% = 0 THEN 'Memory was not allocated
PRINT "Could not Allocate memory!"
ELSE
PRINT "Segment : "; Segment%

I% = 16
PRINT I%;

DEF SEG = Segment%
POKE 0, I%

I% = 0
PRINT I%;

I% = PEEK(0)

PRINT I%

DEF SEG

FreeMem Segment% 'Free the previously allocated memory
END IF

PRINT SETMEM(128000) 'Restore the memory to BASIC

END

Now, none of this allocating and freeing memory does us any good, unless we can do something with. That's where MoveMem comes in. With a memory moving routine, we can move data from BASIC's DGroup memory to our dynamically allocated segments and back, or from storage memory, to video memory (fast blitting). In the final program of this article, I'll show you a handy tip for storing your get and put images.

push bp

mov bp,sp ;Stack Frame

push cx
push es
push di
push ds
push si

mov cx,[bp+6] ;Number of bytes to move

mov ds,[bp+10] ;From Where
mov si,[bp+8]

mov es,[bp+14] ;To Where
mov di,[bp+12]

rep movsb ;Move CX bytes

mov ax,di ;Return next available address

pop si
pop ds
pop di
pop es
pop cx

pop bp

retf 10

This routine is pretty simple, most of the code is setup and breakdown because so many registers are used, but the only important code is: REP MOVSB because this actually moves all the memory from DS:SI to ES:DI

Here is where it all comes together. Using AllocMem, we create a segment of free memory, then with MoveMem (which I aliased as PopVar and PushVar for convenience) we move our array into our free segment making the array available for reuse. Then we clear it and pull it back out, just to prove that ir all worked.

DEFINT A-Z

DECLARE FUNCTION AllocMem (BYVAL Size%) AS INTEGER
DECLARE SUB FreeMem (BYVAL Segment%)

DECLARE FUNCTION MoveMem (BYVAL DestSeg%, BYVAL DestOffset%, BYVAL SrcSeg%, BYVAL SrcOffset%, BYVAL Length%) AS INTEGER

DECLARE FUNCTION PushVar ALIAS "MoveMem" (BYVAL Segment%, BYVAL Offset%, SEG Variable AS ANY, BYVAL Length%) AS INTEGER
DECLARE FUNCTION PopVar ALIAS "MoveMem" (SEG Variable AS ANY, BYVAL Segment%, BYVAL Offset%, BYVAL Length%) AS INTEGER

DIM Segment
DIM Index
DIM MyArray(1 TO 100)

FOR Index = 1 TO 100
MyArray(Index) = Index
NEXT Index

CLS

PRINT "SETMEM(0) :"; SETMEM(0)
PRINT "SETMEM(-128000):"; SETMEM(-128000)

Segment = AllocMem(&HFA00)

PRINT "Segment :"; Segment
IF Segment THEN

PRINT "PushVar :";
PRINT PushVar(Segment, 0, MyArray(1), 200)

FOR Index = 1 TO 100
MyArray(Index) = 0
NEXT Index

PRINT "PopVar :";
PRINT PopVar(MyArray(1), Segment, 0, 200)

PRINT "Array 1,50,100 :";
PRINT MyArray(1); MyArray(50); MyArray(100)

FreeMem Segment
END IF

PRINT "SETMEM(128000) :"; SETMEM(128000)

END

Now, I no what your saying, "Big deal, what does that really do for me ?"
Included with this zip file are four files with the extension "IMG". These are files I created using GET and BSAVE. they are four arrows, each one points a different direction.

DEFINT A-Z
OPTION EXPLICIT

DECLARE FUNCTION AllocMem (BYVAL Size%) AS INTEGER
DECLARE SUB FreeMem (BYVAL Segment%)

DECLARE FUNCTION MoveMem (BYVAL DestSeg%, BYVAL DestOffset%, BYVAL SrcSeg%, BYVAL SrcOffset%, BYVAL Length%) AS INTEGER

DECLARE FUNCTION PushVar ALIAS "MoveMem" (BYVAL Segment%, BYVAL Offset%, SEG Variable AS ANY, BYVAL Length%) AS INTEGER
DECLARE FUNCTION PopVar ALIAS "MoveMem" (SEG Variable AS ANY, BYVAL Segment%, BYVAL Offset%, BYVAL Length%) AS INTEGER

DIM Segment
DIM Memory&
DIM Index
DIM img(1 TO 130)
DIM KeyCode, Direction, a$

Memory& = SETMEM(-128000)
Segment = AllocMem(&HFA00)

IF Segment THEN

SCREEN 13

DEF SEG = Segment

FOR Index = 0 TO 3
BLOAD "img" + MID$(STR$(Index + 1), 2) + ".img", Index * 260
NEXT Index

DEF SEG

DO
Memory& = PopVar(img(1), Segment, Direction * 260, 260)

PUT (0, 0), img

DO
a$ = INKEY$
LOOP UNTIL LEN(a$)

KeyCode = ASC(RIGHT$(a$, 1))
IF LEN(a$) > 1 THEN KeyCode = -KeyCode

PUT (0, 0), img

SELECT CASE KeyCode
CASE -72: Direction = 0
CASE -75: Direction = 3
CASE -77: Direction = 1
CASE -80: Direction = 2
END SELECT

LOOP UNTIL KeyCode = 27

SCREEN 0

FreeMem (Segment)
END IF

Memory& = SETMEM(128000)

END

Here's the big deal: in 64k bytes you can store over 250 16x16 tiles, or 1 256x256 map (if you use one byte per tile).

Here's how you put it all together:

To create a quick library for use in the interprter:
LINK mem.obj, mem.qlb, vbdosqlb.lib /q;
Of course, you should replace the library vbdosqlb.lib with the one that is applicable to your compiler.

And to link it into your program:
LINK Program.obj mem.obj;

That's about all there is to it!
It's so simple it's easy.

Here is a list of all the files I included in this Archive:
mem.8The assembler source code
Dynmem1.basSample application
Dynmem2.basThe Graphic Sample
Arrow1 - 4.imgGraphics for the sample
mem.objThe compiled assembler code, in case you can't get an assembler

In my next installment, I'll add to our assembler library, and show you how to do screen 13 double buffering(Flicker free updates!) and fast transparency blitting (no more XOR and AND masks, use just one img file).


Until Next Time......

cj