YASS-Yet Another Spectrum Simulator

Hazte un Spectrum - Capítulo 4

Publicado originalmente: 31 de Agosto de 2012

En la anterior parte de este largo tutorial, la ULA del Spectrum estaba empezando a tomar cuerpo. A partir de ahora empieza la integración práctica de todos los componentes gracias a la inestimable ayuda de .NET y WPF.

Ya empieza a ver muchos componentes sueltos, y toca empezar a desarrollar el armazón que sujetará todo. Ya que toda la base está desarrollada en C++, bien podríamos seguir en éste lenguaje y completar un emulador totalmente nativo – solo que da como pereza, sobre todo teniendo en cuenta que, para completarlo acabaríamos interactuando con el GDI de Windows “de toda la vida”, y que está más que obsoleto. En cambio pensé en usar .NET y WPF que ofrecen una infinita mayor versatilidad y, sobre todo, una impresionante facilidad de migración hacia el futuro – léase WinRT. Pero no adelantemos acontecimientos.

Para un componente de bajo nivel como nuestro emulador de Spectrum, C++ es sin duda el lenguaje ideal. Pero para completarlo necesitamos un “front-end”, una “aplicación emulador” y pensé cómo convertir todo el código creado hasta ahora como un componente reutilizable. Simplemente, convirtamos el emulador en un control WPF, que podremos emplear desde una sencillísima aplicación .NET y C#.

C++ Gestionado

Parece que existe la convicción de que “todo” se puede (e incluso, “debe”) hacerse en C# para ser molón, para ser “del siglo XXI”, pero para mí no es el caso. C# es una maravilla de lenguaje, encuentro su estructura absolutamente preciosa, pero no me da todo lo que necesito. Lo que la mayoría de la gente pasa por alto es que hay “otro C++” el C++ Gestionado (ó “Managed C++”) que es la combinación perfecta de los dos entornos: la flexibilidad, la potencia de C++, unido a poder emplear un runtime moderno como .NET. En C++ Gestionado se puede mezclar el código nativo, con acceso a bajo nivel a la máquina junto con clases de altísimo nivel de abstracción, todo en un sólo lenguaje, todo en una sola librería. El truco? Las clases “ref”.

Las clases en C++ se declaran como “class NombreDeLaClase”. Esa clase será nativa, con acceso total a la máquina y al API. Pero, y si una clase en C++ desea acceder (y ser accedida) desde .NET? Basta con declarar la clase como “ref class NombreDeLaClase”. Una “ref class” es una clase .NET a todos los efectos, solo que escrita en C++. Eso es justo lo que queremos.

La clase “Emulation.Spectrum”

Vamos a escribir una clase que, vista desde .NET es un control WPF más, pero que internamente implenta un Sinclair ZX Spectrum. El primer paso es crear un nuevo projecto, una “class library” en C++ Gestionado. Y si, apuesto a que muchos de vosotros ni siquiera os habíais fijado en esa parte de los proyectos que se pueden crear con Visual Studio:

ManagedCPPDLL

En qué se diferencia programar en C++ en un entorno gestionado? Absolutamente en nada. Simplemente se abre la puerta a la posibilidad de crear clases “visibles” desde .NET y que soporten toda su inmensa librería de componentes – lo cual, frente a tener que hacerlo usando al API clásico de Windows, es toda una ventaja. El cambio llega al crear clases gestionadas, donde la sintaxis clásica de C++ se extiende. Si, hay una curva de aprendizaje, pero considero que vale la pena. Y no es tan duro como podría parecer en un principio.

Una vez creado el proyecto, simplemente copié dentro todos los fuentes C++ que ya había diseñado. Y compila, aunque claro, no genera nada visible desde .NET. Así que ahora toca implementar el control. Para ello, escogí el control Border (nombre completo, “System.Windows.Controls.Border”) como clase base de la que derivarme porque necesito lo más básico posible. De hecho, me vale cualquier control WPF que tenga un “Background” o un “Fill”… Así que la clase es más o menos como sigue (y nótese que es una “ref class”):

using namespace System;
using namespace System::Threading;
using namespace System::Windows::Media::Imaging;

namespace Emulation
{
  public ref class Spectrum : System::Windows::Controls::Border, IDisposable
  {
    public:
      Spectrum();
      ~Spectrum()
      {
        Stop();
        GC::SuppressFinalize(this);
      };
      !Spectrum()
      {
        Stop();
        emulatorScreen = nullptr;
      }

    public:
      void Start();
      void Stop() { };

    protected:
      void emulatorMain();
      void OnRenderTick(Object^ sender,EventArgs^ e);

    private:
      Thread^ emulatorThread;
      WriteableBitmap^ emulatorScreen;
      LPDWORD screenData;
      bool screenDataDirty;
      int bytesPerScreenLine;
      int quitEmulation;
  };
}

