Optimizaciones

La siguiente es una lista de las optimizaciones más importantes que sería interesante estudiar o tener presentes de cara a la versión 1.0.

Subrutinas

Actualmente la creación de un proceso tiene una sobrecarga considerable: se inicializan todos sus valores locales (potencialmente 2, 4K de datos), se marca el uso de todas sus cadenas locales, se reserva memoria para la estructura que lo describe, etc. Lo que es peor, estos recursos no son liberados hasta el siguiente frame. La programación tradicional (por ejemplo, resolución de problemas matemáticos sencillos mediante funciones recursivas) resulta prohibitiva.

Sería preciso poder marcar un proceso como subrutina. Un proceso de este tipo no se visualiza en pantalla, no contiene datos locales, y no ejecuta nunca la instrucción FRAME. Se comporta como una función en C u otro lenguaje tradicional: recibe una serie de parámetros, tiene un espacio de variables privadas, y al salir éstas son destruídas. Las subrutinas se ejecutarían desde dentro del propio intérprete sin salir de la función que ejecuta un proceso [instance_go], usando espacio de pila para mantener sus variables privadas temporalmente hasta que vuelvan y actuando poco más o menos como un salto JMP con algo más de sobrecarga para inicializar variables privadas.

Debería ser responsabilidad del compilador FXC:
Debe ser responsabilidad del compilador FXI:
Sería deseable incorporar el mantenimiento del marco de pila dentro de los nuevos mnemónicos de llamada a subrutina y retorno de la misma.

Máscaras o sprites RLE

Actualmente el dibujo de gráficos más común se realiza comprobando pixel a pixel la existencia de ceros (color transparente). La versión MMX del mismo proceso hace una combinación de los valores leídos con los existentes en el buffer de destino, lo cual es mucho más lento en memoria de video pero más rápido en la RAM, ya que no ejecuta saltos internos.

Existen técnicas que permiten acelerar considerablemente este proceso, a cambio de hacer el gráfico inmutable: crear una máscara de bits del mismo (con 1 para los pixels transparentes), o una versión RLE que codifique los espacios vacíos por separado de los pixels. Las máscaras permiten acelerar también considerablemente la detección de colisiones.

Ninguna de las dos técnicas resulta interesante cuando los sprites son rotados, amplíados, o reducidos de tamaño. Además, el espejo horizontal requeriría versiones especiales de las rutinas de dibujo por máscara o con RLE. Todo ello sugiere que las versiones actuales del blitter deberían seguir existiendo, y estas optimizaciones coexistir como casos especiales.

El mayor problema es que se requiere una conversión más o menos costosa de un sprite a máscara o a versión RLE. Una modificación en el contenido del sprite puede requerir volver a convertirlo. Para este problema hay dos posibles soluciones:
  1. Que sea el programador el que explícitamente cree la máscara o versión RLE de un gráfico, mediante una función al efecto, y que vuelva a llamarla para actualizarla si modifica el gráfico original. Tiene el inconveniente de que puede ser pesado.

  2. Usar un flag "dirty" en un gráfico para detectar automáticamente cuándo ha sido modificado. Tiene el inconveniente de que algunos cambios son indetectables (p.ej. MAP_BUFFER) y de que erróneamente se puede estar perdiendo el tiempo en crear máscaras de gráficos que siempre se van a dibujar escalados, por ejemplo.
Personalmente me decanto por la primera opción, en todo caso con una variante de la función de crear máscaras que cree máscaras sobre todos los gráficos en memoria, o sobre todos los de una librería, o que active un modo para crearlas automáticamente sobre todos los gráficos cargados en el futuro. Esto aumentaría el número de funciones en tres o cuatro.

En cuanto a la implementación, cada estructura GRAPH debería contener un puntero a una función de dibujo específica, a ser llamado en lugar de gr_blit cuando las condiciones de dibujo sean normales (sin escalado, sin rotado, y sin flags o sólo con un conjunto reducido de flags, tal vez con una máscara especificada en otro campo de la estructura). La detección de la existencia de esta rutina puede venir al principio de la propia gr_blit, haciendo la mejora transparente al sistema.

