PROGRAMACIÓN DE MODOS X

Artículo 6: FUNCIONES GRÁFICAS EN MODO X

Autor: (c) Santiago Romero y Miguel Cubas.
Revista: Sólo Programadores (Tower Communications) n 27, Noviembre-1996


En este último artículo terminaremos de perfilar nuestros conocimientos sobre el modo X con rutinas que nos permitirán por fin completar la base para crear nuestros propios programas.

Se verán a continuación una serie de procedimientos con los que se completa la librería personal que cada lector debe poseer para el desarrollo de aplicaciones gráficas, demos y juegos. Hay muchas funciones gráficas que se podrían explicar (líneas, dibujo de polígonos, escalado de imágenes, etc.), pero al diseñar este artículo hemos de ceñirnos al modo X, pues la programación gráfica de este tipo de efectos ya está contemplada en otras secciones de la revista.

Por lo tanto, en el presente texto se comentarán únicamente aquellas primitivas (líneas, buffers, sprites, etc.) cuya comprensión implique que el lector sea capaz de desarrollar sus propias rutinas para otros efectos. Esto quiere decir que dibujar líneas horizontales o verticales es mucho más que lo que se obtiene en pantalla, pues significa entender la manera de organizar algunos algoritmos dentro de los modos unchained.

Queda claro que las rutinas que se desarrollarán actúan directamente sobre la pantalla de la VGA de una manera didáctica. Para aplicaciones que requieran elevadas velocidades todos los cálculos deberán realizarse en una pantalla virtual de, por ejemplo, 320x200 y volcarse a la Video Ram. Para ello desarrollaremos funciones de vuelque de buffers y segmentos de memoria, de manera que al dibujar en estos buffers (lineales) se podrán usar las funciones normales de dibujo en modo estándar 13h. De esta manera, se puede usar cualquier función de dibujo (elipses, círculos, gráficos 3D, etc.,) que ya se posean dentro de nuestros programas en Modo X.


DIBUJO DE LÍNEAS VERTICALES

Este tipo de línea es el más fácil y rápido de plasmar en pantalla, debido al sistema de planos de la VGA. Supongamos un modo X de 320x200 con organización de 1x4 pantallas (modo X por defecto), como se puede ver en la figura 1, donde también puede observarse la ventaja que se nos presenta al dibujar la línea verticalmente: todos los puntos que forman la línea tienen la misma coordenada X, por lo que todos están en el mismo plano ( plano = 1 << (x&3), ver artículo 1).

Figura 1. Lineas verticales.

Al tener todos los puntos de la línea en el mismo plano, sólo hemos de calcular el plano en que cae el primer punto y escribir los diversos pixels de la línea hasta llegar a la coordenada Y2. Con este tipo de líneas nos evitamos cambiar de plano para cada punto a dibujar, hecho en el que radica su velocidad.

Veamos el algoritmo que describe estas acciones:

Dibujo de una línea vertical entre (X,Y1) y (X,Y2):

    Calcular plano para (X,Y1).
    Seleccionar plano de la VGA.
    Calcular el offset del punto (X,Y1).
    Hacer registros ES:DI = Offset calculado.
    Poner color en registro AL.

    Mientras no lleguemos a Y2:
       Poner punto en ES:DI.
       Incrementar DI en 80 (sig. punto).
    Fin Mientras

En el listado 1 puede verse el código C y Ensamblador resultante de este algoritmo. Este es bastante rápido y compacto, aunque debe hacerse notar que en otras Resoluciones Virtuales no habría que incrementar en 80 el valor del registro DI, sino que habría que tener en cuenta la anchura de la pantalla, según el modo de pantalla actual (2x2 pantallas=160 bytes y 4x1=320 bytes horizontales), como se explicó en el artículo dedicado a las resoluciones virtuales que adoptaba la VGA en modo X. El hecho de que incrementemos DI en 79 y no en 80 es debido a que stosb, que coloca el valor que contenga AL en la posición de memoria especificada por ES:DI, ya incrementa DI en una unidad tras escribir el byte, por lo que debemos saltarnos en este caso 79 bytes más para posicionarnos en el siguiente punto de la línea vertical. Todas las funciones que veremos a continuación están desarrolladas para el modo 13X en sistema 1x4 pantallas, pero son fácilmente adaptables a cualquier otro modo que vayamos a utilizar en nuestros programas.

 Listado 1: Dibujo de líneas verticales

