Virtual screen

Why should we use virtual screens when the physical screen is available and it's a small memory area? Well, there are many reasons.
- First of all, the video memory is not as fast as the main ram memory. (Buffers are fast. General fact.)
- There are several limitations when we use the video memory. (Flickering, no way to scroll etc.)
- The video memory organization is given. You can't realize your own screen structure.

In this example I will show a virtual screen which contains graphical characters. This screen has 8x8 pixels cells and each cell can have a charachter. The screen resolution will be 40x25 chars, so it's exactly 320x200 pixels. A graphical character code is stored in 8bits, so the screen area uses 1000 bytes. For each code we define a box bitmap which has 8*8=64 bytes, so the graphic set needs 256*8*8 = 16384 bytes.

The following pictures shows a graphical charset. In this case we have only a few characters defined. The blue boxes are empty. The first yellow box represents a "space" character.

A (possible) game screen built with these chars is something like this:

That's it. There is a quasi screen in the physical memory, which contains 40x25 graphic chars and this makes the exactly same dimensions as for the real screen. We need a 320x200 bytes memory buffer to build the screen. Becase this memory is not too big, and nowadays the CPUs are as fast as hell, we can regenerate the whole screen every time when we want to show something new. (Everything will be synchronized to the vertical retrace, but this comes later. Also later on we are going to show sprites on this virtual screen.)

