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.
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 MientrasEn 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 */ } }
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.
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 MOVSBTratá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 }
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 3Como 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 } } }
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 */ }
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