void DrawVLine( int x, char y1, char y2, char color )
{
char alto;
unsigned int offs;

 alto = y2-y1;                 /* y1 debe ser < y2 */

 OutPortb( SEQU_ADDR , 0x02 );           /* seleccionar plano */
 OutPortb( SEQU_ADDR+1, 0x01 << ( x & 3 ) );   

 offs = (y1<<6) + (y1<<4) + (x>>2);
           /* offset */

 asm {
      mov ax, 0xa000
      mov es, ax
      mov di, [offs]     /* ES:DI = (X,Y1) */
      mov al, [color]    /* AL = Color */
      mov cl, [alto]     /* CL = Altura */
    }
 VLineLoop:
 asm {
      stosb              /* ES:DI = AL (y DI++) */
      add di, 79         /* siguiente punto */

      dec cl             /* comprobamos si es */
      jnz VLineLoop      /* el último punto */
     }
}


LA IMPORTANCIA DE ESTAS PRIMITIVAS

Probablemente muchos lectores se estarán preguntando de qué sirve saber dibujar líneas verticales. Su importancia se verá en el momento en que intentemos desarrollar una rutina para volcar buffers, sprites o segmentos completos a la memoria de la VGA, que ya no es lineal, sino planar.

Si intentamos desarrollar una rutina simple para crear una línea horizontal directamente en pantalla (como en la figura 1), nos encontraremos con que cada punto de la línea cae en un plano distinto de la VGA. Este problema puede resolverse de dos formas: la primera de ellas sería calcular el plano con que se corresponde cada punto, cambiarlo mediante los registros de la VGA, y dibujar el punto en memoria. Esto significa que para una línea de 50 pixels de ancho, realizamos 50 cambios de plano, con la consecuente pérdida de velocidad.

El otro método, más laborioso, consiste en activar el plano 0 y dibujar los puntos de la línea que caen en el plano 0, haciendo lo mismo después con el plano 1, 2 y 3. Tantos son los factores a tener en cuenta que podemos deducir que en cuestiones de velocidad, la rutina de dibujo de líneas verticales es mucho más rápida que la de líneas horizontales.


EL VOLCADO DE BUFFERS A LA VIDEO-RAM

Veamos ahora para qué ha servido la anterior introducción al dibujo de líneas horizontales y verticales. Debe quedar claro que el volcado de buffers a la video RAM es una de las rutinas más importantes que podamos crear. Mediante esta rutina, puede evitarse el engorroso (y más lento) método de programación de planos y dibujar directamente sobre buffers temporales que más tarde se volcarán a la pantalla. Esta es, pues, una función fundamental para todos los programadores en Modo X cuyos programas requieran gran velocidad (demos, juegos, etc...).


VUELQUE POR LÍNEAS VERTICALES

Analicemos ahora el problema que se nos presenta al intentar volcar, como representa la figura 2, un segmento de memoria convencional que emula una pantalla de 320x200 pixels sobre la pantalla del modo 13X.

Figura 2. Vuelque de buffers

Si estuviéramos bajo el modo estándar 13h, la solución se basaría en un bucle que volcara todos los bytes desde Segmento hasta 0A000h, como describe el siguiente pseudocódigo (80x86):

 Mover 320 bytes desde DS:SI a ES:DI:

    DS:SI = Segmento:0000
    ES:DI = 0A000:0000
    CX = 64000 (320x200)
    REP MOVSB

