KB3004394: Vaya día…

Soy extremadamente celoso de la seguridad y, cuando un buen día, empiezan a pasar cosas muy, muy raras con varios equipos del trabajo, no pude por menos que intentar averiguar lo que estaba pasando. Esto es lo que aprendí y como, lo que me parecía más improbable, era la razón de todo.

La primera señal de alarma llegó a primera hora de la mañana: en uno de los ordenadores de la oficina se desactivó (por las buenas?) Windows Defender. Si, ya lo sé, hay cosas mejores pero qué le vamos a hacer – es lo que hay. Eso, y las malas costumbres (ejem) de los usuarios (ejem, ejem) me hicieron temer lo peor.

Mi primer instinto cuando encuentro un antivirus que no se activa o, simplemente, no aparece, es ir directo al teclado: Windows+R, “regedit”. Lo que normalmente espero es la tranquilizadora imagen del consentimiento de seguridad de Windows:

UAC

Lo que me encontré (y espero que comprendáis que no haya foto – no estaba la cosa para eso) es el editor de registro abriéndose directamente, sin ningún tipo de confirmación. Un antivirus desactivado y UAC desactivado al mismo tiempo (obviamente el usuario jura y perjura que no lo ha desactivado manualmente) me ponen en modo paranoico: tenemos un gamusino.

Lo primero fue restablecer UAC (con su correspondiente reinicio) y verificar que, una vez vuelto a arrancar, el ajuste era efectivo y no volvía a desactivarse. Eureka! Pero espera… hay algo raro…

Al volver a intentar ejecutar RegEdit y tal y como esperaba, apareció el mensaje de confirmación de UAC, pero con otro elemento mosqueante: en lugar de indicar “Microsoft Windows” como Editor Comprobado aparece como “desconocido”. Sigo mosqueado. Más aún cuando incluso al intentar abrir el visor de eventos (EventVwr) también me pide autorización – raro si tenemos en cuenta que el visor de eventos no lo requiere en condiciones normales. Intentar reactivar Windows Defender terminaba también en un error.

Intentar instalar Security Essentials daba otro error durante el proceso: 0x8004FF91. Adelante, buscad ese error (bueno, hoy no creo que tengáis mucho problema para encontrar referencias) y veréis que, ni es nuevo, ni hay documentación al respecto. Vamos bien…

Tras un análisis en profundidad del ordenador y no encontrar absolutamente ningún indicio de un comportamiento extraño, intenté tranquilizar mi espíritu pensando que podría haber sido un intento de infección de algún tipo de virus o troyano que se paró y dejó el ordenador en ese estado: nada escondido, ningún ejecutable extraño, ninguna traza en registry u otros puntos de inicio… nada.

Continué el día – o mejor dicho, intenté continuar el día mientras pensaba si era buena idea hacer un “rollback” a un punto de restauración anterior, seguir mirando cosas o directamente “pegar un leñazo” (término técnico para “reinstalar desde cero”). No había pasado ni media hora cuando otro ordenador empezó a hacer también cosas raras… y luego otro…

No voy a aburriros con una descripción detallada de nuestra arquitectura de red e interconexiones con otros centros – solo os diré que empecé a mirar cosas realmente esotéricas por todas partes (incluyendo si alguien había instalado WireShark en el cortafuegos y se había dejado el driver de captura activado – no os digo más). Pero me recuerdo a mí mismo en mitad de la sala de producción, buscando “algo” mientras decía en voz alta [sic] “o tenemos un gamusino o una tremenda cagada de Microsoft con un parche…”.

Mi sensación era como si los ejecutables de Windows hubiesen sido reemplazados por versiones alteradas, sin la firma digital de Microsoft. Y a nada que lo penséis sabréis que es muy mal indicio. Recordé que mi PC Windows 8 tiene un Windows 7 instalado en una partición que no uso desde hace más de dos años (desde que monté 8, vamos). Busqué RegEdit.exe y lo comparé con la versión extraída desde uno de los PCs con comportamiento raro. Para mi “sorpresa” eran idénticos (no lo esperaba… imaginé que alguna actualización en dos años habría tocado regedit, pero no… ninguna). La parte mala (o buena?) es que, siendo idénticos, significa que ambos portaban la firma digital apropiada de Microsoft – no había ejecutables reemplazados (“yupi?”)

Parecía que el problema se originaba en los certificados para verificar los ejecutables pero, cómo era eso posible?

La solución llegó en forma de un boletín en la web de soporte de Microsoft: la actualización KB3004394 está rota.

La idea de la actualización KB3004394 es sencilla: los certificados digitales que usa Windows para garantizar que las conexiones seguras son, eso, seguras se actualiza hasta ahora una vez a la semana. El problema (y esto es de mi cosecha) es que desde que se supo la brecha de seguridad en Sony y que se habían conseguido robar sus certificados digitales, era perfectamente posible “fabricar” tanto ejecutables como certificados para servidores perfectamente válidos y que podrían usarse para fines maliciosos. Así que Microsoft pensó que no sería mala idea no solo revocar ciertos certificados sino que los ordenadores lo hagan ellos solitos solo que más a menudo: una vez al día.

La idea no es mala – salvo el posible ancho de banda que pueda conllevar el proceso, pero el problema es que el parche que (asumo) revoca los certificados sospechosos y cambia el comportamiento de Windows para actualizarlos “rompe” PCs con Windows 7 (y Windows Server 2008) instalado. Eso y que al destruirse la cadena de validación de certificados, muchos programas de terceros (como Security Essentials) fallan al instalarse con el (ahora conocido) error 0x8004FF91 que podríamos catalogar como “error al verificar la firma digital de uno de los componentes del programa”.

El siguiente paso es montar el parche KB3024777 y ver qué pasa. Revierte el comportamiento de Windows 7 si, pero: reconstruye las cadenas de certificados?

Y lo que más me preocupa: si el ordenador no puede verificar la cadena de certificados AHORA, con las cadenas de certificados rotas… ha hecho Microsoft un parche que, efectivamente, toca las cadenas de certificados sin ir “apropiadamente” firmada digitalmente? Porque imagino que sabéis lo que eso significa, verdad?

Ups…

Anuncios

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: , , , , ,