CURSO DE PROGRAMACIÓN GRÁFICA

Artículo 2: PROGRAMACIÓN DE 320x200x256 (I)

Autor: (c) Santiago Romero.
Revista: Programación Actual (Prensa Técnica) nš 2, Mayo-1997


La anterior entrega de este curso nos sirvió de introducción a este complejo mundillo de los gráficos por ordenador. Ahora es el momento de pasar a programar directamente nuestra tarjeta gráfica.

Cuando en el número anterior se comentó como introducción el sistema BGI se nombraron sus desventajas, derivadas del hecho de ser una librería gráfica no especializada. Por otra parte, su principal ventaja era que nos permitía trabajar en cualquier modo de vídeo sin apenas trabajo de creación por nuestra parte.

Si pretendemos usar los distintos modos gráficos de que disponemos nos vemos en la necesidad de crear nuestro propio set de funciones (putpixel(), getpixel(), line(), etc...) para poder trabajar en el modo gráfico que deseemos. El problema es que cada modo de video (de todos los que vimos la anterior entrega, como 320x200, 640x480, etc...) se programa de una manera; es decir: cuando desarrollemos nuestra función putpixel() para poner un punto en pantalla, según para qué modo gráfico vayamos a realizar el programa o juego habrá que realizar en ella una serie de acciones u otras.

Esto implica que cada modo de video de los (en principio) 19 estándar existentes se gestiona de una forma diferente, y tendremos que crear una librería especializada para cada uno de ellos. Con esto se elimina la desventaja de los BGI: una librería específica implica que al crear las distintas funciones aprovecharemos las ventajas que ese modo nos proporcione para obtener un código más rápido y optimizado. Para ello comenzaremos con los modos más fáciles de programar (320x200x256) hasta llegar a mayores resoluciones y colores (SVGA), pasando por los modos planares y unchained (indocumentados) de la tarjetas VGA.


UN POCO DE ENSAMBLADOR

El lenguaje ensamblador (assembler) es la verdadera base de un programa profesional. Aunque los compiladores actuales realizan buenas optimizaciones de código (lo hacén más reducido y por tanto más rápido), no hay compilador capaz de superar una bien optimizada rutina assembler, si bien en el mercado actual nos encontramos con juegos generados casi totalmente en C puro con las rutinas más críticas en assembler, lo que proporciona mayor facilidad de programación junto con mayor velocidad.

Como un ejemplo, el juego DOOM de ID-Software está integramente programado en Watcom C (para facilitar su portabilidad a otros sistemas), con tan sólo 3 rutinas en ensamblador: control del joystick, dibujo de franjas horizontales con texturas (para el trazado de suelos y techos) y dibujo de franjas verticales (para el trazado de los muros y bloques). Esto es así porque estas rutinas (sobre todo estas dos últimas) son las más llamadas del programa (de cientos a miles de veces para cada fotograma) y necesitan de la velocidad y control que proporciona el assembler para conseguir un juego tan rápido como este, así que aprovecharemos los cursos específicos de que disponemos en la revista para introducirnos en la programación gráfica usando ensamblador para el desarrollo de las rutinas.

Por supuesto, el rendimiento de cualquier programa será mayor bajo bases de programación estables tales como MS-DOS y assembler en contraposición a la disminución de velocidad que se nota bajo sistemas como entornos Windows y otros shells gráficos, y no necesita ningún comentario el resultado si empleamos en el desarrollo lenguajes visuales.


EL MODO DE VIDEO 320x200x256

Hasta ahora, el 90% de los juegos del PC existentes están creados en este modo gráfico. Su principal ventaja es su facilidad de programación y, por lo tanto, la velocidad que desarrolla. Por otra parte, 256 colores suelen ser más que suficientes para la mayoría de juegos y es imprescindible aprender a programar este modo antes de pasar a modos más complejos técnicamente (que no quiere decir que proporcionen mayor rendimiento, más bien al contrario). Nos encontramos con el modo gráfico más sencillo, rápido y útil de todos los que proporciona la VGA.

Como única desventaja, 320x200 puede ser una resolución algo baja para aplicaciones con muchos datos de texto o para programas que necesiten más área de trabajo que 320 pixels horizontales por 200 verticales. Esta resolución puede resultar reducida para bases de datos, juegos con mucha área de visión, etc..., pero lo más seguro es que, para empezar, este modo gráfico (conocido como 13h) cubra nuestras necesidades para la mayoría de programas y juegos.

