Vimos en la anterior entrega la organización del modo x y la manera de trabajar con él en cuanto a la primitiva gráfica que representa el pixel. El modo x es un modo muy complejo que requiere algo más de trabajo que los modos lineales como el 13h o la SVGA mediante VBE VESA 2.0, pero presenta algunas características que lo hacen muy útil en algunas ocasiones.
Veamos un breve repaso de la estructura del modo X para ir introduciendo conceptos nuevos muy útiles en el tema de este mes, que versa sobre la realización de scroll hardware y page flipping aprovechando las especiales características de los modos unchained.
Por otra parte sabemos que hay una serie de registros internos que almacenan los parámetros mediante los cuales la tarjeta es capaz de interpretar de forma correcta los datos que hay en la videoram para trazarlos en el monitor tras cada retrazo vertical, entre 60 y 70 veces por segundo.
Todas las tarjetas VGA tienen como mínimo 256 KB de memoria, y en el modo 320x200 (=64000) sólo se aprovechan 64K. Michael Abrash reprogramó los registros VGA del modo 13h obteniendo un nuevo modo de vídeo (13X, 320x200X) con algunas particularidades especiales.
El hecho de que el mismo byte direccione 4 pixels es un concepto derivado de los modos planares. En este tipo de modos (los de 16 colores), la tarjeta tiene 4 planos (internos) donde tiene almacenado los pixels divididos en estos 4 planos. En nuestro caso el plano 0 contiene los pixels (0,0), (4,0), (8,0), etc. En el segundo plano los pixels (1,0), (5,0)... de la misma forma que vimos en la figura del número anterior.
Cuando escribimos el color 15 (blanco) en 0xA000:0000, el pixel trazado en pantalla dependerá del plano que esté seleccionado actualmente. Si es el plano 0, la tarjeta traza el pixel (0,0), y si es el 3, el (3,0), de esta manera el mismo byte sirve para representar a 4 pixels.
Esto nos obliga a hacer varias cosas a la hora de trazar un pixel: a seleccionar el plano que le corresponde, a calcular su offset y a trazarlo.
El plano en que cae coincide con los 2 últimos bytes de su coordenada X (0, 1, 2 ó 3), ya que es el resto de dividir por 4 al haber 4 planos, y lo obtenemos mediante plano = X % 4 o (más rápido) con plano = X AND 3. Con ese dato forzamos a la VGA a que cambie el plano actual al que queremos nosotros (mediante un registro de la VGA, tal y como vimos el mes pasado) y continuamos.
El offset ahora es distinto de como lo era en lo modo 13h (y*320+x). En 13X, 320 pixels se representan mediante 320/4=80 bytes (ya que cada byte accede a 4 pixels), de manera que el offset del punto es (casi igual de sencillo que en 13h) offset=(y*80)+(x/4);
Con estos datos y el plano ya seleccionado, escribimos en 0xA000:Offset el color deseado y ya tenemos el punto en pantalla. Al recibir un nuevo byte en VRAM la tarjeta comprobará cual es el plano actual y escribirá este nuevo color en uno de sus 4 planos internos (el que corresponda).
El modo 13h lineal es un truco de los fabricantes de tarjetas, que decidieron perder 192K de memoria a favor de un sistema que seleccionase automáticamente el plano en el que escribir/leer, de manera que al escribir un byte en 0xA000, la tarjeta calcula el plano en el que almacenar el nuevo color automáticamente de manera transparente al programador, con la desventaja de desaprovechar 192 KB.
Otra desventaja es que necesitamos más cálculos, pero eso depende de nuestra capacidad de optimización. Si preparamos una tabla con las multiplicaciones por 80 precalculadas (o las sustituimos por desplazamientos) y de igual manera con las divisiones por 4, puede ganarse en velocidad.
Como vamos a ver ahora, el modo x se usa en algunos casos concretos de programas o juegos en que sus características especiales hacen que en vez de reducir la velocidad del programa, la incremente.
Un ventaja muy significativa que vamos a ver aquí es que, como vimos en el artículo de registros VGA, existe un modo de copia (seleccionable) que activa los 4 planos y permite al copiar cada byte estar copiando 4 pixels si hacemos copias de VRAM a VRAM, de manera que resulta muy rentable mantener sprites y gráficos en páginas no utilizadas para realizar esta copia a 4 veces la velocidad normal de copiado lineal (se copian la cuarta parte de bytes). De la misma manera, seleccionando los 4 planos (escribiendo 1111b al Select Write Plane), podemos rellenar 4 pixels de golpe con el mismo color al escribir el byte en VRAM; de esta manera el borrado de pantalla se reduce a, por ejemplo, 16000 escrituras MOVSB.
Su ventaja principal es el motivo de este artículo: Si cada byte representa 4 pixels, cada 320 pixels son sólo 80 bytes, y 320x200 pixels (64000), se almacenan en 64000/4=16000 bytes. Como el segmento 0xA000 es de 65536 bytes, en éste tenemos espacio para 4 pantallas de 320x200 pixels y un par de líneas más (de los 1536 bytes). Aunque escribamos valores en estas pantallas extra no alteramos el contenido de la imagen en pantalla (al menos por ahora), ya que la página activa es la página 0 (desde el offset 0 al 16000).
En estas páginas podemos pues escribir, almacenar gráficos, sprites, fondos, etc, teniendo en cuenta que son 3 pantallas más de 320x200 como la primera de que disponíamos. Pero ahora vamos a ver la particularidad del modo x que le da su funcionalidad, el posicionamiento de la ventana de visualización o cambio del start address register.
Veamos un ejemplo de pseudocódigo que trazaría un pixel en cualquier página de la videoram:
Función PutPixelX( x, y, color, pagina ) { - Seleccionar plano x&3. - Calcular offset y*80+x/4 - offset = offset+(página*16000) - Escribir color en 0xA000:Offset }Con el anterior pseudocódigo nos posicionamos en el offset de la página correcta (offset = (y*80) + (x/4) + (16000*página)) y trazamos el punto en la misma.
Esto puede hacerse gracias al Start Address Register (o registro de comienzo de dirección), que le indica a la VGA desde qué offset ha de empezar a leer bytes para representarlos en pantalla. En principio este registro vale 0, y por eso vemos en pantalla en contenido de la página 0 (empieza en el offset 0), pero nada nos impide modificar su valor a p.ej., 16000, y visualizar en el monitor el contenido de la página 1. De esta manera obtenemos 4 páginas donde trabajar con la posibilidad de trabajar en ellas por separado y cambiar a la que deseemos en cualquier momento.
El Start Address Register consta de 2 registros del CRT Controller, accesibles desde los índices de puerto 0Ch y 0Dh (ver artículos nº 9/10) para poder desplazar el "marco virtual" de la VGA (lo que vemos) a lo largo de la memoria gráfica (todas las páginas de que disponemos).
Como se ha explicado en el anterior párrafo, estos últimos registros son los encargados de indicar la posición de comienzo a partir de la cual el haz de electrones del monitor tiene que dibujar en la pantalla el mapa de bits (imagen) representado en la videomemoria.
La forma de programar este registro es:
outportb (CRTC_ADDR , 0x0C); outportb (CRTC_ADDR+1 , valor >> 8); /* byte alto */ outportb (CRTC_ADDR , 0x0D); outportb (CRTC_ADDR+1 , valor & 0xFF); /* byte bajo */El motivo por el que hay que utilizar los dos registros para enviar el byte alto (valor>>8) y el byte bajo (valor & 0xFF) es el siguiente: el valor que mandamos al registro de la VGA es un WORD (2 bytes), y los puertos del PC son de 8 bits (1 byte) con lo que la única manera de introducirlo es utilizando dos registros que contengan cada uno un byte, uno el alto y el otro el bajo, que la tarjeta compondrá para obtener el word (0-65535) que hemos enviado. Veamos en el listado 2 un ejemplo de función SetStartAddress().
LISTADO 2: SetStartAddress Register void SetStartAddress( unsigned int offset ) { outportb (CRTC, 0x0C); outportb (CRTC+1, offset >> 8); outportb (CRTC, 0x0D); outportb (CRTC+1, offset & 0xFF); }Mediante la posibilidad de modificar el Start Address Register vamos a ver 2 efectos muy útiles tanto en efectos gráficos (demos/intros) como en juegos: hablamos del page flipping y el scroll por hardware.
El modo X nos va a permitir tener 3 páginas virtuales ya que sólo una de ellas va a ser visualizada (la que esté activa mediante el offset introducido en el Start Address Register), y las otras 3 están ocultas, de manera que podemos tener una página activa mientras dibujamos en otra el siguiente fotograma del juego/animación/efecto, y cuando esté finalizada su construcción (y ahora viene lo importante), cambiar la visualización a esta página (con lo cual nos ahorramos un volcado a pantalla si trabajáramos con pantallas virtuales). A esto se le llama page flipping (o mejor dicho, true page flipping), y es muy sencillo de hacer ya que con sólo 2 páginas ya puede ser implementado, teniendo 2 páginas más libres para guardar sprites, fondos, etc.
La idea es tener una página activa (por ejemplo la 0) y dibujar el siguiente fotograma en la 1, y al finalizarlo, cambiar la visualización a la 1, y dibujar el siguiente fotograma en la 0, y así continuamente.
Un pequeño truco en estos casos es que en vez de utilizar ifs() para cambiar de página en el bucle principal:
Bucle: Dibujar_en_pagina( oculta ); if( oculta==0 ) oculta = 1; else oculta = 0; Esperar_retrazo(); Cambiar_startaddress(); Fin bucleEs más rápido usar esto:
oculta = 1-oculta;De esta manera, 1-0=1 y 1-1=0 harán el cambio correcto de la página ahorrándonos los ifs().
Si incrementáramos el valor del Start Address Register en 80 unidades (320 pixels), veríamos una línea menos de la página 0 (es decir, la primera línea visualizada sería la 1, y veríamos en la última línea de la pantalla la primera línea de la siguiente página (ver la figura para orientarse)). De esta manera, incrementando continuamente el valor de este registro para avanzar una línea completa se podrá observar un scroll, siempre y cuando antes de llamar a este registro se haya esperado un retrazo vertical, encargado de ralentizar la operación para obtener un scroll tan suave.
Ya que la intención es desplazar la imagen verticalmente, para desplazarla será necesario incrementar el valor 80 unidades cada vez sobre el valor total, que se mandará al registro 0Ch y 0Dh del CRTC. Como ya sabemos, 320 pixels en la pantalla son 80 bytes en Modo X (320 pixels / 4 planos = 80 pixels por plano ), por lo tanto cada vez que se desplacen 80 pixels, la coordenada Y se moverá en la pantalla, es decir se habrá scrolleado el gráfico en el monitor.
En el listado 3 se puede ver la manera en la que se puede implementar este tipo de scroll.
LISTADO 3: Ejemplo de scroll vertical. void main() { unsigned int y = 0; Set_320x200X(); SetOffsetRegister(80); SetPage(0); LoadImage(); /* scrollea 1 pantalla vert. */ while (y < 80*200) { y+=80; WaitVRetrace(); SetStartAddress(y); } SetVideoMode(3); }
Estos tamaños se programan con el CRTC y el índice 13h (Offset Register), en concreto para obtener 4x1 pantallas habría que reprogramarlo con el valor 160 (ver listado 1). Lo que nos interesa ahora es comprender el concepto del scroll horizontal, así que imaginemos la memoria en formato 4x1 y con una imagen en la página 1.
Simplemente incrementando en 1 el Start Address Register de una manera continua se puede obtener el deseado desplazamiento horizontal (listado 4).
LISTADO 1: Set Offset register. /* SetOffsetRegister(); Se le pasa el valor del número de bytes que queremos como anchura partido por 2. Valores de Anchura: 40 -> (80 bytes por línea, 1x4). 80 -> (160 bytes por línea, 2x2). 160 -> (320 bytes por línea, 4x1). */ void SetOffsetRegister( unsigned char anchura ) { outportb( CRTC, 0x13 ); outportb( CRTC+1, anchura ); } LISTADO 4: Ejemplo de scroll horizontal. void main() { unsigned int x = 0; Set_320x200X(); SetOffsetRegister(160); SetPage(0); LoadImage(); /* scrollea 3 pantallas horiz. */ while (x < 240) { WaitVRetrace(); SetStartAddress(x); x++; } SetVideoMode(3); }Un punto que hay que tener en cuenta siempre que se realice este tipo de scroll, es que cuando se incrementa la variable en 1 se desplaza la pantalla en 4 pixels. Esto es debido a los 4 planos del Modo X. El hecho de que se desplace en 4 pixels no indica que el scroll no vaya a ser suave, sino que la velocidad es cuatro veces mayor que si se realizara pixel a pixel.
Si de lo contrario, se desea realizar el scroll incrementando y desplazando la pantalla en 1 pixel, se tendrá que recurrir al HPP (Horizontal Pixel Panning Register), encargado de realizar este trabajo. Este registro se encuentra en el índice 13h del ATTRIBUTE CONTROLLER (3C0h), pero este es un tema que requiere mucha experimentación por parte del programador, siendo necesario primero entender todos los sistemas simples de scroll, que son los que vamos a comentar.
LISTADO 5: Scroll multidireccional. void main() { unsigned int x , y; char inc_x , inc_y; Set_320x200X(); SetOffsetRegister(160); SetPage(0); x = y = inc_x = inc_y = 1; while ( !kbhit() ) { WaitVRetrace (); SetStartAddress(x); x = x + inc_x; y = y + inc_y; if (x <= 0 || x >= 80) inc_x = inc_x * -1; if (y <= 0 || y >= 200) inc_y = inc_y * -1; } SetVideoMode(3); }En este caso necesitamos reprogramar el offset register a 80 (160/2, ver listado 1) para obtener 2x2 pantallas en modo x. Si estamos haciendo un juego o efecto, borramos los sprites que hayan en pantalla escribiendo porciones de fondo encima previamente almacenadas antes de trazarlos (técnica conocida como Dirty Rectangles), y luego scrolleamos.
El secreto está en el registro Line Compare o Split-Screen. Este registro es el encargado de determinar la línea en la cual se divide la pantalla. En la figura 3 se puede observar la estructura de la pantalla en esta modalidad.
Internamente la VGA trabaja de forma que, durante la representación de una imagen en pantalla, esta va contando internamente la línea actual. Este número no se puede leer a traves de los registros de la tarjeta (grave error de los diseñadores en las tarjetas del PC) pero otro registro (Line Compare, del CRTC, índice 18h) se puede utilizar para una comparación de línea. La VGA sabe la línea actual en que está dibujando de videomemoria a pantalla mientras este registro compara el valor que contiene con el valor que la tarjeta cuenta.
De este modo cuando este valor es alcanzado, el contador de direcciones se carga con un 0, lo que indica que la representación comienza en esta línea con los datos que se encuentran en el Offset 0 de la memoria de vídeo. En pocas palabras, el Line Compare Register actúa como un límite en el que la VGA pasa a redibujar a partir del offset 0, de manera que vemos un número determinado de líneas de la página actual, y cuando la VGA llega al límite, comienza a redibujar desde el offset 0, pudiendo así ver media pantalla de la página 1 y media de la 0 al mismo tiempo que se realiza un scroll.
Dado que el número de línea no se puede guardar en 8 bits (ya que la VGA no reconoce ningún modo con menos de 350 líneas y los modos de 200 líneas son generados por 400 líneas físicas (fenómeno conocido como duplicación de líneas)), el registro Line Compare suele estar dividido en tres registros (que contienen más bits para poder representar valores 0-350), teniendo en las tarjetas Super-VGA hasta cuatro. En el listado 4 se puede observar, para el manejo del Line Compare, la utilización de extensiones de los bits del Line Compare, como el OverFlow Register.
Para utilizar el Line Compare necesitamos pues acceder a dos registros más. Esto se hace de una manera muy sencilla:
El primero de los registros es el Overflow Register (CRTC, índice 7). El bit 4 de este mismo registro será el utilizado en este caso, que se encarga concretamente de cargar en su lugar el bit 8 del Line Compare. El bit 4 del registro Overflow es el único entre los demás que no está protegido por el bit de protección del registro 11h. El bit 9 del registro de comparación de linea se encuentra en el segundo registro que se tiene que utilizar, que es el Maximum Row Address (CRTC, índice 9), concretamente en el bit 6. Para programar por tanto el Line Compare se tendrán que cargar los bits mediante desplazamientos y enmascaramiendos entre estos registros. Este trabajo es realizado por la rutina que se puede observar en el listado 6.
LISTADO 6: SplitScreen(); void SplitScreen ( unsigned char row ) { asm { mov bl, [row] xor bh, bh shl bx, 1 /* duplicamos líneas */ mov cx, bx /* Reg. 7 (Overflow Low) */ mov dx, CRTC mov al, 07h out dx, al inc dx in al, dx and al, 11101111b shr cx, 4 and cl, 16 or al, cl out dx, al dec dx /* Reg. 9 (Maximum Row Address) */ mov al, 09h out dx, al inc dx in al, dx and al, 10111111b shr bl, 3 and bl, 64 or al, bl out dx, al dec dx /* Registro 18h (Line Compare/Split Screen) */ mov al, 18h mov ah, [row] shl ah, 1 out dx, ax } }Lo primero que nos indica el procedimiento de este listado es la duplicación del valor en el modo de 200 líneas. Después se selecciona el Overflow Register para cargar en el bit 4 el bit 8 del valor de la línea mediante desplazamiento y enmascaramiento del mismo. Para esto hay que desplazar 4 bits hacia la derecha en el valor de la línea (SHR valor, 4) con tal de dejar el bit 8 en la posición del bit 4.
A continuación se realiza con este último valor obtenido un AND con 16 para saber si dicho bit (bit 8 del valor, ahora bit 4) está activado o no. Una vez hechos los cálculos se activa o no el bit 4 del Overflow Register dependiendo de si lo está o no al hacer el AND con 16. Estos mismos cálculos se realizan ahora con el bit 9 del la línea y el bit 6 del registro 9h. Finalmente se colocan los restantes 8 bits del número de línea en el registro 18h (Line Compare o Split Screen).
El resultado de esto es la modificación del valor del line compare register, que usado convenientemente nos permite hacer verdaderas maravillas con la tarjeta gráfica.
Pulse aquí para bajarse los ejemplos y listados del artículo (62 Kb).
Figura 1: "Vídeomemoria en el modo X 1x4."
Figura 2: "Vídeomemoria en el modo X 2x2."
Figura 3: "Estructura del Split Screen."
Santiago Romero