La opción de hacer gráficos con rutinas de dibujo personalizables tiene múltiples aplicaciones adicionales. Por ejemplo, se podrían crear DLLs que implementaran blitters alternativos al original (con efectos especiales, u optimizaciones como las comentadas). También sería posible crear mapas compuestos (una versión especial de un mapa de 8 bits donde cada pixel en lugar de ser un color representa el índice de un gráfico dentro de una librería FPG) que serían muy útiles como fondo de un scroll, donde ahora hay limitaciones de tamaño y memoria ya que todo el fondo es un único gráfico. Bastaría con crear una rutina de dibujo especializada al caso.

Señales

Ahora mismo las señales lanzadas con SIGNAL tienen efecto al acabar la sentencia. Es posible para un proceso hijo lanzar una señal para dormir al proceso padre y volver: el proceso padre no llegará a ejecutar la sentencia siguiente.

El precio a pagar es que se comprueba el estado actual del proceso contínuamente. Tras cada mnemónico hay una comprobación de stack vacío para saber si estamos en un punto final de sentencia. Aunque es sólo una comparación de registros, sobra, y el intérprete se vería acelerado en un cierto porcentaje sin su existencia.

Desgraciadamente eso haría errática la actuación de señales, porque una señal tendría efecto cuando el bucle principal llegara al proceso correspondiente. Algunas señales tendrían efecto casi inmediato (las de un proceso contra otro que no lo ha llamado y que aún no se ha ejecutado este frame), mientras otras no actuarían hasta el siguiente. Para solucionar esto, la función SIGNAL no debería cambiar inmediatamente el estado de un proceso, sino guardar el nuevo estado en una variable auxiliar, de manera que sólo al final del actual frame ésta sustituyera al estado real. De esta forma se emularía el funcionamiento del DIV, donde las señales no tienen efecto hasta justo antes de la visualización, y que ya es suficiente como medio de interactuación entre procesos.

Lo que es peor es que se perdería la compatibilidad con los programas ya existentes, que son dependientes del funcionamiento de las señales.

La mejor solución es crear dos versiones del núcleo del intérprete (eventualmente es preciso crear más de una, por ejemplo para habilitar una ejecución paso a paso o con breakpoints). En una de ellas se sigue el mecanismo actual (aunque la comprobación del stack no sigue siendo válida si se implementan subrutinas, ya que estas pueden crear procesos), y en otra alternativa algo más rápida las señales funcionarían como en DIV. Una variable global podría decidir qué versión ejecutar. No sería necesario duplicar las versiones paso a paso del intérprete, ya que en estas el rendimiento no es tan importante y podrían comprobar el estado de dicha variable global para emular ambas versiones.

Bucle principal de ejecución y dibujo

Actualmente el bucle principal de Fenix crea al menos dos listas dinámicas que son ordenadas cada frame con qsort: una lista de procesos a ejecutar, y una lista de objetos a dibujar.

Este funcionamiento no es óptimo, ya que en muchos casos estas listas son largas y bastante constantes (con pocas o ninguna variación de un frame a otro).

Se obtendría algo de mejora en el rendimiento, especialmente en programas con muchos procesos simples, si estas listas se mantuvieran en todo momento creadas y ordenadas (insertando los nuevos elementos en la posición correcta, por ejemplo). La mayor complejidad consiste en detectar cambios en los procesos (por ejemplo, las variables PRIORITY y Z pueden ser cambiadas por procesos remotos que accedan usando punteros o el operador punto [.]).

Tampoco es óptimo el método de dibujo que se emplea actualmente, que redibuja el frame entero. Sería más óptimo en muchos casos (especialmente en juegos en alta resolución con fondo estático) si se creara una lista de zonas "calientes" rectangulares y se limitara el dibujo del frame a esas zonas (repasando los procesos que han sido creados, movidos o alterados en forma o gráfico). Este tipo de implementación tiene el problema de que es necesario mezclar los rectángulos obtenidos. Por ejemplo, una gran cantidad de procesos "partícula" pueden crear 15 ó 20 rectángulos en pantalla que se superpongan en una configuración no trivial. En un caso extremo puede ser más lento realizar este proceso que dibujar la pantalla entera. Sería mejor dejar de manos del usuario la elección, soportando las variables DUMP_TYPE y RESTORE_TYPE del DIV original. Un juego con una gran cantidad de procesos estáticos (por ejemplo un juego isométrico tipo "Alien 8" donde cada cubo en pantalla formando el decorado sea un proceso) no es práctico ahora mismo, pero podría serlo si no se redibujaran todos cada frame.