Para programar gráficos en este modo de video principalmente hemos de saber hacer tres cosas:

  1. Saber inicializar el modo de video 320x200 ó 13h.
  2. Comprender el modo de direccionamiento lineal del 13h
     y su organización interna.
  3. Realizar rutinas gráficas específicas para dicho modo.

Veamos primero como inicializar cualquier modo de video (entre ellos el 13h) utilizando los servicios disponibles en la ROM-BIOS del PC.


LA BIOS DEL PC

La ROM-BIOS del PC es una zona de memoria no modificable (ROM) en la cual disponemos de una serie de funciones y servicios para su uso por el sistema operativo (y utilizables por el programador), que gestionan accesos a disco, control de la tarjeta de video, gestión de impresora, etc. Estas funciones son las llamadas interrupciones software y proporcionan al programador control sobre el hardware del PC.

La manera de llamar a una de estas interrupciones, en lenguaje assembler, consiste en preparar todos los parámetros que ésta requiera (cargar los registros del PC con los valores adecuados) y ejecutar la llamada a la interrupción. Es necesario por tanto saber mover valores a los registros AX, BX, CX, etc... (mov registro, valor) y llamar a la interrupción deseada (int numero_de_int).

Supongamos el ejemplo concreto de inicializar el modo de video 320x200 a 256 colores. Si miramos en la tabla 1 (funciones más importantes de la int 10h) veremos que la interrupción 10h (gestión de la tarjeta gráfica) posee un servicio para inicializar modos de video (servicio 0). Cargamos los registros tal y como los pide la interrupción ( AH = servicio, AL = Modo ), y efectuamos la llamada:


  mov ah, 0        /* AH=0: Init VideoMode */
  mov al, 13h      /* modo: 13h */
  int 10h          /* llamada a int 10h */

Simplemente conociendo los parámetros que necesita cada interrupción y servicio podemos utilizar cualquiera de las funciones de bajo nivel de que nos provee la BIOS del PC. Esta interrupción en concreto, la interrupción 10h, es la llamada interrupción de video, y nos permite inicializar modos de video (servicio 0), poner pixels en pantalla (servicio 0Ch), leer el valor de un pixel (servicio 0Dh), cambiar la paleta disponible (desde AX=1000h a 101Bh) y otros servicios a disposición del programador. Aparte de la interrupción 10h existen más interrupciones con sus correspondientes servicios (33h=servicios del ratón; 16h=servicios de teclado, etc...), que nos harán el proceso de creación mucho más sencillo. Por ello es recomendable seguir el curso de ensamblador de la revista para coger soltura en este lenguaje tan indispensable y comprender el funcionamiento de las interrupciones.

 TABLA 1: Algunos servicios de la int 10h.

-------------------------------------------------------------------------
|  PARÁMETROS   |    SERVICIO     |       FUNCIÓN QUE REALIZA           |
-------------------------------------------------------------------------
| AH = 00h      | Set Video Mode  | Inicializa el modo de video especi- |
| AL = Modo     |                 | ficado en AL, segun los valores de  |
|               |                 | la tabla 2.                         |
------------------------------------------------------------------------- 
| AH = 0Ch      |   Write Pixel   | Dibuja el pixel (CX,DX) en pantalla |
| CX = Coord. X |                 | con el color Al. El parámetro página|
| DX = Coord. Y |                 | debe ser 0 en 13h ya que sólo hay 1 |
| AL = Color    |                 | página de video y esa es la 0.      |
| BH = Página   |                 |                                     |
-------------------------------------------------------------------------
| AH = 0Dh      |    Read Pixel   | Devuelve en Al el color del pixel   |
| CX = Coord. X |                 | de la posición (CX,DX).             |
| DX = Coord. Y |                 |                                     |
| BH = Página   |                 |                                     |
| Devuelve:     |                 |                                     | 
| AL = Color    |                 |                                     |
-------------------------------------------------------------------------
| AH = 0Fh      | Get Video Mode  | Devuelve en AL el modo de video     |
|  Devuelve:    |                 | actual.                             |
| AL = Modo     |                 |                                     |
-------------------------------------------------------------------------
| AX = 1001h    | Set Border Color| Cambia el color del borde de la pan-|
| BH = Color    |                 | talla al color BH (por defecto es 0,|
|               |                 | equivalente al negro).              |
-------------------------------------------------------------------------