Lo primero que llama la atención a un programador C++ (e incluso a uno de C#) es el uso de “^” para representar un puntero. Realmente no es un puntero, es un “handle” a un objeto .NET. Pronto descubriremos como podemos mezclar, en la misma clase, handles a otros objetos .NET y punteros a instancias de clases nativas (como el “LPDWORD” al buffer nativo del bitmap). Todo al mismo tiempo.

La implementación es muy simple: la emulación va a correr en un thread independiente (para simplificar incluso más la programación desde un entorno .NET). La emulación de la pantalla voy a resolverla usando un componente WriteableBitmap, que no es más que una “superficie pintable” (si es que eso significa algo) que voy a emplear como “Background” de mi control.

El método Start de la clase es simple: instanciamos el bitmap (WriteableBitmap) para tener dónde “pintar” la pantalla del Spectrum y arrancamos el thread de emulación:

  Spectrum::Spectrum()
  {
    emulatorScreen = nullptr;
    quitEmulation = false;
    screenData = nullptr;
    screenDataDirty = false;
    m_RenderDelegate = gcnew EventHandler(this,&Spectrum::OnRenderTick);
  }

  void Spectrum::Start()
  {
    if (emulatorThread != nullptr)
      return;		// Already running

    quitEmulation = false;

    // Attach to WPF screen refresh to update
    CompositionTarget::Rendering += m_RenderDelegate;

    // Build the emulated screen and WPF render surface.
    if (emulatorScreen == nullptr)
    {
      // Emulator will draw on this bitmap
      emulatorScreen = gcnew WriteableBitmap(320,240,72,72,PixelFormats::Bgra32,nullptr);
      bytesPerScreenLine = emulatorScreen->BackBufferStride;

      emulatorScreen->Lock();
      screenData = (LPDWORD)emulatorScreen->BackBuffer.ToPointer();

      for (int dd=0;dd<320*240;dd++)
        screenData[dd] = 0xFFC0C0C0;
      emulatorScreen->Unlock();

      // ... and the bitmap is attached as the contents of the control's background.
      ImageBrush^ bkgnd = gcnew ImageBrush();
      bkgnd->ImageSource = emulatorScreen;
      Background = bkgnd;
    }

    emulatorThread = gcnew Thread(gcnew ThreadStart(this,&Spectrum::emulatorMain));
    emulatorThread->Start();
  }

Fijaos en otra diferencia al programar en C++ gestionado: las clases nativas se instancian con “new”, mientras que las clases gestionadas se instancian con “gcnew”. Nunca he entendido el por qué de emplear dos comandos para hacer algo que el compilador podría distinguir él solito…

Lo único reseñable de éste fragmento de código es cómo usamos la clase WriteableBitmap como elemento de unión entre WPF y C++: por un lado obtenemos un puntero “clásico de toda la vida” a los contenidos físicos del bitmap, mientras que a la vez lo usamos para construir una textura que usamos como “fondo” (“Background”) del control. El único trabajo extra es que necesitamos avisar a WPF cada vez que el bitmap cambie, y para eso nos “enganchamos” al delegado de repintado (CompositionTarget.Rendering) del sistema.

Por cierto, sí que necesitamos una cosa más: un módulo que contenga la ROM del Spectrum: nuevamente, empleando nuestros buses la implementación es sencilla:

template<unsigned int B,unsigned int S> class ROM : public BusComponent<B,S>
{
public:
  int Load(char *romName)
  {
    FILE *file = NULL;
    int err = fopen_s(&file,romName,"rb");
    if ((err == 0) && (file != NULL))
    {
      int got = fread(data,1,S,file);
      fclose(file);
    }
    return(err);
  };

  public:
  void Write(unsigned int address,unsigned char value)
  {
    // Do nothing... this is ROM!!!
  }
  unsigned char Read(unsigned int address)
  {
    return(data[address-B]);
  }

protected:
  unsigned char data[S];
};

Para la implementación de la función “Load()” he usado el código más “ANSI” posible – la portabilidad primero (y por eso no hago más que usar “unsigned short”, “unsigned int”, etc, en lugar de los términos habituales de WIN32 como USHORT o DWORD).

Ahora, echemos un vistazo a la función “emulatorMain()”, que es donde se realiza el trabajo de verdad. Observad como para ser una clase .NET, maneja tanto objetos nativos como gestionados con la misma soltura:

  void Spectrum::emulatorMain()
  {
    // Components
    Bus16 bus;              // Main Z80 data bus
    ROM<0,16384> rom;       // Main system ROM
    ULA ula;                // Spectrum's ULA chip + 16KB
    RAM<32768,32*1024> ram; // Remaining RAM (32KB)
    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 busses
    bus.AddBusComponent(&rom);
    bus.AddBusComponent((ULAMemory*)&ula);
    bus.AddBusComponent(&ram);

    // And attach busses to cpu core
    cpu.DataBus = &bus;
    cpu.IOBus = (ULAIO*)&ula;

    cpu.regs.PC = 0;

    // Ready to roll!!
    DWORD dwFrameStartTime = GetTickCount();
    do
    {
      // Emulate next instruction
      cpu.EmulateOne();
    } while(quitEmulation == false);
  }

Dos cosas: la primera es que si os fijáis, he conectado la parte de I/O del chip ULA “a capón” al procesador (otra ventaja más de nuestra estructura de buses). Para que ésto funcione, sólo tengo que crear las dos funciones IORead() e IOWrite() de la ULA:

  unsigned char IORead(unsigned int address)
  {
    return(0xFF);  // pull ups on bus
  }

  void IOWrite(unsigned int address,unsigned char value)
  {
    // ULA is selected when reading an even address
    if (address & 0x01)
      return;

    // Update border color member variable.
    dwBorderRGBColor = dwColorTable[value & 0x07];
  }

La arquitectura de la ULA – y de los buses de I/O del Spectrum en general están simplificados al máximo. Si bien la dirección de I/O “estándar, documentada” de la ULA es 0xFE, cualquier dirección con el bit 0 de direcciones de “0” (o sea, cualquier dirección de I/O par) también sirve. De momento aprovecho para quedarme con el color para el borde de la pantalla, que se configura en los tres bits menos significativos del único puerto de I/O de la ULA. Pero de eso toca hablar un poco más adelante.

En segundo lugar, y si recordáis el capítulo anterior, nuestra implementación de ULA estaba a la espera de que nos diesen un bitmap sobre el que dibujar. Como ya lo tenemos – gracias al WriteableBitmap e instanciado en el método Start() de la clase, ha llegado el momento de hacérselo saber a la ULA para que lo use, por medio de la función “SetNativeBitmap()”. La implementación es trivial:

  void SetNativeBitmap(LPBYTE pBitmap,int bytesPerScanLine)
  {
    nativeBitmap = (LPDWORD)pBitmap;
    dwordsPerRow = bytesPerScanLine / 4;
  }

Sólo queda una cosa más por hacer: WPF nos llamará 50 veces por segundo para saber si tenemos algo que pintar. De momento, vamos a lo bestia:

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

Si, poco optimizado (invalida los contenidos del WriteableBitmap 50 veces por segundo), pero por lo menos nos sirve para ver hasta dónde llegamos.

Compilamos todo y obtenemos una bonita DLL en C++ gestionado. Ahora, habrá que usarla en alguna parte, verdad?

Vuelta a C#

Ya tocaba? Bien, esto será muchísimo más familiar para la mayoría. Vamos a hacer una aplicación WPF en C#, aunque VB sería exactamente igual de válido. Cread un nuevo proyecto de “aplicación WPF” (yo le he puesto “Emulator” de nombre – original que es uno) y agregad la referencia a nuestro flamante control y abrid “MainWindow.xaml”.

Lo primero, es necesario definir un “namespace” XML para referirnos a nuestro control. Simplemente agregadlo en la cabecera del XAML, indicando la DLL que contenga la implementación (“Spectrum.DLL” en mi caso). Para el namespace, escogí (como no podía ser de otro modo) “zx”:

<Window x:Class="Emulator.MainWindow"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   Title="YASS - Yet another Spectrum Simulator"
   Height="350"
   Width="525"
   xmlns:zx="clr-namespace:Emulation;assembly=Spectrum">

Quitamos todo lo que sobra que VisualStudio tiene a bien colocar de forma predeterminada en el cuerpo de la ventana y lo reemplazamos por ésto:

  <Grid>
   <zx:Spectrum x:Name="emulator" />
 </Grid>

Sólo queda arrancar el emulador al inicio del programa. En el constructor de la clase basta con invocar el método Start() del control:

  public MainWindow()
  {
    InitializeComponent();
    emulator.Start();
  }

Nos aseguramos que el proyecto “Emulator” está configurado como predeterminado (VisualStudio tiene tendencia a dejar como predeterminado el primer proyecto que se agrega a una solución), pulsamos F5 (o sea: “Debug/Start”), cruzamos los dedos y nuestro trabajo se verá recompensado con ésto:

FirstStart

Es el Spectrum más “sordo” del universo (no tiene teclado, no tiene sonido, no tiene nada de nada), pero la secuencia de arranque la completó a la perfección y nuestra implementación de ULA es capaz de actualizar un bitmap de WPF recibiendo las escrituras de la cpu a la memoria de vídeo emulada. Vamos bien…

El siguiente paso será darle a nuestro Spectrum un estupendo teclado, para poder comunicarnos con él. También descubriremos cómo necesitaremos recrear las temporizaciones de un televisor para sincronizarlo todo… Tranquilos, es más fácil de lo que parece.