Para poder conseguir la máxima velocidad en los puntos críticos,
sin necesidad de realizar todo el programa en ensamblador, la mayoría
de los lenguajes actuales nos permiten incluir "trozos" de ensamblador
en nuestros programas.
Ya desde las primeras versiones de Turbo Pascal podíamos incluir código máquina "en linea", entre líneas en Pascal, con la orden inline: por ejemplo para imprimir la pantalla, la secuencia de instrucciones en ensamblador sería:
PUSH BP ( Salva en la pila el registro BP, para
que no se modifique )
INT 5 ( Llama a la interrupcion 5,
que imprime la pantalla )
POP BP ( Restaura el valor del registro
BP )
Estas líneas, en código máquina (el ensamblador tiene una traducción casi directa) serían:
PUSH BP -> $55
INT 5 -> $CD $05
POP BP -> $5D
Así que si introducimos esta secuencia de 4 bytes en un punto de nuestro programa, se imprimirá la pantalla. Entonces, nos basta con hacer:
procedure PrintScreen;
begin
inline($55/$CD/$05/$5D);
end;
Desde el cuerpo de nuestro programa escribimos "PrintScreen" y ya está.
Un comentario sobre este sistema: imprime la pantalla a través del DOS, por lo que no habrá problema si es una pantalla de texto, pero puede que nos haga falta tener cargado GRAPHICS o algún dispositivo similar si es una pantalla en modo gráfico.
Desde la versión 6.0 de Turbo Pascal, la cosa es aún más sencilla. Con "inline" conseguíamos poder introducir órdenes en código máquina, pero es un sistema engorroso: tenemos que ensamblar "a mano" o con la ayuda de algún programa como DEBUG, después debíamos copiar los bytes en nuestro programa en Pascal, y el resultado era muy poco legible. A partir de esta versión de TP, podemos emplear la orden "asm" para incluir ensamblador directamente:
procedure PrintScreen;
begin
asm
push bp
int 5
pop bp
end
end;
Es decir, nos basta con encerrar entre asm y end la secuencia de órdenes en ensamblador que queramos dar. Si queremos escribir más de una orden en una línea, deberemos separarlas por punto y coma (;), siguiendo la sintaxis normal de Pascal, pero si escribimos cada orden en una línea, no es necesario, como se ve en el ejemplo. Los comentarios se deben escribir en el formato de Pascal: encerrados entre { y } ó (* y *).
Las etiquetas (para hacer saltos a un determinado punto de la rutina en ensamblador) pueden usar el formato de Pascal (tener cualquier nombre de identificador válido), y entonces tendremos que declararlas con label, o bien podemos emplear las llamadas "etiquetas locales", que no se pueden llamar desde fuera de la rutina en ensamblador, y que no hace falta declarar, pero su nombre debe empezar por @. Un ejemplo puede ser la versión en ensamblador de la rutina para sincronizar con el barrido de la pantalla que vimos en la ampliación 2 ("Gráficos sin BGI"):
procedure Retrace;
begin
asm
mov dx, $03da
@ntrace:
{ Espera fin del barrido actual }
in al, dx
test al, 8
jnz @ntrace
@vtrace:
{ Espera a que comience el nuevo barrido }
in al, dx
test al, 8
jz @vtrace
end;
end;
Como son etiquetas locales, a las que sólo vamos a saltar desde dentro de este mismo procedimiento, comenzamos su nombre con @ y no necesitamos declararlas.
Tenemos a nuestra disposición todas las ordenes de ensamblador del 8086. También podemos acceder a las del 80286 si usamos la directiva {$G+}, y/o las del 8087 si empleamos {$N+}. Por ejemplo, para dibujar puntos en la pantalla en modo 320x200 de 256 colores podemos usar:
{$G+}
Procedure Putpixel (X,Y : Integer; Col : Byte);
Begin
Asm
mov ax,$A000
mov es,ax
mov bx,[X]
mov dx,[Y]
mov di,bx
mov bx, dx
{ bx = dx }
shl dx, 8
{ dx = dx * 256 }
shl bx, 6
{ bx = bx * 64 }
add dx, bx
{ dx = dx + bx (= y*320) }
add di, dx
{ Posición final }
mov al, [Col]
stosb
End;
End;
Es decir: para multiplicar por 320, no usamos las instrucciones de multiplicacion, que son lentas, sino las de desplazamiento, de modo que al desplazar 8 posiciones estamos multiplicando por 256, al desplazar 6 multiplicamos por 64, y como 256+64=320, ya hemos hallado la fila en la que debemos escribir el punto. El {$G+} lo hemos usado porque instrucciones como "shl dx, 8" sólo están disponibles en los 286 y superiores; en un 8086 deberíamos haber escrito 8 instrucciones "shl dx,1" o haber usado el registro CL.
Podemos hacer una optimización: en todos los procedimientos que hemos visto, todo era ensamblador. Esto no tiene por qué ocurrir así: podemos tener Pascal y ensamblador mezclados en un mismo procedimiento o función. Pero cuando sea sólo ensamblador podemos emplear la directiva assembler, que permite al compilador de Turbo Pascal hacer una serie de optimizaciones cuando genera el código. El formato de un procedimiento que emplee esta directiva es:
procedure Modo320; assembler;
asm
mov ax,$13
int $10
end;
(debemos indicar "assembler;" después de la cabecera, y no hacen
falta el "begin" y el "end" del procedimiento).
Como ejemplo de todo esto, un programita que emplea estos procedimientos para dibujar unas líneas en pantalla, esperando al barrido antes de dibujar cada punto (al final de este tema hay otro ejemplo más):
{--------------------------}
{ Ejemplo en Pascal: }
{ }
{ Dibujo de puntos en }
{ pantalla con ensam- }
{ blador }
{ GRAFASM.PAS }
{ }
{ Este fuente procede de }
{ CUPAS, curso de Pascal }
{ por Nacho Cabanes }
{ }
{ Comprobado con: }
{ - Turbo Pascal 7.0 }
{--------------------------}
program GrafAsm;
{$G+}
uses crt;
procedure Modo320; assembler;
asm
mov ax,$13
int $10
end;
procedure ModoTxt; assembler;
asm
mov ax,3
int $10
end;
Procedure Putpixel (X,Y : Integer; Col : Byte); assembler;
Asm
mov ax,$A000
mov es,ax
mov bx,[X]
mov dx,[Y]
mov di,bx
mov bx, dx { bx = dx }
shl dx, 8 { dx = dx * 256 }
shl bx, 6 { bx = bx * 64 }
add dx, bx { dx = dx + bx (= y*320) }
add di, dx { Posición final }
mov al, [Col]
stosb
end;
procedure Retrace;
begin
asm
mov dx, $03da
@ntrace: { Espera fin del barrido actual }
in al, dx
test al, 8
jnz @ntrace
@vtrace: { Espera a que comience el nuevo barrido }
in al, dx
test al, 8
jz @vtrace
end;
end;
var i, j: integer;
begin
Modo320;
for i := 0 to 40 do
for j := 1 to 200 do
begin
PutPixel(j+i*3,j,j);
retrace;
end;
readkey;
ModoTxt;
end.
Finalmente, también tenemos la posibilidad de usar un ensamblador externo, como Turbo Assembler (TASM, de Borland), o MASM, de Microsoft. Estos programas crean primero un fichero objeto (con extensión OBJ), antes de enlazar con el resto de módulos (si los hubiera) y dar lugar al programa ejecutable.
Pues nosotros podemos integrar ese OBJ en nuestro programa en Pascal usando la directiva {$L}, y declarando como "external" el procedimiento o procedimientos que hallamos realizado en ensamblador, así:
procedure SetMode(Mode: Word); external;
{$L MODE.OBJ}
Finalmente, otro ejemplo más elaborado. Se trata de "sprites", figuras transparentes que se mueven por la pantalla. Con "transparentes" me refiero a que si hay algún hueco, debe verse el fondo a través suyo. Esto es totalmente imprescindible en los videojuegos: por ejemplo, mientras que anda nuestro personaje, tiene que verse el fondo entre sus piernas o junto a su cabeza en vez de un fondo negro.
Está tomado de una práctica que hice para una asignatura de la Universidad, en la que teníamos que manejar la pantalla VGA en modo gráfico 320x200x256. Como el lenguaje era libre, empleé Pascal, que es mi favorito, y como había que conseguir el menor tamaño posible (en el ejecutable) y una cierta rapidez, incluí bastantes cosas en ensamblador. Este es el resultado...
{--------------------------}
{ Ejemplo en Pascal: }
{ }
{ Dibujo de "sprites" }
{ (imágenes transpa- }
{ rentes) en pantalla }
{ NSPRITE.PAS }
{ }
{ Este fuente procede de }
{ CUPAS, curso de Pascal }
{ por Nacho Cabanes }
{ }
{ Comprobado con: }
{ - Turbo Pascal 7.0 }
{--------------------------}
program nSprite;
{$G+ Dibuja Sprites en Pantalla }
const
segVideo: word = $a000;
NumSprites = 10; { Número de sprites }
xSize = 30; ySize = 30; { Tamaño de cada uno }
type
tipoSprite = array[1..xSize, 1..ySize] of byte; { El sprite en sí }
const
sprite : tipoSprite =
((0,0,0,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,0,0,0),
(0,0,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,0,0),
(0,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,0),
(2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3),
(2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3),
(2,1,1,1,1,2,2,2,2,2,2,2,2,1,1,1,1,2,2,2,2,2,2,2,2,1,1,1,1,3),
(2,1,1,1,3,0,0,0,0,0,0,0,0,2,1,1,3,0,0,0,0,0,0,0,0,2,1,1,1,3),
(2,1,1,1,3,0,0,0,0,0,0,0,0,2,1,1,3,0,0,0,0,0,0,0,0,2,1,1,1,3),
(2,1,1,1,3,0,0,0,0,0,0,0,0,2,1,1,3,0,0,0,0,0,0,0,0,2,1,1,1,3),
(2,1,1,1,3,0,0,0,0,0,2,3,0,2,1,1,3,0,0,0,0,0,2,3,0,2,1,1,1,3),
(2,1,1,1,3,0,0,0,0,0,3,3,0,2,1,1,3,0,0,0,0,0,3,3,0,2,1,1,1,3),
(2,1,1,1,3,0,0,0,0,0,0,0,0,2,1,1,3,0,0,0,0,0,0,0,0,2,1,1,1,3),
(2,1,1,1,1,3,3,3,3,3,3,3,3,1,1,1,1,3,3,3,3,3,3,3,3,1,1,1,1,3),
(2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3),
(2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3),
(2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3),
(2,1,1,1,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,1,1,1,3),
(2,1,1,1,3,0,0,0,2,1,1,1,1,1,1,1,1,1,1,1,1,2,0,0,0,0,1,1,1,3),
(2,1,1,1,3,0,0,0,0,0,2,1,1,1,1,1,1,1,1,2,0,0,0,0,0,0,1,1,1,3),
(2,1,1,1,3,0,0,0,0,0,0,0,2,1,1,1,1,2,0,0,0,0,0,0,0,0,1,1,1,3),
(2,1,1,1,3,0,0,0,0,0,0,0,0,2,2,2,2,0,0,0,0,0,0,0,0,0,1,1,1,3),
(2,1,1,1,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1,1,1,3),
(2,1,1,1,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1,1,1,1,3),
(2,1,1,1,1,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1,1,1,1,1,3),
(2,1,1,1,1,1,1,3,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,1,1,1,1,1,1,3),
(2,1,1,1,1,1,1,1,1,3,3,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,2),
(0,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,0),
(0,0,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,0,0),
(0,0,0,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,0,0,0),
(0,0,0,0,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,0,0,0,0)
);
type SprDat= record
x, y: word; { Coordenadas de cada sprite }
vx, vy: shortint; { Velocidad (incremento) según cada eje }
end;
{ --- Componentes R,G,B para un color --- }
procedure setpal(col,r,g,b:byte); assembler;
asm
mov dx,03c8h;
mov al,col;
out dx,al;
inc dx;
mov al,r; out dx,al;
mov al,g; out dx,al;
mov al,b; out dx,al;
end;
{ --- Copia 64000 bytes de una pantalla virutal a otra (física) --- }
procedure copia(org,dst:word); assembler;
asm
push ds;
mov ds,[org];
xor si,si;
mov es,[dst]
xor di,di;
mov cx,320*200/2;
rep movsw;
pop ds;
end;
{ --- Borra 64000 bytes en una zona de memoria --- }
procedure cls(dst:word); assembler;
asm
mov es,[dst];
xor di,di;
xor ax,ax;
mov cx,320*200/2;
rep stosw;
end;
{ --- Sincroniza con el barrido de la pantalla --- }
procedure retrace; assembler; asm
mov dx,03dah;
@vert1: in al,dx;
test al,8;
jnz @vert1
@vert2: in al,dx;
test al,8;
jz @vert2;
end;
{ --- Escribe un sprite en la pantalla --- }
procedure putsprite(x,y,sprseg, sprofs,virseg:word); assembler;
asm
push ds
mov ds,sprseg;
mov si,sprofs { Segmento y desplaz. del sprite }
mov es,virseg; xor di,di { Segmento y desp. de la pantalla virtual }
mov ax,[y]; { Numero de fila }
shl ax,6;
mov di,ax;
shl ax,2;
add di,ax { Fila * 320 }
add di,[x] { Fila * 320 + columna }
mov dx,320-xsize { Pixels restantes en la línea }
mov bx,ysize { Altura del dibujo }
@l1:
mov cx,xsize { Anchura de cada fila }
@l0:
lodsb; { Leo un byte del sibujo }
or al,al;
jz @noDib { Si es 0 (transp.), lo salto }
mov [es:di],al { Si no, lo dibujo }
@noDib:
inc di; { Siguiente pixel }
dec cx; { Queda uno menos por dibujar en la fila }
jnz @l0; { Si aun quedan, repito }
add di,dx; { Si no quedan, voy al principio de la sgte fila }
dec bx; { Queda una fila menos }
jnz @l1; { Repito hasta que se acaben las filas }
pop ds
end;
{ --- Variables que usaré en el cuerpo --- }
var
PantVirt: pointer; { La pantalla virtual }
SegVirt: word; { Segmento donde se encuentra }
Fondo: pointer; { La pantalla de fondo }
SegFon: word; { y del fondo }
D: { Datos de cada sprite }
array[1..numSprites] of SprDat;
i,j: integer; { Bucles }
label bucle;
{---------------------------- }
{ --- Cuerpo del programa --- }
{---------------------------- }
begin
asm mov ax,13h; int 10h; end; { Cambio a modo 320x200x256 }
randomize; { Números aleatorios }
getmem(PantVirt,320*200); { Reservo y vacío pantalla virtual }
SegVirt := seg(PantVirt^);
cls(SegVirt);
getmem(Fondo,320*200); { Y la de fondo }
SegFon:=seg(Fondo^);
cls(SegFon);
for i := 1 to NumSprites do { Datos aleatorios de los Sprites }
with d[i] do
begin
x := random (219-xSize)+50;
y := random (99-ySize)+50;
repeat
vx := random(6) - 3;
until vx<>0;
repeat
vy := random(6) - 3;
until vy<>0;
end;
for i:=1 to 128 do { Paleta de colores del fondo }
setpal(127+i,20+i div 5,i div 3,20+i div 7);
SetPal(1,10,10,45);
SetPal(2,0,0,25);
SetPal(3,20,20,60);
for i:=0 to 319 do { Dibujo el patrón de fondo }
for j:=0 to 199 do
mem[SegFon:j*320+i]:=128+abs(i*i-j*j) and 127;
bucle:
copia(SegFon,SegVirt); { Copio el fondo en la pantalla virtual }
for i := 1 to numSprites do { Dibujo los sprites }
begin
PutSprite( d[i].x, d[i].y, seg(Sprite), ofs(Sprite), SegVirt);
inc(d[i].x,d[i].vx); { Actualizo las coordenadas }
if (d[i].x < 5) or (d[i].x > (315-xSize)) then
d[i].vx := -d[i].vx;
inc(d[i].y,d[i].vy);
if (d[i].y < 5) or (d[i].y > (195-ySize)) then
d[i].vy := -d[i].vy;
end;
retrace; { Sincronizo con el barrido }
copia(SegVirt,SegVideo); { Y copio la pantalla virtual en la visible }
{ Repito hasta que se pulse una tecla }
asm mov ah, 1; int 16h; jz bucle; end;
{ Absorbo esa pulsación de tecla }
asm mov ah, 0; int 16h; end;
freemem(PantVirt,320*200); { Libero la memoria reservada }
freemem(Fondo,320*200);
asm mov ax,3; int 10h; end; { Vuelvo a modo texto }
end.
Este programa está comprobado con Turbo Pascal 7.0. Eso
sí, como reserva dos pantallas virtuales puede que no quede memoria
suficiente para ejecutarlo desde el IDE normal (TURBO.EXE). Entonces
habría que usar el compilador de línea de comandos (TPC.EXE)
o el de modo protegido (TPX.EXE).