En la tabla 2 disponemos del listado de los modos de video que pueden ser inicializados en una VGA estándar mediante el servicio 0 de la interrupción 10h. Podemos llamar a este servicio dentro de una función C, como puede verse en el siguiente código:


 void SetVideoMode ( char modo )
 {
 asm {
      mov ah, 0
      mov al, [modo]
      int 10h
     }
 }


 TABLA 2: Modos de vídeo de la BIOS.

----------------------------------------------------------------------
 MODO     TIPO     RESOLUCIÓN    COLORES    VRAM        SISTEMA
----------------------------------------------------------------------
 00h      Texto      40x25         16       B800h    CGA/MCGA/EGA/VGA
 02h      Texto      80x25      16 grises   B800h    CGA/MCGA/EGA/VGA
 03h      Texto      80x25         16       B800h    CGA/MCGA/EGA/VGA
 04h     Gráfico    320x200         4       B800h    CGA/MCGA/EGA/VGA
 06h     Gráfico    640x200         2       B800h    CGA/MCGA/EGA/VGA
 07h      Texto      80x25        mono      B000h    MDA/Herc/EGA/VGA
 0Dh     Gráfico    320x200         16      A000h         EGA/VGA
 0Eh     Gráfico    640x200         16      A000h         EGA/VGA
 10h     Gráfico    640x350         16      A000h     EGA/VGA (256Kb)
 12h     Gráfico    640x480         16      A000h           VGA
 13h     Gráfico    320x200        256      A000h        MCGA/VGA
------------------------------------------------------------------------

Para inicializar ahora el modo de video 320x200x256 (llamado 13h porque éste es el valor que hay que introducir en el registro AL), bastaría con llamar a nuestra nueva función con este parámetro:


SetVideoMode( 0x13 );

Para volver de nuevo al modo de texto 80x25, usaremos la orden SetVideoMode(3); ya que el modo 3 corresponde, según la tabla 2, al modo de video 80x25 a 16 colores.

Es muy importante ir agrupando todas las nuevas funciones que vayamos creando (como SetVideoMode) en librerías externas o ficheros .C o .H que más tarde podrán ser incluidos durante el proceso de compilación o creación en la línea de comandos o mediante el parámetro #include de C, tal y como se hace en los ejemplos de este curso.


ORGANIZACIÓN INTERNA DEL MODO 13h

Ya sabemos inicializar el modo de video 320x200x256 (y cualquier otro modo de video) mediante una simple llamada a la interrupción 10h. Ahora hemos de comprender como gestiona el ordenador esos pixels que escribimos en pantalla para empezar a trabajar en 13h. Es algo muy sencillo y visual y requiere tan sólo conocimientos sobre la memoria del PC.

En cierta manera, de toda la memoria del PC los primeros 1024 KiloBytes (1 MegaByte) están divididos en segmentos de 64kb (65.536 bytes) a los que se accede mediante un segmento y un desplazamiento u offset.

Dicho de otro modo, es como si el primer megabyte de memoria RAM estuviera compuesto por bloques de 64Kb cada uno. Cuando queremos escribir en una posición de memoria, le indicamos al ordenador en qué bloque está (segmento), y dentro de ese bloque cual es el byte que queremos modificar (offset o desplazamiento).

Esto es así porque en el 8086 los registros eran de 16 bits (sólo pueden adoptar valores entre 0 y 65.536), por lo que para acceder a la memoria idearon este método para, pudiendo utilizar tan sólo hasta el número 65.535, hacer referencia a posiciones de memoria más elevadas (cuando se creó el PC nadie se podía imaginar que algún día necesitaría más de 64Kb de RAM). Con este sistema de segmentación de memoria, escribir un byte en la posición 65.536 se reduce (de una manera intuitiva a modo de ejemplo) a escribirlo en el bloque 1, offset 0. Realmente en el PC las direcciones absolutas se construyen mediante:p>

     Dir_Física = (Segmento*16)+Offset

Esto sólo ocurre en modo real, pues a partir de la aparición de micros de 32 bits (386+), con registros de este tamaño puede accederse a la memoria de manera lineal, en un nuevo modo del micro llamado modo protegido, permitiendo la manipulación de hasta 4 gigabytes.

El objetivo de la anterior introducción a la segmentación de memoria es el segmento 0A000h. Este segmento de memoria (64Kb, 65.536 bytes) es el segmento de VideoRAM, es decir, es donde la VGA guarda los datos de las imágenes gráficas que dibuja en el monitor. Si escribimos algún valor en este segmento, la próxima vez que la tarjeta gráfica redibuje la pantalla (lo hace entre 50 y 70 veces por segundo) el valor que hemos escrito aparecerá en pantalla en forma de punto. Pero veamos que es lo que hace la tarjeta gráfica con esta VideoMemoria.


