Draw pixels and horizontal/vertical lines

Coordinate to address calculation

When we are talking about drawing, we think of coordinates of a graphic object. That's fine, but as we know from the lesson 1, the screen is just a simple linear memory area, which contains 320x200=64000 bytes. We assume, that the point(0,0) is the leftmost/upper pixel on the screen. This is general for computers, though it's not the way we use coordinates in Math.

If we would like to use coordinates, we have to calculate the right screen address for a given (X,Y) pair. It is really simple, because we know that each scanline has a constant size 320 bytes. Here we go:
   ADDRESS := 320*Y + X ;
The same in assembly:
   mov  ax,320 
   mul  y       
   add  ax,x     { now ax has the address }
We don't have to deal with the segment part of the address, because the entire screen area fits into one segment (it's smaller than 64K).
Therefore, the address is just the offset part of the real mode address.

Remember for this calculation! It's important and we're gonna use it many times.

Setting a pixel

So, let's write our first Pixel procedure:

procedure Pixel(X,Y:integer; Color:byte);
begin
  mem[$a000:320*Y+X]:=Color;
end;

Cool. It would work, though it does not check the parameter values, so giving a wrong parameter can ruin the result. Anyway, why don't we turn this small routine into assembly? We do...

procedure Pixel(x,y,color:integer); assembler;
asm
   mov  ES,SegA000      { Predefined segment base variable }
   mov  ax,320	
   mul  y               { 1st step: ax = 320*y     }
   add  ax,x            { 2nd step: ax = 320*y + x }
   mov  bx,ax           { move the address into bx }
   mov  al,color.byte   { that's the color code    }
   mov  ES:[bx],al      { show the pixel           }
end;

Here comes the description. In the first line we load the predefined SegA000 into the ES (Extra Segment register). The SegA000 is defined by the unit system, and surely it points to the screen segment. Then we do the calculation by simple MUL and ADD instructions. Once we have the ADDRESS, it moves to the BX, then we load the color parameter into the AL, and in the last line we put the pixel out to the screen. I don't think the assembly version is complicated. Perhaps the only one thing you need to memorize is the way how we pass the color parameter as integer and then later on we get only the lower byte of it. (mov al,color.byte). The Pascal passes bytes as words anyway, so in the Pascal version the Color is a 16bits word as well, even if we can't see this.

Drawing horizontal lines

A horizontal line has four parameters: X,Y,Width and Color. X,Y is the coordinate for the beginning of the line, while Width is the length of the line in pixels. Because on the screen the difference between two pixels horizontally is one byte, the horizontal line can be drawn by a simple loop, like this:

procedure HorizLine(X,Y,Width:integer; Color:byte);
var i : integer;
begin
 for i:=0 to Width-1 do   
  mem[$a000:320*Y+X+i]:=Color;
end;

Go to assembly again. The address calculation will be the same as we did it for the procedure Pixel. Then we could have a loop that draws the line. But, because the address difference is one byte, we can use the STOSB (memory fill) instruction in this case.

procedure HorizLine(x,y,width,color:integer); assembler;
asm
   mov  ES,SegA000         { Screen segment      }
   mov  ax,320
   mul  y                  { Address calcualtion }
   add  ax,x
   mov  di,ax              { we need the di register this time }
   mov  al,color.byte
   mov  cx,width           { length of the line          }
   cld                     { stosb will increment the di }
   rep  stosb              { draw the line               } 	
end;

Drawing vertical lines

Technically vertical line is kinda same as the horizontal one. Of course we need a loop that vertically puts the pixels now. Our parameters: X,Y,Height and Color.

procedure VertLine(X,Y,Height:integer; Color:byte);
var i : integer;
begin
 for i:=0 to Height-1 do   
  mem[$a000:320*(Y+i)+X]:=Color;
end;

Compare this one to the pascal version of HorizLine above. You wil see that in this case we increment the Y instead of the X. In assembly:

procedure VertLine(x,y,height,color:integer); assembler;
asm
   mov  ES,SegA000       { Screen segment }
   mov  ax,320
   mul  y
   add  ax,x             { Address calculation } 
   mov  di,ax
   mov  al,color.byte
   mov  cx,height        { A loop takes height times           }
@l0:
   mov  ES:[di],al       { put the pixel                       } 
   add  di,320           { the next pixel's address vertically } 
   loop @l0
end;

Test

Now we're ready to test all of these routines.

program test1;

uses gfx256;

var
   i : integer;

BEGIN
  Graph320x200;

  for i:=0 to 999 do Pixel(random(320),random(200),random(256));
  readln;

  for i:=1 to 15 do HorizLine(10,i*4,10+i*5,i);
  readln;

  for i:=1 to 15 do VertLine(160+i*4,10,10+i*5,i);
  readln;

  Text80x25;
END.

Downloads: [ Gfx256 unit | Test1 ]

The first loop (0 to 999) draws pixels randomly on the screen. This will look like a starfield.

The second loop draws horizontal lines.

The third loop draws vertical lines.

Back