en esta sección vamos a empezar a explorar los aspectos visuales de la computadora varvara ¡hablamos sobre los aspectos fundamentales del dispositivo de pantalla para que podamos empezar a dibujar en ella!
antes de pasar a dibujar en la pantalla, tenemos que hablar de bytes y cortos :)
## bytes y cortos
aunque uxn es un ordenador que trabaja de forma nativa con palabras de 8 bits (bytes), nos encontramos en varias ocasiones que la cantidad de datos que es posible almacenar en un byte no es suficiente.
cuando utilizamos 8 bits, podemos representar 256 valores diferentes (2 a la potencia de 8). en cualquier momento dado, un byte almacenará solo uno de esos posibles valores.
en la sección anterior, hablamos de un caso en el que esta cantidad no es suficiente en uxn: el número de bytes que alberga la memoria principal, 65536.
ese número corresponde a los valores que se pueden representar usando dos bytes, o 16 bits, o un "corto": 2 a la potencia de 16. esa cantidad también se conoce como 64KB, donde 1KB corresponde a 1024 o 2 a la potencia de 10.
además de expresar direcciones en la memoria principal, hoy veremos otro caso en el que 256 valores no siempre son suficientes: las coordenadas x e y de los píxeles de nuestra pantalla.
contando de derecha a izquierda, el 6º bit de un byte que codifica una instrucción para el ordenador uxn es una bandera binaria que indica si el modo corto está activado o no.
siempre que el modo corto esté activado, es decir, cuando ese bit sea 1 en lugar de 0, la cpu uxn realizará la instrucción dada por los 5 primeros bits (el opcode) pero utilizando pares de bytes en lugar de bytes individuales.
el byte que esté más adentro de la pila será el byte "alto" del corto y el byte que esté más cerca de la parte superior de la pila será el byte "bajo" del corto.
en primer lugar, recapitulemos. el siguiente código empujará el número 02 hacia abajo en la pila, luego empujará el número 30 (hexadecimal) hacia abajo en la pila y finalmente los sumará, dejando el número 32 en la pila:
en el día anterior mencionamos que la runa hexadecimal literal (#) es una abreviatura de la instrucción LIT. por lo tanto, podríamos haber escrito nuestro código de la siguiente manera:
podemos utilizar la runa hexadecimal literal (#) con un corto (cuatro nibbles) en lugar de un byte (dos nibbles) y funcionará como una abreviatura de LIT2:
¡así es! la pila tendrá los siguientes valores, porque estamos empujando 4 bytes hacia abajo en la pila, sumando (ADD) los dos más cercanos a la parte superior y empujando el resultado hacia abajo en la pila.
puede que no necesitemos pensar demasiado en las manipulaciones por byte de las operaciones aritméticas, porque normalmente podemos pensar que están haciendo la misma operación que antes, pero utilizando pares de bytes en lugar de bytes individuales. su orden no cambia realmente.
en cualquier caso, es útil tener en cuenta cómo funcionan para algunos comportamientos que podríamos necesitar más adelante :)
### DEO2, DEI, DEI2
hablemos ahora de la instrucción DEO ("device out" o salida de dispositivo) de la que hablamos el día anterior, ya que su modo corto implica algo especial.
la instrucción DEO necesita un valor (1 byte) para salir y una dirección entrada/salida (1 byte) en la pila, para poder sacar ese valor en esa dirección.
la instrucción DEI toma una dirección de entrada/salida (1 byte) de la pila y va a empujar hacia abajo en la pila el valor (1 byte) que corresponde a la lectura de esa entrada.
recuerda que las 256 direcciones de entrada/salida ya están cubiertas usando un solo byte, por lo que usar un corto para ellas sería redundante: el byte alto sería siempre 00.
considerando esto, los siguientes son los comportamientos que podemos esperar:
la instrucción DEO2 necesita un valor (1 corto) para salir y una dirección entrada/salida (1 byte) en la pila, para poder sacar ese valor a esa dirección. por lo tanto necesita un total de 3 bytes en la pila para operar.
por otro lado, la instrucción DEI2 necesita una dirección entrada/salida (1 byte) en la pila y empujará hacia abajo en la pila el valor (1 corto) que corresponde a esa entrada.
en la siguiente sección veremos algunos ejemplos en los que podremos utilizar estas instrucciones.
el puerto de 'escritura' del dispositivo de la consola que utilizamos la última vez tiene un tamaño de 1 byte, por lo que no podemos utilizar estas nuevas instrucciones de forma significativa con él.
el dispositivo del sistema es el dispositivo varvara con una dirección de 00. sus puertos de salida (que comienzan en la dirección 08) corresponden a tres cortos diferentes: uno llamado rojo (r), el otro verde (g) y el último azul (b).
como recapitulación: mencionamos que el dispositivo de pantalla solo puede mostrar cuatro colores diferentes en un momento dado y que estos colores están numerados del 0 al 3. fijamos estos colores usando los puertos correspondientes en el dispositivo del sistema.
en los programas uxntal para el ordenador varvara puedes encontrar las etiquetas correspondientes a los puertos de este dispositivo de la siguiente manera:
para hacer esto necesitamos establecer un par de coordenadas x,y donde queremos que se dibuje el píxel y necesitamos establecer el byte 'píxel' a un valor específico para realizar realmente el dibujo.
una pregunta para ti: si quisiéramos establecer las coordenadas como (x: 4, y: 8), ¿cuál de los cortos en el código anterior deberías cambiar por 0004?
aquí hay otra pregunta para ti: ¿cómo escribirías una macro ADD-X que te permita incrementar la coordenada x en una cantidad arbitraria que pongas en la pila?
```
%ADD-X { } ( incremento -- )
```
## instrucción INC
añadir 1 al valor de la parte superior de la pila es tan común que hay una instrucción para conseguirlo utilizando menos espacio, INC:
agradable, ¿no? ¡las operaciones ahora se ven más claras! y si quisiéramos tener esta línea disponible para usarla en otras posiciones, podríamos definir una macro para ella:
los mosaicos o "tiles" de 1bpp usan dos colores y se codifican usando 8 bytes; usar un bit por píxel significa que solo podemos codificar si ese píxel está usando un color o el otro.
los tiles de 2bpp utilizan cuatro colores y se codifican utilizando 16 bytes; el uso de dos bits por píxel significa que podemos codificar cuál de los cuatro colores disponibles tiene el píxel.
almacenaremos y accederemos a estos tiles desde la memoria principal.
cada byte corresponde a una fila del tile y cada bit de una fila corresponde al estado de un píxel de izquierda a derecha: puede estar "encendido" (1) o "apagado" (0).
vale la pena notar (o recordar) que los grupos de cuatro bits corresponden a un nibble y cada combinación posible en un nibble puede ser codificada como un dígito {hexadecimal}.
en uxntal, necesitamos etiquetar y escribir en la memoria principal los datos correspondientes al sprite. escribimos los bytes que van de arriba a abajo del sprite:
tengamos en cuenta que aquí no estamos utilizando la runa hexadecimal literal (#): queremos utilizar los bytes en bruto en la memoria y no necesitamos empujarlos hacia abajo en la pila.
para asegurarse de que estos bytes no son leídos como instrucciones por la cpu uxn, es una buena práctica precederlos con la instrucción BRK: esto interrumpirá la ejecución del programa antes de llegar aquí, dejando a uxn en un estado en el que está esperando entradas.
¡una nueva runa está aquí! la runa de dirección absoluta literal (;) nos permite empujar hacia abajo en la pila la dirección absoluta de la etiqueta dada en la memoria principal.
si se observa con atención, se puede ver algún patrón: cada bit del nibble alto del byte del sprite corresponde a un aspecto diferente de este comportamiento.
lo siguiente muestra el significado de cada uno de estos bits en el nibble alto, suponiendo que estamos contando los bits del byte de derecha a izquierda y de 0 a 7:
como por ejemplo, cuando el nibble alto del 'sprite' es 0, que en binario es 0000, significa que todas las banderas están apagadas: por eso dibuja un sprite de 1bpp (0) en el fondo (0), que no esta invertido ni verticalmente (0) ni horizontalmente (0).
por ejemplo, esto significa que si establecemos el nibble bajo de 'sprite' con el valor de 6, varvara dibujará el sprite utilizando el color 2 para los pixeles "encendidos" y el color 1 para los pixeles "apagados".
notemos que un 0 en el este nibble borrará el tile.
además, 5, 'a' y 'f' en el nibble bajo dibujarán los píxeles que están "encendidos" pero dejarán los que están "apagados" como están: esto le permitirá dibujar sobre algo que ha sido dibujado antes, sin borrarlo completamente.
=> ./img/screenshot_uxn-tiles.png captura de pantalla del resultado del programa, mostrando 16 cuadrados coloreados con diferentes combinaciones de contorno y relleno.
podemos pensar que, para asignar estos colores, codificaremos uno de los cuatro estados en cada uno de los píxeles del sprite.
cada uno de estos estados puede codificarse con una combinación de dos bits. a estos estados se les puede asignar diferentes combinaciones de los cuatro colores del sistema utilizando los valores apropiados en el byte 'sprite' de la pantalla.
un solo tile de 2bpp de 8x8 píxeles necesita 16 bytes para ser codificada. estos bytes se ordenan según un formato llamado chr.
para demostrar esta codificación, vamos a remezclar nuestro cuadrado de 8x8, asignando uno de los cuatro estados posibles (0, 1, 2, 3) a cada uno de los píxeles:
la codificación chr requiere una interesante manipulación de esos bits: podemos pensar que cada par de bits tiene un bit alto en la izquierda y un bit bajo en la derecha.
para escribir este sprite en la memoria, primero almacenamos el cuadrado correspondiente a los bits bajos y luego el cuadrado correspondiente a los bits altos. cada uno de ellos, de arriba a abajo:
notemos que estos ocho valores tienen todos un bit más a la izquierda en 1: este bit señala que vamos a dibujar un sprite de 2bpp. los otros tres bits del nibble se comportan como se ha descrito anteriormente en el caso de 1bpp.
### sprite de nibble bajo para 2bpp
el nibble bajo nos permitirá elegir entre muchas combinaciones de colores asignados a cada uno de los diferentes estados de los píxeles:
=> ./img/screenshot_uxn-tiles-2bpp.png captura de pantalla de la salida del programa, mostrando 16 cuadrados coloreados con diferentes combinaciones de contorno y relleno.
el siguiente código mostrará nuestro sprite en las 16 diferentes combinaciones de color. hay un poco de margen entre las baldosas para poder apreciarlas mejor:
## screen.tal y las combinaciones del byte del sprite
el ejemplo screen.tal en el repo de uxn consiste en una tabla que muestra todas las posibles (¡256!) combinaciones de nibbles altos y bajos en el byte del sprite.
=> ./img/screenshot_uxn-screen.png captura de pantalla del ejemplo screen.tal, que muestra un sprite coloreado y volteado de diferentes maneras.
=> https://git.sr.ht/~rabbits/uxn/tree/main/item/projects/examples/devices/screen.tal código de screen.tal
comparémoslo con todo lo que hemos dicho sobre el byte "sprite".
## diseñando sprites
nasu es una herramienta de 100R, escrita en uxntal, que facilita el diseño y la exportación de sprites 2bpp.
además de usarlo para dibujar con los colores 1, 2, 3 (y borrar para obtener el color 0), puedes usarlo para encontrar los colores de tu sistema, para ver cómo se verán tus sprites con los diferentes modos de color (también conocidos como modos de mezcla) y para ensamblar objetos hechos de múltiples sprites.
lo último que cubriremos hoy tiene que ver con las suposiciones que hace varvara sobre el tamaño de su pantalla y algunas estrategias de código que podemos usar para lidiar con ellas.
sin embargo, y a modo de ejemplo, el ordenador virtual también funciona en la nintendo ds, con una resolución de 256x192 píxeles (32x24 tiles), y en el teletipo, con una resolución de 128x64 píxeles (16x8 tiles)
como programadorxs, se espera que decidamos qué hacer con ellos: nuestros programas pueden adaptarse a los distintos tamaños de pantalla, pueden tener distintos modos según el tamaño de la pantalla, etc.
## cambiando el tamaño de la pantalla
adicionalmente, podemos cambiar el tamaño de la pantalla varvara escribiendo en los puertos .Pantalla/ancho y .Pantalla/alto.
por ejemplo, el siguiente código cambiaría la pantalla a una resolución de 640x480:
tengamos en cuenta que esto solo funcionaría para las instancias del emulador varvara en las que el tamaño de la pantalla puede cambiarse realmente, por ejemplo, porque la pantalla virtual es una ventana.
¡sería importante tener en cuenta los aspectos de la capacidad de respuesta que se discuten a continuación, para los casos en los que no podemos cambiar el tamaño de la pantalla!
### tamaño de pantalla por defecto
originalmente, la forma de cambiar el tamaño de la pantalla en uxnemu implicaba editar su código fuente.
si te has descargado el repositorio con el código fuente, verás que dentro del directorio src/ hay un uxnemu.c, con un par de líneas parecidas a las siguientes:
como recordarás de los puertos de dispositivos de pantalla mencionados anteriormente, la pantalla nos permite leer su anchura y altura como cortos.
si quisiéramos, por ejemplo, dibujar un píxel en el centro de la pantalla independientemente del tamaño de la misma, podemos traducir a uxntal una expresión como la siguiente:
```
x = anchopantalla/2
y = altopantalla/2
```
### división uxntal
para esto, vamos a introducir las instrucciones MUL y DIV: funcionan como ADD y SUB, pero para la multiplicación y la división:
* MUL: toma los dos elementos superiores de la pila, los multiplica y empuja el resultado ( a b -- a*b )
el nibble inferior del valor de desplazamiento indica a uxn cuántas posiciones hay que desplazar a la derecha y el nibble superior expresa cuántos bits hay que desplazar a la izquierda.
0a es 0000 1010 en binario y 05 es 0000 0101 en binario: los bits de 0a se desplazaron una posición a la derecha y se introdujo un cero como bit más a la izquierda.
14 en hexadecimal (20 en decimal), es 0001 0100 en binario: los bits de 0a fueron desplazados una posición a la izquierda y un cero fue introducido como el bit más a la derecha.
notemos que la macro MITAD2 que utiliza SFT2 necesitará un byte menos que la que utiliza DIV2. esto puede o no ser importante dependiendo de tus prioridades :)
como ejercicio para ti, te invito a que escribas el código que lograría algo o todo lo siguiente:
* dibuja un tile de 8x8 completamente centrado en la pantalla
* dibujar un tile de 8x8 en cada una de las esquinas de la pantalla
* dibujar un tile de 8x8 tocando cada uno de los bordes de la pantalla, centrados en cada uno de ellos
una vez que lo logres, te invito a que hagas lo mismo, pero utilizando una imagen compuesta por múltiples tiles (por ejemplo, tiles de 2x2, tiles de 1x2, etc).
# instrucciones del día 2
además de cubrir los fundamentos del dispositivo de pantalla hoy, discutimos estas nuevas instrucciones:
* DEI: lee un valor en la pila, desde la dirección del dispositivo dada en la pila ( dirección -- valor )
* INC: incrementa el valor en la parte superior de la pila ( a -- a+1 )
* BRK: rompe el flujo del programa, para cerrar subrutinas
* MUL: toma los dos primeros elementos de la pila, los multiplica y empuja el resultado ( a b -- a*b )
* SFT: toma un número a desplazar y un valor de desplazamiento y empuja el número desplazado las posiciones que indica el valor. el nibble inferior del valor de desplazamiento indica el desplazamiento a la derecha y el nibble superior, el desplazamiento a la izquierda ( número valordesplazamiento -- númerodesplazado )