Pintando pixeles: Introducción a la memoria de video

Click here to see in English

Como ya vimos en el tutorial anterior Rellenando la pantalla de color en c y en ensamblador (Pasmo, z88dk y sdcc), el Amstrad cpc tiene tres modos de video: "Mode 0" 160×200 píxeles con 16 colores, "Mode 1" 320×200 píxeles con 4 colores y "Mode 2" 640×200 píxeles con 2 colores. La memoria de video está situada entre las direcciones C000 a FFFF, es decir, tiene un tamaño de 3FFF (16383) bytes. En función del modo, cada byte representa 2, 4 o 8 pixeles de la pantalla. Pero ¿la memoria de video es lineal? es decir, ¿el primer byte es la esquina superior izquierda de la pantalla y el ultimo es la inferior derecha?. Pues me temo que no. A este respecto el diseño del Amstrad CPC nos dejó una enorme sorpresa que nos complica (y a la vez divierte) su programación grafica.

Para ver como está estructurada la memoria de video, nada más fácil que escribir un programa que la rellena para ver como se van distribuyendo las líneas por la pantalla. Código fuente para sdcc (al final de tutorial se puede descargar un zip):

///////////////////////////////////////////////////////////////////////
// pixel01.c
// Fillin the screen with random colors
// Mochilote - www.cpcmania.com
////////////////////////////////////////////////////////////////////////
#include <stdio.h>
#include <stdlib.h>

void main()
{
  unsigned char *pScreen = (unsigned char *)0xC000;
  unsigned int nByte = 0;

  //SCR_SET_MODE 0
  __asm
    ld  a, #0
    call  #0xBC0E
  __endasm;

  for(nByte = 0; nByte < 0x3FFF; nByte++)
    pScreen[nByte] = (unsigned char)(rand() % 256);
  
  //KM_WAIT_CHAR  
  __asm
    call #0xBB06
  __endasm; 
}

Compilamos y generamos dsk con los siguientes comandos (pixel01.bat):

sdcc -mz80 --code-loc 0x6038 --data-loc 0 --no-std-crt0 crt0_cpc.rel pixel01.c
hex2bin pixel01.ihx
CPCDiskXP -File pixel01.bin -AddAmsdosHeader 6000 -AddToNewDsk pixel01.dsk

Ejecutamos en el emulador y obtenemos esto:

fill

Esta secuencia de lineas nos es muy familiar si recordamos la carga de la pantalla de presentación de muchos juegos (especialmente de cinta). Como vemos las líneas se rellenan 'linealmente' de izquierda a derecha, pero al terminar la línea, en vez de pasar a la siguiente inferior, se salta 8... En esta tabla (extraída de Amstrad CPC Firmware Manual) podemos apreciar la organización de la memoria de video:

LINE R0W0 R0W1 R0W2 R0W3 R0W4 R0W5 R0W6 R0W7
1 C000 C800 D000 D800 E000 E800 F000 F800
2 C050 C850 D050 D850 E050 E850 F050 F850
3 C0A0 C8A0 D0A0 D8A0 E0A0 E8A0 F0A0 F8A0
4 C0F0 C8F0 D0F0 D8F0 E0F0 E8F0 F0F0 F8F0
5 C140 C940 D140 D940 E140 E940 F140 F940
6 C190 C990 D190 D990 E190 E990 F190 F990
7 C1E0 C9E0 D1E0 D9E0 E1E0 E9E0 F1E0 F9E0
8 C230 CA30 D230 DA30 E230 EA30 F230 FA30
9 C280 CA80 D280 DA80 E280 EA80 F280 FA80
10 C2D0 CAD0 D2D0 DAD0 E2D0 EAD0 F2D0 FAD0
11 C320 CB20 D320 DB20 E320 EB20 F320 FB20
12 C370 CB70 D370 DB70 E370 EB70 F370 FB70
13 C3C0 CBC0 D3C0 DBC0 E3C0 EBC0 F3C0 FBC0
14 C410 CC10 D410 DC10 E410 EC10 F410 FC10
15 C460 CC60 D460 DC60 E460 EC60 F460 FC60
16 C4B0 CCB0 D4B0 DCB0 E4B0 ECB0 F4B0 FCB0
17 C500 CD00 D500 DD00 E500 ED00 F500 FD00
18 C550 CD50 D550 DD50 E550 ED50 F550 FD50
19 C5A0 CDA0 D5A0 DDA0 E5A0 EDA0 F5A0 FDA0
20 C5F0 CDF0 D5F0 DDF0 E5F0 ED50 F550 FD50
21 C640 CE40 D640 DE40 E640 EE40 F640 FE40
22 C690 CE90 D690 DE90 E690 EE90 F690 FE90
23 C6E0 CEE0 D6E0 DEE0 E6E0 EEE0 F6E0 FEE0
24 C730 CF30 D730 DF30 E730 EF30 F730 FF30
25 C780 CF80 D780 DF80 E780 EF80 F780 FF80
spare start C7D0 CFD0 D7D0 DFD0 E7D0 EFD0 F7D0 FFD0
spare end C7FF CFFF D7FF DFFF E7FF EFFF F7FF FFFF

Los tres modos de video tienen 200 lineas de alto, por lo que estas direcciones son fijas para los tres modos. Cada una de estas 200 lineas tiene 80 bytes de tamaño, que representan los 160, 320 o 640 pixeles de ancho en función del modo. Como ya dijimos antes, cada byte representa 2, 4 o 8 pixeles de la pantalla en función del modo, pero para complicar aun mas la programación, los bits de cada pixel están salteados en el byte de la siguiente forma:

  • Modo 2, 640×200, 2 colores (cada byte de la memoria de video representa 8 pixeles):
    bit 7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 bit 0
    pixel 0 pixel 1 pixel 2 pixel 3 pixel 4 pixel 5 pixel 6 pixel 7

  • Modo 1, 320×200, 4 colores (cada byte de la memoria de video representa 4 pixeles):
    bit 7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 bit 0
    pixel 0 (bit 1) pixel 1 (bit 1) pixel 2 (bit 1) pixel 3 (bit 1) pixel 0 (bit 0) pixel 1(bit 0) pixel 2 (bit 0) pixel 3 (bit 0)

  • Modo 0, 160×200 píxeles con 16 colores (cada byte de la memoria de video representa 2 pixeles):
    bit 7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 bit 0
    pixel 0 (bit 0) pixel 1 (bit 0) pixel 0 (bit 2) pixel 1 (bit 2) pixel 0 (bit 1) pixel 1 (bit 1) pixel 0 (bit 3) pixel 1 (bit 3)

Viendo todos estos datos y este desorden uno sólo puede acordarse "con cariño" de todos los familiares ascendentes del que diseño esto...

Ejemplos practicos:

  • En Modo 2, poner el pixel en x=81, y=7 al color 1: Primero buscamos en la tabla la linea 7 (empezamos a contar desde 0) y la dirección es F800, como en modo 2 cada byte son 8 pixeles, para movernos hasta el byte donde está el pixel 81 horizontal, aumentamos la dirección 81 / 8 = 10 y nos sobra 1, sumamos 10 a la dirección y nos da F80A, ahora en esa dirección para escribir el pixel 1 tenemos que poner a 1 el bit 6.
  • En Modo 1, poner el pixel en x=50, y=22 al color 3: La dirección de la línea 22 en la tabla es F0A0, para movernos al pixel 50, sumamos a la dirección 50 / 4 = 12 y nos sobran 2, la suma nos da F0AC, y para poner el color 3 al pixel 2 de esa dirección, tenemos que poner a 1 los bits 5 y 1.