When the new phase of the screen is done (it's rendered), we simply move - copy over - each pixels from virtual screen to the physical screen.

Modify the gfx256 unix: vgfx256, the unit for our virtual screen

From the previous lessons we have several functions and procedures who work with the physical screen. Now, if we want to do the same things on the virtual screen, we have to change the destination memory area. For the physical screen we had the predefined variable SegA000 which points to the screen memory. Becase the virtual screen will have exactly the same structure as the physcial one, the only one difference is the destination address.

Remember, in each procedure we use a simple MOV ES,SegA000 instruction to address the screen, then the register DI points to the right pixels. The solution is obvious then. Instead of using the SegA000 variable, we're gonna use our own segment pointer that addresses our virtual screen. I call the modified unit vgfx256. This unit does have a new variable called ScreenSeg.

Pay attention! This variable will point to the segment A000 by default, so if you use the unit, there is no difference at all. BUT! If you allocate a new memory area and set the ScreenSeg to this area, the vgfx256 will work with that new memory, which is the virtual screen from then on. Clear?

There must be some other procedures to handle the virtual screen. They are quite simple, though.

procedure InitVirtualScreen;
begin
 if vsptr=nil then getmem(vsptr,64000);
 ScreenSeg:=seg(vsptr^);
end;

procedure DoneVirtualScreen;
begin
 if vsptr<>nil then freemem(vsptr,64000);
end;

procedure ShowVirtualScreen; assembler;
asm
   push DS
   cld
   mov  cx,32000
   xor  si,si
   xor  di,di
   mov  ES,SegA000
   mov  DS,ScreenSeg
   rep  movsw
   pop  DS
end;

The virtual character screen

This is the method how we draw the virtual character screen:

    INIT pointers (charscreen, charbitmaps, virtual screen)
    LOOP for the rows (25)
      LOOP for the columns (40)
        GET graphchar code
        CALC graph bitmap address (ADDR:=64*CODE)
        SHOW one graphchar
        NEXT (advance pointers)        
      END LOOP for the columns 
    END LOOP for the rows
Of course, the procedure is written completely in assembly. Both the charscreen and the character bitmaps are stored in the pascal's data segment, therefore DS will point them. The virtual screen will be addressed by the ES:DI pointer. We use BX to access the charscreen, while SI is for reach the bitmaps.

This is the inner loop that shows one char. Note that we use the MOVSW to show 2 pixels.

   { 1 graphic char }
   mov  cl,8
@cloop:
   movsw
   movsw
   movsw
   movsw                           { 8 pixels (4*2 pixels)                }
   add  di,320-8                   { next line                            }
   dec  cl
   jnz  @cloop
I hope that's clear. This small code shows one 8x8 pixels box - a.k.a graphic char - on the virtual screen. DS:SI points to the graphic char's source bitmap, while ES:DI points to the virtual screen.

The first outer loop is for drawing one character line. Here CH counts the characters, while BX points to the character array. ES:DI has the destination (virtual screen) address.

   mov  ch,40
@cline:
   { character calculation }
   xor  ax,ax
   mov  al,[bx]                    { get the charcode from the charscreen }
   inc  bx                         { next char                            }
   shl  ax,6                       { CODE=CODE shl 6, so CODE=CODE*64 !   }
   mov  si,chrsOffs                { base address for the bitmaps         }
   add  si,ax                      { done, SI got the source address      }

   { 1 graphic char, see above }

   add  di,8-8*320                 { position for the next char           }
   dec  ch
   jnz  @cline
The other outer loop is for the rows. We have 25 character rows on the screen. To make life easier, I save the DI in the stack, therefore the address calculation for the next charline is a piece of cake.
   mov  dx,25
@sloop:
   push di

   { show a charline, see the code above ... }

   pop  di
   add  di,8*320
   dec  dx
   jnz  @sloop
Now put all together. The procedure takes two parameters.
  - scrnOffs: offset for the character screen in the data segment
  - chrsOffs: offset for the character bitmaps in the data segment

procedure VirtualCharScreen(scrnOffs,chrsOffs:word); assembler;
asm
   cld
   xor  di,di
   mov  ES,ScreenSeg
   mov  bx,scrnOffs

   mov  dx,25
@sloop:
   push di
   mov  ch,40
@cline:
   { character calculation }
   xor  ax,ax
   mov  al,[bx]
   inc  bx
   shl  ax,6
   mov  si,chrsOffs
   add  si,ax

   { 1 graphic char }
   mov  cl,8
@cloop:
   movsw
   movsw
   movsw
   movsw
   add  di,320-8
   dec  cl
   jnz  @cloop
   add  di,8-8*320
   dec  ch
   jnz  @cline
   pop  di
   add  di,8*320
   dec  dx
   jnz  @sloop
end;

Test

This is the screen that we are going to show. The test program has some new things. It should be clear though, it's just plain pascal. Check out the comments for the details.

program vtest1;

uses vgfx256;

var
   gfxset : array[0..16383] of byte;     { the bitmap data - 8*8*256 bytes    }
     vscr : array[0..24,0..39] of byte;  { the character screen - 40*25 bytes }

procedure LoadGfx(fname:string);         { load the bitmap data from file     }
var
   f : file;
begin
  assign(f,fname);
  reset(f,1);
  blockread(f,gfxset,16384);
  close(f);
end;

procedure floor(x,y,w:integer);          { show floor - horizontal blue walls on the picture }
var i : integer;
begin
 for i:=0 to w-1 do vscr[y,x+i]:=2;
end;

procedure ladder(x,y,h:integer);         { show ladder - the organge ladders on the picture }
var i : integer;
begin
 for i:=0 to h-1 do
  begin
   vscr[y+i,x]:=6;
   vscr[y+i,x+1]:=7;
  end;
end;

var
  i,j : integer;
  x,y : integer;

begin
 graph320x200;
 InitVirtualScreen;       { init the virtual screen        }

 LoadGfx('mychar.dat');   { load the bitmaps for the chars }

 fillchar(vscr,1000,0);   { clear the charscreen (spaces)  }

 { ****************************** draw our game map ****************************** }
 for i:=0 to 39 do        { red wall - horizontal frame lines }
  begin
   vscr[0,i]:=1; vscr[24,i]:=1;
  end;

 for i:=1 to 23 do        { red wall - vertical frame lines   }
  begin
   vscr[i,0]:=1; vscr[i,39]:=1;
  end;

 floor(2,5,8); floor(2,9,25); floor(5,14,25); floor(12,19,15); { blue floors }

 for i:=0 to 24 do  { some random items only on the floor }
  for j:=0 to 39 do
   begin
    if (vscr[i,j]=2) and (random(100)>90) then vscr[i-1,j]:=3;
    if (vscr[i,j]=2) and (random(100)>80) then vscr[i-1,j]:=5;
   end;

 ladder(5,4,5); ladder(20,8,11); ladder(15,18,6); { ladders }
 { ******************************************************************************* }

 { show the characters on the virtual screen }
 VirtualcharScreen(ofs(vscr),ofs(gfxset));

 { show the rendered screen }
 ShowVirtualScreen;

 readln;
 { free the allocated buffer } 
 DoneVirtualScreen;
 text80x25;
end.

Downloads: [ Vgfx256 unit | vtest1 | Grpahic charset ]

Back