C#–Cómo escribir código realmente rápido

Vuelvo tras una larga ausencia con un tema con el que llevo luchando el último año: optimizar código para mejorar la respuesta a los usuarios tanto a nivel de servidor como de aplicación usando incluso menos recursos que antes. Y si piensas en distribuir tu aplicación en la nube, incluso puede que ahorres dinero.

Cualquiera que me conozca sabe que mi obsesión es cómo exprimir las máquinas al máximo. Y por “exprimir” no significa “tenerlas echando humo” haciendo cosas, sino más bien al contrario: que un solo servidor pueda ejecutar las mismas funcionalidades pero con un consumo mínimo de CPU y memoria. Y en C#, mínimos cambios en los programas pueden acelerarlos de forma más que notable.

No hay que pensar en segundos… toca hacerlo en milisegundos

Es lo que veo muchísimas veces hablando con otros programadores que no ven ningún problema en su código: prácticamente hagas lo que hagas hoy en día con un ordenador, si sólo lo haces una vez, es virtualmente instantáneo. Escriben código, ejecutan el programa de prueba y violà! el resultado en “menos de un segundo”. El problema es que cuando eso mismo hay que hacerlo decenas de miles de veces por minuto (o segundo!) la cosa se complica: lo que antes tardaba “menos de un segundo” ahora tarda horas. Y es donde las micro-optimizaciones o los “refactorings” complejos (que veremos como parte final de este artículo) muestran sus resultados: limando no segundos, sino milisegundos a cada operación se obtienen beneficios simplemente espectaculares en el rendimiento de nuestros programas.

Importante: hay cosas que no se pueden optimizar: hay que rehacerlas

Y no se nos deben caer los anillos por ello. Puede que algo simplemente sea lento. Muy lento. Excesivamente lento. Y es muy probable que no tenga por qué serlo y la “optimización” no sea la respuesta, sino más bien un rediseño. Y no pasa nada: es lo que he hecho durante el último año. Si estás leyendo esto pensando en que vas a encontrar una pauta de programación que apliques “en dos líneas” a tu código y que de repente funcione deprisa, deja de leer ya: si, algo ganarás, pero no mucho. Pero sí hay infinidad de cosas a saber para mejorar de forma más que notable la velocidad y el rendimiento de nuestros actuales programas sin tener que tirarlos enteros y empezarlos de cero.

El truco? Optimizar bloque por bloque, función por función, y no pensando en que “quede más bonito”, sino que quede “más rápido” (lamentablemente en muchos casos no es compatible lo uno con lo otro, pero recuerda: comenta tu código). Y siempre: usa el Profiler de Visual Studio!

Pero vamos a un ejemplo real…

new() y el recolector de basura

O “garbage collector” como muchos lo conocemos. Nos permite olvidarnos de la gestión de memoria, de acordarnos de liberar los objetos que ya no usamos, pero con un precio: reservar memoria es “caro”, pero es que la gestión de la recogida de basura es carísima. Y eso que cada generación de la máquina virtual .NET mejora particularmente en eso, en la reserva y la limpieza de la memoria.

Por supuesto no estoy diciendo que crear objetos con new() sea malo, en absoluto. El problema, particularmente con C# y otros lenguajes gestionados, es que hay montones de new()’s implícitos en infinidad de operaciones que podemos evitar. Menos reservas de memoria, más velocidad. Menos objetos que gestionar, mucha más velocidad todavía. El otro problema es asumir que el coste de hacer new() es cero: nada más lejos de la realidad (creo que sólo hay otra operación más cara que reservar memoria: lanzar una excepción, pero eso es otra historia).

Un ejemplo simple pero práctico: típico programa que recibe registros en texto separados por comas. Cuantas veces habéis visto (o escrito) algo como esto:

static int[] Decode(string data)
{
  string[] dataFragments = data.Split(new char[] { ',' });
  List dataValues = new List();
  foreach (string fragment in dataFragments)
  {
    dataValues.Add(Int32.Parse(fragment));
  }
  return (dataValues.ToArray());
}

Alguien ve algo “raro”? No?

Algo tan simple como ese new char[] { ‘,’ } tan inocente es responsable de una merma en la velocidad de la función.

Si, esta función se ejecuta en muy, muy poco tiempo – si sólo se llama una vez. El problema es si hay que llamarla miles de veces. Miles de veces crearemos un array en memoria, de un carácter de capacidad, pero toca buscar memoria, asignarla, inicializarla… Una vez no pasa nada… un par de millones de veces y la cosa se complica.

Esa es la típica situación que mejora simplemente con un mínimo cambio en el programa:

private static readonly char[] splitChars = new char[] { ',' };
static int[] Decode(string data)
{
  string[] dataFragments = data.Split(splitChars);
  List dataValues = new List();
  foreach (string fragment in dataFragments)
  {
    dataValues.Add(Int32.Parse(fragment));
  }
  return (dataValues.ToArray());
}

Tan sencillo como usar una variable estática, inicializada al principio de la ejecución del programa, y eliminamos miles y miles de reservas de memoria. Para comprobarlo, vamos a escribir un programa de pruebas que ejecute las dos funciones un millón de veces, para comprobar si hay alguna diferencia:

public static void Run()
{
  Console.WriteLine("Código inicial en ejecución.");

  string record = @"23,51,181,47,193,119,651,45,213,194,413,188";
  var start = DateTime.Now;

  int sum = 0;
  for (int dd = 0; dd < 1000000; dd++)
  {
    var dataElements = Decode(record);
    sum = 0;
    for (int zz = 0; zz < dataElements.Length; zz++)
      sum += dataElements[zz];
  }

  GC.Collect();
  var span = DateTime.Now - start;
  Console.WriteLine("Suma: " + sum + " - Tiempo total: " + span.ToString());
}

Este programa simplemente decodifica el mismo registro 1 millón de veces y, por hacer algo, simplemente suma los valores de los números (así verificamos que todas las variantes devuelven los mismos resultados). Ahora ejecutamos el programa en versión Release con las dos versiones de la función:

Base y primera optimización

Simplemente por evitar esa reserva de memoria nos ahorramos 140 milisegundos: un 5% del tiempo total (he calculado el ahorro sobre varias ejecuciones – nunca tarda lo mismo). No está mal para una sola línea modificada.