Tratándose de un modo unchained, nos encontramos de nuevo con el problema del cambio de plano. Cada punto del buffer tendrá una localización en pantalla con su correspondiente plano, presentándose así dos posibilidades para volcarlo. Por una parte podemos tratar el segmento como una serie de líneas horizontales y mediante un bucle volcarlas a pantalla. La otra opción es considerarlo como una serie de líneas verticales, con lo que descubrimos la utilidad del análisis anterior sobre dibujo de líneas que tan obsoleto parecía. Esta opción está desarrollada dentro del listado 2, y está en gran parte en C para facilitar su comprensión, aunque por supuesto no será así en la rutina incluida en la librería.

  Listado 2: Volcado de segmentos por líneas verticales.


void FlipSegmentC( unsigned int Segm )
{
 unsigned int offs;
 int f,f2;

 asm push ds
 asm mov ax, 0a000h
 asm mov es, ax        /* ES:DI = VGA */
 asm mov ds, [Segm]    /* DS:SI = Segm */  
 asm xor si, si

 for( f=0; f<320; f++ )   /*320 líneas verticales */
 {

  offs = f>>2 ;             
  asm mov di, [offs]           /* DI = (x/4) */
  asm mov si, [f]              /* SI = x */ 
  OutPortb( SEQU_ADDR , 0x02 );
  OutPortb( SEQU_ADDR+1, 0x01 << (f & 3) );
         /* Poner plano */

  for( f2=0; f2<200; f2++ )  /* volcar 200 pixels */
  {
     asm movsb
     asm add di, 79
     asm add si, 319
  }

  }
  asm pop ds
}


EL ALGORITMO MÁS RÁPIDO

Puede ser que con el apartado anterior el problema quede resuelto, pero los programadores en ensamblador sabemos que todos los algoritmos se pueden optimizar, así que con una rutina tan importante como esta hay que rizar el rizo y para esto disponemos de una rutina más rápida, con sólo 4 cambios de plano, que los más avispados ya habrán intuido.

Para conseguir tan sólo 4 cambios de plano, bastaría con activar el plano 0 y volcar en pantalla todos los puntos del buffer que recaigan sobre el plano 0, repitiendo el proceso para los planos 1, 2 y 3.

Esto lo podemos hacer mediante un simple bucle, ya que sabemos que si el punto 0 cae en el plano 0, el 1 sobre el 1, etc..., entonces el punto 4 corresponderá de nuevo al plano 0. Veámoslo en pseudocódigo para aclarar dudas:

    DS:SI = Segmento
    ES:DI = 0
    Activar Plano 0
    Repetir 16.000 veces (64000/4)
       AL = DS:SI  ->Leemos byte del segmento virtual
       ES:DI = AL  ->Lo escribimos
       DI = DI+1   ->x = x+4
       SI = SI+4   ->siguiente punto
    Repetir para Planos 1, 2 y 3

Como puede apreciarse en el algoritmo, activamos el plano 0, leemos un byte del buffer virtual (AL = DS:SI), lo escribimos en pantalla (ES:DI = AL) y pasamos al siguiente punto que corresponda al plano 0 (DI+=1 y SI+=4). Repitiendo esto para 320x200/4 = 16000 bytes volcamos a pantalla, en el plano 0, todos los puntos del buffer que se deberían corresponder con este plano. Haciendo esto para el resto de los planos se obtiene en pantalla el buffer que se pretendía volcar, y todo esto, aunque parezca más complicado que la anterior rutina, reduce el acceso a puertos de 320 a 4 llamadas. Todo un lujo de vuelque por software, que podemos ver traducido a código C y ASM en el listado 3.

  Listado 3: Volcado de segmentos por planos.


