r99 -> r110. Unos cuantos días después, a un ritmo mucho más relajado; sin prisa pero sin pausa.

  • Refactorizaciones mayores en diferentes componentes. Limpieza parcial de código.
  • Correcciones en IoFileMgrForKernel. Una cosa que impedía funcionar algunos juegos era que el IoFileMgrForKernel, creaba Streams a la hora de abrir archivos y se los pasaba a la máquina virtual como un puntero de la máquina host, que el guest tomaba como un identificador entero. El puntero se perdía de la heap del host y de las pilas y como el valor estaba guardado en una zona de memoria no controlada por el GC y que no se puede gestionar, cuando se ejecutaba el recolector de basura, se cargaba streams que todavía estaban en uso. La solución fue guardarse una lista de Streams abiertos en la zona controlada por el GC. Se seguía pasando el puntero, pero ya no se perdía. Aunque lo que haré en un futuro será crear IDs de valor bajo que tengan asignado el puntero. Así es más controlable y más cercano al comportamiento original. Con esto empezó a funcionar el Trigonometry Wars.
  • Añadí el módulo sceReg para gestionar el registro de la psp y le añadí un par de claves fake al registro para probar que funcionaba. No está implementado todavía 100%, ni guarda el estado del registro, ni carga el registro de un archivo, pero permite obtener claves del registro que era la primera aproximación par a hacer pruebas con ciertos homebrew.
  • Deshabilité temporalmente los threads de audio; ralentiazaban bastante todo, y actualmente el sonido no funciona correctamente. Tengo que hacer un mixer o usar uno ya hecho (como fmod por ejemplo).
  • Implementé más métodos de diferentes módulos del kernel.
  • Mejoré la gestión de errores y la información de debug que se muestra, aunque( todavía no hay un debugger gráfico en condiciones).
  • Hice cambios menores ThreadManForUser para que arrancase pspsolitaire. Los threads suele ser uno de los aspectos que más cosas rompen. Hasta que estén correctamente implementados, habrán problemas por todas partes.
  • Añadí una clase Logger por una parte para no depender de la consola cuando se trabaje más la GUI y por otra parte para poder filtrar a petición ciertos mensajes.
  • El tema de tests estaba completamente roto desde hacía bastantes revisiones, así que me puse a apañarlo. Reestructuré las syscalls; les cambie los códigos de >= 0x2000 a >= 0x1000 ya que la PSP no reserva esos códigos para las funciones del kernel y es más “seguro”. Añadí una syscall nueva para especificar un rango de memoria del que se hará un CRC32 para verificar que el contenido es el esperado. Preparé un ejecutable que lanzaría los tests buscando archivos .expected.
  • Empecé un componente de edición hexadecimal para ver y editar la memoria del guest.
  • Añadí un registro CLOCK por thread incrementado por cada instrucción ejecutada.
  • Metí debugging de JAL y JR r31 para poder localizar problemas con mayor facilidad.
  • Empecé la implementación de la relocalización de Elf relocalizables.
  • Arreglé un par de problemas gráficos: transform2D 480 -> 512 for glOrtho (esto hacía que se “comiesen” algunos pixeles a la derecha en juegos con todo/parte en 2D); glReadPixels en gpu.impl.GpuOpengl (hacía que hubiesen problemas al renderizar mediante GE en targets que no fuesen RGBA8888).
  • Hice cambios en el ensablador. Y añadí la posibilidad de que el emulador pudiese cargar, ensamblar y ejecutar ensamblador mips directamente. Esto permitiría crear tests muy sencillos en ensamblador para probar instrucciones individualmente. Creé una demo en ensamblador de unas pocas instrucciones (unas 12) para probar el rendimiento. Pese a ser 12 instrucciones, hacía un proceso muy costoso, que era pintar todo el displayBuffer (0x04000000-0x04088000) de un color que iba cambiando (y no pixel a pixel, sino componente a componente). Eso es mas o menos medio millón de iteraciones. Y cada iteración tiene varias instrucciones. En el modo intérprete esto iba a unos 6fps.
  • Esto era el preludio de lo que quería hacer. Quería empezar ya la recompilación dinámica. Y a las 6 de la mañana de hoy he dejado el ejemplo en ensamblador sencillo que hice funcionando a rozando 50fps y sin hacer demasiadas optimizaciones :P. Esto es unas 8 veces la velocidad en modo intérprete.
  • El hecho de estar trabajando en el portátil, en uno que ya tiene algún tiempo, implica tener menos potencia de ejecución. Esto tiene ventajas: me hace ver las cosas que van lentas y tratar de hacer que vayan más rápido. Desventajas: pierdo mucho más tiempo; los programas que “cascan” pasado un rato o el tiempo que invierto compilando, es tiempo perdido. Tengo que probar DSSS para ver si mejoro los tiempos de compilación sustancialmente.