Si nos fijamos en la tabla de direcciones veremos que existe una relación entre ellas, agrupadas de 8 en 8 líneas, con saltos de 2048 bytes entre ellas y de 80 bytes respecto al grupo de 8 siguiente. Gracias a esta relación podemos sacar fácilmente la dirección de cualquier línea con la siguiente formula: Dirección = 0xC000 + ((Linea / 8) * 80) + ((Linea % 8) * 2048)

Vamos a aplicar lo que acabamos de aprender sobre el programa anterior, para rellenar la pantalla de arriba a abajo. Código fuente para sdcc (al final de tutorial se puede descargar un zip):

////////////////////////////////////////////////////////////////////////
// pixel02.c
// Calculating the addresses of the display
// Mochilote - www.cpcmania.com
////////////////////////////////////////////////////////////////////////
#include <stdio.h>
#include <stdlib.h>


unsigned char *GetLineAddress(unsigned char nLine)
{
  return (unsigned char *)0xC000 + ((nLine / 8) * 80) + ((nLine % 8) * 2048);
}


void main()
{
  unsigned char *pScreen = (unsigned char *)0xC000;
  unsigned int nByte = 0;
  unsigned int nLine = 0;

  //SCR_SET_MODE 0
  __asm
    ld  a, #0
    call  #0xBC0E
  __endasm;

  for(nLine = 0; nLine < 200; nLine++)
  {
    pScreen = GetLineAddress(nLine);

    for(nByte = 0; nByte < 80; nByte++)
      pScreen[nByte] = (unsigned char)(rand() % 256);
  }

  //KM_WAIT_CHAR  
  __asm
    call #0xBB06
  __endasm; 
}

Compilamos y generamos dsk con los siguientes comandos (pixel02.bat):

sdcc -mz80 --code-loc 0x6038 --data-loc 0 --no-std-crt0 crt0_cpc.rel pixel02.c
hex2bin pixel02.ihx
CPCDiskXP -File pixel02.bin -AddAmsdosHeader 6000 -AddToNewDsk pixel02.dsk

Ejecutamos en el emulador y obtenemos esto:

fill

Y ahora más dificil todavía, vamos a rellenar pixel a pixel la pantalla con columnas de izquierda a derecha y para ello ya tenemos que programarnos una función que rellene los bits correspondientes para cada pixel y color. Código fuente para sdcc (al final de tutorial se puede descargar un zip):

////////////////////////////////////////////////////////////////////////
// pixel03.c
// Filling the screen from left to right
// Mochilote - www.cpcmania.com
////////////////////////////////////////////////////////////////////////
#include <stdio.h>
#include <stdlib.h>


unsigned char *GetLineAddress(unsigned char nLine)
{
  return (unsigned char *)0xC000 + ((nLine / 8) * 80) + ((nLine % 8) * 2048);
}

void SetMode0PixelColor(unsigned char *pByteAddress, unsigned char nColor, unsigned char nPixel)
{
  unsigned char nByte = *pByteAddress;

  if(nPixel == 0)
  {
    nByte &= 85;

    if(nColor & 1)
      nByte |= 128;

    if(nColor & 2)
      nByte |= 8;

    if(nColor & 4)
      nByte |= 32;

    if(nColor & 8)
      nByte |= 2;
  }
  else
  {
    nByte &= 170;

    if(nColor & 1)
      nByte |= 64;

    if(nColor & 2)
      nByte |= 4;

    if(nColor & 4)
      nByte |= 16;

    if(nColor & 8)
      nByte |= 1;
  }

  *pByteAddress = nByte;
}

void main()
{
  unsigned char *pScreen = (unsigned char *)0xC000;
  unsigned int nLine = 0;
  unsigned int nColumn = 0;
  unsigned char nColor = 0;
  unsigned char nPixel = 0;

  //SCR_SET_MODE 0
  __asm
    ld  a, #0
    call  #0xBC0E
  __endasm;

  for(nColumn = 0; nColumn < 160; nColumn++)
  {
    nColor = (nColor + 1) % 16;
    nPixel = nColumn % 2;

    for(nLine = 0; nLine < 200; nLine++)
    {
      pScreen = GetLineAddress(nLine) + nColumn / 2;
      SetMode0PixelColor(pScreen, nColor, nPixel);
    }
  }

  //KM_WAIT_CHAR  
  __asm
    call #0xBB06
  __endasm; 
}

