En la anterior entrega (FPS con BIOS), vimos el funcionamiento de los servicios de BIOS. Lo que hoy haremos será exactamente lo mismo, pero usando las bibliotecas bibliotecas gráficas del compilador, en nuestro caso graph.lib de Watcom C/C++, es decir, como nos enseñarían a hacerlo, incluso en la universidad.
El código tiene un aspecto bastante distinto. En vez de BIOS, usamos _setvideomode para asignar el modo gráfico, _remappalette para fijar la paleta de colores y la combinación de kbhit y getch para comprobar el estado del teclado. Sin embargo a nivel estructural es equivalente, y el dibujo se hace también sobre píxeles individuales, que requieren para cada uno de ellos una llamada a _setcolor y _setpixel.
Obtenemos los siguientes resultados:
- graph.lib: 17 KPixeles/segundo (0,26 FPS).
Que comparado con los resultados de los ejemplos anteriores, nos dan el peor rendimiento de todos:
- BIOS: 18 KPixeles/segundo (0,28 FPS).
- Acceso al hardware: 5.952 KPíxeles/segundo (93 FPS).
Vemos que estamos obteniendo un desempeño todavía peor que usando la BIOS. Es lógico, ya que la mayoría de lenguajes implementan gran parte de sus librerías usando los servicios de BIOS.
¿Qué ventajas obtenemos entonces? Es sencillo, las primitivas gráficas del compilador, disponen de funcionalidades listas para ser usadas, desde dibujo de lineas, que no es algo trivial, hasta gráficos, fuentes, polígonos, etc. Por tanto nos ahorran trabajo y errores, en comparación con desarrollarlas nosotros. Por otro lado,y al igual que con la BIOS, contamos con soporte universal de todos los modos de pantallas que se soporten, sin necesidad de modificar el código, y lo que es más importante, soporte de diferentes plataformas.
#include <stdio.h> #include <time.h> #include <conio.h> #include <graph.h> #if (defined(__FLAT__)) #define far #else #endif static unsigned char far gacPerin[]={...}; int main (void) { register unsigned int iX, iY, iPos, iCont; clock_t clkStart, clkEnd; /* Modo 13 */ _setvideomode(_MRES256COLOR); iX=0; for (iPos=sizeof(gacPerin)-768; iPos<sizeof(gacPerin); iX++) { _remappalette(iX, ((long) gacPerin[iPos++]>>2)+((long) gacPerin[iPos++]<<6)+((long) gacPerin[iPos++]<<14)); } iCont=0; clkStart=clock(); do { iPos=0; /* Volcar al framebuffer 320x200 pixeles */ for (iY=0; iY<200; iY++) { for (iX=0; iX<320; iX++) { _setcolor(gacPerin[iPos]); _setpixel(iX, iY); iPos++; } } iCont++; } /* Comprobar pulsación de tecla */ while (!kbhit()); clkEnd=clock(); /* Leer tecla del buffer */ getch(); /* Modo texto */ _setvideomode(_DEFAULTMODE); /* Mostrar en pantalla número de cuadros, y FPS */ cprintf("Frames: %d; KPixels/seg: %d\n", iCont, ((long) iCont*CLOCKS_PER_SEC*64)/(clkEnd-clkStart)); return(0); } |
Pero no acabamos aquí, porque como ya vimos en portabilidad, un conocimiento interno de como funcionan nuestras herramientas, puede ayudarnos enormemente. En este caso concreto a simplificar el código, y mejorar el rendimiento.
La mayoría de compiladores de C, pero también de otros lenguajes como Basic o Pascal, nos ofrecen funciones de manejo de sprites, en nuestro caso es _putimage, que tiene por finalidad dibujar una imagen en la pantalla. Desconozco el motivo, pero tanto la mencionada _putimage, como las implementaciones de PUT en Basic almacenan el bloque a dibujar con dos añadidos al principio, el ancho y el alto en píxeles del bloque. Así a la hora de dibujarlo, basta con especificar las coordenadas X e Y.
Modificamos nuestro ejemplo, e incluimos en el array gacPerin dos enteros cortos de 16 bits con sus dimensiones, antes de los datos propiamente dichos, y estamos listos.
#include <stdio.h> #include <time.h> #include <conio.h> #include <graph.h> #if (defined(__FLAT__)) #define far #else #endif static unsigned char far gacPerin[]={ 64, 1, /* Width: 320 */ 200, 0, /* Height: 200 */ ... }; int main (void) { register unsigned int iX, iCont; clock_t clkStart, clkEnd; /* Modo 13 */ _setvideomode(_MRES256COLOR); iX=0; for (iCont=sizeof(gacPerin)-768; iCont<sizeof(gacPerin); iX++) { _remappalette(iX, ((long) gacPerin[iCont++]>>2)+((long) gacPerin[iCont++]<<6)+((long) gacPerin[iCont++]<<14)); } iCont=0; clkStart=clock(); do { _putimage(0, 0, gacPerin, _GPSET); iCont++; } /* Comprobar pulsación de tecla */ while (!kbhit()); clkEnd=clock(); /* Leer tecla del buffer */ getch(); /* Modo texto */ _setvideomode(_DEFAULTMODE); /* Mostrar en pantalla número de cuadros, y FPS */ cprintf("Frames: %d; FPS: %d\n", iCont, ((long) iCont*CLOCKS_PER_SEC)/(clkEnd-clkStart)); return(0); } |
Los resultados:
- graph.lib (_putimage): 2.368 KPixeles/segundo (37 FPS).
Y recopilando las cifras anteriores:
- graph.lib (_setpixel): 17 KPixeles/segundo (0,26 FPS).
- BIOS: 18 KPixeles/segundo (0,28 FPS).
- Acceso al hardware: 5.952 KPíxeles/segundo (93 FPS).
Es decir, hemos conseguido obtener un rendimiento del orden de 2,5 veces más lento que accediendo al hardware directamente, y de 150 veces más rápido que usando _setpixel. No es nada despreciable, si consideramos que _putimage no está limitado a operar con la pantalla entera como hacíamos al usar memcpy, sino que es capaz de dibujar cualquier sprite rectangular, incluso con operaciones lógicas a la hora de volcar los píxeles individuales.
La conclusión de este artículo, y de momento de toda la saga, es que más importante que las técnicas y herramientas que se usen, es el conocimiento interno de las mismas, pudiendo enfocarnos hacia más simplicidad, flexibilidad, rendimiento, o tamaño, según nos convenga en cada caso.
Podéis descargar los dos códigos fuente y ejecutables del artículo aquí (145 Kb. en formato ZIP).