Statistics

Members: 1927
News: 293
Web Links: 1
Visitors: 3962465

Who's Online

We have 1 guest online
Damn Vulnerable LinuxDamn Vulnerable Linux (DVL) is a Linux-based (modified Damn Small Linux) tool for IT-Security & IT-Anti- Security and Attack & Defense. [CLICK HERE FOR MORE INFOS! ]

Featured Conference Video

T16-Recon2006-Joe_Stewart-OllyBonE.gif OllyBone - Semi-Automatic Unpacking on IA-32. View the conference video here!
Home arrow Articles - Black Hat Methods arrow Assembly arrow Programming for the one and only universal graphics mode
Programming for the one and only universal graphics mode
User Rating: / 1
PoorBest 
Written by Jan Verhoeven   


If you need to write a graphics routine that has a reasonable resolution and which is nearly always present, there is just one choice: mode 12h or the well known 640 x 480 x 16. This mode is the highest resolution mode which is always available in all VGA cards.
800 x 600 is better but it either needs a VESA driver installed or the user must himself figure out how to switch the machine to that mode. Not an easy task for the majority of "experienced Windows users" (isn't this a paradox?).

Mode 12h is treated as a worst case by many Superior Operating Systems. But for most purposes it is just fine. It's fast, reasonably easy to use and it is omni present.

That's why I decided to port my textmode windows to this graphics mode.

 

 

The application.

I built a simple AD converter that measures voltages and converts them into digits. The ADC fits on a COM port and is completely controlled from software. The idea was to have different reference voltages, sample rates, scaling factors, a bar graph display and a 4 digit LED-style read-out. And in the bottom window there is a "recorder" that plots pixels in real-time.

If all parts have been explained I might post the full package (the sources, the schematics and such) so that everyone can build one for your own.

How to switch to Mode 12h?


Going to mode 12h is easy. Just use the BIOS interrupt 10h as follows:
        mov     ax, 012
int     010

and you're in. Remember, I use A86 syntax, so all numbers starting with a nought are considered hexadecimal.

Plotting in a graphics screen.


Now that we're in Mode 012, we should also try to fill that clear black rectangle. But first we should define a way of remembering WHERE to put our cute little dots.

For all my plotting, I use the following structure:

-------------------------------- Window Information Block ------ Infoblk1 STRUC

    Win_X    dw    ?        ; top-left window position, X and ...
Win_Y    dw    ?        ;     ... Y
Win_wid  dw    ?        ; window width and ...
Win_hgt  dw    ?        ;     ... height
CurrX    dw    ?        ; within window, current X-coordinate, ...
CurrY    dw    ?        ;     ... and Y

DeltaX dw ?
DeltaY dw ?

    Indent   dw    ?        ; Indentation for characters in PIXELS!
Multiply dw    ?        ; screenwidth handler
Watte01  dw    ?        ;
BoxCol   db    ?        ;     border colour
TxtCol   db    ?        ;       text colour
BckCol   db    ?        ; background colour
MenuCol  db    ?        ;  menu text colour
ENDS

-------------------------------- Window Information Block ------

It will be clear after looking into this list, that each InfoBlock describes a window, a rectangular portion of the screen, which is treated as a unity.

Each window is defined by the topleft (x,y) coordinates and the window width and height. Knowing these four words, the window is defined and fixed on screen. If the window is to be moved, just adjust the topleft (x,y) position.

Since it is handy to know where in this window we are plotting, I defined two more X and Y values: "CurrX" and "CurrY". When a request to (un)plot is made, it will start on these coordinates.

For line drawing and such there are the "DeltaX" and "DeltaY" variables. The former is for horizontal lines, the latter for vertical lines.

Now that we have our fancy window, where we can plot and draw lines, we also need some text to see what it's all supposed to be about. The text is plotted at the CurrX and CurrY postions. Each character is PLOTTED there, so tokens can be put at ANY location on screen, not just on byte boundaries.

For nice and easy alignments, I defined the variable "Indent" which defines how many pixels from the left or right margin must remain blank.

Since this software should be as easy to adapt to other resolutions as possible, there is a need for a "Multiply" variable. This is filled with the offset address of a dedicated screen multiplier routine. In Mode 012 there are 640 pixels on a line. That's 80 bytes. So in order to calculate the pixel address you need to use the following formula:

PixAddr = CurrY * 80 + CurrX / 8

So we need a set of damned fast Mul_80 routines. If needed you can make some of them and at init-time find out the CPU and hardware and assign a suitable routine and fill it in in the Window definition structures.

The "Watte01" field is just a filler. Reserved by me.

Since the Mode 012 has 16 colours to spare we should also use them. Therefore I set up space for 4 colours: Box-, Text-, Background- and Menu-colours. Each printing routine will make sure the right colour is set.

It will be clear that each window is very flexible to use. If the position is wrong, just change a few numbers. Also if the colours are not optimal. And by having several windows assigned to the same area on screen, you can easily build special effects:

    fullscrn dw     0,  0,640,480, 0, 0, 0, 0, 4, mul_80, 0
db    12, 14,  3, 15               ; main screen window

FullScrn just describes the complete screen. It is used for some very general printing an plotting tasks. It starts at topleft (0,0) and is 640 wide and 480 high.

    ParWin2  dw     5, 30,630,150, 8, 9, 0, 0, 4, mul_80, 0
db    10, 11,  3, 11               ; Parameter window

This is a window which is a subwindow of the Full Screen for storing data and parameters.

    PlotWin  dw     5,195,630,260, 0, 0, 0, 0, 4, mul_80, 0
db     9, 15,  3,  7               ; Virtual plotting window

This is the Virtual Plotting Window. It has some text, plus the actual plotting window:

    PlotWin2 dw     6,196,628,256, 0, 0, 0, 0, 4, mul_80, 0
db     9, 15,  3,  7               ; Actual plotting window

This is the place where the pixels live. It starts one pixel down/right of the virtual window and also ends one pixel short of it. The reason for making this "dummy" window structure was that this way there is no need for an elaborate checking of extreme ends of the window while erasing pixels. On the extremes of the "Virtual Plotting Window" there are the pixels that make up a nice coloured box. It looks not nice when these lines are erased. And the easiest way to prevent this was by defining two separate windows: one for constructing the box and one for the actual work.

The 4 digit LED-style read-out is also controlled by four different windows. Each digit has its own window definition:

------------ Digit Space ------------------------------- Start ---

DigSpac1 dw 16, 90, 40, 50, 0, 0, 0, 0, 0, mul_80, 0

db 9, 11, 14, 3 ; Digital display, digit 1, MSD DigSpac2 dw 56, 90, 40, 50, 0, 0, 0, 0, 0, mul_80, 0

db 9, 11, 14, 3 ; Digital display, digit 2 DigSpac3 dw 96, 90, 40, 50, 0, 0, 0, 0, 0, mul_80, 0

db 9, 11, 12, 3 ; Digital display. digit 3 DigSpac4 dw 136, 90, 40, 50, 0, 0, 0, 0, 0, mul_80, 0

db 9, 11, 12, 3 ; Digital display, digit 4, LSD

MSD = Most Significant Digit LSD = Least Significant Digit

------------ Digit Space -------------------------------- End ----

This way it is convenient to allign the digits on screen. As with normal LEDstyle digits, the seven segments of them are drawn piece by piece. And erased if necessary.

As you will know from voltmeters, the MSD is the least likely to change in time and the LSD is most likely to be different between any two samples. So in a way it is necessary to control erasing of just one digit without massive software overheads. Therefore I again chose to use a separate window for each digit. It makes erasing the digit easier and independent of the other three.

Something else to observe is, that the two or three digits behind the decimal point have another colour from those before it. This way the user can easily see the approximate magnitude of the number without having to search for a decimal point. This is accomplished easily by having different BckCols in the LSD windows.

This all costs a few bytes extra, but it saves a lot of coding.

How to quickly load a segment register.


Segment registers cannot be loaded with immediate data. So you normally put a register on the stack and use that to transfer the constant to the actual segment register. This is not necessary. It can be done much easier like below:

VGA_base dw 0A000 ; for ease of loading segment registers

And the corresponding code:

mov es, [VGA_base]

The detour via the stack or via AX takes more cycles and bytes.

Defining what to print.


In a graphics screen there are an awful lot of places where to store our text. So we need a way to define where to put which tokens. For this I use the following construct:

-------------- Topic ----------------------------------- Start ---

    Topic MACRO             ; start of printing message
dw   #1, #2
db   #3, #4
#EM
TopicEnd MACRO          ; topics stop here
dw   0F000
#EM
Topic 180, 9, 'Start : '

ParaStrt db 'Manual ', 0

Topic 9, 28, 'Power : '
ParaPowr db 'OFF', 0

Topic 360, 55, 'Group : '
ParaGrup db '16 ', 0

TopicEnd
-------------- Topic ------------------------------------ End ----

The Topic Macro puts the first two arguments (the new values for CurrX and CurrY) in the first two WORD positions of the definition table. The actual text is then put in the BYTE positions. In most cases there will be no #4 argument, but A86 doesn't care about that.

Each "to-print" table is shut down by an EndTopic Macro. It defines a new CurrX of -4096. That clearly is out of range, so this is end of table. In normal operation, small negative values of CurrX and CurrY are accepted and taken care of, although it can be dangerous to use this feature.

Multiplying by 80.


On all CPU's form the 486, the MUL instruction is single cycle, so it'll be damn fast. For all older CPU's, the following code could mean some significant speed increases:

-------------------- Multiply ------------------------ Start ----

    mul_80:  push  bx               ; PixAddr in Mode 012
shl   ax, 4
mov   bx, ax           ; bx = 16 x SCR_Y
shl   ax, 2            ; ax = 64 x SCR_Y
add   ax, bx           ; ax = 80 x SCR_Y
pop   bx
ret

-------------------- Multiply ------------------------- End -----

This routine is used over and over again, so a few microseconds more or less will make a big difference.

Where to leave our pixels?


Suppose you need to plot pixel (3,0). That's an easy one. It will fit in the very first byte of the VGA memory array. It's segment is 0A000 and it's offset is plain 0.
But not the full byte, since that would produce a line. No, we need to access bit 4 of byte 0.

Yes, the first pixel is bit 7 of byte 0 and the 8th pixel is bit 0 of byte 0. Or, in index-language, CurrX = 0 addresses bit 7, and so on.

So we need to invert the screenposition into a bitposition. We'll come to that later. Suppose, by some sheer magic, we succeeded in making that conversion, we still need to tell the VGA which bit is involved. That's done by means of the following routine:

--------------------- SetMask ------------------------ Start -------

    SetMask: push  dx               ; ah = mask
mov   dx, 03CE
mov   al, 8
out   dx, ax           ; set bit mask
pop   dx
ret

--------------------- SetMask ------------------------- End --------

This is an optimized routine. The VGA is a 16 bit card, so we can use 16 bit I/O instructions for adjacent I/O ports. The construct:

             mov   al, 8
out   dx, ax           ; set bit mask

is identical to:

             mov   al, 8
out   dx, al
inc   dx
mov   al, ah
out   dx, al

Anyway, the plottingmask is defined to be as loaded in the AH register. We can put any value in AH, not just one pixel, but also "no pixels" and "all pixels".

Defining colour in Mode 012.


Colours to use during plotting are defined in a comparable fashion:

--------------------- Set Colour --------------------- Start -------

    SetColr: push  dx               ; ah = colour
mov   dx, 03C4
mov   al, 2
out   dx, ax           ; select page register and colour
pop   dx
ret

--------------------- Set Colour ---------------------- End --------

In Mode 013 you just can load a bytevalue colour into a memory location and that's it. So that's an ultrafast resolution, but at the price of resolution.

In Mode 012 we define colour with a series of I/O instructions. If a colour got set, it remains active until canceled by another SetColr call. Try to remember this when all on a sudden all kinds of fancy colours start to appear on screen....

Where to put the pixel?


I have presented the formula some paragrpahs before this one. Basically we work with virtual coordinates and must translate these to real coordinates before trying to calculate an address. This is done by:

------------------ VGA memory address ---------------- Start -------

    VGaddr:                         ; calculate address in VGA memory
mov   es, [VGA_base]   ; quickly load segment register
mov   ax, [di.CurrY]   ; ax = current Y
add   ax, [di.Win_Y]   ; adjust for window offset
call  [di.Multiply]    ; multiply by bytes per row
mov   bx, [di.CurrX]   ; bx = current X
add   bx, [di.Win_X]   ; adjust for window offset
shr   bx, 3            ; divide by 8
add   bx, ax           ; bx = index address into video segment
ret

------------------ VGA memory address ----------------- End --------

It's all fairly straightforward.

How do we plot pixels in Mode 012?


This is a silly process. We cannot access all the 4 colour planes at once, so we have used SetColr to define which colourplanes are to be affected. This all is rather complicated. You may either believe me on my word, or consult a 1200 page reference....

Now that we're ready to plot pixels, we do so by the following code:

------------------ VgaPlot -------------------- Start --------------

    VgaPlot: mov   al, [es:bx]      ;  Do the actual plotting
mov   al, [ToPlot]
mov   [es:bx], al
ret

------------------ VgaPlot --------------------- End ---------------

The first line is a read command. It notifies the VGA controller about the address of the pixelbyte. The resulting data from the read is of no concern. We immediately replace it with the value of "ToPlot". For plotting there is a value of "FF" in this byte and for erasing there is a "00" in it.

After this comes the actual plotting function. The write to the specified address sets the pixels as defined by AL and SetMask.

Adding it all up gives the following code to really plot a pixel:

-------- PlotPix ------------------------------- Start ----------- PlotPix: push ax, bx, cx, es ; plot a point on screen

             call  VGaddr
mov   cx, [di.CurrX]   ; calculate plottingmask
add   cx, [di.Win_X]
and   cx, 0111xB       ; cl = position in byte
mov   ah, 080
shr   ah, cl           ; now move the high bit backwards...
call  SetMask          ; use it to set mask
call  VgaPlot          ; and do the plotting
pop   es, cx, bx, ax
ret

-------- PlotPix -------------------------------- End ------------

That's it to plot a pixel: just a few calls to some procedures we defined earlier on. The msjority of this procedure is comprised of the way to find the actual bit-position in the VGA memory byte. Remember, to plot pixel 0 we need bit 7!
Therefore we load CX with the current X value, correct this for the current window position and isolate the lower 3 bits. These indicate the position of the pixel in screenmemory.

             mov   cx, [di.CurrX]   ; calculate plottingmask
add   cx, [di.Win_X]
and   cx, 0111xB       ; cl = position in byte

At this point, CL contains the n-th bit in this byte. So I load AH with the binary pattern 10000000 and shift it right until the corresponding bit position is reached:

             mov   ah, 080
shr   ah, cl           ; now move the high bit backwards...

I don't know if there are batches of Intel CPU's that have a problem with the SHR instruction is CL equals zero, but I have not yet noticed any.

Lines: series of pixels.


There are three kinds of lines: horizontal, vertical and sloped ones. Vertical lines are plotted pixel by pixel since all of them end up in different bytes of VGA memory. Sloped lines are best taken care of by a Bresenham-style line drawing algorithm (although the digital differential analyser is better).

Horizontal lines are a different kind of line. In these, several adjacent pixels are plotted. And adjacent pixels mainly are in the same VGA memory byte. Therefore I made two horizontal line drawers. The one for short lines (less than 17 pixels) just plots the pixels one by one. The other algorithm, for lines of 17 pixels or more, tries to fill VGA memory with as much byte writes as possible.

Taking care of longer horizontal lines.


Suppose our line is composed as follows:
    First       1       2      3 ... K    Last    ; byte in video memory
......## ######## ######## ##...## ###.....  ; # = pixel to be set

So our line starts at pixel 6 (i.e. bit 1) of VGA memory byte "First". Next it lasts for N pixels and the last pixel to plot is pixel 2 (or bit 5). We need some variables to calculate how to proceed with this in the shortest possible time. This needs some calculations, so for short lines the math overhead is more work than the actual plotting will take up.

    First       1       2      3 ... K    Last    ; byte in video memory
......## ######## ######## ##...## ###.....  ; # = pixel to be set

We first need to know the E-value which describes the number of pixels to plot in the very first byte. The E-value is calculated as follows:

E-val = 8 - ((CurrX + Win_X) AND 7)

Now we know the number of pixels to plot in the very first VGA memory location. It would however come in handy if we would know with which plotting mask this would correspond. That's why we use it to derive the E-mask:

E-mask = FF shr ((8 - E-val) AND 7)

Next we need to know how many pixels there need to be plotted in the last memory location. L-value and L-mask are determined as follows:

L-val = (Total - E-val) AND 7
L-mask = 080 sar L-val

With the SAR we shift signbits to the right until the number of pixels corresponds with the number of bits in the mask.

The last parameter we need to know is the actual speeding-up part: the full bytes that can be plotted. The octet-part of the routine. We do this as follows:

K-val = (T - E-val - L-val)/8

Now it also becomes clear why I kept the E-val and L-val parameters. They're just needed for getting the right value for K-val.

There is, however one exceptional situation. Suppose the line we need to plot is 26 pixels long, starting at pixel 6. This would produce the values:

  E-val = 2                                     E-mask = 00000011
L-val = (26 - 2) AND 7 = 24 AND 7 = 0         L-mask = 00000000

K-val = (26 - 2 - 0)/8 = 3

So, if the line ends on a byte boundary, we may NOT try to plot <A LOT> of pixels past it (in a plotting loop that starts with CX = 0).

What the H_line procedure does is no more than what I decribed above. Here comes the source:

-------- H_Line -------------------------------- Start -----------

    L0:      mov   cx, [di.DeltaX]      ; do a short line
L1:      call  PlotPix              ; by just repeating a single pixel-
inc   [di.CurrX]           ; plot and update of CurrX
loop  L1                   ; until done
pop   es, cx, bx, ax
ret
H_Line:  push  ax, bx, cx, es       ; optimized horizontal line drawing
cmp   [di.DeltaX], 17      ; too few pixels for a bulk draw?
jb    L0
mov   cx, [di.CurrX]       ; do a long line
add   cx, [di.Win_X]       ; first get the E-value as described
and   cx, 0111xB           ;   above
mov   bx, 8
sub   bx, cx
mov   [E_val], bx          ; pixels to plot in leftmost byte
mov   al, 0FF              ; now compose the mask to use there
shr   al, cl
mov   [E_mask], al         ; and store it in memory
mov   cx, [di.DeltaX]      ; CX = length of line
sub   cx, [E_val]          ; compensate for first-byte pixels
mov   ax, cx
and   ax, 0111xB           ; this many pixels in rigthmost byte
mov   [L_val], ax          ; and store it in memory
sub   cx, ax               ; CX = number of pixels inbetween
shr   cx, 3                ; divide by 8 pixels per byte
mov   [K_val], cx          ; number of "full" bytes to plot
clr   al                   ; AL := 0
mov   cx, [L_val]          ; prepare to compose L-mask
cmp   cx, 0                ; any bits in "last byte"
IF ne mov  al, bit 7       ; if any bits, setup AH register
dec   cx                   ; compensate for pixel 0, ...
sar   al, cl               ; ... compose plotting mask and ...
mov   [L_mask], al         ; ... store it into memory.
; that's it. Let's plot!
call  VGaddr               ; load BX with address of byte in
; VGA memory
mov   ah, [E_mask]
call  SetMask              ; set plotting mask and ...
call  VgaPlot              ; ... plot leftmost part
inc   bx                   ; get adjacent address
mov   cx, [K_val]          ; prepare for bulk-filling
jcxz  >L4                  ; if nothing to do, jump out
mov   ah, 0FF              ; else set ALL PIXELS mask
call  SetMask
L3:      call  VgaPlot              ; plot middle part
inc   bx
loop  L3                   ; until done
L4:      mov   ah, [L_mask]
call  SetMask
call  VgaPlot              ; plot remaining pixels
mov   ax, [di.DeltaX]
add   [di.CurrX], ax       ; make sure CurrX is updated
pop   es, cx, bx, ax       ; and git outa'here
ret

-------- H_Line --------------------------------- End ------------

The preparations are the bulk of the work, but after that is done, the line is plotted with the lowest amount of I/O overhead.

Vertical lines.


Vertical lines are simply plot by repeatedly calling PlotPix. It's so simple that neither need nor want to elaborate on it:

-------- VertLin ------------------------------- Start -----------

    VertLin: push  cx                   ; draw a vertical line
mov   cx, [di.DeltaY]
L0:      call  PlotPix
inc   [di.CurrY]           ; adjust Y coordinate
loop  L0                   ; but not X value!
pop   cx
ret

-------- VertLin -------------------------------- End ------------

What to do with linedrawing functions?


Now that we can draw lines, we can also draw boxes and window borders. This all looks very professional and the overview of a program is enhanced considerably. Try to figure out how to make the box-drawers by yourself.

Plotting text.


Now that we have windows that can be put at any plotting position, we also need to be able to position text at any position. It doesn't look nice if different windows force text to default to byte boundaries. And with the experience we got from the H_line function, we are able to make a character plotter that puts text on screen at ANY position.

I use a 9 x 16 character set. The nineth bit is just always blank, but it enhances readability considerably. The pixels in the bitmap are all 8 bits wide and 16 pixels tall.

In exceptional cases, the bitmaps can be plotted at byte boundaries. In 85+ % of the time this will not be the case. Therefore I do the following:

  • do some positioning math first
  • repeat 16 times:
    • load the byte of the bitmap in AH
    • shift AX to the right the correct number of pixels
    • plot the AH part
  • if plotting on a byte boundary, we're done, else
    • repeat 16 times:
      • load the byte of the bitmap in AH
      • shift AX to the right the correct number of pixels
      • plot the AL part

Let's just have a look:

-------- PutChar ------------------------------- Start -----------

    L0:      add   [di.CurrY], 16       ; process 'LF'
L1:      pop   es, si, cx, bx
ret
L2:      mov   bx, [di.Indent]      ; process 'CR'
mov   [di.CurrX], bx
jmp   L1
PutChar: push  bx, cx, si, es       ; print char in al at (x,y)
cmp   al, lf
je    L0
cmp   al, cr
je    L2
mov   bx, [di.CurrX]
add   bx, CHR_WID
cmp   bx, [di.Win_wid]     ; still safe to print character?
jbe   >L3                  ; if so, skip over this part
mov   bx, [di.Indent]
mov   [di.CurrX], bx       ; mimick 'CR'
add   [di.CurrY], 16       ; mimick 'LF'
L3:      mov   cx, [di.CurrX]
add   cx, [di.Win_X]
and   cx, 0111xB
mov   [C_val], cl          ; store shiftcount for masks
mov   bx, 0FF00
shr   bx, cl               ; setup plotting mask and ...
mov   [P_mask], bx         ;     ... store it
clr   ah                   ; ax = ASCII code
mov   si, ax               ; make address of pixels in bitmap
shl   si, 4
add   si, offset bitmap
call  VGaddr               ; bx = -> in video memory
mov   ax, [P_mask]         ; only the AH part is used ...
call  SetMask              ; ... here.
mov   cx, 16               ; 16 pixel lines per token
L4:      push  cx                   ; we're in the loop now
mov   ah, [si]             ; AH = pixelpattern
clr   al                   ; AL = empty
mov   cl, [C_val]          ; get shiftcount
shr   ax, cl               ; distribute pixelBYTE across a WORD
mov   cl, [es:bx]          ; dummy read, CL is expendable
mov   [es:bx], ah          ; actual plotting of this half
add   bx, 80               ; point to next pixelbyte address
inc   si                   ; next pixeldata address
pop   cx
loop  L4                   ; and loop back
sub   bx, 16 * 80 - 1      ; back to original position
mov   ax, [P_mask]
cmp   al, 0                ; if nothing to do, ...
je    >L6                  ; ... skip this chapter
mov   ah, al               ; else repeat the lot for the rightcall  
SetMask              ; most pixels....
mov   cx, 16
sub   si, cx               ; correct SI
L5:      push  cx
mov   ah, [si]
clr   al
mov   cl, [C_val]
shr   ax, cl
mov   cl, [es:bx]
mov   [es:bx], al
add   bx, 80
inc   si
pop   cx
loop  L5
L6:      add   [di.CurrX], CHR_WID  ; adjust CurrX value before ...
jmp   L1                   ; ... getting a hike

-------- PutChar -------------------------------- End ------------

So far for plotting text. This routine will dump any character in any place of the graphics screen. But it needs a CurrX and a CurrY value to know where to plot things. This is both an advantage and a disadvantage. The advantage is that we can plot ANYWHERE we like. The disadvantage is that we need to elaborately specify CurrX and CurrY before the text is where we would like to have it.

That's why I made the constrcut with the Topic and TopicEnd macro's, as described above.

Here comes the code for printing a table on screen. We spent a lot of time on the preparations, and this is the stage where it is going to pay off. Look how much code we need for printing neat sets of tokens and characters on screen.

-------- Print --------------------------------- Start -----------

    print:   mov   ah, [di.TxtCol]      ; print a table of text
call  SetColr
L0:      lodsw                      ; get Xpos
cmp   ax, 0F000            ; end of table?
je    ret                  ; exit, if so
mov   [di.CurrX], ax
lodsw                      ; get Ypos
mov   [di.CurrY], ax
L1:      lodsb                      ; get text
cmp   al, 0
je    L0
call  putchar              ; and print it
jmp   L1                   ; until this line is done

-------- Print ---------------------------------- End ------------

Wit this approach, and starting from a working (empty) framework of routines, you can design the userinterface of your software within the hour. And it will look just fine.
The actual code is then the only thing you need to worry about.....

Having such routines, which have been tested and found reliable, you make the user interface easily and are able to concentrate on the actual coding the maximum amount of time. If the screen needs another layout (since you couldn't realize the function you considered), just change a few entries in the table. Many times just the X or Y values need some adjustment for better lining up, or for regrouping. No need to worry about the order of the plotting. Just make sure that the correct window is selected (for the colours) and that the table is terminated by a TopicEnd.

Conclusion.


So far my elaboration on the VGA mode 12h. Again, I would rather use 800 x 600 but that mode is not standardised. VGA 12h is standard on all VGA cards, so it's the best we can universally get and for many applications it is more than enough.

Please try to make the BoxDrawing function. I will submit the "solution" to the next issue. For future issues I will start working on an explanation about mouse-usage. This little rodent is nice to control many applications. If the screen is well layed out, you don't need the keyboard for data entry. Just drag the mouse along the screen and poke him in the eye.

The bitmap data for the character generator can be obtained from

{http://asmjournal.freeservers.com/supplements/univ-vmode.html} where the complete text of the article has been archived.