void FlipSegmentASM( unsigned int Segm )
{
asm {
     push ds
     mov ds, [Segm]
     xor si, si        /* DS:SI = Segm */
     mov ax, 0a000h
     mov es, ax
     xor di, di        /* ES:DI = VGA */
     xor bx, bx        /* BX = 0 (Plano) */
    }

CuatroPlanos:
asm {
     mov dx, SEQU_ADDR
     mov al, 2         /* selección de plano */   
     out dx, al        /* OutPortb( SEQU_ADDR, 2 ); */

     xor ax, ax
     inc ax            /* AX = 1 */
     mov cl, bl        /* CL = plano */ 
     shl ax, cl        /* AX = 1 << plano */

     inc dx
     out dx, al        /* OutPortb( SEQU_ADDR, AL ); */

     xor di, di        /* offset inicial 1er punto */
     mov si, bx
     mov cx, 16000     /* repetir 16000 veces */
    }

BuclePlano:
asm {
     mov al, ds:[si]      /* AL = DS:SI */
     mov es:[di], al      /* ES:DI = AL */
     inc di               /* Ste. punto de pantalla */
     add si, 4            /* Ste. punto del buffer */

     dec cx
     jnz BuclePlano       /* Repetir 16000 veces */

     inc bl
     cmp bl, 4            /* Repetir 4 veces (planos) */
     jb CuatroPlanos

     pop ds
   }
}
Hay que decir también que aunque este volcado entraña gran dificultad en cuanto a la comprensión del algoritmo para los programadores de alto nivel (C/Pascal/etc..), cuando se entiende permite ya desarrollar funciones de dibujo de sprites por líneas verticales, bloques y otros tipos de gráficos, como puede verse en el listado 4.

 Listado 4: Dibujo de Sprites por líneas verticales.

void DrawSpriteX( char *SprArray, int x, int y, char ancho, char alto )
{
unsigned int offs;
int f,f2;

 asm mov ax, 0a000h
 asm mov es, ax

 for( f=0; f<ancho; f++ )
  {

  offs = (y<<6) + (y<<4) + ((x+f)>>2 );
  asm mov di, [offs]
  OutPortb( SEQU_ADDR , 0x02 );
  OutPortb( SEQU_ADDR+1, 0x01 << ( (x+f) & 3 ) );

    for( f2=0; f2<alto; f2++ )
    {
       _AL  = SprArray[(f2*ancho)+f];
       asm stosb
       asm add di, 79
    }
  }
}


DIBUJO DE SPRITES

Considerando los sprites como buffers de un ancho y alto más reducidos podemos adaptar nuestras rutinas de volcado para dibujar bloques gráficos en pantalla. En el listado 4 tenemos la rutina DrawSpriteX(), realizada gran parte en C con el objetivo de ser fácilmente comprensible. Esta rutina resulta rápida al estar basada en el dibujo de líneas verticales del sprite. No obstante, la manera más rápida de dibujar sprites en Modo X es almacenar estos por planos. primero los datos del plano 0, luegos los del plano 1, etc... y volcarlos así en pantalla. Veamos esto con más detenimiento para realizar una rutina rápida de impresión de sprites en Modo X 1x4.


LA VGA A FONDO

Ya que se trataron en anteriores artículos el tema de los registros de la VGA, y que sólo se utilizaron para la programación del Modo X, vamos a dejar un poco de lado un tema tan interesante como es el Modo X para adentrarnos más en los registros de la VGA que nos proporcionan una amplia gama de posibilidades con las que se pueden crear nuevos efectos visuales.

Este apartado se basa en su mayoría en la manipulación de los registros ya que estos nos permiten una cantidad inagotable de posibilidades. Los registros aparentemente resultan inútiles, pero de ellos se pueden extraer multitud de efectos interesantes, sólo hay que utilizar la imaginación y experimentar con ellos. En un principio se pueden manipular realizando pruebas con todos los controladores, pero hay que tener cuidado con los registros de Timing (registros CRTC 0-7) ya que con una utilización continuada de valores inapropiados puede dañar el monitor, (el monitor empieza a pitar) llegando hasta su destrucción por completo si este no se logra apagar a tiempo. El resto de registros no son peligrosos, lo más que pueden llegar a hacer es bloquear el monitor.