Cosas a comentar:

Lo más interesante en este bloque de revisiones ha sido la recompilación dinámica. Era una primera aproximación. Si bien ya había hecho alguna demo técnica de recompilación dinámica en D, había sido muy sencilla y poco realista.

Sobre cómo está montado:

Está montado por capas. Hay una primera capa de un emisor de código que no se centra en ninguna plataforma host, ni en ninguna guest. Esta primera capa se encarga de ofrecer una serie de funciones para escribir en un buffer (que se podrá ejecutar) valores de 8, 16 y 32 bits. También se encarga de la creación de etiquetas y slots referenciando a etiquetas, y la posibilidad de escribir en los slots, una vez las etiquetas han sido definidas. Permite escrituras de 32 bits relativas y absolutas. Luego monté otra capa encima que ya se enfocaba en generación de código de host X86 para ejecución de 32 bits. Ofrece una serie de funciones limitadas para generación de código. (Voy añadiendo conforme me van haciendo falta.).

Luego monté una última capa enfocada al guest MIPS. Básicamente ofrece una serie de funciones que emiten código de 8086 en función de características del MIPS.

Funcionamiento básico:

El tema de los registros del mips. Los PCs tienen 8 registros de uso “general” (aunque para ciertas instrucciones son de uso específico). Algunos registros se tienen que preservar tras la ejecución de funciones, como se especifica en las convenciones de llamada de los X86:

http://en.wikipedia.org/wiki/X86_calling_conventions

EAX, EDX y ECX se pueden usar sin miedo a fastidiar valores usados en el ámbito superior. Así que decidí trabajar con esos principalmente, para evitar marrones de PUSHes y POPes o de guardar los registros y restaurarlos en alguna otra zona de memoria.

Como no se pueden mapear 8 registros en 32, la implementación más sencilla es trabajar con un puntero donde estén los registros y usar el acceso a memoria directo tan molón de los X86.

Decidí usar el registro ECX para almacenar el puntero a los registros. Así que lo primero que se hace antes de empezar ejecutar código es meter en ECX el puntero a los registros.

Después de esto las operaciones normales consisten en guardar/cargar valores de los registros de MIPS en EAX y EDX, hacer la operación pertinente y guardar en el registro de salida el valor, usando ECX como base.

Luego, al margen de los saltos que son la parte más complicada (especialmente por el delayed branch de los mips y el likely de las revisiones posteriories). Al margen de eso, hay que hacer switching de threads y gestionar interrupciones “cada cierto tiempo”. Como sabemos que tarde o temprano el código llegará a un branch o un jump, por narices, pues los branches y los jumps son buenos momentos para hacer estas costosas gestiones. Antes de los saltos, hago una llamada a una función “tick” que se encarga de hacer estos procesos y devolver true o false dependiendo de si ha habido un cambio en PC. Si ha habido un cambio de PC, hay que salirse de la ejecución y volver a empezar. Esto ocurre también en las syscalls, en los jalr y en los jr que tienen direcciones indeterminadas cuando se recompila el código.

La gestión de los delayed branch:

Una características indentificativa de los MIPS son los delayed branch. Para saber cómo implementarlo primero hay que saber cómo funciona. Simplificando, la delayed branch tiene la característica de que la instrucción posterior a un salto, se ejecuta después de haber determinado el salto. Esto es un engorro ya que en X86 no hay nada parecido, con lo que hay que reordenar algunas cosas.

Esto en sí mismo no es “demasiado” follón. El problema es que hay ciertos saltos “Likely”, cuya instrucción en el delayed slot se ejecuta únicamente si se va a realizar el salto, si no, se skippea. Esto complica un poco el tema.

