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.8 | The assembler source code |
Dynmem1.bas | Sample application |
Dynmem2.bas | The Graphic Sample |
Arrow1 - 4.img | Graphics for the sample |
mem.obj | The compiled assembler code, in case you can't get an assembler |