EL RETRAZADO DE PANTALLA

Aproximadamente entre 50 y 70 veces por segundo (según el modo de vídeo), la tarjeta gráfica lee de su memoria todos los valores que contiene y transforma esta información digital (unos y ceros, o sea: números) en los puntos que vemos en pantalla. Tras nuestro monitor hay un haz de electrones que "bombardea" la pantalla con electrones que producen las diferentes tonalidades. Estas partículas tienden a apagarse, por lo que es necesario redibujar (retrazar) la pantalla el número de veces necesario por segundo para que la imagen no desaparezca.

En este proceso, llamado retrazado de pantalla, el haz de electrones se desplaza hasta la esquina (0,0) del monitor (incluyendo el borde) y comienza a leer bytes de la VideoRAM (segmento 0A000h), transformándolos en pixels y trazándolos en pantalla. Al llegar al final de una línea horizontal, el haz vuelve en a la siguiente línea (retrazado horizontal) y continúa con el proceso hasta llegar a la esquina inferior derecha, donde vuelve en diagonal a (0,0) para repetir el proceso. En la figura 1 puede verse el proceso con más claridad. El refresco de la pantalla consiste en un continúo retrazado actualizando la pantalla para ofrecernos la imagen contenida en la videomemoria (que en realidad constituye RAM de la tarjeta a la que se nos permite acceder tras el proceso de autoarranque del encendido).

El refresco de pantalla


DIRECCIONAMIENTO LINEAL

Visto de esta manera, sabiendo que la tarjeta transforma los bytes del segmento 0a000h (0xA000 en hexadecimal de C) en pixels, si averiguamos qué representa cada byte, cuando tratemos de dibujar un pixel bastaría con escribir en este segmento el color que queremos para que la tarjeta gráfica lo represente durante el próximo retrazado. Nada más sencillo en este modo gráfico, modo de 8 bits por pixel.

Esto quiere decir que cada número del 0 al 255 se corresponde con un color. Por defecto, el 0 es el negro, el 1 el azul, y así hasta llegar al 255. Entre el 0 y el 255 disponemos de gamas de azules, verdes, amarillos, etc..., que componen la paleta por defecto de la VGA.

Que este modo gráfico sea de un byte por pixel significa que al escribir un byte en este segmento de memoria, su equivalente en pantalla será un pixel, que aparecerá automáticamente en cuanto el haz de electrones pase por esa posición al refrescar la imagen.

En la figura 2 tenemos una representación de cómo está organizada la VideoRAM en el modo 13h.

Videoram en modo 13h

Como puede verse, al byte 0 le corresponde el pixel (0,0) (el primero de la pantalla); al byte 1 le corresponde el pixel (1,0), al byte número 320 le correspondería el pixel (0,1), (primer pixel de la línea 1, porque hay 320 pixels de resolución horizontal) y así hasta el byte 63.999 del segmento, que corresponde a la posición (319,199). Depende del offset en que coloquemos el byte, el punto aparecerá en distinta posición en el monitor (cada byte es un pixel individual en la pantalla).

El segmento de la VideoRAM se comporta en este modo de video como si fuera una larga línea de pixels de manera que al llegar al final de una línea horizontal de pantalla, el siguiente byte de la VideoMemoria es el que continúa en la siguiente línea de pantalla. De ahí el término direccionamiento lineal: es como si la pantalla fuera un array de C o PASCAL unidimensional desde 0 a 64.000 donde cada 320 bytes estamos situados en una nueva línea de pantalla (el byte 320 es el primer pixel de la segunda línea). Así, durante el retrazado la tarjeta únicamente tiene que dedicarse a leer bytes (todos ellos consecutivos) y representarlos en pantalla.

Tambien podríamos comparar la VideoRam con una gran pantalla de una sóla linea de ancho (de 320x200=64.000 pixels de ancho), y cuando la tarjeta traslada esos colores al monitor, cada 320 bytes salta a una nueva línea. Así obtenemos en pantalla una imagen de 320x200 pixels.