Compilamos y generamos dsk con los siguientes comandos (pixel03.bat):

sdcc -mz80 --code-loc 0x6038 --data-loc 0 --no-std-crt0 crt0_cpc.rel pixel03.c
hex2bin pixel03.ihx
CPCDiskXP -File pixel03.bin -AddAmsdosHeader 6000 -AddToNewDsk pixel03.dsk

Ejecutamos en el emulador y obtenemos esto:

fill

Para terminar, vamos a ver un ejemplo, con una función PutPixelMode0 completa, con un pixel rebotando por la pantalla. Código fuente para sdcc (al final de tutorial se puede descargar un zip):

////////////////////////////////////////////////////////////////////////
// pixel04.c
// Bouncing pixel
// Mochilote - www.cpcmania.com
////////////////////////////////////////////////////////////////////////
#include <stdio.h>
#include <stdlib.h>

void SetMode0PixelColor(unsigned char *pByteAddress, unsigned char nColor, unsigned char nPixel)
{
  unsigned char nByte = *pByteAddress;

  if(nPixel == 0)
  {
    nByte &= 85;

    if(nColor & 1)
      nByte |= 128;

    if(nColor & 2)
      nByte |= 8;

    if(nColor & 4)
      nByte |= 32;

    if(nColor & 8)
      nByte |= 2;
  }
  else
  {
    nByte &= 170;

    if(nColor & 1)
      nByte |= 64;

    if(nColor & 2)
      nByte |= 4;

    if(nColor & 4)
      nByte |= 16;

    if(nColor & 8)
      nByte |= 1;
  }

  *pByteAddress = nByte;
}

void PutPixelMode0(unsigned char nX, unsigned char nY, unsigned char nColor)
{
  unsigned char nPixel = 0;
  unsigned int nAddress = 0xC000 + ((nY / 8) * 80) + ((nY % 8) * 2048) + (nX / 2);
  nPixel = nX % 2;

  SetMode0PixelColor((unsigned char *)nAddress, nColor, nPixel);
}


void main()
{
  unsigned char *pScreen = (unsigned char *)0xC000;
  int nX = 81;
  int nY = 101;
  char nYDir = 1;
  char nXDir = 1;

  //SCR_SET_MODE 0
  __asm
    ld a, #0
    call #0xBC0E
  __endasm;

  while(1)
  {
    nX += nXDir;
    nY += nYDir;

    if(nX < 0)
    {
      nX = 0;
      nXDir = 1;
    }

    if(nX >= 160)
    {
      nX = 159;
      nXDir = -1;
    }

    if(nY < 0)
    {
      nY = 0;
      nYDir = 1;
    }

    if(nY >= 200)
    {
      nY = 199;
      nYDir = -1;
    }

    PutPixelMode0(nX, nY, rand() % 16);
  }
}
////////////////////////////////////////////////////////////////////////

Compilamos y generamos dsk con los siguientes comandos (pixel04.bat):

sdcc -mz80 --code-loc 0x6038 --data-loc 0 --no-std-crt0 crt0_cpc.rel pixel04.c
hex2bin pixel04.ihx
CPCDiskXP -File pixel04.bin -AddAmsdosHeader 6000 -AddToNewDsk pixel04.dsk

Ejecutamos en el emulador y obtenemos esto:

fill

 

Podéis bajar un zip con todos ficheros (codigo fuente, bat's para compilar, binarios y dsk's) aquí: Pintando_pixeles_introduccion_a_la_memoria_de_video.zip

 

www.CPCMania.com 2012