Hasta hace poco el modo 13h era el modo gráfico más usado por los programadores, debido a su simplicidad y fácil manejo. Esta entrega vamos a analizar los registros de la VGA, y mediante su utilización, obtendremos nuevos y mejores modos gráficos que nos saquen de la rutina del ya conocido 320x200.
Hasta hace poco tiempo, las tarjetas de vídeo no tenían más de 256K de memoria, de la cual tenían que sacar el mayor rendimiento de ella obteniendo modos gráficos como el modo 13H de 320x200x256 o 640x400x16. A pesar de no tener gran cantidad de vídeo RAM para mejores resoluciones, el modo 13H ha sido utilizado por casi todos los programadores debido a su fácil manejo de memoria lineal. A medida que el hardware va desarrollándose, las tarjetas van adquirieron más memoria VRAM, con lo que la capacidad de obtener más resolución y colores por pixel aumenta notablemente. De todos modos este no es el caso que vamos a tratar en la presente entrega, ya que con un mínimo de memoria de vídeo se van a obtener nuevos y variados modos gráficos en los que poder trabajar a grán velocidad. Cuando, por ejemplo, iniciamos el modo de vídeo 13H, los registros de la VGA se programan automáticamente de forma que nos da la resolución y colores particulares de este modo. Pues bién, en esta entrega nos vamos a encargar de programar nosotros mismos los registros con el fin de conseguir diferentes resoluciones con más de una página de VRAM a nuestra disposición para volcar los gráficos que queramos. Ahora puede que lector se pregunte como es posible que manipulando los registros de la VGA podamos obtener mayores resoluciones, y aun más tener de reserva otras tantas páginas para nuestro uso adicional con tan sólo el mínimo de memoria de vídeo de la que disponemos. Esto es algo sencillo que se va a explicar a continuación.
Pués este programador, lo que hizo fue reprogramar el modo 13H cambiando los valores de los registros, y experimentando con ellos intentó dar uso a esa memoria inútil que no se utilizaba. Pero además, esa memoria sería útil para realizar efectos de scroll por hardware, split screen, etc..., con lo que además de tener más resolución tendríamos otras páginas de memoria para trabajar con ellas en estos efectos. Con grán trabajo y esfuerzo, descubrió que la VGA contaba con un bit que si estaba a 0 forzaba a la VGA a usar los 4 planos, como es el caso del modo 640x480x16 que hace uso de estos, y la mayoría de los modos EGA. Y el bit estaba a 1 cuando los modos eran lineales (como el caso del 13H). Este bit es el llamado CHAIN-4, porque encadena los cuatro planos. De ahí los términos chained y unchained que significa que el bit está a 1 o a 0 respectivamente. Por tanto cuando hablamos de modos chained nos referimos a modos lineales y cuando son unchained a los modos planares donde hacemos uso de los cuatro planos. Son los modos unchained a los que nos referimos cuando hablamos de MODOS X, y por tanto los que vamos a tratar en este artículo. Teniendo la idea básica de que es y como se inicializa el MODO X, tenemos grán parte del trabajo hecho. Ahora sabemos que inicializando el modo 13H y cambiando el bit en cuestión, que se hace enviando el valor al puerto correspondiente, forzaríamos a la VGA a gestionar el 13H como un modo de 4 planos. Pero con esto no hemos terminado de programar el MODO X, ahora es necesario modificar algunos registros que den estabilidad al modo gráfico que deseamos establecer, tales como registros de sincronización, refresco, etc... La figura 2 muestra como el modo X utiliza los 4 planos, aprovechando hasta el último rincón de la memoria disponible. Teniendo en cuenta que cada plano dispone de 64000 bytes y tenemos 4 planos, son 256000 bytes (256K) los que podemos utilizar para la programación de los modos gráficos.
LISTADO 1: Inicialización de 320x200X #include <dos.h> /* para outport() */ #define SEQU_ADDR 0x3c4 #define CRTC_ADDR 0x3d4 void Set_320x200X ( void ); void Set_320x200X ( void ) { asm mov ax, 13h /* inicializamos modo 13h */ asm int 10hM outport( SEQU_ADDR, 0x0604 ); /* bit CHAIN-4 a 0 */ outport( CRTC_ADDR, 0xE317); /* ajuste modo palabras */ outport( CRTC_ADDR, 0x0014); /* ajuste modo dobles palabras */ outport( SEQU_ADDR , 0x0F02); /* seleccionar los cuatro planos para...*/ asm { /* ...borrar la pantalla */ mov ax , 0xA000 mov es , ax xor di , di xor ax , ax mov cx , 32000 rep stosw } } LISTADO 2: Inicializacion de 320x240X #include <dos.h> /* outport */ #define SEQU_ADDR 0x3c4 #define CRTC_ADDR 0x3d4 void Set_320x240X ( void ); void Set_320x240_X( void ) { Set_320x200X( ); /* inicializamos 13X */ outportb(0x3C2, 0xE3); outport(0x3D4, 0x2C11); /* permitir escritura */ outport(0x3D4, 0x0D06); /* Total vertical */ outport(0x3D4, 0x3E07); /* Overflow Register */ outport(0x3D4, 0xEA10); /* Vertical retrace start */ outport(0x3D4, 0xAC11); /* Vertical retrace end y Write */ outport(0x3D4, 0xDF12); /* Vertical disp/enable end */ outport(0x3D4, 0xE715); /* Start vertical blanking */ outport(0x3D4, 0x0616); /* End vertical blanking */ }
El sistema de planos es algo más complicado que el lineal del modo 13h. Como se puede ver en la figura, en el primer plano de la VGA se guardan los pixels 0, 4, 8, etc... ; en el segundo: 1, 5, 9, etc.. y así con los 4 planos. Entonces, para poner un píxel en pantalla, basta con seleccionar el plano correspondiente y poner en A000:offset el valor del color de nuestro píxel. Los planos están guardados en la memoria que acabamos de aprovechar (los 192K) y podemos usar el segmento de la VGA (0A000h) para comunicarle a la tarjeta los cambios que queremos hacer en estos planos. Esto se reduce a colocar en el segmento A000h bytes que indiquen los colores de los pixels. Esto se va pareciendo cada vez más al modo 13h lineal. Con una única diferencia, que hay que posicionarse en el plano correcto antes de calcular el offset. Estos serían los pasos a seguir:
A. Seleccionar el plano correcto. B. Calcular el offset desde A000. C. Usar Stosb para poner el punto.
outportb( 0x3c4, 0x02 ) /* función de seleccionar plano */ outportb( 0x3c5, 1 ) /* plano 1 */O bién utilizando la función outport(), que envía un word o un entero (valor de 16 bits), se puede hacer todo de una.
outport( 0x3c4 , 0x0201 ); /* los 2 bytes en uno */Pero como lo que interesa es poner un pixel en pantalla, necesitamos saber donde (en que plano) va a caer dicho byte. Esto es así de fácil, si nos fijamos en la figura 2, en un mismo plano los pixels aumentan de 4 en 4 debido a que existen tres planos más que se encargan de escribir los 3 pixels restantes. Esto es muy sencillo, si tenemos un valor como coordenada X y queremos averiguar a que plano corresponde, deberemos dividir la X entre el número de planos que existan, es decir entre 4 planos. Con esto sabemos que si la operación da como resultado 0, deducimos que el pixel cae en el plano 0. Si de lo contrario, la operación da un resto, sabemos que quiere decir que ese resto nos indica el plano al que corresponde. Este cálculo se puede hacer en C usando el signo de módulo mediante: (X % 4), o bién (X & 3) que podría ser transformado a ASM como: AND AX , 3 donde AX tiene el valor de la X. Y si queremos hacerlo en PASCAL la sentencia sería así: (X MOD 4).
Una vez sabido el plano en el que cae el pixel, hay que indicarle al registro Write-Plane-Mask (0x3C4) cual es el plano mediante un número. Ese número se podría saber en C usando plano = 1 << (X % 4) y en Pascal con plano = 1 SHL (X MOD 4), pero es mejor usar algo que se pueda transportar a Asm y que sea más optimizado que el operador %, pués este implica una división y el posterior cálculo del resto:
Plano = 1 << ( X & 3 );Y con esta formula que depende de la X ya se calcula el plano a activar. También se pueden activar los 4 planos al mismo tiempo por lo que al colocar un valor en memoria aparecerían cuatro pixels consecutivos, algo bueno para dibujar líneas horizontales y rellenar polígonos. Los programadores más avanzados se habrán percatado de que el resultado de 1 << (X & 3) es un número superior a 1, ya que hay que activar algún plano.
Los últimos 4 bits del byte que le enviamos a la VGA le indican que planos activar según los bits estén activados o no. Así, si le enviamos el byte 00000100b activará el tercer plano, ya que está activado el tercer bit.
Lo que hacemos en la fórmula es colocar un uno en el último bit (00000001b) y desplazarlo hacia la izquierda hasta que el uno se coloque sobre el plano que realmente queremos activar. Veamos un ejemplo: supongamos que tenemos un pixel que ha de dibujarse en la coordenada (2,0). El resultado de x & 3 será 2 (el plano en el que está, que no es el segundo, sino el tercero puesto que el 0 también se cuenta ). Ahora colocamos el último bit a uno, 0001b, y lo desplazamos a la derecha el número obtenido, cuyo resultado sería 0100b. Este es el valor que debemos enviar al puerto 0x3c5 y que le indica a la VGA el plano que deseamos activar. No se puede usar sólo x & 3 porque en el caso de que el resultado fuera cero no activaríamos ningún plano ya que siempre tenemos que indicar con un bit el plano a activar.
Recordemos que SHL a, b desplaza los bits del operando a, y lo hace b veces hacia la izquierda:
SHL 00111010b, 1 = 01110100bLa forma de activar los 4 planos es enviar como número de plano el valor 15 (1111b) porque al tener activados los 4 bits la VGA asume que se va a escribir en los 4 planos, de manera que al colocar, por ejemplo, el byte 1 en A000:0000 nos aparecerán cuatro pixels consecutivos con el color azul. Hay que hacer notar que hasta que no seleccionemos otro plano la VGA mantendrá siempre activado el último plano seleccionado.
Sabemos que la resolución horizontal de la pantalla es 320. Si hay cuatro planos, cada uno de estos planos guardará 320 / 4 = 80 pixels por linea. Por tanto, una línea es representada en cada plano por 80 pixels. Y al dividir por 4 la X ya calculamos su offset , y si saltamos 80 bytes por cada línea, el resultado es el punto que buscábamos.
Sobre la multiplicación por 80, hay que hacer notar que en modo X disponemos de 4 páginas y podemos colocarlas como queramos, ya sea las 4 en sentido vertical, horizontal o mixto, es decir, teniendo 2 páginas de ancho por 2 de alto. Con esto quiere decirse que no siempre ha de multiplicarse la Y por 80 para posicionarse en la línea correcta, sino que dependiendo de la anchura lógica que se tenga la multiplicación será de 80, 160, 240 o 320. El criterio de selección de un sistema u otro depende del tipo de programa o efecto a crear; es decir, si queremos realizar un scroll de 4 pantallas horizontales, lógicamente tendremos que inicializar el modo 4x1. En este caso, habría que saltarse 320 píxels por cada línea, ya que las 4 pantallas están colocadas una a continuación de la otra. Este tema será el desarrollado principalmente en el próximo número, donde se explicará con más detenimiento. Volviendo con nuestro modo X colocado en 1x4 ( 1 pantalla horizontal, 4 hacia abajo ), tenemos que después de haber seleccionado el plano correcto, el cálculo que se muestra a continuación:
Offset = (80 * Y) + (X / 4);Como ejemplo, imaginemos que colocamos en A000:0000 el color 15 (Blanco). Si antes de poner el valor en memoria seleccionamos el plano 1, aparecerá en la posición (0,0) un pixel blanco. Si el plano seleccionado era el 2, aparecerá en (1,0) y si seleccionásemos los 4 mediante el valor 1111b, aparecerían 4 pixels blancos (0,0), (1,0), (2,0) y (3,0). Cada offset de la memoria sirve para poner 4 puntos, indicándole el plano correcto. Por eso en vez de 320 píxels necesitamos 320/ 4 = 80 para dibujar una línea horizontal completa. De esta manera, 80*200 pixels da = 16.000 bytes. Como el segmento de la VGA es de 65.536 bytes, disponemos de 65.536 / 16.000 = 4'1 páginas gráficas, donde poder dibujar, hacer scrolls y otras cosas que ya comentaremos en posteriores artículos. Y sin embargo, caso del modo 320x240 en vez de tener 4,1 páginas disponemos de 4 páginas justas, debido a las 40 líneas extras. En los modos X tenemos otras resoluciones como pueden ser 360x360, 320x400, etc ... donde el número de las paginas de reserva variará dependiendo del modo gráfico al que nos refiramos.
void PutPixelX( int, int, char ); void PutPixelX( int x, int y, char color ) { unsigned int offset; outportb( 0x3c4 , 0x02 ); /* Función seleccionar plano */ outportb( 0x3c5 , 0x01 << (x & 3) ); /* Esto calcula automáticamente el plano a partir de los 2 últimos bits de la coord.x */ offset = (80 * y) + (x / 4); /* calculamos el offset */ asm { mov ax , 0xa000 mov es , ax mov di , offset mov al , color stosb /* ponemos el punto */ } }Multiplicar por 80 y dividir por 4 puede ser profundamente optimizado mediante instrucciones de desplazamiento SHL y SHR ( << y >> ) para acelerar la colocación de los puntos. La división entre cuatro la podemos sustituir por x >> 2 y la multiplicación por 80 es los mismo que multiplicar por 16 más la multiplicación por 64. Aunque parezca algo complicado podemos sustituir la línea
offset = (80 * y) + (x / 4);por su variación optimizada:
offset = (y << 6) + (y << 4) + (x >> 2);
Pulse aquí para bajarse los ejemplos y listados del artículo (19 Kb).
NOTA: Ver las figuras de este artículo en el curso de modos X.
Santiago Romero