[El fichero que esquematiza el funcionamiento actual del intérprete contiene una posible implementación de este sistema]

Otra alternativa es dividir la pantalla en una rejilla y marcar cada cuadro de la rejilla que sea "pisado" por un proceso para ser redibujado. Con un número ajustado de celdas esto puede ser más adecuado para ciertos casos que la solución anterior de los rectángulos, a costa de dibujar potencialmente una zona de pantalla bastante más amplia.

Scroll "parallax"

Ahora mismo no hay ninguna rutina que dibuje scrolls, sino que simplemente se dibujan las capas del mismo la una sobre la otra. Esto resulta lento especialmente en alta resolución.

Un primer paso es dibujar una rutina de dibujo "parallax" que mezcle dos o más capas, dibujando las capas inferiores a través de los agujeros de las capas superiores. Esta rutina puede beneficiarse de la compresión RLE del gráfico que forme la capa superior. Sería preciso alterar las estructuras actuales, pensadas para la existencia de sólo dos capas por scroll. El resultado podría no ser compatible con los juegos actuales.

Un inconveniente es que puede ser interesante usar rutinas particulares para dibujar planos de scroll, como por ejemplo la rutina comentada más arriba de dibujo de gráficos por bloques a partir de otros gráficos. Para implementar este caso, la rutina de scroll podría mantener una caché con la zona visible de cada plano (de tamaño un poco superior a la superficie visible de pantalla) e irla actualizando llamando a la rutina de dibujo a medida que se desplace la pantalla. Esto permitiría también habilitar planos con flags simples, como el espejo horizontal, sin implementar un caso especial de la rutina de parallax para ello, a costa de un incremento en el uso de memoria (más o menos elevado; a 640x480 un plano a pantalla completa de 16 bits son unos 600K de RAM).

Un problema clásico de los juegos DIV con scroll es la no existencia de planos superiores de scroll (o mejor dicho, no poder intercalar procesos entre planos de scroll). Sólo habiendo dos planos no es especialmente importante y el usuario recurre a procesos para simular un plano de scroll superior, pero permitiendo la existencia de más planos sería mejor soportar esta característica. Fenix permite ahora el apilado de capas de scroll, pero salvo a baja resolución, no es una solución óptima. Si se implementa un parallax de más de dos planos sería interesante poder dibujar procesos entre plano y plano. Este problema no tiene una solución sencilla. La rutina de parallax podría dibujar los procesos encima del scroll, una vez dibujado éste al completo, y luego redibujar las capas que estén encima de ellos (limitandose a la zona que ocupan en pantalla, una técnica parecida a un RESTORE_TYPE por rectángulos). Esta solución sería más rápida que tener que dibujar los planos en múltiples pasos, pero de todas formas son bastantes pixels los que se dibujan varias veces. Una solución mejor pero más compleja: estudiar la disposición en pantalla de todos los procesos y hacer una clasificación de la pantalla en zonas rectangulares, de forma similar a como funciona la GUI.DLL para dibujar ventanas sin pintar dos veces el mismo pixel. En los rectángulos donde no haya procesos, actuar normalmente; en donde haya algún proceso, dibujar los planos inferiores, seguidos de los procesos, seguidos de los planos superiores.

Para complicar el asunto sigue siendo interesante poder hacer transparencias o efectos entre planos de scroll. Sin embargo este tipo de recursos ya no hay más remedio que dibujarlos después de haber dibujado el scroll inferior. La solución más simple es permitir el apilado de planos de scroll como hasta ahora, de manera que si el usuario desea hacer una franja de nubes que se desplacen, por ejemplo, puede hacer un scroll apilado sobre una región reducida de pantalla.