Para comenzar con las aplicaciones de los registros, este apartado tratará en primer lugar el registro 3C2h llamado Miscellaneous OutPut Register. A simple vista no parece un registro al cual se le puede sacar partido; tiene apariencia de poca utilidad que tan sólo el hardware interno puede utilizar, pero eso no es cierto, tiene más utilidades de las que uno podría imaginar.

En primer lugar este registro es capaz de activar o desactivar la polaridad mediante los bits 6-7. El bit 6 es capaz de poner negativa o positiva la polaridad horizontal del retrazo, y el bit 7 hace lo mismo pero con la polaridad vertical del retrazo. El bit 5 nos selecciona la página para el direccionamiento par/impar. En este caso si el bit 5 está a 1 indica que en todos los planos se ocupan las direcciones impares, de lo contrario si está a 0 nos ocuparan todas las direcciones pares. Los bits 2-3 se encargan de la selección del reloj de video que determina la resolución horizontal (a través de la frecuencia de pixels). Si activamos el bit 1 nos dará acceso a la RAM de la VGA por parte de la CPU, y si se desactiva no será posible ver nada en pantalla: ya no existe acceso ninguno.

Es aconsejable ver los ejemplos que aparecen en el apartado correspondiente al Modo X del CD de la revista.También puede verse como ejemplo el listado 5 que indica la manera de programar, a partir del modo 13h, una resolución vertical de 480 líneas.

  Listado 5: Modo de 480 líneas.

void Set480Lines ( void )
{
 InitModo13H();                  /* modo 320x200x256 */

 asm {
    mov  dx, 0x3CC       /* registro de lectura del    */
                         /* Miscellaneous O. R.        */
    in   al, dx
    mov  dx, 0x3C2       /* registro de escritura  */
                         /* del Misc. O. R.        */
    and  al, 00111101b
    mov  ah, 11000010b   /* bits 6-7 activados       */
                         /* bit 1 regula el acceso a */
                         /* la VRAM (RAM de la VGA). */
    or   al, ah
    out  dx, al
     }
}

Esta ha sido la aplicación al registro Miscellaneous OutPut Register, cuya utilidad parecía escasa.

Como anteriormente se ha dicho, la mayoría de los registros són útiles para programar efectos gráficos. A continación se explicará el funcionamiento de otro registro un poco más conocido por las aplicaciones que ha tenido en este curso de Modo X. El registro 09h del CRTC Controller (Max Scan Line Register) tiene utilidad a la hora de programar los efectos gráficos debido al efecto que produce. Este registro es capaz de duplicar las líneas en un modo gráfico. En caso de estar cargado el modo 13h de 320x200, este registro puede duplicar cada línea con tal de tener 320x400 líneas, cada una duplicada. Debido a su duplicación solo se podrán ver en pantalla las 100 primeras líneas del gráfico original. Por lo tanto la resolución resultante no es más que de 320x100 pixels en pantalla.

Esto quiere decir que este registro se basa en la repetición de cada línea, pero no solo la duplica, sino que tiene la posibilidad de repetir una misma línea un máximo de 15 veces, dependiendo de su programación. El resultado es que, si por ejemplo, hemos de crear un plasma de 160x100 que dibujaremos en 2x2 pixels, seleccionando una duplicación de 2 líneas tan sólo habremos de dibujar los pixels como 2x1 para que sea la VGA la que doble las líneas horizontales.

El funcionamiento de este registro es muy sencillo, tan solo hay que leer de este puerto (Max Scan Line Register) un byte para poder activar el bit 7 y activar el modo Double Scan (modo de duplicar líneas). Una vez activado el bit 7 se tiene que indicar el número de veces que se repetirá una sola línea con el fin de lograr una nueva resolución. Se puede repetir una línea como máximo 15 veces debido a que son los bits 0-3 los que se encargan de indicar dicha función.

