The QBNews Page 33 ---------------------------------------------------------------------- T h e Q B N e w s P r o f e s s i o n a l L i b r a r y ---------------------------------------------------------------------- Fast Screen Printing by Christy Gemmel [EDITOR'S NOTE] All code for this article may be found in FASTPRNT.ZIP. This article also refers to figures which included in FIGURES.TXT which is in FASTPRNT.ZIP. This article presents a rapid method of displaying a string of ASCII characters on the screen without updating the cursor position. The calling program should pass the string, the row/column co-ordinates of the screen location where it is to be printed and the number of the colour or display attribute required. Many routines in my Assembly-Language Toolbox are concerned with the video display, this being the primary means through which we communicate with the users of our program. Later, when we get around to developing such things as Popup Windows and Dialogue Boxes, we will need to acquire some pretty sophisticated screen-handling techniques. For now, however, let us look at the more mundane task of displaying text on the screen. For most applications the QuickBASIC PRINT statement is perfectly adequate. Because it is a general-purpose statement, however, which can be used to direct output to the printer or a file as well as to the screen, PRINT is not as fast as it might be. Moreover, to display a string of characters at a specific place on the screen you must first position the cursor with a LOCATE statement. Then, if you want to display the string in a different colour than the background text, you must issue a preliminary COLOR statement before printing. What we need is a screen-specific display routine that combines these three statements into one. Naturally, since we are going to the trouble of writing it in Assembly-Language, we expect it to be as fast as possible. Let us, then, set out our objectives for FastPrint: 1) The ROW/COLUMN location where the string is to be printed is specified to the routine, so that there is no need for a seperate LOCATE statement to position the cursor. 2) You must be able to place the string anywhere on the display, including the bottom line of the screen, no matter what VIEW PRINT parameters may be in effect. 3) Output to the bottom line of the display should not cause the screen to scroll. 4) You should be able to specify the colour and/or display attribute of the text to be printed, so there is no need for a seperate COLOR statement. Furthermore the attribute specified should appy The QBNews Page 34 Volume 2, Number 2 May 31, 1991 only to the text being printed, it must not effect the colour of any subsequent PRINT statements. 5) To obtain maximum performance, FastPrint should write directly to video display memory. This will be particularly useful when we need to output large blocks of text. This last clause needs some explanation. In general, there are two methods of outputting text to the screen. The first makes calls to the video services built into the PC's ROM-BIOS and can, consequently, be used by any computer that is BIOS-compatible with the IBM family of personal computers. This method, however, is considered to be a little slow by today's standards. The alternative technique bypasses ROM-BIOS completely and writes directly to the video display buffer. Since the display, in most PCs, is mapped by the video hardware to a fixed location in memory, any characters that are written to this area will immediately appear on the screen. This is the fastest method of all, but has the disadvantage that it will only work on computers that are hardware- compatible with the original IBM PC. However, since this includes all modern XT, AT and PS/2 class machines, it is not unduly restrictive, but it does eliminate some older models like the Tandy 2000. We will adopt the second method. It is, after all, the one used by 90% of today's software and has become something of a de facto standard in its' own right. This is the only way we can achieve some real performance in our programs. The bulk of the work is done by a short assembly-language procedure that is designed to be called from QuickBASIC. It can be part of a Quick Library for use in the programming environment or assembled into a single module that can be linked to stand-alone programs compiled with BC's /O switch. I will show you both methods later. Although short, FASTPRINT illustrates some important features of mixed-language programming that I will discuss in some detail. It was written for MicroSoft's Macro Assembler (MASM) version 5.1 and makes use of the simplified segment directives that are a feature of that assembler. This lets us define the Memory Model of the routine to match that of the calling program. .model medium All QuickBASIC programs use the MEDIUM memory model, which means that all simple data fits within a single 64K segment, although the program code may be greater than 64K. When we enter the routine, therefore, the DS (Data Segment) register is already pointing to the segment containing the string of characters we will be printing and we can use near pointers, offset from DS, to address this string. The main procedure, however, must be declared FAR so that the Code Segment (CS) register is set correctly when it is called. FastPrint is the name you will be using when you call this routine from your program, declare it PUBLIC so that QuickBASIC can find it. The QBNews Page 35 Volume 2, Number 2 May 31, 1991 public FastPrint Now for the routine itself, here are the first few lines: .code FastPrint proc far push bp ; Save Base pointer mov bp,sp ; Establish stack frame push es ; Save Extra Segment push si ; and index pointers push di Remember that QuickBASIC uses the Medium Memory model. This means that we can't assume our assembled routine will be linked into the same code segment as the main program that calls it, so the procedure must be declared as FAR. The next two lines are concerned with setting up a pointer to the stack so that we can easily get at the parameters that the routine is expecting, more about this in a minute. We must also preserve the values of any segment registers (except for CS, which has already been modified by the call itself) that are altered by the routine, also the Index registers DI and SI if they are used. When the routine is called, QuickBASIC pushes a two-byte value corresponding to each of the supplied parameters onto the program stack. These are followed by the return address which, since this is a FAR procedure, is a four-byte pointer (Segment and Offset) to the statement following the call to FastPrint. When we have finished saving our own working registers, the stack will look like this: Figure 1.1 Stack contents after entry into FastPrint. Note that, since we copied the contents of the Stack Pointer to BP immediately after BP itself was pushed, This register can now be used as a fixed pointer through which we access the parameters that were passed. Normally, QuickBASIC passes these arguments as 2-byte offset addresses, which point to the named variables in the program's Data Segment. We, however, are going to override this and pass the numeric arguments BY VALue, forcing QuickBASIC to copy the actual contents of the variables into the appropriate stack locations. Then all we need do is read the data supplied, directly into the target registers: mov ax,[bp+12] ; Get row number Passing by VALUE saves us having to hunt for the variables containing our data and so makes our routine faster and more compact. It also, incidently, makes it easier if we ever need to convert FastPrint to a C function, since the C language does it by default. We'll make use of the technique in a minute or two, but first we must collect some important information about the computer our program is running in. The QBNews Page 36 Volume 2, Number 2 May 31, 1991 Although we have decided that our program need only support machines that are hardware-compatible with the IBM PC family, this still leaves a variety of video environments to cater for. We must, for example, find out whether the host computer is fitted with a colour or monochrome display adaptor since the address of the screen buffer we will be writing to depends on this. Computers with EGA and VGA cards, moreover, can switch between up to eight display pages so, if our program is running in one of these machines, we must make sure that FastPrint sends its output to the correct page. Last, but not least, if the host system does have an EGA or VGA we must find out whether the current screen is set to a height of 25, 43 or 50 rows, so that we can format our printing properly. We could, of course, code the instructions that collect this data directly into FastPrint itself. It is likely, however, that many other programs we write will need the same information. Better, then, to make our video system identification function an independent procedure that can be called by whichever program needs it. This saves us repeating the same block of code in every screen routine and makes the Toolbox tighter and more efficient. call VideoType ; Get video parameters Since VideoType, as we have just decided, is going to be called by many Toolbox routines, we must make sure that the linker can find it when your program is built. This means that we have to declare it PUBLIC by putting the following line at the top of the source code. public VideoType Note that versions 5 and 5.1 of MASM have a bug which makes all procedures PUBLIC by default. If you are using either of these versions of the Macro Assembler, therefore, this line is not strictly necessary. However, because we cannot rely on this feature being retained in future releases, it is best to use an explicit PUBLIC declaration. VideoType also needs some space to store the data it collects. Insert the following lines in your source file, immediately after the .CODE segment declaration but before the first PROC statement. I will explain them more fully in a few moments. SnowFlag db 0 ; Snow prevention flag VideoRam dw 0B000h ; Default video segment VideoPort dw 03BAh ; Default video status port Param1 label word Mode db 7 ; Current screen mode Columns db 80 ; Current screen width Param2 label word Rows db 25 ; Current screen height ActivePage db 0 ; Current video page Notice that VideoType, like FastPrint itself, is a FAR procedure. This is necessary if it is to be called by routines in seperately-linked The QBNews Page 37 Volume 2, Number 2 May 31, 1991 modules that may have different Code segments. See the VideoType routine in DISPLAY.ASM. VIDEOTYPE As with all assembly-language procedures, it is good practice to preserve the contents of any registers that will be changed by the routine and are not used to return data. Do this by pushing the contents of these registers onto the stack immediately after entry and popping them again just before you return. Our first task is to check the current display mode to see if we are running in a machine with a colour or monochrome monitor. The easiest way is to call a service routine in the computer's BIOS and let the system do the work for us. Fortunately there are many such services, grouped by the hardware devices that they interface with, and with each group being accessed through a dedicated software interrupt. The Video services are called through Interrupt 16 (10 Hex). To gather the information we want, VideoType issues an INT instruction together with the interrupt number. Interrupt 16 (10 Hex) is a gateway to some very useful video services. When called with AH set to 15 (0F hex) it returns with the current display mode in AL, the number of screen columns in AH, and the current video page in BH. An interrupt is a signal to the microprocessor that its attention is required. When one occurs, the processor suspends the current task and jumps to a routine that is designed to deal with the event that triggered off the interrupt, this routine is called an interrupt handler. When the handler has done whatever is required, control returns to the next instruction in the interrupted task and execution resumes from there. Just like the return from a BASIC subroutine. The 80x86 family of processors allows for 256 diffent interrupts, numbered from 0 to 255. Some are dedicated to hardware devices, every 54.936 milliseconds, for example, the 8253 timer chip generates interrupt number 8, which executes a subroutine to update the system clock and date and time flags. Most interrupt numbers, however, are reserved for software interrupts that can be generated by programs, using either the assembly-language INT instruction or the QuickBASIC CALL INTERRUPT statement. You call an interrupt handler, not by specifying its address, but by supplying the interrupt number you require. The actual addresses are stored in an interrupt vector table at the very beginning of memory, being written there by the initialisation process when your computer is booted-up. When an interrupt is issued, the processor simply looks up the table entry for that interrupt number and jumps to the address contained there. Because different models of computer have different hardware configurations as well as different versions of DOS and BIOS, the actual addresses in the table will vary from machine to machine. But, since the interrupt numbers themselves do not change, programs The QBNews Page 38 Volume 2, Number 2 May 31, 1991 that use them are still portable between all computers in the PC family. When an interrupt is executed, the processor makes the following actions: 1) The FLAGS register, the current Code Segment (CS) and the Instruction Pointer (IP) are saved on the stack. 2) The address of the handling routine is looked up in the Interrupt Vector Table, starting at location 0000:0000 in memory. Since each table entry is four bytes long, the correct entry can be found by multiplying the interrupt number by four. 3) The segment and offset address of the interrupt handling routine are loaded into the CS:IP registers, transferring control to that routine. 4) The code at the handling routine is executed until the processor encounters an IRET (RETurn from Interrupt) instruction. 5) The original Instruction Pointer, Code Segment and Flags register are then restored from the stack and execution proceeds with the instruction that followed the original interrupt call. Figure 1.2 The interrupt sequence used on computers such as the IBM-PC, which use the Intel 80x86 family of micro- processors. On return from Interrupt 16, VideoType first checks the current video display mode. Notice that we are looking to see if AL contains a value of 7. This is only returned if your computer has a monochrome display. If AL does equal 7, we can skip the rest of the procedure, since our local variables are set for a monochrome display by default. Anything other than 7, however, means that we have a graphics adaptor to contend with and must adjust the defaults accordingly. Two variables are involved. The first, labelled VIDEORAM is set to the segment address of the display memory we will be using. This is at Hex 0B000 for mono adaptors (the default) and 0B800 for colour adaptors. The other variable is the number of the hardware port that controls the video display. For mono adaptors this is numbered 3BA (hex). For colour machines the port number must be reset to 3DAh. Notice that we are accessing local variables through the CS register, using a SEGMENT OVERIDE directive; e.g. mov CS:VideoRam,0B800h The reason for this is that the DS register, which is normally used to address data, is still pointing to the calling program's Data Segment. We need to leave DS intact so that we can use it to get at our display string when we eventually return to FastPrint. For now, however, we carry on and store the screen width and display mode, which were The QBNews Page 39 Volume 2, Number 2 May 31, 1991 returned in the upper and lower bytes of AX, in another of our local variables. The active page, which is in BH, is saved, temporarily, on the stack until we can obtain the screen height to go with it. In computers fitted with EGA and VGA adaptors the video hardware adds its own set of video functions to the BIOS video services. One of these, interrupt 16 sub-function 17/48, returns amongst other things, the information we require; the screen height in rows set in DL. What if our host computer doesn't have an EGA or VGA? No need to worry, calling an interrupt service that doesn't exist will not cause an error. All that happens is that the function returns with registers unchanged. Since other video adaptors only allow screen heights of 25 rows, if we previously set DL with this number, the correct value will still be in the register after the interrupt call. Even if an EGA or VGA isn't present. Notice, however, that the value we set is actually 24. BIOS routines typically use a numbering system based on zero rather than one, so a screen height of 25 rows is returned as 24. This is why we increment the return value by one, before storing it in its destination variable. One final thing that VideoType must do is to detect whether the host computer is using a Colour Graphics Adaptor (CGA). The reason for this is that there is a special problem with CGAs when we write directly to video memory. I'll go into more detail about this later. For now we just need to set a flag to indicate the presence of a CGA. How do we find out if this computer has a CGA? Easy. The trick is to find a video interrupt service which only works on machines fitted with an EGA or VGA and set an impossible value into one of the return registers. If, after the interrupt call has completed, this register is unchanged then an EGA or VGA is not present and we must have a CGA. Remember we have already eliminated systems with Monochrome Display Adaptors (MDA). Video service 18/16 is a suitable candidate. It returns a value of between 0 and 3 in BL to indicate the amount of memory, in 64K blocks, installed on your video card. VideoType presets BL with 16 (10 hex) before calling the interrupt. If this value is still in BL when the interrupt returns, then we have a CGA to contend with and we must set the SnowFlag accordingly. Before popping its saved registers and returning, VideoType's final task is to retrieve all the information it collected from local storage. Back in FastPrint, we resume our task by isolating the information that is needed next, the current screen dimensions. These are collected in DX so that we can check them against the start position of the string to be printed, making sure that it is within the screen boundaries. mov dh,ah ; Load screen dimensions mov dl,bl ; into DX QuickBASIC, conventionally numbers screen rows from 1 to 25 and screen columns from 1 to 80. Since the ROM-BIOS co-ordinate system uses row and column numbers starting from zero we need to convert our The QBNews Page 40 Volume 2, Number 2 May 31, 1991 parameters to base zero before we can start printing. At the same time it would be a good idea to check for legal values. This is where we start collecting the parameters that were passed to us by our caller. First the row co-ordinate: mov ax,[bp+12] ; Get row number dec al ; Convert to base zero cmp al,0 ; Top screen row jae Fast_01 ; Jump if not below xor al,al ; Don't go over the top! Fast_01: cmp al,dl ; Bottom row? jb Fast_02 ; Go no further mov al,dl ; Substitute maximum dec al ; row number Now let's do the same for the column co-ordinate supplied: Fast_02: mov bx,[bp+10] ; Get column number mov ah,bl ; into AH dec ah ; Convert to base zero cmp ah,0 ; Leftmost column? jae Fast_03 ; Jump if not below xor ah,ah ; Don't fall off the screen Fast_03: cmp ah,dh ; Rightmost column? jb Fast_04 ; Go no further mov ah,dh ; Substitute maximum dec ah ; column number Once we have a set of legal row and column co-ordinates we need to translate them into the actual address in the video display buffer where the data will be written. Since, like VideoType, this is a task that our Toolbox routines will have to perform often, we had better write a seperate procedure to handle it. Fast_04: call ScreenAddress ; Calculate target address ScreenAddress, again, must be a FAR procedure so that it can be called from programs in seperately-linked modules that may have different Code segments. As always, we preserve on the stack any working registers that are not used to return values. The row and column co- ordinates which ScreenAddress will convert into a memory address are supplied in the low and high bytes of AX. See the ScreenAddress routine in DISPLAY.ASM. ADDRESSING MEMORY The 80x86 CPU uses two registers to fully describe memory addresses. It does this by dividing the absolute address by 16 and storing the The QBNews Page 41 Volume 2, Number 2 May 31, 1991 result of this calculation in a SEGMENT register. Then another, INDEX, register is used to hold the remainder, which is called the OFFSET of the object from the start of the segment. The byte at absolute decimal location 753666, for instance, would be addressed as; Segment = (753666 \ 16) = 47104 Offset = (753666 MOD 16) = 2 Using hexadecimal (base 16) notation, makes it much simpler to work with segments and offsets, since all you need do is shift all digits of the absolute address, one place to the right, the rightmost digit falling off to become the offset part of the address: 753666 = B8002 (Hex) --------> Segment B800 : Offset 0002 More conventionally, this is written by assembly-language programmers as B800:0002. It is actually the memory address of the character displayed in the second column of the top row of the screen (if your computer has a color adaptor). Using this method, the PC's 1-megabyte of address space can be divided into 65536 (0 - FFFFh) possible segments. Each segment starts at a different paragraph boundary (a paragraph is a block of 16 bytes) and contains up to 65536 (0 - FFFFh) different byte addresses. Any segment larger than 16 bytes, of course, will overlap with the segments starting at the next and subsequent paragraph boundaries. To obtain the address of the position on the screen where our text is to be printed, ScreenAddress's first move is to copy the supplied column co-ordinate into BH. It then obtains the screen width, in columns, from the local variable that was previously set by VideoType. Since each column on the screen takes up two bytes of memory, one for the character stored there and the other for the attribute that governs how it is displayed, we now need to multiply the screen width by two to get the number of bytes per row. In assembly-language this is easy; the SHL instruction shifts all bits of the binary number in BL one position to the left, effectively doubling its value. Since AL contains the row number, we can now multiply it by BL and get the byte offset of this row. The MUL instruction extends the result of this calculation into AX, which is why we moved the column number into BH out of the way. Now we must use SHL again to convert this into bytes and add the result to AX. We now have the target address, offset from the beginning of the screen and it only remains for us to copy it into DI, one of the index registers used for addressing memory. If PCs only used a single screen page we could return right away. However machines with CGA, EGA and VGA cards are capable of displaying multiple pages so we must find which one is currently on the screen. We already have the page number from VideoType but BIOS makes it even easier by providing the actual address of the current display page, which is what we really want. The QBNews Page 42 Volume 2, Number 2 May 31, 1991 We don't need to use an interrupt call this time. BIOS maintains its own data block at segment 40h in low memory and this contains, amongst many other things, the exact information that we're looking for. The address of the current display page is stored as a two-byte number at offset 4Eh into this block. Notice, however, that ScreenAddress actually uses 0000:044Eh to reach it. This is because it is slightly quicker to clear the ES segment register than it is to load it with a value. The actual memory location remains the same. Figure 1.3 PC Memory map highlighting the BIOS data area in low RAM Once we have the page offset all that remains is to add it to the row and column offset that is already in DI. VideoType has already stored the video segment and port address for us so we just need to retrieve these parameters before returning to FastPrint. The result of all this register juggling is that we now have the ES:DI register pair pointing to the address in memory where output is to start. DX is also loaded with the address of the video controller port. Now we're ready to find the string to display. What QuickBASIC passes when a string is specified as parameter to an external procedure, is a pointer to a STRING DESCRIPTOR in the Data Segment. This is a four-byte block of data of which the first two bytes are the string length. In QuickBASIC strings may be up to 32767 bytes in length: Figure 1.4 Structure of the String Descriptor passed by QuickBASIC The second two bytes of the string descriptor contain a pointer to the first character of the string itself. This is an offset address, relative to the start of DGROUP, QuickBASIC's default Data Segment. This is the address that is returned by the SADD function in QuickBASIC. When used with a string, VARPTR returns the address of the string descriptor instead. If you are using Microsoft's BASIC 7 Professional Development System then the method of passing strings is rather different. See the sidebar for more details. Before going any further, we should check that there is a string to be printed. If the first two-bytes of the string descriptor evaluate to zero, a null string has been passed so there is no point in proceeding further. Consequently we set the AX register to an error code and jump to the exit. It is good practice to return a code indicating the success or failure of an operation when we leave, even if the calling program will not read it. Most DOS applications return such an ERRORLEVEL as a matter of course. Well the string is there, so we can load its address, which is the second two bytes of the string descriptor, into our other Index Register, SI. The QBNews Page 43 Volume 2, Number 2 May 31, 1991 mov bx,[bp+8] ; Index to string descriptor mov cx,[bx] ; Get string length cmp cx,0 ; Make sure it isn't ja Fast_05 ; a null string mov ax,1 ; If so set Error code jmp short Fast_07 ; and abort Fast_05: mov si,[bx+2] ; DS:SI==> string data mov ax,[bp+6] ; Get display attribute xchg ah,al ; into AH cld ; Clear direction flag This is a good opportunity to get the final parameter, the display attribute we are to use, into the AH register. The CLD (Clear Direction Flag) instruction ensures that any subsequent string manipulation instructions move data forwards in memory. Now that we are ready to start printing, the processor registers are set up as follows: Segment Registers: ES points to the current video segment. DS points to the calling program's DATA segment. Index Registers: DI points to the offset address, relative to ES, where the string will be copied to. SI points to the beginning of our source string, relative to the DS register. General Registers: DX contains the video status port address. CX contains the length of the source string. AH contains the display attribute to be given to each output character. The last section of code is a simple loop that copies each byte of the source string to its destination address in video memory. Instead of using a MOV instruction to copy each byte of the string into the AL register before transferring it to the screen, we use another assembly instruction, LODSB. Fast_06: lodsb ; Get a byte from the string LODSB is one of a set of string manipulation primatives built into the Intel family of microprocessors. Why they are called 'primitive' I don't know. They are, in fact, extremely powerful. In just one statement, LOSB will load a byte from the address pointed to by DS:SI into the AL register and then automatically increment SI to point to the next byte. If we were to prefix it with a REP instruction it would repeat the whole operation for the number of times that the value in CX is set to. This, as you can see, is just the thing for loops. In fact, if we didn't have to cater for systems with CGA video cards, we could do the remainder of the job in just one line. LODSB has a companion instruction, MOVSB, which, as it's name implies, moves the byte at DS:SI to a destination address pointed to by ES:SI. In this The QBNews Page 44 Volume 2, Number 2 May 31, 1991 case both of the index registers, SI and DI, are incremented ready for the next byte to be transfered. Like LODSB, MOVSB takes a REP prefix and, since we already have the length of our string in CX, all that would be necessary is to enter the instruction .... rep movsb ; Shift the whole string ... and the job would be done. But we do have to contend with CGA cards so our task is a little more complicated and, since it is one that we will need to repeat in other programs, we had better do it in a sub-routine. call ScreenWrite ; Write a byte and attribute loop Fast_06 ; Repeat for each character See the ScreenWrite routine in DISPLAY.ASM. The problem with CGAs occurs only when we write directly to video memory. If the 6845 CRT Controller attempts to fetch a character from the screen buffer whilst data is being written to that same location, the resulting interference will cause 'snow' or glitches to appear on the screen. We can prevent this happening by making sure that we only output to the display when the 6845 is not reading from it. Fortunately we do not have to wait long, since every 12 microseconds, the 6845 performs what is known as a horizontal retrace. During this, time the controller is not doing any reading so it is safe for our program to write to the display. We can detect when a horizontal retrace is in progress by testing bit zero of the video adaptor status port, its address is now in DX, remember? If this bit is set then a retrace is under way. In practice we let any current retrace complete and wait for the next to begin, before moving data. This ensures that we have we a full retrace period in which to place the character and attribute byte into their proper buffer addresses. Computers with monochrome adaptors and those with more modern versions of the CGA, do not experience 'snow' problems and, if you are sure that you will only be using FastPrint in such systems, you are welcome to leave out the routine. Me, I wouldn't bother. Using it ensures a good clean display, no matter which computer you use the program in, and the increase in processing time does not slow your programs down enough to be noticed. It is a lot faster than PRINT, at any rate. All that remains, now that our string has been output to the display, is to tidy up the stack and return safely to the QuickBASIC program that called us. First we recover all the registers that were pushed on entry to the routine. xor ax,ax ; Report success The QBNews Page 45 Volume 2, Number 2 May 31, 1991 Fast_07: pop di ; Clean up the stack pop si pop es pop bp The final return instruction must adjust the stack so that any parameters that were originally passed are now discarded. Our routine received four parameters, each of which took up two bytes of stack, so we issue a RET 8 to decrement the Stack Pointer by eight, ensuring that the correct return address is popped back into CS:IP. ret 8 ; Return to Quick BASIC FastPrint endp end Last of all, don't forget to tell the assembler where the current procedure and program end. Here is the full listing of FastPrint again. If you type it in, don't forget to include the code for the three procedures ScreenAddress, ScreenWrite and VideoType in their proper places before the final END directive. Notice that, since all the routines in the file are related to the video display, I have given it the name DISPLAY.ASM. I suggest that you use this name as well. We are going to add to it in the ensuing chapters. ASSEMBLY AND LINKING Since all the procedures we have written so far are concerned with the video display, I propose that we call the source file that contains them DISPLAY.ASM. Save it as such and then assemble it using the following command: MASM /V/W2/Z display; The /V (VERBOSE) switch instructs the assembler to report statistics such as the number of program lines and symbols processed. The /W2 switch sets the assembler's WARNING level to maximum. This instructs it to report statements that may result in inefficient code, as well as actual errors. It reports, for example, when you have issued a normal JMP instruction where a short jump will do. This helps you write tighter code. The /Z switch directs MASM to list any lines, where errors are found, on the screen. Another help in debugging. If you use an assembler other than MASM, of course, the above instructions do not apply. In this case consult your own assembler's documentation. If the assembly is successful, you will have a file, DISPLAY.OBJ, The QBNews Page 46 Volume 2, Number 2 May 31, 1991 containing the assembled object code. This is all ready for linking directly to compiled Quick BASIC programs which are to be run from the DOS command line. You can link it with the instruction: LINK yourprog DISPLAY; (where 'yourprog' is the name of your own program) If you want to use FastPrint in the QuickBASIC environment, you will also have to include it in a Quick Library. Use the Linker for this as well, the command is: LINK /QU DISPLAY,,,BQLB45.LIB; BQLB45.LIB is the standard library interface module supplied with QuickBASIC 4.5, (if you are using the Extended QuickBASIC which comes with MicroSoft's BASIC 7 PDS, the equivalent module is named QBXQLB.LIB). Whichever one you use, make sure that it is in the current directory when you issue the LINK command. You should now have a file DISPLAY.QLB. If you've got this far, you will, no doubt be anxious to try out FastPrint in a QuickBASIC program. Let's start with a small one which draws coloured boxes on the screen, just to test that it is working properly. We'll call it PANELS.BAS. Start up QuickBASIC with the following ... QB PANELS.BAS /L DISPLAY.QLB The first thing to do is to declare our routine as an external procedure. We won't be expecting it to return any value, so we declare it as a SUBprogram rather than a FUNCTION. DECLARE SUB FastPrint (BYVAL Row%, BYVAL Col%, Text$, BYVAL Attr%) Make sure that all the parameters are correctly listed and of the correct type. Integers for the Row, Column and Attribute arguments and a string for the text which we will be supplying. Okay, was that fast enough for you? What you have done, in effect, is to add your own extension to the QuickBASIC language. Provided that the procedure is properly declared at the top of your program, and the object file is linked or in an attached Quick Library, you can use FastPrint as easily as any other QuickBASIC statement or function. You don't even have to preface it with the CALL directive, nor do you need to enclose the parameter list with parentheses. If you are not sure how to work out proper values for the colours and attributes which you want your display to have, Appendix A goes into this in some detail. By all means experiment, your taste is very likely different from mine. You may even want to write special SUBS The QBNews Page 47 Volume 2, Number 2 May 31, 1991 for commonly-used attributes, for instance ... SUB RevPrint (Row%, Column%, Text$) STATIC FastPrint Row%, Column%, Text$, 112 END SUB ... calls FASTPRINT with the attribute set for REVERSE VIDEO text, Black characters on a White background (actually Green on monochrome monitors. SUB NormalPrint (Row%, Column%, Text$) STATIC FastPrint Row%, Column%, Text$, 7 END SUB ... calls FASTPRINT with the attribute set to normal video. Just like PRINT in fact, only faster. By the way, what did all those panels remind you of? That's right, WINDOWS. Well you'll just have to wait. The Windows will be opened in a future article. ---------------------------------------------------------------------- ROM-BIOS Video Services ROM-BIOS provides services which enable us to determine the current display mode, select the video page to which output will be directed, find the current cursor position and the character and attribute underneath it, and to selectively scroll a specified area of the display. They are accessed by loading the AH register with the number of the service required, and then issuing interrupt 16 (10 Hex). You may have to preset values into other registers, depending upon the service being called. Unless used for returned values, the contents of the BX, CX and DX registers are preserved across these calls. Likewise, the segment registers will be returned intact. You should not assume, however, that the contents of other registers will be maintained. Remember also that the system, used by the BIOS for numbering screen rows and columns, differs from QuickBASIC in that the x/y co-ordinate of the top-left corner of the screen is regarded as being 0,0 instead of 1,1. ---------------------------------------------------------------------- INT 10h Get current video display mode. all systems Call with: AH = 0Fh (15 decimal) Returns: AL = display mode (see table below) AH = screen width in columns BH = cureent display page Mode Resolution Colours Screen Mode Resolution Colours Screen ---------------------------------------------------------------------- 0 40 x 25 2 text 1 40 x 25 2 text 2 80 x 25 2 text 3 80 x 25 16 text 4 320 x 200 4 graphics 5 320 x 200 4 graphics The QBNews Page 48 Volume 2, Number 2 May 31, 1991 6 640 x 200 2 graphics 7 80 x 25 2 text 8 160 x 200 16 graphics 9 320 x 200 16 graphics 10 640 x 200 4 graphics 13 320 x 200 16 graphics 14 640 x 200 16 graphics 15 640 x 350 2 graphics 16 640 x 350 16 graphics 17 640 x 480 2 graphics 18 640 x 480 16 graphics 19 320 x 200 256 graphics ---------------------------------------------------------------------- Computers fitted with the Monochrome Display Adaptor (MDA) will only support mode 7, (80 x 25 character text). Modes 4 and 6 correspond to QuickBASIC's SCREEN 1 and SCREEN 2. Modes 8, 9 and 10 are only available on the IBM PCjr (and Tandy 1000). Modes 13 and up are only available on machines fitted with EGA or VGA adaptors, Mode 16 supports either 4 or 16 colours, depending upon the amount of video memory available. ROM-BIOS VIDEO SERVICES ---------------------------------------------------------------------- INT 10h Get Font information EGA, VGA, MCGA only Call with: AH = 11h (17 decimal) AL = 30h (48 decimal) BH = 00h for default character set. Returns: CX = point size (number of bytes per character) DL = screen height in rows - 1 ES:BP => address of character definition table Note: information about the alternative character sets provided by your video adaptor is obtained by loading BH with the appropriate font number. See your EGA or VGA reference manual for more details. ---------------------------------------------------------------------- INT 10h Get Configuration information EGA, VGA only Call with: AH = 12h (18 decimal) BL = 10h (16 decimal) Returns: BH = display type (0 = colour, 1 = monochrome) BL = video memory installed (0 = 64K, 1 = 128K, 2 = 192K, 3 = 256K) CX = Adaptor feature bits and DIP switch settings Notes: BL will still return a value of 3 even if more than 256K of video memory is installed. for more information about the meaning of the value returned in CX consult your EGA or VGA reference manual. ---------------------------------------------------------------------- The QBNews Page 49 Volume 2, Number 2 May 31, 1991 Using FastPrint with BASIC 7 If you intend using FastPrint with programs compiled under the BASIC 7 Professional Development System then a few changes are necessary. This is because BASIC 7 uses Far Strings which gives strings a memory segment of their own instead of putting them into DGROUP, the default Data Segment pointed to by DS. To enable you to find the address of a far string, BASIC 7 provides a built-in function called StringAddress. This must be declared at the head of your source code file between the .MODEL and .CODE directives, so that the Linker can fix-up references to the function when your program is built: extrn StringAddress: proc This done, you should rewrite the lines between labels Fast_04: and Fast_06: as follows: Fast_04: push ax ; Save display co-ordinates mov ax,[bp+8] ; Index to string descriptor push ax ; Call BASIC for call StringAddress ; string parameters pop bx ; Recover co-ordinates jcxz Fast_06 ; Abort if a null string mov ds,dx ; String segment to DS mov si,ax ; DS:SI==> string data mov ax,bx ; Co-ordinates to AX call ScreenAddress ; Calculate target address mov ax,[bp+6] ; Get display attribute xchg ah,al ; into AH cld ; Clear direction flag Notice that, just like QuickBASIC 4.5, we still have to obtain the address of the string descriptor from our argument list. With BASIC 7, however, we pass it on to StringAddress by pushing it back onto the stack before calling this function. On return, BASIC 7 will have set our registers as follows: DX = segment of target string CX = length of target string AX = offset of target string The explanation in the BASIC 7 manual and on-line Help implies that you need to call a seperate function, StringLength, to find the length of the string. In practice this is not necessary since StringAddress, itself, returns this information in CX. ********************************************************************** Christy Gemmell resides in England and was the major author of the Waite Group book QuickBASIC Bible. Christy also has a shareware called the Assembly-Language Toolbox for QuickBASIC. Christy can be reached in care of this newsletter. **********************************************************************