Por otra parte, hay que hacer notar que los 256 colores de que disponemos pueden ser adaptados a nuestras necesidades. La paleta por defecto contiene unos 32 tonos de azul, 32 de rojos, grises, etc... pero si estamos dibujando (por ejemplo) una selva, probablemente necesitaremos más tonos de verdes de los 32 que hay por defecto. La manera de cambiar la paleta (es decir, que cada número 0-255 corresponda a una tonalidad o color) la abordaremos en un próximo artículo, de manera que podríamos (en este ejemplo en concreto) hacer que los primeros 128 colores sean tonos de verdes y los restantes 128 tonos de azules (para el cielo de nuestra selva). Esto quiere decir que el color 0 no tiene porqué ser el negro (como en la paleta por defecto), sino que podemos adaptar cada color (desde el 0 al 255) a la tonalidad (mezcla de rojo, verde y azul) que deseemos. En el programa ejemplo5.exe de los ejemplos que acompañan al artículo podemos observar la paleta por defecto, para hacernos a la idea de los colores de que disponemos.


OPERACIONES BÁSICAS

Estando en el modo 320x200, y sabiendo que cada byte representa un pixel en pantalla, es sencillo implementar la operación de borrado de pantalla. Borrar la pantalla (o lo que es lo mismo, rellenar toda ella con un color, generalmente negro) equivale a rellenar los 64.000 primeros bytes del segmento 0A000h (320x200 = 64.000) con el color que deseemos utilizar. Supongamos el caso de borrar la pantalla con el color 0 (negro por defecto). Esto se haría en C puro usando la orden memset, que escribe en la dirección de memoria especificada, el valor que se le pase como parámetro n veces:


  unsigned char *vgaseg = (unsigned char *) MK_FP(0xA000, 0);
  memset(vgaseg, 0, 64000);

o también podemos aprovechar las órdenes de cadena del lenguaje ensamblador como stosb, stosw o stosd, precedidas del comando rep:


asm {
      mov ax, 0xA000
      mov es, ax
      xor di, di
      mov al, 0
      mov cx, 64000
      rep stosb
    }

En el listado 1 podemos ver la función ClearScreen() que se encarga de borrar la pantalla con el color especificado como parámetro usando instrucciones 8086 (stosb).


 LISTADO 1: Función ClearScreen() de borrado de pantalla.

/*----------------------------------------------------
 ClearScreen(); Borrado de la pantalla (segm. 0A000h).

 Pseudocódigo:
	ES:DI = A000:0000
	AL = Color
	Repetir CX veces
		ES:[DI++] = AL
----------------------------------------------------*/

void ClearScreen ( char color )
{
asm {
      mov ax, 0xA000
      mov es, ax
      xor di, di
      mov cx, 64000
      mov al, [color]
      rep stosb
    }
}

La instrucción assembler stosb mueve el valor de AL a la posición de memoria apuntada por ES:DI e incrementa DI en uno tras la operación. El prefijo rep le indica al procesador que repita el proceso CX veces. De esta manera, el micro ejecutaría el siguiente pseudocódigo para el anterior bloque de ensamblador:

  ES:DI = A000:0000h
  CX = 64.000
  Repetir hasta que CX sea 0
  {
    ES:[DI] = AL
    DI = DI + 1
    CX = CX - 1
  }

Por supuesto, cambiando los valores iniciales podemos borrar tan sólo la mitad de la pantalla (CX=32.000), borrar las 10 primeras líneas (CX=320*10), borrar la parte inferior de la pantalla (CX=32.000, DI = 32.000), etc...

Stosw es el equivalente en 16 bits de stosb que almacena cada vez 2 bytes (AX), incrementado DI en 2, resultando por tanto más rápido al tener que hacer la mitad de repeticiones (CX = bytes totales/2 ). Como actualmente el equipo mínimo es un 386/486, es más recomendable usar stosw e incluso stosd si programamos en 32 bits.


EN LA PRÓXIMA ENTREGA

En el próximo artículo completaremos estos conocimientos de 320x200 para desarrollar funciones C y ASM de dibujo de pixels, lineas horizontales y verticales, etc... unido a algunos trucos de optimización para la aceleración de nuestras rutinas. Hasta entonces, "jugar" escribiendo valores en la VRAM es muy recomendable para comprender a fondo la organización de la VideoMemoria.

Pulse aquí para bajarse los ejemplos y listados del artículo (19 Kb).

Figura 1: "Proceso de retrazado de la pantalla.."
Figura 2: "Organización de la VideoRAM en modo 13h."

Santiago Romero


Volver a la tabla de contenidos.