La solución que he aplicado yo (que no tengo claro si es buena en comparación con otras porque todavía no he visto otras implementaciones), es generar un código antes y otro código después del delayed slot en función de la instrucción de salto. En los branches normales, se hace la comparación antes, se guardan los flags de comparación si la operación del delayed slot los modifica y luego se hace el salto después de haber ejecutado el delayed slot si la condición evaluada antes es cierta con respecto al tipo de salto.

Los Likely todavía no los he implementado, pero supondrán añadir un salto más. Posiblemente uno condicional a la siguiente instrucción al delayed slot, y otro incondicional entre el delayed slot y la instrucción siguiente. Es un coñazo sí, pero es lo que hay.

Análisis de funciones, reutilización de código compilado y caché:

La otra parte complicada es la gestión y análisis de bloques compilados. Por una parte hay que analizar el código que se va a ejecutar en busca de saltos condicionales y de saltos incondicionales que producen un callejón sin salida, que serán los puntos que hagan que la función de análisis termine: J, JR.

Inicialmente lo hice de otra forma, pero como me dio algunos problemas, acabé creando una etiqueta por cada instrucción compilada y luego usar referencias. Al hacerse por cada instrucción, se puede hacer en un solo paso (sin necesidad de un paso de análisis [para determinar las etiquetas] y otro de emisión) por contra debería ocupar más memoria y quizá cuando no esté compilado el código, le podría costar un poco más de compilar. Aunque creo que estos dos factores son los de menos, ya que una vez compilado es cuando el asunto vuelta.

El tema de selección de bloques de código a ejecutar.

Lo primero que se hace cuando se quiere empezar a ejecutar desde una posición de memoria, es mirar a ver si hay algún bloque compilado que contenga esa dirección. Si se contiene, va a ese bloque, se localiza la etiqueta y se empieza a ejecutar. Esto tiene actualmente un supuesto coste de dos accesos a un hashmap (2 veces logarítmico con una parte lineal para colisiones). Quizá se podría mejorar, pero eso será más adelante. Por lo pronto mejora mucho el rendimiento.

Si no se localiza (miss de caché de código), se empieza el proceso de análisis que genera un bloque compilado, y cuando se termina, se vuelve a buscar en la caché.

Una cosa que no he hecho todavía que habrá que hacer y para la que he preparado un unittesting (que se pasa sin problemas en la versión interpretada) es la invalidación de bloques. En determinadas ocasiones (por ejemplo recompilación dinámica en el guest o carga dinámica de librerías ocupando el mismo espacio de memoria), un código ejecutado puede modificarse. Hay que gestionar las escrituras y si se toca una parte cacheada invalidarla. Se me ocurre alguna forma de hacer esto relativamente bastante rápido, pero ya hablaré sobre el tema cuando llegue el momento.

Por lo demás nada más que añadir por el momento. El tema de la recompilación dinámica es la primera vez que lo hago en serio y no tengo claro si lo he hecho bien. Quería de todas formas hacer un primer intento a mi “bola”, sin aprovechar los errores aprendidos por otras personas y queriendo descubrir cómo montarlo por mí mismo. Una vez esté funcionando miraré otras implementaciones para ver cómo lo han montado ellos y comparar. Quedarme con las cosas buenas de la mía, y con las cosas buenas de la otra/otras.

Y nada más. Aquí dejo algunos screenshots de algunos juegos homebrew que empiezan a funcionar con las últimas versiones del SVN:

http://pspemu.soywiz.com/screenshots

Trigonometry wars. Fue el primer juego homebrew en funcionar. Con los threads de audio bloqueados, va a una velocidad medianamente aceptable en mi portátil en modo intérprete.

Gold Miner. Una demo hecha posiblemente para algún concurso veraniego en SangHai. Carga y va a una velocidad bastante buena en mi portátil.

Demo técnica de rpg. Wandria: Tears of Fate. Le cuesta bastante cargar en modo intérprete y luego va entre 10 y 35 fps en mi portátil.

Un inicio de port del juego Skyroads. Este está en 3D. Va bastante bien en mi portátil.