Si se quiere programar la repetición de líneas, el bit 7 siempre debe estar activado (puesto a 1). Si el bit 7 no está puesto a 1, la VGA toma su forma extendida de 400 líneas ( su forma real en este modo gráfico ya que la VGA no reconoce ningún modo de menos de 350 líneas. Los modos de 200 líneas son generados por 400 líneas físicas). Si el bit 7 se pone a 1 se podrá programar el número de repeticiones mediante los bits 0-3.

Los ejemplos (ENTER400.C, ENTER600.C, ET13H400.C, ET13X400.C) del CD de la revista muestran la programación de este registro. Un ejemplo práctico se puede ver en el listado 6 que programa la duplicación de líneas en el modo de 320x200 (13h).

 Listado 6: Modo de duplicación de líneas en 13h.

void main()
{
  SetModo13H();

  asm {
    mov  dx, 0x3D4     /* CRTC Controller      */
    mov  al, 0x09      /* Registro 09h         */
                       /* MaxScanLine Register */
    out  dx, al
    inc  dx
    in   al, dx
    and  al, 01110000b
    add  al, 129       /* 10000001b - activo bits 7 y 0*/
    out  dx, al        /* Bit 0 indica repetir una vez */
    }                  /* Bit 7 activa el modo repetición */
}


PROGRAMACIÓN DE LA PALETA

Cualquier modo chained o unchained al que se acceda desde el modo 13h estándar hereda su sistema DAC de tripletes RGB. Esto quiere decir que para cambiar la paleta en Modo X pueden ser utilizadas exactamente las mismas funciones que se usaban para 13h, como puedan serlo el uso de funciones de la BIOS (interrupción 10h) o el sistema de acceso a registros DAC controlados por los puertos 3c7h, 3c8h y 3c9h, siendo este último sistema el más recomendable por su velocidad, como pudo verse en el Curso de Programación de Demos correspondiente al número 23 de Sólo Programadores.


PROYECTOS PARA EL LECTOR

Con lo aprendido en este curso de Modo X pueden hacerse desde verdaderas virguerías gráficas hasta utilidades con un espectacular entorno gráfico (imagine el lector por ejemplo una utilidad de copiado de discos en 480x300), pasando por juegos que probablemente, aprovecharán el sistema de scroll de Modo X dibujando directamente en pantalla y restaurando el fondo al siguiente frame, sin olvidar el aprovechar la duplicación de líneas VGA para realizar efectos tan rápidos como el fuego de SP-BBS del directorio INFO que viene en el CD de la revista, que funciona a 70 frames por segundo en un 486 a 25 Mhz. Es posible así acelerar plasmas, fuegos, etc... que estén dibujados con ampliación 2x2, 4x4, etc...


MÁS SOBRE EL MODO X

Para cualquier persona que quiera introducirse el conversaciones sobre el Modo X con expertos sobre el tema puede mirar en el Mode X Frequent Answered Questions: (Mode X FAQ), situado en comp.sys.ibm.pc.demos y rec.games.programmer donde podrá encontrar textos y programas del creador del Modo X, Michael Abrash y de otros expertos en la materia.

Respecto a programas o utilidades sobre el modo X y los registros nos permitimos recomendaros el programa TWEAK de Robert Schmidt, una gran utilidad de lectura y modificación de registros de la VGA, así como todos los textos que circulan por Internet y BBSs acerca de este tema. Sobre estos textos, se han incluido algunos de ellos en el subdirectorio DOCS del software que acompaña al artículo. Estos ficheros son programas o listados que pueden ayudar a comprender diferentes aspectos de la programación en 13h para posteriormente ser adaptados a modo X según las necesidades de cada programador.

Las dudas sobre el tema pueden ser enviadas tanto a la revista como a Compiler Software. Ante todo, gracias por haber seguido atentamente este curso de programación avanzada de la sección indocumentada de la VGA.

Pulse aquí para bajarse los ejemplos y listados del artículo (74 Kb).
Figura 1: "Dibujo de líneas en modo X."
Figura 2: "Volcado de segmentos a pantalla."

Santiago Romero


Volver a la tabla de contenidos.