Pero vayamos un poco más allá. Hay otra reserva de memoria al final de la función, al hacer una conversión a array (return(dataValues.ToArray()). Si devolvemos directamente el objeto dataValues, sin conversión, qué pasa? Cambiemos

static int[] Decode(string data)
{
  ...
  return (dataValues.ToArray());
}

por

static List<int> Decode(string data)
{
  ...
  return (dataValues);
}

y ajustamos un poco la función de prueba para que use la propiedad Count en lugar de la propiedad Length de los arrays. El resultado:

StringOptimizationRun2

Otros 50 milisegundos que hemos limado al tiempo original. Y todavía no hemos trabajado mucho para un ahorro del 7%.

Poco más se puede rascar, verdad? Pues no… aún queda mucho por hacer.

Empieza el refactoring

Hasta ahora hemos simplemente cambiado cosas de sitio o hemos eliminado alguna reserva de memoria. Cuando ese proceso no da para más, toca pensar cómo cambiar la estructura del código para que sea más eficiente.

El siguiente responsable de reservas de memoria es el objeto List<int> que usamos para almacenar los valores mientras se leen del registro de entrada. Y si en lugar de eso usamos un array permanente para devolver los valores? Vamos con el refactoring, y aquí es donde tenemos que poner un límite a los datos.

Hasta ahora nuestro código podía soportar cantidades arbitrarias de elementos en un registro, pero es necesario realmente? O podemos poner un límite de, digamos 100 elementos? 200? 500 quizá?

Cambiemos la función para que quede como sigue:

private static readonly char[] splitChars = new char[] { ',' };
static List Decode(string data)
{
  string[] dataFragments = data.Split(splitChars);
  List dataValues = new List();

  foreach (string fragment in dataFragments)
  {
    dataValues.Add(Int32.Parse(fragment));
  }
  return (dataValues);
}

Si os fijáis la función espera ahora un array de enteros que rellenará con los valores que obtenga del registro, devolviendo como resultado el número de elementos que ha leído. Otro poco más de refactoring en la función de pruebas para que quede como sigue:

int[] values = new int[512];
int sum = 0;
for (int dd = 0; dd < 1000000; dd++)
{
  var elementCount = Decode(record,values);
  sum = 0;
  for (int zz = 0; zz < elementCount; zz++)
    sum += values[zz];
}

La función reserva primero un único array de enteros que usa para todas las llamadas a la función. Da igual que el array sea local a la función o miembro de la clase: sólo se crea uno y se reutiliza continuamente. Si ejecutamos el programa ahora con la nueva versión obtenemos lo siguiente:

StringOptimizationRun3

Ahora sí se ve una mejora realmente sólida del rendimiento, del orden nada menos que del 20% con respecto al código original.

Pero se puede ir mucho, muchísimo más lejos.

El siguiente responsable de la velocidad de la función es el Split() que hacemos de la cadena. Split tiene que fabricar internamente un List<String> para almacenar cada cadena que va truncando, reservar cada objeto String de cada elemento separado, inicializar los contenidos (sacando copias de fragmentos de la cadena original), agregarlo a la colección para finalmente invocar el método ToArray() y devolvernos el array de cadenas. Y una vez que terminamos la función, todo ese trabajo se destruye y pasa al recolector de basura. Y todo eso destruye el rendimiento, cada reserva de memoria y copias por la RAM machacan los contenidos del caché del procesador, volviéndolo prácticamente una inutilidad. Los procesadores modernos son felices leyendo memoria secuencial, y el mero concepto de crear miles de objetos desperdigados por la memoria del PC va en detrimento del rendimiento.

Pero qué se puede hacer en ese caso?

Optimización final

Pues en este caso, nada tan facil como no usar lo que parece obvio. Vamos a pensar en el registro que nos dan no como en una cadena que sólo podemos utilizar por medio de otras funciones, creando copias, fragmentándola, sino como un simple array de caracteres. Porque, curiosamente, lo es.

La versión final de la función recorre la cadena de caracteres uno por uno: si el caracter es un dígito lo acumula, si el caracter es una “coma” completa la acumulación y añade el valor al array de enteros del resultado, haciendo simplemente operaciones aritméticas simples, con las que el procesador es feliz (por cierto: si sabes C++, te sentirás como en casa):

static int Decode(string data,int[] values)
{
  int charCount = data.Length;
  int valueIndex = 0;
  int acc = 0;
  for (int dd=0;dd<charCount;dd++)
  {
    char c = data[dd];
    if ((c >= '0') && (c <= '9'))
    {
      acc *= 10;
      acc += (c & 0xF);
      continue;
    }
    if (c == ',')
    {
      values[valueIndex++] = acc;
      acc = 0;
      continue;
    }
    throw new Exception("Invalid character in data.");
  }
  // Añadir el último valor
  values[valueIndex++] = acc;

  return (valueIndex);
}

Tampoco ha sido para tanto… pero veamos cuanto hemos ganado:

StringOptimizationRun4

No, no es una broma: de un proceso que tardaba 2,5 segundos en ejecutarse se ha quedado al final en 100 milisegundos. 25 veces más rápido que el original.

Y el truco simplemente consiste en optimizar el código en bloques funcionales de tamaños como el que acabamos de ver, que permiten concentrarse en un problema concreto, determinar los requerimientos exactos y escribir un código que haga “eso” y “nada más que eso”.

Pensando en la nube

Y si realmente te estás planteando hospedar tu flamante proyecto en la nube, empezar pensando en las optimizaciones como las que acabas de ver te puede ahorrar un dineral, en éste caso concreto en el costo de máquinas virtuales, o en caso de máquinas físicas poder emplear PCs mucho más económicos con el mismo rendimiento. Pero hay que ser metódico, no puedes decirte a tí mismo “bueno, ya optimizaré al final”, porque quizá, cuando llegue el momento, descubras que el “refactoring” requiere de una reescritura completa. Y créeme, llegado ese momento ya es tarde…

Por cierto, otra pequeña optimización de la que no he hablado: cuantos componentes de .NET tenéis referidos en la sección References de vuestros proyectos? Seguro que los usáis todos? Quizá puedas eliminar alguno de ellos o más de los que crees. Y una DLL que no se carga (junto con sus propias dependencias) es otro ahorro de recursos.

El proyecto completo con todas las versiones de optimización podéis descargarlo aquí. Y podéis comprobar qué referencias usa. Sólo una: System.DLL.

Categorías:Desarrollo, Tutorial Etiquetas: , , ,

Temporizadores nativos de Windows con C++ 11

Existen multitud de razones para programar en C++. Pero el API de Windows puede parecer un poco intimidante al principio y esa es la razón por la que tanta y tanta gente prefiere usar o bien librerías que se hagan cargo del bajo nivel o de entornos gestionados donde todo es mucho más fácil.

Pero la verdad es que con poquísimo esfuerzo es posible ir creando pequeñas herramientas que nos faciliten la vida y que llevan a la creación de código extremadamente legible y fácil de mantener.

El problema con los temporizadores

Hacer un temporizador en el API nativo de Windows puede ser, bien extremadamente simple o bien sorprendentemente complejo, dependiendo de los conjuntos de APIs que queramos emplear. Si usamos ventanas clásicas (ay! esos tiempos), con la función SetTimer() conseguimos que nos llegue un mensaje WM_TIMER periódicamente a nuestra ventana “y ya está”. La cosa se complica cuando queremos timers asíncronos, donde nos veremos obligados a emplear threads para hacerlo.

Lo que busco es poder aprovechar la versatilidad de C++ en su versión 11 y poder escribir algo como sigue:

CTimer timer;timer.Set(1000,1000,[]() 
  { 
    putc('.',stdout); 
  });

Bonito, verdad? Sin crear threads, sin declarar cosas globalmente, sin mensajes a ventanas. Todo ello se puede hacer empleando el API de Windows nativo, con una simple clase que nos haga un poco el trabajo por debajo aprovechando la función CreateTimerQueueTimer():

BOOL WINAPI CreateTimerQueueTimer(
  _Out_     PHANDLE phNewTimer,
  _In_opt_  HANDLE TimerQueue,
  _In_      WAITORTIMERCALLBACK Callback,
  _In_opt_  PVOID Parameter,
  _In_      DWORD DueTime,
  _In_      DWORD Period,
  _In_      ULONG Flags
);

Vamos a ver un poco por encima qué son todos esos argumentos que requiere el API. Para empezar, phNewTimer es un puntero a un HANDLE estándar de Windows (PHANDLE). Es una variable que deberemos suministrar al API y donde, una vez invocado, nos devolverá el handle sobre el que realizaremos el resto de las operaciones con el mismo. TimerQueue es un argumento opcional, que nos permite emplear colas de timers “custom” hechas a medida, si es que deseamos un control tan fino de la ejecución. En nuestro ejemplo vamos a emplear NULL para solicitar la cola predefinida por el sistema.

Aquí es donde viene la parte interesante, en el argumento Callback. Un tipo WAITORTIMERCALLBACK básicamente define un tipo concreto de función a la que el sistema operativo invocará al dispararse el temporizador. Con eso iremos en unos minutos, junto con Parameter, argumento que se pasará de vuelta a nuestra Callback.

DueTime y Period son, como podréis haber podido imaginar, los tiempos para el primer disparo y subsiguientes, definidos en milésimas de segundo. Finalmente, Flags es una variable donde podremos incluir opciones de funcionamiento de nuestro timer.

Dejémoslo bonito

Vamos a la implementación. Para empezar, necesitaremos guardar en nuestra clase CTimer el handle que el API de Windows nos devolverá al crear el timer, y que inicializamos a nulo en el constructor de la clase. También necesitaremos una forma de almacenar el código que deseamos ejecutar con el timer, y que nos llegará como expresión lambda de C++:

#include <functional>
class CTimer
{
public:
  CTimer()
  {
    m_hTimer = NULL;
  }
  virtual ~CTimer()
  {
    Cancel();
  }

private:
  HANDLE m_hTimer;
  std::function<void()> m_callback;
};

La plantilla std::function<> (declarada en functional.h) es un utilísimo almacén especializado precisamente en eso: en almacenar expresiones lambda, dada simplemente su firma (en nuestro caso “void()”). Aprovechamos para incluir el destructor y una llamada a una función, Cancel() que destruirá el timer junto con nuestro objeto. Ya que tenemos los miembros preparados e inicializados, vamos con la parte realmente divertida: la función Set() que nos permitirá configurar el timer:

HRESULT Set(DWORD dueTime,DWORD period,std::function<void()> callback)
{
  if (m_hTimer == NULL)
  {
    if (CreateTimerQueueTimer(&m_hTimer,NULL,&internalCallback,this,
                              dueTime,period,
                              period ? WT_EXECUTEDEFAULT : WT_EXECUTEONLYONCE) == FALSE)
      return(GetLastError());
  }
  else
  {
    if (ChangeTimerQueueTimer(NULL,m_hTimer,dueTime,period) == FALSE)
      return(GetLastError());
  }
  m_callback = callback;
  return(S_OK);
}

La función Set() crea o altera el timer nativo dependiendo de si la variable m_hTimer ya tiene un valor. De esta forma podremos alterar el timer al vuelo, cambiando por ejemplo los tiempos de disparo. Cada llamada a Set() tambien almacena en la variable m_callback la expresión lambda que queremos emplear – esto nos permitiría, por ejemplo, emplear el mismo timer con distintas funciones lambda dependiendo de nuestras necesidades.

Si os fijáis, al llamar al API siempre usamos la misma función callback: internalCallback. Esta función, junto con el parámetro “this” que le sigue, nos permitirán a la hora de recibir la notificación del sistema operativo, saber qué lambda debemos invocar.

La implementación de la función internalCallback() sería como sigue:

private:
static VOID CALLBACK internalCallback(PVOID lpParameter,BOOLEAN TimerOrWaitFired)
{
  CTimer *pTimer = (CTimer*)lpParameter;
  if (pTimer->m_callback)
    pTimer->m_callback();
}

La función es estática (el API de Windows no sabe de clases de C++), pero hemos incluido la instancia del objeto como argumento al crear el timer, y nos llegará en el argumento lpParameter. Simplemente castear el parámetro a CTimer* nos devuelve un puntero utilizable para determinar el contenido de la función lambda e invocarla.

Finalmente, la función Cancel() destruye el timer. Podremos invocar esta función manualmente o simplemente, dejar que se llame desde el destructor de la clase:

HRESULT Cancel()
{
  if (m_hTimer != NULL)
  {
    DeleteTimerQueueTimer(NULL,m_hTimer,NULL);
    m_hTimer = NULL;
  }
  return(S_OK);
}

La función DeleteTimerQueueTimer() del API destruye un timer creado previamente con CreateTimerQueueTimer(), pasando el handle que nos devolvió al crearlo.

Y ya está. Hemos creado desde cero un componente que encapsula el API de Windows dentro de una clase con todas las funcionalidades del lenguaje C++. Vamos a darle un poco de contexto a nuestro ejemplo al principio del artículo, para poder probar el componente:

int _tmain(int argc, _TCHAR* argv[])
{
  printf("AsyncTimer test\n");

  CTimer timer;
  timer.Set(1000,1000,[]()
  {
    putc('.',stdout);
  });

  printf("Press any key ");
  _getch();

  return 0;
}

Chorra, pero nos sirve: el programa simplemente imprime un mensaje (“Press any key”) mientras nuestro timer imprimirá un punto cada segundo hasta que pulsemos una tecla. El programa está totalmente detenido en la función __getch(), que detiene la ejecución hasta que se pulse una tecla. El timer se ejecutará de forma totalmente asíncrona, en un thread gestionado por el sistema operativo. Al terminarse el programa y salir de la función, el objeto timer se destruirá, cancelando el temporizador y limpiando los recursos antes de la salir.

A continuación, la clase completa, y a continuación, enlaces al proyecto completo de ejemplo:

#pragma once

#include <functional>

class CTimer
{
public:
  CTimer()
  {
    m_hTimer = NULL;
  }
  virtual ~CTimer()
  {
    Cancel();
  }

public:
  HRESULT Set(DWORD dueTime,DWORD period,std::function<void()> callback)
  {
    if (m_hTimer == NULL)
    {
      if (CreateTimerQueueTimer(&m_hTimer,NULL,&internalCallback,this,
      dueTime,period,
      period ? WT_EXECUTEDEFAULT : WT_EXECUTEONLYONCE) == FALSE)
      return(GetLastError());
    }
    else
    {
      if (ChangeTimerQueueTimer(NULL,m_hTimer,dueTime,period) == FALSE)
        return(GetLastError());
    }
    m_callback = callback;
    return(S_OK);
  }

  HRESULT Cancel()
  {
    if (m_hTimer != NULL)
    {
      DeleteTimerQueueTimer(NULL,m_hTimer,NULL);
      m_hTimer = NULL;
    }
    return(S_OK);
  }

private:
  static VOID CALLBACK internalCallback(PVOID lpParameter,BOOLEAN TimerOrWaitFired)
  {
    CTimer *pTimer = (CTimer*)lpParameter;
    if (pTimer->m_callback)
      pTimer->m_callback();
  }

private:
  HANDLE m_hTimer;
  std::function<void()> m_callback;
};

El proyecto completo está aquí.

Categorías:Desarrollo, Tutorial Etiquetas:

YASS ha encontrado un hogar

Los que hayáis seguido el tutorial que he publicado durante el verano acerca del desarrollo de un emulador de Sinclair ZX Spectrum, sabed que YASS (“Yet Another Spectrum Simulator”) ha encontrado un hogar adoptivo en Speccy.org – el mayor foro de usuarios de Spectrum en castellano.

YASSAtSpeccyPoco después de terminar el tutorial (imagino que ya me vale) descubrí Speccy.org, uno de los mayores (si no el mayor) foro de usuarios de Sinclair ZX Spectrum de habla hispana. Lleno a rabiar de gente que sabe muchísimo más que yo de tan adorable máquina, mi intención era simplemente dejarles saber que había un nuevo emulador y que, quizá, alguien podría encontrar mi trabajo interesante.

El recibimiento por parte de la gente de Speccy fue simplemente como si fuese uno más de la familia: mensajes de bienvenida, sugerencias para mejorar el emulador y ampliar sus capacidades, ideas a cual más interesante para continuar con el proyecto. Y para mi sorpresa, los administradores me invitaron cordialmente a hospedar YASS y los contenidos del tutorial en sus propios servidores, junto con otra inmensa cantidad de proyectos también relacionados con la emulación y preservación del ZX Spectrum.

Me pareció una excelente idea, para preservar YASS, para facilitar su acceso – tenerlo como artículos sueltos en un blog hace bastante complicado no perderse en la navegación. Así que rehíce los contenidos y podréis acceder a ellos de forma mucho más cómoda (y mejor organizada) en http://yass.speccy.org.

Desde aquí, agradecer a los administradores de speccy.org la posibilidad de sentirme uno más de una familia. Gracias!

Categorías:Desarrollo, Noticias Etiquetas: , , , , , ,

Hazte un Spectrum (8ª Parte y final)

Todos los componentes están realizados y en su sitio, el código ha ido tomando forma con una maravillosa fluidez y he aprendido lo indecible durante estas breves semanas. Aprovecho que mañana vuelvo a la dura vida cotidiana para cerrar esta serie de artículos – unos remates finales y completar el código fuente.

Hemos llegado al final del camino – unas semanas que me han servido para conocer un poco más uno de los mitos de la informática personal de los años ‘80 y que para mí, hasta hace unas semanas, era casi un completo desconocido. Hemos empezado por diseñar una máquina virtual, ir aprendiendo cómo funcionaban los diferentes componentes de la misma y emulando su comportamiento. Hoy concluye este fascinante viaje. Vamos a divertirnos un poco y darle incluso un aire más “retro” al emulador, aprendiendo de paso cómo aprovechar las capacidades de aceleración hardware de gráficos de WPF.

Vamos a empezar con una herramienta imprescindible para diseñar interfaces de usuario en aplicaciones modernas: Photoshop. Con ella vamos a realizar una capa gráfica (“overlay”) que podremos activar a voluntad para darle ese toque retro, imprescindible para mi: el “efecto TV” o, como me ha dado por llamarlo en el emulador, el “modo Telefunken” (y apuesto a que mi hermano estará de acuerdo conmigo a que el nombre le viene que ni pintado gracias a nuestras experiencias de chavales).

Una nueva herramienta de desarrollo: Photoshop

Comencemos (sorprendentemente) buscando en Google Images por un fondo apropiado. Nada más facil que navegar http://images.google.com y buscar por “TV frame”. Busco un “marco de televisor”, y he escogido el que me ha parecido más simpático, que incluso ya viene con el hueco transparente. Veamos los pasos siguientes, uno a uno:

El primer paso es abrir la imágen que más nos guste en Photoshop y ajustar su tamaño a 640×480. Invocando el menú Image/Image Size podremos ajustarlo. Si habéis cargado la imagen como “Background”, haced doble click sobre ella para convertirla en una capa normal (Layer). Ponedle como nombre “TV Frame” por ejemplo y borrad todo lo que pudiese verse dentro la imagen para que quede transparente: TVLayer-4
Vamos a crear una nueva imagen, con un tamaño algo peculiar: 1 pixel de ancho por 2 de alto. Esta imagen nos servirá como “relleno” para el familiar efecto de las líneas en pantalla TVLayer-1
Tras crear la imagen, seleccionamos la herramienta “lápiz” y rellenamos el pixel de arriba en negro, dejando el pixel de abajo transparente TVLayer-2
Seleccionad la imagen con Ctrl+A y luego Edit/Define Pattern. Yo le puse de nombre “ScanLine”. TVLayer-3
Volved ahora a la imagen principal con nuestro televisor. Añadid una nueva capa y aseguraos que queda debajo del marco del televisor. Ponedle de nombre por ejemplo “Scanlines”: TVLayer-5
Aseguraos que tenéis la capa Scanlines seleccionada y rellenadla con el patrón que diseñamos antes. Pulsad mayúsculas+BackSpace, seleccionad “Pattern” de la lista de opciones y el patrón Scanlines como “Custom Pattern”: TVLayer-6
Ya tenemos nuestro familiar efecto de líneas de televisor… simplemente ajustad la opacidad de la capa “al gusto” (40-60%) TVLayer-7
Agregad una nueva capa al diseño, y colocadla entre el marco del televisor y la capa de líneas. Yo la he llamado “Glare”: TVLayer-8
Seleccionad la capa y empleando la herramienta de selección, dibujad un rectángulo que ocupe más o menos el tercio superior de la pantalla. Luego redondead la selección con la herramienta Select/Modify/Feather y algo así como 25 píxeles: TVLayer-9
Comprobad que vuestros colores son el blanco para la tinta y el negro para el fondo, seleccionad la herramienta de gradientes y en la barra superior escoged la configuración “Foreground to transparent” (esto es, de blanco – nuestro color actual – a transparente). TVLayer-10
Ahora pulsad con el botón más o menos en la zona superior de la selección y arrastradla hasta un poco antes de la zona inferior. Luego ajustad un poco la opacidad de la capa, quizá entre 60-70%: TVLayer-11

Ya tenemos el overlay listo – simplemente guardadlo dentro del proyecto del emulador como PNG de 32 bits (con transparencia!!) y ya podemos importarlo en el proyecto. Para ello pulsad con el botón derecho del ratón sobre el proyecto en el que queráis añadir la imagen y seleccionad “Add/Existing Item”. Buscad la imagen PNG, aceptad el diálogo y aseguraos que en las propiedades de la imagen, la entrada Build Action aparece configurada como “Resource”: el compilador “incrustará” la imagen dentro del binario, permitiéndonos olvidarnos de distribuir los ficheros sueltos junto a la aplicación.

Ahora abrid el archivo MainWindow.xaml, y añadid una nueva imagen justo debajo del control zx:Spectrum:

    <zx:Spectrum x:Name="emulator" Grid.Row="1" />
    <Image x:Name="tvOverlay" Grid.Row="1" Source="/YASS;component/Resources/TVEffect.png" Stretch="Fill" Visibility="Collapsed" />

La propiedad Source especifica la imagen que debe emplear el control, en este caso Source=”/YASS;component/Resources/TVEffect.png”. La primera parte (“/YASS;”) indica el nombre del ensamblado que lleva el archivo, en este caso “YASS.exe” que será el nombre del ejecutable. Lo que sigue es el camino (o “path”) hasta el recurso: en mi caso el archivo se llama “TVEffect.png” y está dentro de la carpeta “Resources” del proyecto. Si no queréis tener que averiguar el camino al archivo, insertad una imagen sin campo “Source” y luego simplemente usad las propiedades del control en Visual Studio. Navegad hasta la propiedad Source en la lista y seleccionad directamente la imagen con el menú desplegable – mucho más fácil que ponerlo a mano y sin errores!!! Fijaos que los dos elementos (el control zx:Spectrum y la imagen “tvOverlay” están situados en la misma celda del Grid que los contiene. El efecto es que la imagen cubrirá permanentemente al control, sin tener que hacer absolutamente nada en código – solo cambiar su visibilidad.

Aprovechad tambien para añadir al menú de la aplicación una entrada para activar o desactivar el “modo Telefunken”:

<Menu>
  ...
  <MenuItem Header="Load tape..." Click="OnSelectTape"/>
  <Separator/>
  <MenuItem x:Name="tvModeMenu" Header="Telefunken mode" Click="OnTVMode" />
  ...
</Menu>

Finalmente, basta con agregar la función “OnTVMode()” al programa, que alternará la visibilidad de la capa:

  private void OnTVMode(object sender, RoutedEventArgs e)
  {
    // Invert visibility of tv overlay
    tvOverlay.Visibility = tvOverlay.Visibility == Visibility.Collapsed ? Visibility.Visible : Visibility.Collapsed;
    tvModeMenu.IsChecked = tvOverlay.Visibility == Visibility.Visible;
  }

Si ejecutamos el emulador y seleccionamos el menú “Telefunken Mode”, el efecto será un emulador incluso más retro:

YASS-OldieLook

El overlay está acelerado por hardware, así que la capa no afecta en lo más mínimo a la velocidad de ejecución del emulador, ni incrementa el consumo de la cpu – la tarjeta gráfica hace todo el trabajo.

Alguna cosita más?

Pues si, un par de cosas que he añadido al código final antes de publicarlo. Lo más significativo es que al seleccionar un archivo de cinta ya no es necesario escribir LOAD “” para iniciar la carga. La solución, rápida y simple: una función que emula la pulsación de las teclas para escribir el comando tras abrir el archivo TAP. Muy simple, pero cumple con su cometido – con la pega de que necesitamos que el Spectrum esté dispuesto a aceptar el comando para que funcione. También encontraréis opciones para parar, arrancar y resetear el emulador – funciones que he empleado para depurar algunas cosas y asegurarme de que no me dejaba nada “tirado” por ahí.

Bueno… creo que eso es todo

El emulador está completo y el viaje ha llegado a su fin. Me he divertido mucho, realmente mucho durante estas semanas y me ha encantado intentar documentar todo el proceso. El emulador que os dejo está lejos de ser completo o de ejecutar “absolutamente todos los programas del Spectrum” – no había tiempo para tanto, siendo más bien mi intención la de ofreceros un tutorial completo sobre cómo desarrollar un emulador “desde cero”, cómo combinar varios lenguajes de programación para obtener los mejores resultados y, sobre todo, mostraros cómo era la informática de los años 80. He intentado mantener el código fuente lo más limpio y estructurado posible y creo que es bastante facil de leer y, sobre todo, ampliar.

Como os decía al principio, hace unas semanas no conocía apenas nada del Sinclair ZX Spectrum, y seguro que algún error habré cometido al documentar o emular alguna funcionalidad – espero que sepáis disculparme y, sobre todo, dejadme saber dónde he metido la pata – quiero seguir aprendiendo!

Mañana me toca volver a los quehaceres diarios, pero esta “retromanía” me sigue afectando… cual podría ser el próximo proyecto?

Enlaces:

Emulador YASS para Windows
Código fuente del Emulador YASS
Código fuente del Emulador YASS – actualizado a VS2015

– Hazte un Spectrum: 1ª Parte, 2ª Parte, 3ª Parte, 4ª Parte, 5ª Parte, 6ª Parte, 7ª Parte, 8ª Parte

Categorías:Desarrollo, Tutorial Etiquetas: , , , , ,

Hazte un Spectrum (7ª Parte)

Ya casi llegamos al final y hoy vamos a añadir dos componentes a nuestro emulador: la capacidad de hacer “ruido” y la posibilidad de emular un joystick Kempston.

Y para comenzar, vamos con lo facilito primero: el joystick. El hardware de un joystick Kempston es lo más simple del mundo: solamente un registro de I/O que se direcciona poniendo el bit de dirección 5 a cero – muy parecido al direccionamiento de un bit de la ULA del Spectrum. Al leer el registro (típicamente de 0x1F == 00011111b por ejemplo) se obtiene un byte donde los cinco bits menos significativos son el estado de las cinco posiciones del joystick (arriba, abajo, izquierda, derecha y “fuego”):

KempstonBits

Dado que hasta ahora estábamos conectado la ULA directamente al bus de I/O de la cpu Z80, ya no puede ser el caso, y necesitamos un decodificador intermedio. La clase hospeda los componentes (ULA y Kempston), decodifica la dirección y reenvía la petición al controlador apropiado:

class SpectrumIOBus : public BusComponent<0x00,0x10000>
{
public:
  SpectrumIOBus()
  {
    ULA = NULL;
    Kempston = NULL;
  }

  unsigned char Read(unsigned int address)
  {
    // Kempston interface is addressed by A5 == 0
    if (Kempston && ((address & 0x20) == 0))
      return(Kempston->Read(address));

    // ULA is addressed by A0 == 0
    if (ULA && ((address & 0x01) == 0))
    return(ULA->Read(address));

    // No device
    return(0xFF);
  }

  void Write(unsigned int address,unsigned char value)
  {
    // ULA is addressed by A0 == 0
    if (ULA && ((address & 0x01) == 0))
      return(ULA->Write(address,value));
  }

public:
  BusComponentBase *ULA;
  BusComponentBase *Kempston;
};

Si os fijáis, en el Write del bus ni siquiera nos molestamos en enviar las escrituras al controlador Kempston…

La implementación de la clase Kempston es igual de simple:

#define KEMPSTON_RIGHT        0x01
#define KEMPSTON_LEFT         0x02
#define KEMPSTON_DOWN         0x04
#define KEMPSTON_UP           0x08
#define KEMPSTON_FIRE         0x10

class Kempston : public BusComponent<0x1F,1>
{
public:
  Kempston()
  {
    kempstonData = 0;
  }

public:
  void SetKempstonData(unsigned char data)
  {
    kempstonData = data;
  }

public:
  unsigned char Read(unsigned int address)
  {
    return(kempstonData);
  }

  void Write(unsigned int address,unsigned char value)
  {
    // Do nothing
  }

  unsigned char kempstonData;
};

La variable miembro kempstonData (pública) es donde la aplicación host establece el estado del joystick, y la clase simplemente conserva el valor y lo devuelve a la cpu cada vez que nos lo pida. Finalmente, en la clase Spectrum vamos a modificar los buses de nuestro procesador para integrar el nuevo decodificador de I/O y agregar el interface Kempston:

  void Spectrum::emulatorMain()
  {
    // Components
    Bus16 bus;              // Main Z80 data bus
    SpectrumIOBus ioBus;    // Main Z80 I/O bus
    ROM<0,16384> rom;       // Main system ROM
    ULA ula;                // Spectrum's ULA chip + 16KB
    RAM<32768,32*1024> ram; // Remaining RAM (32KB)
    Kempston kempston;      // Kempston joystick
    Z80 cpu;                // and finally, the CPU core

    // Load ROM contents
    if (rom.Load("48.rom"))
      throw gcnew System::IO::FileNotFoundException("Unable to load rom '48.ROM'");

    // Configure the ULA with the native Windows bitmap used to emulate the screen
    ula.SetNativeBitmap((LPBYTE)screenData,bytesPerScreenLine);

    // Populate buses
    bus.AddBusComponent(&rom);
    bus.AddBusComponent((ULAMemory*)&ula);
    bus.AddBusComponent(&ram);

    ioBus.ULA = (ULAIO*)&ula;
    ioBus.Kempston = &kempston;

    // And attach busses to cpu core
    cpu.DataBus = &bus;
    cpu.IOBus = &ioBus;

    pCurrentUla = &ula;
    pCurrentKempston = &kempston;

Con una enumeración de .NET y un par de funciones, tenemos el API para poder usar el joystick desde una aplicación host:

namespace Emulation
{
  // Kempston joystick status values.
  [Flags]
  public enum class KempstonFlags
  {
    Right = KEMPSTON_RIGHT,
    Left = KEMPSTON_LEFT,
    Down = KEMPSTON_DOWN,
    Up = KEMPSTON_UP,
    Fire = KEMPSTON_FIRE
  };

  public ref class Spectrum : System::Windows::Controls::Border, IDisposable
  {
    ...
  public:
    void SetKempstonStatus(KempstonFlags status)
    {
      if (pCurrentKempston != nullptr)
        pCurrentKempston->SetKempstonData((unsigned char)status);
    }

  private:
    ULA *pCurrentUla;
    Kempston *pCurrentKempston;
  }

Sólo queda ampliar nuestra gestión del teclado en C# para mapear las teclas de cursor y “ctrl” (por ejemplo) al joystick Kempston (nuevamente, tanto en OnKeyDown() como en OnKeyUp()):

    Emulation.KempstonFlags kempston;

    protected override void OnKeyDown(System.Windows.Input.KeyEventArgs e)
    {
      base.OnKeyDown(e);
      switch (e.Key)
      {
        ...
        // Kempston
        case System.Windows.Input.Key.Left: kempston |= Emulation.KempstonFlags.Left; break;
        case System.Windows.Input.Key.Right: kempston |= Emulation.KempstonFlags.Right; break;
        case System.Windows.Input.Key.Up: kempston |= Emulation.KempstonFlags.Up; break;
        case System.Windows.Input.Key.Down: kempston |= Emulation.KempstonFlags.Down; break;
        case System.Windows.Input.Key.LeftCtrl: kempston |= Emulation.KempstonFlags.Fire; break;
      }
      emulator.SetKempstonStatus(kempston);
    }

    protected override void OnKeyUp(System.Windows.Input.KeyEventArgs e)
    {
      base.OnKeyUp(e);
      switch (e.Key)
      {
      ...
        // Kempston
        case System.Windows.Input.Key.Left: kempston &= ~Emulation.KempstonFlags.Left; break;
        case System.Windows.Input.Key.Right: kempston &= ~Emulation.KempstonFlags.Right; break;
        case System.Windows.Input.Key.Up: kempston &= ~Emulation.KempstonFlags.Up; break;
        case System.Windows.Input.Key.Down: kempston &= ~Emulation.KempstonFlags.Down; break;
        case System.Windows.Input.Key.LeftCtrl: kempston &= ~Emulation.KempstonFlags.Fire; break;
      }
      emulator.SetKempstonStatus(kempston);
    }

Vamos finalmente con la parte de audio que, lamentablemente, es algo más compleja que la implementación del interface Kempston. Veamos primero un poco de la teoría

Sonido de 1 bit?

Aunque siendo pragmáticos el sonido del Spectrum realmente son “2” bits: uno para manipular el altavoz y otro para la generación de audio para la unidad de cassette, la única diferencia entre ambos es una resistencia para limitar la amplitud de la segunda señal. El audio de “1 bit” sólo permite establecer dos niveles de audio, frente a los 65536 que se pueden definir con (por ejemplo) una moderna tarjeta de sonido de 16 bits. Usando Audacity (un estupendo editor de audio open source) podemos comparar audio de 1 bit con audio de 16 bits:

Audio16vs1

Con una limitación como ésta, parecería imposible que el Spectrum pudiese producir nada más que tristes pitidos, verdad? Pues no: utilizando pulsos escrupulosamente medidos (codificación PWM o “Pulse Width Modulation”) se puede obtener un audio que simule características muy avanzadas: voces múltiples, control de volumen, etc. El truco del audio PWM es que, ya que no puedo “colocar el altavoz en la posición que yo quiero”, lo que sí puedo hacer es dar “empujoncitos” para aproximarlo. El símil que se me ocurre es lo que hacemos en los juegos de conducción: aunque los controles “izquierda” o “derecha” sean botones y un volante real sea “analógico”, puedo dar toques, más o menos largos, a los controles de izquierda o derecha para aproximar la posición del volante a la deseada. Fijaos en el siguiente gráfico de una demo del Spectrum (“Garfield”):

PWMAudio

Audio PWM en estado puro!! El programa cuenta ciclos para subir o bajar la señal de audio y controlar el ancho de los pulsos… Luego nuestro cerebro hace el resto – y creedme que el resultado es sorprendente!!

Generación de audio

Ya conocemos un poco la teoría, vamos ahora a la práctica. En el ZX Spectrum, para generar audio se usan los bits 4 y 3 de la ULA, escribiendo (por ejemplo) en la posición 0xFE de I/O. Cuando se manipula el bit 4 el nivel de la señal es muy amplio (más “alta”) mientras que el bit 3 se usa para modular la señal que se dirige a la cinta de cassette. Parece simple entonces que, cada vez que nos llegue una escritura a la ULA podamos calcular el nivel de la señal. Pero no nos basta con una muestra: necesitamos generar una señal en el tiempo, o sea que necesitamos muchas muestras. Pero cuantas?

Recordemos que la emulación deja correr al procesador durante un cuadro de pantalla (1/50 de segundo, o lo que es lo mismo 20 milisegundos) para luego hacer una pausa y “sincronizar” la velocidad del Spectrum emulado en nuestro rápido PC. Podemos trabajar en bloques de audio de 1/50 de segundo aprovechando el contador que usamos en la ULA para saber nuestra posición en pantalla.

Ahora sabemos que necesitamos 20 milisegundos de audio por cada cuadro de pantalla. Cuantas muestras son esas? un par de cálculos nos lo dirán: vamos a generar audio a 48KHz, esto es, 48000 muestras por segundo. Eso son (48000/50) = 960 muestras cada 20 milisegundos. Pero qué valor le damos a esas muestras?

No podemos predecir cuándo van a escribir los programas en los controles de audio del chip ULA, pero sí sabemos la posición (en ciclos) con respecto a la pantalla. Aprovechando la función AddCycles() de la ULA, podemos actualizar la muestra que corresponda con la posición del tubo CRT:

#define   AUDIO_SAMPLE_RATE         (48000)
#define   SAMPLES_PER_FRAME         (AUDIO_SAMPLE_RATE / 50)

class ULA : public ULAMemory, ULAIO
{
  unsigned char ULAIOData;
  short FrameAudio[SAMPLES_PER_FRAME];

  ...
  void AddCycles(unsigned int cycles,bool& IRQ)
  {
    dwFrameTStates += cycles;
    dwScanLineTStates += cycles;

    // Update the analog audio output from ULA
    // First, compute audio output value for this cycle
    int signal = 0;
    signal = (ULAIOData & 0x10) ? +16384 : -16384;
    signal += (ULAIOData & 0x08) ? +8192 : -8192;

    // Now, add audio output over an 8 tap filter:
    // 1: Maintain 7/8ths of the original signal
    audioOutput -= (audioOutput / 8);
    // 2: ...and add 1/8th of the new one
    audioOutput += (signal / 8);

    // Update the audio sample corresponding to this screen tState
    unsigned int offset = (dwFrameTStates * SAMPLES_PER_FRAME) / (TSTATES_PER_SCANLINE * TVSCANLINES);
    // As clocks don't match and this is a quick approximation, limit offset output
    if (offset < SAMPLES_PER_FRAME)
      FrameAudio[offset] = audioOutput;

    if (dwScanLineTStates > TSTATES_PER_SCANLINE)
      ScanLine(IRQ);
  }

  void IOWrite(unsigned int address,unsigned char value)
  {
    ...
    // save a copy of ULA's state
    ULAIOData = value;
    ...
  }

La función AddCycles() se invoca después de cada instrucción ejecutada por el procesador, y recorrerá de forma más o menos uniforme los casi 69000 ciclos que forman la pantalla. Dado que 69000 ciclos entre 960 muestras dan para muchas repeticiones en cada muestra de audio, la función integra repetidamente la señal con un filtro de 8 taps, esto es, sólo 1/8 de la señal de agrega en cada interacción, algo así como un “condensador en software”. De esta forma, los rápidos cambios en los controles de audio de la ULA se convierten en señales un poco más “analógicas” que, posteriormente, la tarjeta de sonido terminará de digerir. Para la amplitud de la señal he usado +/-16384 para la señal del altavoz y +/- 8192 para la del cassette. Arbitrario? Totalmente.

Ahora que ya tenemos la muestra de 20 milisegundos de audio, es necesario enviarla a la tarjeta de sonido para que la reproduzca, y es aquí donde aparece el mayor problema (con diferencia) para la emulación: cómo sincronizar a la perfección el Spectrum con el audio para que no dé tirones ni cortes.

Para mi la solución consiste en emplear el cronómetro más exacto que se pueda conseguir en un PC.

El temporizador más preciso de un PC: la tarjeta de sonido

Hasta ahora, la solución para “ralentizar” el Spectrum a su velocidad real era ver el tiempo que habíamos necesitados para correr durante 20 milisegundos y complementarlo con un Sleep() del sistema operativo. Aunque funciona (y es válido si no hay sonido), hay que tener en cuenta que la mayoría de los sistemas operativos no son realmente “de tiempo real”: una instrucción Sleep(17) no dura realmente 17 milisegundos, sino cualquier cosa entre 1 (o 0!!) y 30 (o más), dependiendo de la resolución de los “timers” del hardware, de lo ocupada que está la cpu, etc. Cómo conseguimos sincronizar entonces nuestro audio?

Aunque parezca mentira, el mejor cronómetro de un PC moderno es su tarjeta de sonido. En la tarjeta de sonido, 20 milisegundos son realmente 20 milisegundos. De hecho, los reproductores multimedia sincronizan los streams de audio y vídeo empleando el audio como reloj maestro. Nosotros vamos a hacer lo mismo.

Dado que generamos bloques de audio de 20 milisegundos, en lugar de intentar cronometrar nosotros mismos el retardo vamos a emplear el evento de la tarjeta de sonido, enviándole bloques de 20 milisegundos y “congelando” la emulación hasta que nos avise que ya se han terminado de reproducir. Obviamente, tenemos el problema de que cuando “termine de reproducirlos” vamos a tardar un poco en generar otros 20 milisegundos de audio, así que para solucionarlo vamos a utilizar tres bufferes de 20 milisegundos que enviaremos al sistema de audio de golpe. A medida que se vayan consumiendo, tendremos 40 milisegundos de audio todavía pendiente de reproducir para seguir emulando instrucciones y alimentando al sistema de sonido. El resultado? Audio de bajísima latencia sin cortes ni chasquidos.

Cualquiera que haya programado el sistema de audio de Windows sabe las complicaciones que conlleva la reserva de los bloques de memoria, las cabeceras, abrir y cerrar los dispositivos de audio… Vamos a hacerlo encapsulando toda la funcionalidad en una sola clase (bueno, en un par) que nos evite perder el enfoque en nuestro principal problema, que es la emulación. Aquí va el código completo del sistema de audio:

#pragma comment(lib,"WinMM.lib")

template<unsigned int sampleRate> struct AudioBlock
{
  WAVEHDR Header;
  short AudioData[sampleRate / 50];
};

#define   AUDIOBLOCK_COUNT      (3)

template<unsigned int sampleRate> class WaveOutAudioManager
{
public:
  WaveOutAudioManager()
  {
    hWaveDev = NULL;
    hWaveEvent = INVALID_HANDLE_VALUE;
  }
  virtual ~WaveOutAudioManager()
  {
    Close();
  }

public:
  int Open()
  {
    if (hWaveDev != NULL)
      return(ERROR_INVALID_STATE);

    if (hWaveEvent != INVALID_HANDLE_VALUE)
      CloseHandle(hWaveEvent);
    hWaveEvent = CreateEvent(NULL,FALSE,FALSE,NULL);

    WAVEFORMATEX wf;
    ZeroMemory(&wf,sizeof(wf));
    wf.cbSize = sizeof(wf);
    wf.wFormatTag = WAVE_FORMAT_PCM;
    wf.nChannels = 1;   // mono audio
    wf.nSamplesPerSec = sampleRate;
    wf.nAvgBytesPerSec = sampleRate * sizeof(short);
    wf.nBlockAlign = sizeof(short);
    wf.wBitsPerSample = 8 * sizeof(short);
    int result = waveOutOpen(&hWaveDev,WAVE_MAPPER,&wf,(DWORD_PTR)hWaveEvent,NULL,CALLBACK_EVENT);
    if (result != 0)
    {
      Close();
      return(result);
    }

    // Prepare audio buffers. Each one will be 20ms long
    for (int dd=0;dd<AUDIOBLOCK_COUNT;dd++)
    {
      ZeroMemory(&audioBlocks[dd],sizeof(AudioBlock<sampleRate>));
      audioBlocks[dd].Header.lpData = (LPSTR)&audioBlocks[dd].AudioData[0];
      audioBlocks[dd].Header.dwBufferLength = sizeof(audioBlocks[dd].AudioData);
      result = waveOutPrepareHeader(hWaveDev,&audioBlocks[dd].Header,sizeof(audioBlocks[dd].Header));
      if (result != MMSYSERR_NOERROR)
      {
        Close();
        return(result);
      }
    }

    // Everything ready...
    return(0);
  }

  void Close()
  {
    if (hWaveDev != NULL)
    {
      waveOutReset(hWaveDev);
      waveOutClose(hWaveDev);
      hWaveDev = NULL;
    }

    if (hWaveEvent != INVALID_HANDLE_VALUE)
    {
      CloseHandle(hWaveEvent);
      hWaveEvent = INVALID_HANDLE_VALUE;
    }
  }

protected:
  AudioBlock<sampleRate> *FindAvailableBlock()
  {
    for (int dd=0;dd<AUDIOBLOCK_COUNT;dd++)
    {
      if ((audioBlocks[dd].Header.dwFlags & WHDR_INQUEUE) == 0)
        return(&audioBlocks[dd]);
    }
    return(NULL);
  }

public:
  bool QueueAudio(short* audioData)
  {
    // Check if any block is available right now
    AudioBlock<sampleRate> *block = FindAvailableBlock();
    // If no audio block is available, just wait for one (max 20ms)
    if (block == NULL)
    {
      WaitForSingleObject(hWaveEvent,1000);   // Wait for 1 second max
      block = FindAvailableBlock();
    }

    // This is weird... something really strange happened.
    if (block == NULL)
      return(false);

    // Fill block with audio data
    for (int dd=0;dd<sampleRate/50;dd++)
      block->AudioData[dd] = audioData[dd];
    // and send again to audio subsystem
    int result = waveOutWrite(hWaveDev,&block->Header,sizeof(block->Header));
    return(result == MMSYSERR_NOERROR ? true : false);
  }

protected:
  HANDLE hWaveEvent;
  HWAVEOUT hWaveDev;
  AudioBlock<sampleRate> audioBlocks[AUDIOBLOCK_COUNT];
};

La clase WaveOutAudioManager<> inicializa el sistema de sonido y tres bufferes (de 20 milisegundos de audio). La función que nos interesa es QueueAudio(), que recibe 20 milisegundos de audio desde el bucle principal de la emulación. Si encuentra bufferes disponibles (que no estén en ejecución) la función envía el bloque al sistema de audio para su reproducción y vuelve de inmediato. Si todos los bufferes están encolados, la función espera pacientemente a que el sistema de audio nos notifique por medio de un evento que uno de los bloques se ha completado, encolando un nuevo bloque.

Para los bufferes y cabeceras empleo una clase template (AudioBlock<>)que agrega las dos estructuras como una sola, simplificando enormemente el código y haciendo innecesario el uso de reserva dinámica de memoria.

Volviendo ahora a la función principal de emulación (emulatorMain), el código queda así:

  void Spectrum::emulatorMain()
  {
    ...
    // Native audio driver
    WaveOutAudioManager<AUDIO_SAMPLE_RATE> audMgr;

    ...

    audMgr.Open();

    // Ready to roll!!
    do
    {
      // Tape load trap
      if ((cpu.regs.PC == 0x056B) && (OnLoad))
      {
         .... 
      }

      // And emulate next instruction
      cpu.tStates = 0;
      cpu.EmulateOne();

      // After each instruction, report the ULA the number of cycles we've used
      bool irq = false;
      ula.AddCycles(cpu.tStates,irq);

      // As in the real Spectrum, the ULA will trigger an IRQ for every frame. This
      // implementation uses cpu clock cycles to know where the screen beam is.
      if (irq)		// Ula signals a frame interrupt
      {
        cpu.INT();		// Generate system interrupt

        // If screen contents have been modified, set a flag for the WPF rendering event.
        if (ula.IsDirty)
        {
          screenDataDirty = true;
          ula.IsDirty = false;
        }

        // Submit audio to driver. The "driver" will hold the loop until an audio buffer
        // is available - that is, 20 ms. This will match the speed of our emulated
        // Spectrum to the real one.
        audMgr.QueueAudio(ula.FrameAudio);
      }
    } while(quitEmulation == false);

    pCurrentUla = nullptr;
    pCurrentKempston = nullptr;
  }

SpectrumSaving

El resultado? Probad simplemente a arrancar el emulador, escribir un programa de un par de líneas y salvarlo con SAVE “test”… veréis las familiares líneas en el borde de la pantalla, junto con el igualmente familiar sonido de la grabación, ese sonido que nos acompañó durante largas tardes de juego. Pero si queréis ver realmente de lo que era capaz el Spectrum, os recomiendo como un ejemplo absolutamente encantador la música de Alter Ego y de lo que se puede hacer un audio “de un solo bit”.

AlterEgo

Pues creo que eso es todo…

Nuestro emulador está completo, y tenemos un flamante ZX Spectrum de 48KB, capaz de generar sonido (y recuerdos) y un estupendo joystick que podéis adaptar a todas vuestras necesidades. En el último capítulo de este tutorial daremos los últimos toques a la emulación… y podréis descargar el código fuente completo de la misma.

Enlaces:

Emulador YASS para Windows
Código fuente del Emulador YASS

– Hazte un Spectrum: 1ª Parte, 2ª Parte, 3ª Parte, 4ª Parte, 5ª Parte, 6ª Parte, 7ª Parte, 8ª Parte

Categorías:Desarrollo, Tutorial Etiquetas: , , , , ,

Hazte un Spectrum (6ª Parte)

Queda poco para completar un emulador de Sinclair ZX Spectrum, y hoy nos vamos a dedicar a emular una cinta de cassette para poder volver a disfrutar de esos videojuegos de los ‘80.

A ver, pregunta: cuántos de vosotros no recordabais o incluso sabíais que se usaban cintas de cassette como medio de almacenamiento masivo en los ordenadores personales de los ‘80? Yo me he dado cuenta de lo raro que suena hoy cuando al escribir “cassette” el corrector ortográfico se ha quejado porque no reconoce la palabra… Hoy vamos a darle a nuestro emulador soporte para leer el formato más habitual: los archivos .TAP.

Nuevamente, y gracias a la información encontrada en WorldOfSpectrum.org he obtenido todo lo necesario para implementarlo. Lo primero, un poco de teoría…

Usar cintas de cassette como almacenamiento

Cualquier ordenador que use cintas de audio para almacenar o recuperar programas lo que realmente hace es generar sonidos con ciertos patrones que luego puede reconocer al reproducir la señal. En el caso del ZX Spectrum éste formato es totalmente conocido y se genera (cómo no) con el chip ULA. El chip ULA puede generar sonidos por dos vías diferentes, pero con exactamente las mismas herramientas: un sólo bit (0 ó 1) se emplea para generar una señal de alto nivel (1) o bajo nivel (0) en una salida de audio. Hay dos bits en el registro de I/O de la ULA, uno genera una señal más amplia que la otra. La primera se emplea para activar el altavoz interno del ordenador, mientras que la otra se envía al conector MIC que se enchufa a la cinta de cassette. Realmente las dos señales están unidas, siendo la única diferencia que la salida de la unidad de cassette pasa por una resistencia para atenuar la señal.

Para grabar datos en la cinta basta con modificar la señal de audio a 1 y a 0 contando ciclos entre cada “flanco”. Por ejemplo, el tono de aviso previo a los datos de una cabecera consiste en 8063 pulsos (de 0 a 1 y vuelta a 0) con una duración de 2168 ciclos (tStates) entre flancos. Los bits se codifican igualmente con pulsos: un bit a “0” se codifica como dos pulsos de 855 ciclos entre flancos y un bit a “1” se codifica como dos pulsos de 1710 ciclos entre flancos. La recuperación es igual de simple: la señal de audio entra al chip ULA, que pone un bit a 0 o a 1 dependiendo del nivel de la señal. Los programas en la ROM del Spectrum simplemente leen el bit y calculan el tiempo que la señal está en “alto” o en “bajo”.

El formato .TAP reproduce exactamente el formato binario de los bloques que genera la ROM del Spectrum, sin las señales de sincronismo. Simplemente añade dos bytes antes de cada uno de ellos para poder recuperarlos de un archivo secuencial.

Las rutinas en ROM del Spectrum graban la información como dos bloques separados. El primer bloque contiene una cabecera y mide 17 bytes. El bloque que sigue contiene la información propiamente dicha. Aprovechando el ejemplo más documentado del mundillo (grabar en cinta los dos primeros bytes de la ROM de un Spectrum con SAVE “ROM” CODE 0,2), un ZX Spectrum grabaría los siguiente:

– 8063 pulsos de 2168 ciclos (tStates) cada uno para la señal de aviso de cabecera.
– Un pulso de sincronización de 667 ciclos.
– Un segundo pulso de sincronización de 735 ciclos.
– Un bloque de datos de 19 bytes con la cabecera: un byte con flags, más los datos de la cabecera que incluye tipo de bloque, longitud, ubicación en memoria, etc, y finalmente un byte de suma de control.

Tras una breve pausa, se grabarían los datos propiamente dichos:

– 3223 pulsos de 2168 ciclos para la señal de aviso de cabecera.
– Un pulso de sincronización de 667 ciclos.
– Un segundo pulso de sincronización de 735 ciclos.
– Un bloque de datos con un byte con flags, los dos bytes de datos y un byte con la suma de control del bloque.

Un fichero .TAP equivalente contendría lo siguiente:

– Dos bytes (0x13+0x00 == 0013h ó 19 decimal) con la longitud del bloque
– Los 19 bytes grabados físicamente en la cinta (flags+datos+suma de control)
– Dos bytes (0x03+0x00 == 0004h) con la longitud del bloque.
– Los 4 bytes grabados físicamente en la cinta (flags+datos+suma de control)

Parece obvio que los archivos .TAP contienen los datos grabados por la ROM del Spectrum “tal cual”… no sería posible leerlos de la misma forma?

Interceptando la ROM

Pues realmente sí. Tras leer bastante al respecto y gracias a la inestimable (e imprescindible) ayuda del libro “The Complete ZX Spectrum ROM Dissasembly” que encontraréis (cómo no) en WorldOfSpectrum.org/documentation.html, vamos a interceptar el curso normal de ejecución del código de la ROM y esperar tranquilamente hasta que ésta llegue al punto donde se leen bloques de cinta.

No hay que buscar mucho (nuevamente gracias a la ingente cantidad de información disponible). La función que queremos interceptar está a partir de la posición 056Bh de la ROM y su misión es leer un bloque (tanto de cabecera como de datos propiamente dicho) desde la cinta. Esta función (documentada como “LD-BREAK” en el desensamblado) es el primer (y frontal!!) paso en la lectura de un bloque.

Como la idea es hacer el emulador programable, en lugar de implementar los archivos .TAP directamente en el control lo vamos a implementar como un delegado normal y corriente de .NET. Cada vez que la ROM necesite un bloque de datos, la función que hayamos asignado a dicho delegado (escrita en C#) recibirá la petición y podrá devolver los contenidos. El cambio en la clase es mínimo:

namespace Emulation
{
  public delegate array<Byte>^ LoaderHandler(int bytesToLoad);

  public ref class Spectrum : System::Windows::Controls::Border, IDisposable
  {
  public:
    LoaderHandler^ OnLoad;

   ....

La firma del delegado es la de una función que recibe un número de bytes a cargar y que devuelve un array de bytes con los datos. Ahora vamos a utilizar al delegado desde la emulación.

Dado que cada bucle que ejecuta el emulador consiste en una instrucción de la cpu Z80, para saber si hemos llegado al punto crítico para interceptar la carga basta con comparar el valor del registro PC de la cpu con la dirección que nos interesa antes de emularla:

    // Ready to roll!!
    do
    {
      // Tape load trap
      if ((cpu.regs.PC == 0x056B) && (OnLoad))
      {
        try
        {
          LoadTrap(cpu);
          // set up registers for success
          cpu.regs.BC = 0xB001;
          cpu.regs.altAF = 0x0145;
          cpu.regs.CF = 1;		// Carry flag set: Success
        }
        catch(Exception^)
        {
          // set up registers for failure
          cpu.regs.CF = 0;		// Carry flag reset: Failure
        }
        cpu.regs.PC = 0x05e2;	// "Return" from the "tape block load" routine
      }

      // And emulate next instruction
      cpu.tStates = 0;
      cpu.EmulateOne();
      ...

Fácil, no? Justo antes de emular cada instrucción comprobamos si el registro PC (“Program Counter”) indica que hemos llegado a la dirección del programa de carga. Si es así y el delegado está inicializado en el emulador, invocamos una pequeña función que hará el trabajo sucio: pedir los datos y cargarlos en la memoria del Spectrum. El uso de C++ gestionado hace fácil hablar los dos “idiomas” (.NET y nativo) al mismo tiempo:

void Spectrum::LoadTrap(Z80& cpu)
{
  // Call the delegate to obtain the next tape block
  array<Byte>^ data = OnLoad(cpu.regs.DE);

  // First byte of data contains value for the A register on return. Last
  // byte is blocks checksum (not using it).
  int nBytes = data->Length-2;
  if (cpu.regs.DE < nBytes)
    nBytes = cpu.regs.DE;

  // We must place data read from tape at IX base address onwards
  // DE is the number of bytes to read, IX increments with each byte read.
  for (int dd=0;dd<nBytes;dd++)
  {
    // Write block using cpu's data bus and cpu's registers...
    cpu.DataBus->Write(cpu.regs.IX++,data[dd+1]);
    cpu.regs.DE--;
  }
}

La implementación es tremendamente simple, pero funciona. Si la función delegado devuelve un array de bytes (lo esperado), simplemente usamos los buses del procesador (y sus registros!!) para ir escribiendo los contenidos en la memoria del Spectrum. Cualquier excepción en la función será interpretada como un error que llegará al Spectrum emulador como un “error de cinta”. Una vez completado el trabajo, volvemos a colocar el registro PC de la cpu a la dirección de programa donde todo debería continuar normalmente de emplear una unidad de cinta “de verdad”.

Ficheros .TAP en C#

La parte pesada de todo ésto (la manipulación de los archivos y sus bloques propiamente dicha) la vamos a implementar en C#, porque precisamente para éstas cosas es el lenguaje ideal. La implementación es como sigue:

  class TapBlock
  {
    public TapBlock(BinaryReader stream)
    {
      // A TAP block consists of a two byte header plus data bytes.
      // Data is raw data as saved by Spectrum - and as expected
      // by ROM routines we are replacing.
      UInt16 size = stream.ReadUInt16();
      data = stream.ReadBytes(size);
    }

    public Byte[] Data
    {
      get
      {
        return (data);
      }
    }

    protected byte[] data = null;
  }

  class TapFile
  {
    public void Open(String fileName)
    {
      System.Collections.Generic.List<TapBlock> blockCol = new System.Collections.Generic.List<TapBlock>();

      try
      {
        using (var file = File.OpenRead(fileName))
        {
          var stream = new BinaryReader(file);
          while (stream.BaseStream.Position < stream.BaseStream.Length)
          {
            var newBlock = new TapBlock(stream);
            blockCol.Add(newBlock);
          }
        }
      }
      catch (Exception) { }

      blocks = blockCol.ToArray();
    }

    public TapBlock[] Blocks
    {
      get
      {
        return (blocks);
      }
    }

    protected TapBlock[] blocks = null;
  }

Realmente no es más que eso: un objeto TapFile abre el archivo en disco y lee todos los bloques, añadiéndolos a una colección de objetos TapBlock que obtenemos de su propiedad Blocks. Cada bloque simplemente contiene los datos en su miembro Data.

Cómo no, lo siguiente es que nuestro emulador pueda leer alguna cinta y empezar a hacer cosas verdaderamente divertidas con nuestro Spectrum. Para ello, vamos a añadir un menú a nuestro emulador con la opción “Load”. En el archivo MainWindow.xaml añadimos lo siguiente a nuestra ventana:

  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto"/>
      <RowDefinition/>
    </Grid.RowDefinitions>
    <Menu IsMainMenu="True" Grid.Row="0">
      <MenuItem Header="File">
        <MenuItem Header="Load tape..." Click="OnSelectTape"/>
      </MenuItem>
    </Menu>
    <zx:Spectrum x:Name="emulator" Grid.Row="1" />
  </Grid>

Hemos añadido un simple menú con la opción de leer cintas. Ahora en el archivo de código MainWindow.xaml.cs añadimos lo siguiente:

  public partial class MainWindow : Window
  {
    TapFile currentTape = null;
    int currentTapeBlock = 0;
    ...

    private void OnSelectTape(object sender, RoutedEventArgs e)
    {
      FileDialog dialog = new OpenFileDialog();
      dialog.AddExtension = true;
      dialog.CheckFileExists = true;
      dialog.CheckPathExists = true;
      dialog.Filter = "Tape files (*.tap)|*.tap";
      dialog.DefaultExt = "tap";
      bool? result = dialog.ShowDialog(this);
      if (result == null)
        return;
      if ((bool)result == false)
        return;

      try
      {
        var tape = new TapFile();
        tape.Open(dialog.FileName);
        currentTape = tape;
        currentTapeBlock = 0;
      }
      catch (Exception)
      {
        MessageBox.Show("Unable to load tape file");
      }
    }

En el delegado OnSelectTape() (invocado al selecciona la opción de menú) creamos una instancia de la clase TapFile que obtiene los contenidos del archivo y la asignamos a una variable que leeremos bloque a bloque a medida que el emulador nos lo vaya pidiendo. Sólo queda activar la intercepción de la carga de bloques:

  public partial class MainWindow : Window
  {
    public MainWindow()
    {
      InitializeComponent();

      // Set up load traps
      emulator.OnLoad = TapeLoad;

      emulator.Start();
    }

    protected Byte[] TapeLoad(int bytesToLoad)
    {
      TapBlock currentBlock = currentTape.Blocks[currentTapeBlock++];
      return (currentBlock.Data);
    }

En el constructor conectamos el delegado del emulador a nuestra implementación (la función TapeLoad()). La función intentará leer los bloque secuencialmente (empleando la variable currentTapeBlock como índice) y devuelve directamente el array de bytes al control.

Lo siguiente es una nueva visita (cómo no!) al inmenso archivo de programas de WorldOfSpectrum y descargar nuestros juegos favoritos. En dos clicks encontramos el archivo TAP de Head Over Heels. Vamos a probar el emulador!!

A jugar!

Una vez arrancado el emulador, abrimos el menú y seleccionamos la opción de cargar un archivo. Simplemente buscamos la ubicación del archivo TAP y lo abrimos.

LoadingFirstTape

El hecho de cargar una cinta en el programa no da ninguna indicación al emulador de que la lea – al igual que en un Spectrum real, tras insertar la cinta de cassette requiere que el usuario inicie la carga de la misma. Tecleamos LOAD “” en el emulador (tecla “J” para “LOAD” y CTRL+P para las comillas) y el emulador se pondrá a cargar la cinta que le hemos preparado:

FirstLoad

Tras pulsar retorno de carro, nuestro emulador pedirá bloque a bloque los contenidos de la cinta… Y el resultado es éste:FirstGame

Jugar con el emulador es divertido, pero he perdido (hace mucho!!) la costumbre de usar el teclado y más todavía empleando las combinaciones de teclas que se usan en el Spectrum. Como remate final a nuestro emulador, en el próximo capítulo vamos a añadir lo que creo que pueden ser los dos únicos componentes que quedan para completarlo: la emulación de un joystick Kempston y el sonido! Y no nos vamos a quedar en emular el “beep” tradicional del Spectrum, sino que vamos a soportar (y emular!) los programas que generaban audio multicanal usando un solo bit: el audio PWM.

Enlaces:

Emulador YASS para Windows
Código fuente del Emulador YASS

– Hazte un Spectrum: 1ª Parte, 2ª Parte, 3ª Parte, 4ª Parte, 5ª Parte, 6ª Parte, 7ª Parte, 8ª Parte

Categorías:Desarrollo, Tutorial Etiquetas: , , , , ,

Hazte un Spectrum (5ª Parte)

En el capítulo anterior conseguimos arrancar por primera vez nuestro nuevo emulador de Spectrum, pero sin ninguna forma de recibir entradas del mundo exterior su utilidad es francamente nula. Hoy implementaremos la simulación del teclado y las temporizaciones de la máquina.

La implementación del teclado tiene dos partes realmente: la primera, el interface hardware entre la cpu y las teclas, misión encargada (obviamente) al chip ULA. Por otra parte, el Spectrum necesita un reloj que periódicamente le haga “visitar” el teclado y leerlo. Todos los ordenadores necesitan este tipo de relojes y la mayoría incluyen relojes internos programables que facilitan la tarea. En los ‘80 éste no era el caso y la mayoría empleaban la temporización de la pantalla para sincronizar sus operaciones. Y cuando digo “pantalla” me refiero a “televisor” y su conocido refresco de 50 cuadros por segundo.

El chip ULA del Spectrum es el responsable (como ya hemos visto) de generar la imagen que aparece en la pantalla. Para hacerlo, el chip ULA cuenta meticulosamente ciclos de la máquina para determinar dónde está el cañón del televisor en cada momento. Durante unas cuantas líneas (el borde superior de la pantalla) el chip simplemente genera el color del borde y periódicamente la señal de sincronismo horizontal para que el cañón vuelva al flanco izquierdo de la pantalla. Cuando la ULA determina que ha llegado a la zona de pantalla propiamente dicha, comienza a leer la memoria de vídeo y convertir los contenidos en imágenes durante 256 píxeles, luego genera un poco más de borde, un sincronismo horizontal, un poco más de borde (en el lado izquierdo) y luego otra línea de pantalla, así hasta que completa las 192 líneas. FInalmente, vuelve a generar color de borde hasta que se completa el cuadro de pantalla (312 líneas) y genera la señal de sincronismo vertical, para que le haz de electrones del cañón vuelva a la esquina superior izquierda de la pantalla. Y esto, 50 veces por segundo. Es en éste momento cuando el chip ULA envía una señal a la cpu (la señal INT) que interrumpe el proceso de la cpu cada cuadro y que el Spectrum emplea para sincronizar sus procesos.

En nuestra emulación no existe un cañón de electrones de una televisión, ni nada que se le parezca, pero necesitamos simular esa interrupción 50 veces por segundo. Podríamos simularla con un simple temporizador, pero su precisión sería desastrosa. Os recuerdo que muchos juegos del Spectrum hacían simpáticos efectos con el borde de la pantalla (cambiando el color muchas más veces que 50 por segundo). También quiero simular ese efecto.

Afortunadamente, el chip ULA emplea los mismos relojes que la cpu Z80 y los usa para cronometrar sus operaciones. El chip ULA empieza a contar ciclos de reloj y considera que los 16384 primeros ciclos son el borde superior de la pantalla, justo hasta el borde izquierdo del contenido de pantalla de la primera línea de visualización. Luego cada línea son 224 ciclos exactos: 128 ciclos para presentar los 256 píxeles de pantalla (esto es, 2 píxeles por cada ciclo), mas 96 ciclos para el borde derecho, el sincronismo horizontal y el borde izquierdo de la siguiente línea. En total, cada fotograma de pantalla son 224 ciclos por 312 líneas, esto es, 69888 ciclos (o tStates) y si multiplicáis esta cifra por 50 fotogramas por segundo obtenemos 3494400 ciclos (los famosos 3.5 MHz a los que funciona el Spectrum). De hecho, el chip ULA realmente funciona a 3.5 MHz, lo cual resulta en una visualización en pantalla de 50.08 cuadros por segundo… afortunadamente, a ningún televisor le afecta esta diferencia.

Así que vamos a implementar la función de la ULA que cuenta los ciclos de reloj a medida que avanza la visualización, en la función AddCycles():

#define   TSTATES_PER_SCANLINE   (224)
#define   TVSCANLINES            (312)
#define   TSTATES_PER_FRAME      (TSTATES_PER_SCANLINE * TVSCANLINES)

void AddCycles(unsigned int cycles,bool& IRQ)
{
  dwFrameTStates += cycles;
  dwScanLineTStates += cycles;

  if (dwScanLineTStates > TSTATES_PER_SCANLINE)
    ScanLine(IRQ);
}

Empleamos dos contadores porque es más simple que hacer operaciones sobre uno solo, y mantenemos un contador de líneas y un contador de cuadros. Cuando detectamos que se ha completado una línea de visualización completa, invocamos a la función ScanLine():

void ScanLine(bool &IRQ)
{
  dwScanLineTStates %= TSTATES_PER_SCANLINE;
  dwScanLine++;
  if (dwScanLine >= TVSCANLINES)
  {
    // Frame complete - trigger IRQ
    IRQ = true;
    dwFrameTStates %= TSTATES_PER_FRAME;
    dwScanLine = 0;
    if (++dwFrameCount > 16)    // each 16 full frames...
    {
      dwFrameCount = 0;         // reset counter...
      blinkState = !blinkState; // ... invert blink ...
      UpdateBlink();            // ... and update screen contents
    }
  }

  // Check if current scanline (0-311) lies within the "visible portion"
  // of screen bitmap (lines 0-239), i.e., skip first and last 36 lines (front/back porches)
  if ((dwScanLine < 36) || (dwScanLine > (239+36))) return;

  // Now check if current background color for scanline is different
  // from the required one
  DWORD bitmapLine = dwScanLine-36;
  if (dwCurrentScanLineBackColor[bitmapLine] == dwBorderRGBColor)  // Already right color
    return;

  // Redraw current scanline with proper color
  LPDWORD scr = nativeBitmap;
  scr += (bitmapLine * 320);	// Get pointer to start of line

  // top/bottom borders?
  if ((bitmapLine < 24) || (bitmapLine > 24+191))
  {
    // Draw whole line
    unsigned int bCount = 320;
    do
    {
      *scr++ = dwBorderRGBColor;
    }while (--bCount);
    }
  else
  // screen contents area
  {
    // First 32 pixels
    unsigned int bCount = 32;
    do
    {
      *scr++ = dwBorderRGBColor;
    } while (--bCount);

    // Last 32 pixels
    scr += 256;
    bCount = 32;
    do
    {
      *scr++ = dwBorderRGBColor;
    } while (--bCount);
  }

  // and remember this scanline's colour
  dwCurrentScanLineBackColor[bitmapLine] = dwBorderRGBColor;
  IsDirty = true;
}

La función realiza tres operaciones: cuenta los ciclos de reloj tanto para la línea en curso como para la pantalla completa, y si determina que ésta se ha dibujado totalmente indica que es necesario invocar la interrupción del procesador poniendo a verdadero la variable IRQ. También cuenta bloques de 16 fotogramas para hacer parpadear los caracteres de pantalla que tengan el atributo “blink”. Por último la función comprueba si ha cambiado el color del borde y repinta cada línea de la pantalla (si es que ha cambiado) con el nuevo color.

Para añadir la interrupción, basta un pequeño cambio en la función emulatorMain() que ya vimos en un capítulo anterior:

    DWORD dwFrameStartTime = GetTickCount();
    do
    {
      // Emulate next instruction
      cpu.tStates = 0;
      cpu.EmulateOne();

      // After each instruction, report the ULA the number of cycles we've used
      bool irq = false;
      ula.AddCycles(cpu.tStates,irq);

      // As in the real Spectrum, the ULA will trigger an IRQ for every frame. This
      // implementation uses cpu clock cycles to know where the screen beam is.
      if (irq)		// Ula signals a frame interrupt
      {
        cpu.INT();		// Generate system interrupt

        // If screen contents have been modified, set a flag for the WPF rendering event.
        if (ula.IsDirty)
        {
          screenDataDirty = true;
          ula.IsDirty = false;
        }

        // The PC executes code a lot faster than the original Z80.
        // As we now that "20ms" (1 frame) have ellapsed, pause execution to
        // match host PC and emulated computer
        DWORD dwNow = GetTickCount();
        DWORD dwEllapsed = dwNow - dwFrameStartTime;
        if (dwEllapsed < 20)    // Running above real time?
        {
          Sleep(20 - dwEllapsed);
          dwNow = GetTickCount();
        }
        dwFrameStartTime = dwNow;
      }
    } while(quitEmulation == false);

Utilizando el contador de ciclos de la clase Z80 vamos actualizando la ULA para que mantenga la posición de pantalla. Si AddCycles() activa la variable local que le pasamos como referencia (irq), simplemente invocamos el método INT() del Z80 para que dispare una interrupción. Fijaos que también podemos aprovecharnos de esta característica para hacer que nuestro emulador corra más o menos a la misma velocidad que un Spectrum: dado que sabemos que la máquina ha estado corriendo durante un cuadro de pantalla completo (1/50 de segundo = 20 milisegundos), y que nosotros sabemos cuanto tiempo hemos empleado en hacerlo (dwFrameStartTime – dwNow), basta con hacer un Sleep() del tiempo restante hasta cumplir los 20 milisegundos. No es exacto, pero el efecto es muy, muy aproximado.

Finalmente, añadimos una variable (screenDataDirty) para saber si los contenidos de pantalla han cambiado o no y optimizar la función de refresco de WPF (y no repintar cada fotograma completo como hasta ahora):

void Spectrum::OnRenderTick(Object^ sender,EventArgs ^e)
{
  if (screenDataDirty)
  {
    emulatorScreen->Lock();
    emulatorScreen->AddDirtyRect(Int32Rect(0,0,320,240));
    screenDataDirty = false;
    emulatorScreen->Unlock();
  }
}

Ya tenemos interrupciones! Ahora viene la parte en la que emulamos el hardware de teclado de la ULA.

8 ó 16 bits?

Al que se haya fijado, comprobará como el bus de I/O que hemos definido para nuestro procesador Z80 es de 16 bits. En cambio, y según la documentación de Zilog, el bus de direcciones de I/O del procesador real es de sólo 8 bits. Y esto? Bueno, no tengo documentación de la época y afortunadamente el datasheet actualizado lo describe, pero es que realmente cuando el procesador hace una operación de I/O coloca direcciones en los 16 bits de direcciones, aprovechando el contenido de otros registros del procesador.

Cuando leemos un puerto con una instrucción como IN A,(C), el procesador realmente coloca en el bus de direcciones el contenido completo (16 bits) del registro BC, y no sólo la parte baja del mismo (C) como cabría esperar. De igual forma, cuando hacemos un IN A,#puerto, el procesador tiene la necesidad de colocar “algo” en los 8 bits más significativos del bus de direcciones, y coloca los contenidos del acumulador (registro A) porque es lo que tiene más a mano. Por ejemplo, las instrucciones LD A,#14; IN A,$FE realmente leen el puerto de I/O $14FE. Y lo más curioso es que el Spectrum hace uso de esta característica para leer el teclado.

El chip ULA sólo utiliza una línea de direcciones para saber si están hablando con él: el bit de dirección 0 del bus. Esto es, cualquier dirección par de I/O accede a la ULA. Por convención, siempre se usa la dirección 0xFE, pero cualquier dirección par obtiene el mismo resultado. Y para leer el teclado aprovecha los 8 bits superiores de la dirección de I/O para saber qué sección del teclado deseamos leer.

El teclado del Spectrum

Ahora veamos cómo se conecta el teclado al resto del sistema. Las 40 teclas del Spectrum (4 filas de 10 teclas) están divididas en 8 bloques de cinco teclas cada uno:SpectrumKeyboard

Para leer una fila del teclado, hay que leer el puerto de I/O de la ULA (0xFE) colocando en el byte más significativo la fila en la que estamos interesados. La fila se codifica poniendo a “cero” el bit que represente su posición. Por ejemplo, para leer la fila “0” (teclas Shift, Z, X, C y V) se coloca “11111110” (0xFE), mientras que para leer la fila “5” (con las teclas Y, U, I, O y P) se coloca “11011111” (o lo que es lo mismo: 0xDF) en la parte alta de la dirección del puerto. Tras cada lectura, el chip ULA devuelve el estado de las cinco teclas correspondientes a esa fila (los cinco bits menos significativos) estando a cero el bit que represente una tecla pulsada. Fijaos también que la ordenación de los bits de las semilíneas 0 a 3 es la inversa que para las semifilas 4 a 7.

Para la implementación del teclado del emulador, voy a seguir el mismo patrón: un array de 8 bytes que representa las ocho “semifilas” de teclas y que podremos manipular desde el exterior para simular las pulsaciones:

protected:
  unsigned char keyMatrix[8];

public:
  void PressKey(unsigned int keyRow,unsigned int keyCol,bool down)
  {
    if (keyCol > 9)
      return;
    if (keyRow > 3)
      return;

    // Spectrum keyboard layout
    //       D0 D1 D2 D3 D4    D4 D3 D2 D1 D0
    // A11      ROW3              ROW4          A12
    // A10      ROW2              ROW5          A13
    //  A9      ROW1              ROW6          A14
    //  A8      ROW0              ROW7          A15

    int rowNdx;
    int bitMask;
    if (keyCol < 5)   // Left bank
    {
      rowNdx = 3 - keyRow;
      bitMask = 0x01 << keyCol;
    }
    else              // Right bank
    {
      rowNdx = 4 + keyRow;
      bitMask = 0x01 << (9 - keyCol);
    }

    if (down)
      keyMatrix[rowNdx] |= bitMask;
    else
      keyMatrix[rowNdx] &= ~bitMask;
  }

La función recibe las coordenadas de una tecla en el formato físico del Spectrum como “fila/columna”, y modifica el bit correspondiente a la configuración física por medio de unas simples operaciones, dependiendo de si la tecla ha sido pulsada (down == TRUE) o liberada (down == FALSE). El array keyMatrix queda totalmente configurado para poder leerlo muy rápidamente (y con la misma facilidad que en el caso real) cuando el código del Spectrum lo necesite.

Las peticiones de lectura del teclado llegarán a la ULA desde el procesador por medio de la función IORead(), que hasta ahora no hacía nada salvo devolver 0xFF (valor habitual en buses vacíos):

  unsigned char IORead(unsigned int address)
  {
    // ULA is selected by A0 being low
    if ((address & 0x01) == 0)
    {
      // Get the scancodes
      unsigned char kData = 0xFF;   // Pull ups
      unsigned char row = (address >> 8) ^ 0xFF;
      for (int dd=0;dd<8;dd++)
      {
        if (row & (1 << dd))        // Select scanline?
          kData &= ~keyMatrix[dd];  // pull down bits representing "pressed" lines
      }
      return(kData);
    }
    return(0xFF);
  }

La función decodifica la dirección de I/O de la misma forma que el chip ULA real: viendo si el bit menos significativo de la dirección es “0”. En tal caso, recorre el array de filas, comprobando si la posición coincide con la máscara requerida. Si es así, limpia los bits correspondientes a las teclas pulsadas en la fila. La función recorre las ocho filas de teclas: si se solicita un identificador de fila con varios bits a cero, simplemente combina las teclas pulsadas en todas las filas seleccionadas.

Publicando el API del teclado

Dado que nuestro emulador de Spectrum es un control, necesitamos publicar funciones que nos permitan interactuar con él desde nuestra aplicación host. Dado que la instancia de la clase ULA es interna a la función emulatorMain() (y que hace todo el trabajo), vamos a publicar un puntero en nuestra clase que nos permita acceder a la instancia desde el exterior. Nada más simple: en la declaración de la clase Spectrum añadimos lo siguiente:

private:
  ULA *pCurrentUla;

y en la función emulatorMain()

void Spectrum::emulatorMain()
{
  // Components
  ...  
  ULA ula;     // Spectrum's ULA chip + 16KB
  ...

  pCurrentUla = &ula;
  ...

Cuando la emulación termina la función limpia la variable. Ahora, nada más fácil ahora que implementar dos funciones públicas (PressKey() y ReleaseKey()) que nos permitan simular las pulsaciones de teclas desde la aplicación host:

public:
  void Spectrum::PressKey(int keyRow,int keyCol)
  {
    if (pCurrentUla != nullptr)
      pCurrentUla->PressKey(keyRow,keyCol,true);
  }

  void Spectrum::ReleaseKey(int keyRow,int keyCol)
  {
    if (pCurrentUla != nullptr)
      pCurrentUla->PressKey(keyRow,keyCol,false);
  }

Terminemos añadiendo la emulación de teclado a nuestra aplicación host en C#. Sobrecargando las funciones OnKeyDown() y OnKeyUp() de la clase base, podemos pulsar o liberar las teclas del Spectrum. Aquí sigue un fragmento de la función OnKeyDown() – como podréis imaginar la función OnKeyUp() es básicamente la misma, solo que invocando a la función ReleaseKey() del control:

protected override void OnKeyDown(System.Windows.Input.KeyEventArgs e)
{
   base.OnKeyDown(e);
   switch (e.Key)
   {
     .....
     case System.Windows.Input.Key.Q: emulator.PressKey(1, 0); break;
     case System.Windows.Input.Key.W: emulator.PressKey(1, 1); break;
     case System.Windows.Input.Key.E: emulator.PressKey(1, 2); break;
     case System.Windows.Input.Key.R: emulator.PressKey(1, 3); break;
     case System.Windows.Input.Key.T: emulator.PressKey(1, 4); break;
     case System.Windows.Input.Key.Y: emulator.PressKey(1, 5); break;
     case System.Windows.Input.Key.U: emulator.PressKey(1, 6); break;
     case System.Windows.Input.Key.I: emulator.PressKey(1, 7); break;
     case System.Windows.Input.Key.O: emulator.PressKey(1, 8); break;
     case System.Windows.Input.Key.P: emulator.PressKey(1, 9); break;
     .....

Podemos aprovechar también para fabricarnos “atajos” en el teclado del Spectrum. Por ejemplo, para borrar un carácter (tecla “Delete” del Spectrum) es necesario pulsar la combinación “mayúsculas” y “0”. Podemos mapear la tecla del PC a una combinación de ambas ( y recordad hacer lo mismo en la función OnKeyUp()!!)

     // Keyboard shortcuts:
     case System.Windows.Input.Key.Back:
       emulator.PressKey(3, 0);      // Shift
       emulator.PressKey(0, 9);      // '0'
       break;

Ya podemos escribir!

SpectrumWKeyboard

Gracias a todo lo visto hoy nuestro control Spectrum está casi listo – ya podemos iniciar de nuevo nuestro emulador y disfrutar escribiendo código como se hacía en los ‘80… Quien no ha escrito algo así? Es lo primero que hicimos cuando, de chavales, descubríamos el maravilloso mundo de la informática, los ordenadores, la programación… y los videojuegos!

En la próxima entrega de este tutorial implementaremos la simulación de una unidad de cinta de cassette… porque todos queremos jugar a Head Over Heels o Skooldaze, no?

Enlaces:

Emulador YASS para Windows
Código fuente del Emulador YASS

– Hazte un Spectrum: 1ª Parte, 2ª Parte, 3ª Parte, 4ª Parte, 5ª Parte, 6ª Parte, 7ª Parte, 8ª Parte

Categorías:Desarrollo, Tutorial Etiquetas: , , , , ,