Curso de programacion

Para consultas y debates sobre el contenido de este artículo, visita el Hilo oficial.


Este curso originalmente fue iniciado en entuwii.net hace ya bastante y utiliza una librería libogc antigua, por lo que algunos métodos podrían cambiar (por ejemplo, el acceso a libfat ha cambiado). Sin embargo, siempre que uséis el instalador que se recomienda y las librerías precompiladas, no deberíais tener problemas para poder usarlo.

"No importa si sabes mucho o sabes poco. Lo único que me importa es si eres capaz de asimilar lo suficiente para conseguir tu objetivo"
Hermes


Contenido

Introducción

  • Este es un curso de programación dedicado obviamente a la Wii. La idea es que una persona con conocimientos de C mas o menos vago, pueda programar para dicha consola, facilitando en lo posible la entrada a este mundillo, con un poco de voluntad por vuestra parte.
  • Si habéis entrado aquí pensando que se os va a enseñar a programar para Wii, os equivocas. Sois vosotros los que tenéis que hacer el esfuerzo de aprender y este tutorial solo puede allanaros el camino en lo posible. Puede que os rindáis a las primeras de cambio u os animes mucho al principio, pero luego dejéis el tema apartado debido a que la programación no es un camino de rosas o mejor dicho, es como una rosa con espinas, donde te vas pinchando todo el rato con el tallo hasta acceder a la flor.
  • Muchos programadores opinan que para hacer determinadas tareas, C no es el lenguaje mas conveniente y que no tienen pegas en aprender diferentes lenguajes de programación si hace falta, pero sin embargo ponen muchas pegas a aprender cosas de una determinada maquina que es muy diferente a lo que ellos suelen estar acostumbrados.
  • Desde un punto de vista práctico, todo lo que se os pueda ofrecer aquí seguramente no lo podáis utilizar profesionalmente de una forma directa, pero no os olvidéis que la programación es un proceso creativo y que requiere una buena capacidad de adaptación. Por tanto ¿qué mejor aprendizaje que tratar con maquinas como las consolas, que disponen de un hardware relativamente limitado y tan diferente con los ordenadores de hoy día?, ¿divertirse mientras aprendemos y hacemos algo diferente?. No tiene precio e incluso si eres un programador profesional, a lo mejor te sirve para recuperar aquello que os motivó a elegir esta profesión y que habéis perdido con la rutina.
  • La idea del tutorial es proporcionaros una base para que podáis entreteneros haciendo programas en Wii, pero el uso que queráis hacer de ello, depende de vosotros.

Comenzando por el Principio

  • Lo primero que debéis hacer es descargaros DevkitPro. Descargaros la version 1.4.7 del instalador:
http://sourceforge.net/project/showfiles.php?group_id=114505&package_id=160396
  • Si estáis utilizando otras plataformas, mirate esto:
http://wiki.devkitpro.org/index.php/Getting_Started
  • El instalador está desactualizado (y el curso tambien, por culpa de libfat) y os creará conflictos. Mira éste hilo que te ayudará a conseguir lo ficheros necesarios:
http://www.elotrolado.net/viewtopic.php?f=165&t=1257837&p=1716884329#p1716884329
  • Bajo Linux marcan os puede echar una mano y seguramente otros usuarios del foro.
  • Una vez instalado DevkitPro, tendréis un entorno basado en MinGW que os permitirá programar para diversas consolas y podréis usar el famoso "make" (luego se explica algo más sobre este tema). Además, se incluye una utilidad por si necesitáis un editor, llamada Programmer Notepad o algo similar, que tal vez os resulte útil.
  • Ahora necesitas los parches de Hermes:
http://mods.elotrolado.net/~hermes/curso_wii/devkitPro_Wii_by_Hermes.rar
  • Esto debeis copiarlo para sustituir las librerías y los ficheros de cabecera de libogc donde tengais instalado DevkitPro, sobrescribiendo todo lo que sobre escriba. Se tratan de las librerías ya compiladas con lo ultimo del git de hackmii y que incluye cambios que no encontrarás en ninguna parte.
  • Y por ultimo, algunos ejemplos hechos por Hermes:
http://mods.elotrolado.net/~hermes/curso_wii/hermes_examples.rar

Esto lo podéis instalar en devkitPro/examples/wii/hermes por ejemplo, para que tengáis una referencia.

  • Algunos entenderéis bastantes cosas viéndolas, porque no hay mejor maestro que disponer de un código que muestre como se hacen las cosas. Ahí se incluye los fuentes de la screenlib, que es la base gráfica que vamos a utilizar en este cursillo, ejemplos que muestran el uso de las fuentes de letra o el ultimo ejemplo que muestra como simular una batería usando el Nunchuk y el Wiimotes como baquetas.
  • Por cierto, en este ultimo ejemplo, quizá os preguntéis ¿cómo diablos se controlan los acelerómetros?, si sacas los valores a pantalla, puedes ver que si pones el mando en Vertical hacia arriba, te da unos valores distintos, que si los bajas y hay cosas que se sobreentienden como "gforce" (fuerza G). La experimentación es algo que vais a tener que usar por norma.
  • Por ejemplo, preparando este ejemplo, Hermes se dio cuenta de que hay un fallo en la librería que hace que no se puedan usar los valores gforce u orientación (orient) del Nunchuk, si no únicamente, los valores de aceleración en RAW.
  • Por ultimo y en el apartado de las descargas, quizá os interese contar con:
  • Dentro de los ejemplos que Hermes ha subido, ha incluido un par de utilidades: filetochar que es una potente utilidad que convertir archivos binarios a C usando varios tipos de variables y añadiendo alineación, etc. y Wiiload, la utilidad complemento del HBC para poder subir los ejecutables mediante WIFI.
  • También tenéis un directorio de recursos con sonidos y algún gráfico (mirar resources). Hay un ejecutable .bat que os muestra el uso de filetochar de modo práctico.

Cosas que debes saber del entorno de programación

Los Makefile

  • Si vienes de plataformas MSDOS/Windows, sabrás lo que es un .bat. Pues el Makefile es como uno de esos viejos archivos de procesamiento por lotes, pero mas a lo bestia, por así decirlo. Lo que aquí tienes que saber, es que dispones de una serie de plantillas de ejemplo y no importa que no conozcas todos los detalles, solo importa que sepas lo suficiente para poder aprovecharlo a tu favor.
  • Por defecto los Makefiles que vienen en DevkitPro, tratan de compilar todos los archivos C que estén dentro del subdirectorio "source" y el ejecutable toma el nombre del directorio en el que se encuentra el Makefile.
  • Esto es, si tenemos un directorio llamado examples y dentro de el, el Makefile y un subdirectorio source conteniendo distintos archivos C, si desde linea de comandos hicieramos "make" el resultado sería que se compilarían todos los archivos dentro de source y obtendriamos un archivo examples.dol otro llamado examples.elf (que no deberíamos incluir en la release) y nos crearía un subdirectorio build conteniendo los objetos de dicha compilación.
  • Estos objetos dentro de build se utilizan para compilar rápidamente, evitando recompilar los archivos que no hemos cambiado. Por lo tanto, si eliminamos el subdirectorio build podremos recompilar limpiamente todo de nuevo. A veces es conveniente hacerlo para evitar ciertos problemas curiosos.
  • Sin embargo, en los Makefile suele existir una orden especifica que nos permite limpiar de objetos de una forma rápida.

Si en linea de comandos ponemos "make clean" se ejecutará una sección de código (si existe) que tratará de borrar todos esos objetos.

  • Así pues:
make        -> se utiliza para compilar
make clean  -> se utiliza para borrar el resultado de las compilaciones


  • Una forma de acelerar esto desde Windows, sería crear un fichero bat como los que se usa en los ejemplos:

MakeIt.bat

make
pause

"pause" se utiliza para que la ventana no se cierre automáticamente y quede a la espera de que se pulse una tecla.


  • Por lo general, es mejor idea utilizar un IDE que sea capaz de hacer "make" y capturar el resultado a una ventana del programa. Esto suele tener la ventaja añadida de que si se producen muchos errores, bajo linea de comandos se suele perder parte del listado y que algunos IDE permiten ir directamente a la linea causante del problema pinchando en el error.

Interioridades de un Makefile

  • Nos adentramos en tierras Marcianas para aprender a controlar algunas cosillas de nuestros Makefiles.
  • Si miráis algunos de los ejemplos, quizá habréis notado que los Makefile compilan solo los ficheros que se necesitan y que incluso se permite compilar ficheros que están fuera del subdirectorio source tomándolos desde el directorio resources.

¿Cómo se consigue eso?. Pues, si miramos en las "tripas" del Makefile, vemos cosas como:

TARGET      :=   $(notdir $(CURDIR))
BUILD      :=   build
SOURCES      :=   source
RESOURCES   :=   ../../resources
DATA      :=   data  
INCLUDES   :=   include ../resources
  • Esto son como las variables de entorno que se pueden usar en Windows y en este caso están indicando:
  • TARGET: este es el nombre del fichero que queremos obtener, el codigo "raro" que le sigue, le está diciendo que pille el nombre de actual directorio pero sin ruta. Esa es la razón de que si se compila example1, obtengmos un example1.dol.
  • BUILD y SOURCES: son la razón de que se compile lo que hay dentro del subdirectorio source y que los objetos se almacenen en el subdirectorio build.
  • RESOURCES: es una variable que se crea para poder compilar los fuentes externos. El .. implica subir un directorio y dado que al coger los fuentes, el compilador estará en el directorio source ../../ nos sitúa fuera del directorio donde estamos compilando y ../../resources nos mete dentro del directorio resources de los ejemplos.
  • DATA: por lo general devkitPro utiliza este subdirectorio (que deberiais crear vosotros) para tratar con ciertos tipos de datos.
  • INCLUDE: esta variable os permite especificar varios destinos donde encontrar ficheros .h necesarios para vuestra aplicación. En este caso, señalan a un hipotético subdirectorio include y a ../resources, para que encuentre los ficheros .h de los datos que tenemos en resources.


  • Bien, bajando un poco vemos:
LIBS   := ../../tremor/libtremor.a -lfat -lasnd -lmodplay -lwiiuse -lscreen -lbte -lz -logc -lm


  • Eso haría referencia a la libreria libtremor.a que se encuentra en el directorio "tremor" que es "hermano" del directorio de la aplicación que estamos compilando.
  • Un detalle importante: El orden de colocación de las librerías, es importante. Hay librerías que usan funciones que proceden de otras librerías y que si no están correctamente ordenadas, se produce un error. La forma de ordenarla, es la siguiente: aquí el último elemento, es el primero.

Es decir, que para construir nuestro ejecutable, libm.a (-lm) se utiliza antes que libtremor.a.

  • Bueno, aclarado esta parte, bajemos un poco más:
#CFILES      :=   $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c)))
CFILES      :=   main.c $(RESOURCES)/econmy.c


  • Aquí hay varios detalles importantes. Dentro del Makefile, el símbolo # se utiliza para comentar, por lo que el resto de la línea es ignorado. Si echáis un ojo a esa linea "rara" se ve que es la responsable de que se compilaran todos los ficheros .c presentes en el subdirectorio source.
  • En lugar de eso, se ha reemplazado por los ficheros a compilar. El primero es main.c que está dentro de source y el segundo emplea el contenido de la variable RESOURCES para componer la ruta donde encontrar "econmy.c" que es el fichero MOD que usa el primero de los ejemplos que se han pasado.
  • Como veis, la forma de usar el contenido de la variable RESOURCES es $(RESOURCES).
  • ¿Y si se quiere añadir mas ficheros .c que es lo que se tiene que hacer?. Pues añadirlos a la línea, como estáis viendo.
  • Esta forma de trabajar es conveniente, por que por ejemplo, si se usara la forma "automatica" que utilizan los ejemplos de DevkitPro, añadiría todos los ficheros C presente en RESOURCES, cuando solo necesitamos uno.
  • Bueno, hemos visto cosas bastante importantes, pero ¿como puedo hacer que el Makefile interprete otras ordenes?. Por ejemplo, en algunos de los programas se utiliza el Makefile para ejecutar el programa haciendo "Make run".

Pues bien imaginate que al principio añades una variable como esta:

export WIILOAD =tcp:192.168.2.14

wiiload necesita una variable de entorno llamada WIILOAD con la IP de la Wii para poder pasar el ejecutable al HBC, al añadir la palabra "export" el make se ocupará de ello, haciendo visible esa variable para todos los programas.


  • Ahora nos vamos mas abajo en el Makefile y encontramos este código:
run:
wiiload $(OUTPUT).dol


  • Es decir: que el Makefile tiene la posibilidad de ejecutar "make run" pero necesitáis dos cosas: añadir la variable de entorno WIILOAD tal y como se explica (cambiando la IP por la de vuestra Wii obviamente) y por otro lado, teneis que añadir wiiload.exe en alguna ruta señalada por el PATH. Un buen sitio sería C:\devkitPro\msys\bin
  • También teniendo en cuenta que está en la carpeta "util" que es un directorio "hermano" a la aplicación, podríais utilizar esta fórmula:
run:
../util/wiiload $(OUTPUT).dol


  • En los ejemplos, se usa ejecutar.bat que directamente, crea la variable de entorno y pasa un parámetro para evitar que las aplicaciones, al no ver ningún parámetro, interpreten que no se puede volver al cargador y al hacer reset, se vuelva al menú del sistema.
  • Si vosotros quereis añadir otros usos. pues por ejemplo, podeís añadir:
convertir:
[tab]../util/filetochar <loquesea>


  • Nótese que [tab] representa al pulsar el tabulador para separar el comando. Luego la cosa sería hacer "make convertir" para que hicera esa tarea en concreto

Cosas que debes saber de la programación bajo Wii

  • "Programar en C, no es programar en BASIC". Suena de perogrullo, pero esto tiene sus implicaciones.

El Endian

  • Lo primero que debes saber sobre Wii, es que utiliza "Big Endian" para almacenar los datos. Los procesadores Intel usan justo lo contrario "Litte Endian".
  • Eso tiene que ver con la forma de almacenar el dato. Cuando un dato ocupa mas de un byte, existen dos formas de organizarlo de mayor a menor peso, que eso se conoce como Big Endian y de menor a mayor peso, que se conoce como Little Endian.
  • En plataformas Intel se utiliza la ordenación de menor a mayor (Little), asi por ejemplo, para almacenar en memoria el valor 256 en decimal, el primer byte sería 0 y el segundo 1 (1*256+0= 256). Sin embargo, en la Wii sería al reves, primero el 1 y despues el 0.

Con lo cual si por ejemplo, quieres importar un BMP, o un WAV, pues te toca cambiar el orden de los bytes de los datos.

  • Esa es una de las razones por las que es util exportar los datos a C en RAW, puesto que si importas una lista de numeros de 16 bits, no se ve afectada por la forma de almacenar el numero en memoria (es decir, 32767 es el mismo numero en ambos sistemas, pero cambia la forma en que ese numero se almacena en memoria o dentro de un fichero, por ejemplo).
  • Siempre que importes datos desde una plataforma Intel, tendrás que tener en cuenta el tipo de Endian que usan los datos.
  • Para convertir el Endian, puedes usar estas funciones:
unsigned short inline swap_16(unsigned short a) { // para numeros de 16 bits
     return( (a<<8) | (a>>8));
}

unsigned inline swap_32(unsigned a) { // para numeros de 32 bits
     return( (a<<24) | (a>>24) | ((a<<8) & 0xff0000) |  ((a>>8) & 0xff00));
}

Los tipos de datos

  • En Wii se usan los tipos de siempre, pero ademas añade otros tipos que son abreviaciones, de forma usual:

char

s8

entero de 8 bits con signo: rango -128 a 127

unsigned char

u8

entero de 8 bits sin signo: rango 0 a 255

short

s16

entero de 16 bits con signo: rango -32768 a 32767

unsigned short

u16

entero de 16 bits sin signo: rango 0 a 65535

int

s32

entero de 32 bits con signo: rango -0x80000000 a 0x7fffffff

unsigned int

u32

entero de 32 bits sin signo: rango 0 a 0xffffffff

long

s3

lo mismo que int

unsigned long

u32

lo mismo que unsigned int

long long

s64

entero de 64 bits con signo: rango -0x8000000000000000 a 0x7fffffffffffffff

unsigned long long

u64

entero de 64 bits sin signo: rango 0 a 0xffffffffffffffff

float

f32

Numero con coma flotante de 32 bits

double

f64

Numero con coma flotante de 64 bits


  • Tambien podeis ver formas como vu16 que se refieren a un tipo "volatile" y otros tipos predefinidos en devkitPro/libogc/include/gctypes.h.

La alineación de los datos

  • Esto es algo que tenéis que, necesariamente, tener en cuenta en maquinas como Wii y es que su procesador, además de manejar los tipos que he listado arriba, necesita almacenarlos en memoria alineados con respecto a su tamaño.

Es decir, si se esta manejando un int el procesador solo puede leer ints o almacenar ints en direcciones alineadas a 4 bytes: 0, 4, 8, 12, 16.....

  • En plataforma Intel estas acostumbrado a que un int, lo puedes guardar en direcciones desalineadas. y esto es algo que tienes que tener, muy pero que muy en cuenta.

Por ejemplo, si defines un struct:

struct {
     char lie;
     int repido;
} manue;


  • Esa estructura no será igual compilando en Windows que en Wii, ¿por qué?. Pues porque en Windows la alineación del dato no es problema, por lo que "lie" ocupará un byte y "repido" 4 bytes. Sin embargo, en Wii el compilador además de eso, añadirá 3 bytes de padding despues de "lie" para que asegurar que "repido" esté alineado a 4 bytes.
  • Esto es algo que tendréis que tener en cuenta y usar inteligentemente las estructuras y gestionar las variables de distinto tamaño adecuadamente: acostumbrarse a alinear los datos y paddearlos de forma adecuada, puede redundar en una mayor velocidad e incluso menor espacio.
  • ¿Que sucede si el procesador lee un dato de forma desalineada al tamaño del dato?. Pues que os volveréis locos para encontrar el bug seguramente.

La alineación de datos que usen DMA y el tratamiento de la memoria

  • El tema es que el procesador trabaja sobre memoria cacheada normalmente, mientras que las DMA (periféricos que hacen uso) no. Por lo tanto, debemos tener en cuenta dos cosas: que los periféricos suelen acceder a memoria usando una determinada alineación, al igual que pasa con los datos normales y que existe un mecanismo que se encarga de leer/escribir de la memoria para refrescar la caché.
  • La alineación utilizada es de 32 bytes. Para obtener memoria con esa alineación, podemos recurrir a usar memalign(32, x) en lugar de malloc(x) para asignar memoria, pero tambien podemos declarar una variable global de forma que tenga esa alineación:
u8 buffer[1024] __attribute__ ((aligned (32)));

u8 buffer[1024] __attribute__ ((aligned (32)))={0}; // si quereis fijar algun dato.


  • Las líneas de cache usan 32 bytes, por lo tanto, es conveniente alínear el dato a 32 y asi mismo asegurar el padding (bytes de relleno):
#define MAX_BYTES 17
u8 buffer[MAX_BYTES+32] __attribute__ ((aligned (32))); // forma rapida de aplicar un padding de seguridad


  • Por ejemplo, si quierees subir una textura o un sonido con ASNDLIB, es preciso que la memoria pasada esté alineada (fundamental) y con el correspondiente padding (conveniente). Si no se tienen en cuenta esas reglas, es posible que el flush de la cache, MACHAQUE datos de la pila o de otras variables.

El mecanismo de la cache

  • Existen una serie de funciones encargadas del refresco de la cache y de su escritura en memoria. Prestad atención porque no comprender el uso de estas funciones, suele acarrear muchos problemas. Dentro de screenlib o de la ASNDLIB, no precisáis utilizarlas, pues ya se usan de forma automática, pero os será útil para otras cosas:
  • DCFlushRange(void *startaddress,u32 len);: esta función recibe la dirección de memoria (debería estar alineada a 32) y la longitud en bytes (debería estar paddeada, pero ya lo hace ella) de la memoria que queremos "flushear". Es decir, lo que hace es escribir los datos desde la caché a la memoria, si en la cache hay cambios que no se han reflejado en la memoria en todo ese rango. Por eso es útil llamarla antes de hacer una DMA.
  • DCInvalidateRange(void *startaddress,u32 len);: esta función recibe la dirección de memoria (debería estar alineada a 32) y la longitud en bytes (debería estar paddeada, pero ya lo hace ella) de la memoria que queremos "invalidar" en la caché. Es decir, lo que hace es leer desde la memoria todo ese rango, perdiendo todos los datos almacenados en la caché. Resulta útil si un periférico nos suministra datos en memoria mediante DMA, para que la caché se refresque y al leer desde el procesador, obtengamos los datos correctos.

El truco del almendruco

  • Las excepciones son un fastidio, cada vez que el programa la casca, ¿de donde viene el problema?.
  • En la librerías se han pasado, tenéis una modificación particular de Hermes del código encargado de gestionar excepciones. En lugar de pegar el pantallazo y quedarse muerto en espera de que pulses un boton del PAD de Gamecube, aquí a los 8 segundos tratará de volver al cargador.
  • Pero también tiene otro truco, existe una variable de cadena que podeis cambiar de forma externa, para que os muestre un texto al ocurrir la excepcion:
extern char *debug_str;


  • Si en una funcion añades por ejemplo:
void funcion() {
     debug_str="funcion()";
     ......
     ......

     debug_str="Indefinido";
}


  • Si dentro de función se produce una excepción, se mostrará la cadena "funcion()" y sabréis que la excepción ha ocurrido en ese punto.
  • También existe un truco para conocer exactamente, en que función ha ocurrido la excepción. Con debug_str tal vez puedas saber en que función global haya ocurrido, pero de forma imprecisa. Mas bien os puede servir para acorralar el problema, si es reiterativo, pero existe otro truco: cuando sale la pantalla de excepción, aparece un listado de los registros y justo abajo, aparece una serie de direcciones de memoria que son la traza de donde ocurrió la excepción.

Pues bien, existe una utilidad que permite a partir de la información de debug que proporciona el elf, conocer a que función pertenece esa dirección de memoria:

set path=%path%;C:\devkitPro\devkitPPC\bin

powerpc-gekko-addr2line.exe -e Guitarfun.elf -f 0x805d5400

pause


  • Ahí podeis ver un uso específico.

La screenlib (prolegómenos)

  • Bueno, toca meterse en materia directa, así que primero nos alejaremos un poco para ver las cosas en perspectiva.
  • La librería gráfica, como todas, se basa en las GX (podéis descargar un interesante documento sobre el tema aquí). Nuestra consola utiliza un mecanismo para pasarle los datos al GX, llamado FIFO (First In, First Out) que tiene como una de sus cualidades que permite pasar datos desalineados con su tamaño (es decir, aquí es posible enviarle un int sin que esté alineado a 4 con la memoria empleada en el FIFO) y que en definitiva, marca la capacidad total de instrucciones/ datos/ vértices que podeis enviar al GX.
  • El tamaño empleado por defecto en screenlib es fijado desde FIFO_SIZE a 1MB exacto. Evidentemente, es posible usar otros tamaños mayores si es necesario, pero 1MB es un tamaño mas que suficiente para muchos usos e incluso se puede provocar un salto para que el GX utilice otra memoria fuera del FIFO para desarrollar sus tareas (mi programa Guitarfun utiliza esa técnica).
  • Pero bueno, no vamos a perdernos en divagaciones, basta con saber que en cada frame de vídeo, disponemos de un tope de 1MB para enviar datos al GX, pero que el GX va machacando instrucciones al mismo tiempo, por lo que va dejando espacio libre para nuevas instrucciones / datos (trabaja en anillo) y que posiblemente, es algo de lo que no te tengas que preocupar.
  • Por otro lado, conviene que sepáis que las GX utilizan por hardware dos matrices de transformación de polígonos: una de Proyección y otra Mundial. En screenlib la Mundial no se utiliza (se le pasa una matriz unitaria, que no provoca cambios) y la de Proyección es fijada para usar el método Ortográfico que es mas adecuado para usos 2D.
  • En dicha matriz se establece un rango X/Y ajustado a la resolución de pantalla y con un ligero reajuste en el caso de resolución NTSC. Para la Z, se utiliza un rango de 1000.
  • Externamente, podéis conocer el ancho y el alto de la resolución empleada gracias a las variables SCR_WIDTH y SCR_HEIGHT. Si la resolución es <=480 es obvio que se trata de una señal progresiva o NTSC que refrescan a 60Hz (aqui la resolución sería de 640x480). En modo PAL la resolución es de 640x528 y el refresco de 50Hz.
  • El uso de Z, como ya se ha comentado utiliza un rango de 1000. De forma interna, el rango sería de 0 a -999, siendo ese -999 el punto mas alejado. En la librería puesto que tratamos con 2D, se utiliza el término de layer (capa) puesto que en 2D se suelen utilizar varias capas de gráficos y para hacer mas amigable el tema, se niega el valor para hacer que el rango sea de 0 a 999 y evitar el uso de números negativos. Asi pues, podemos hacer uso de 1000 capas y la capa 999 se podría considerar como el fondo de pantalla, mientras que layer= 0 es poner los graficos en primer plano. Al utilizar una proyección Ortográfica, no se produce un escalado con la distancia para disminuir el tamaño, tenedlo en cuenta.

Transparencias y Translucidez

  • Screenlib es inicializada para utilizar colores con Alfa por debajo de 8 (en un rango de 0 a 255) como colores transparentes, tanto en texturas como en color directo a los polígonos. Esto significa que no se debe hacer uso de los modos de color que no empleen Alpha (modo 16bits RGB565) dado que dichos colores no serán dibujados en pantalla.
  • Gracias a esto podemos dibujar Sprites sin que su color de fondo, se superponga al fondo (evidentemente, siempre que el color usado de fondo, sea considerado transparente). Si utilizáis el programa Spritegen y queréis exportar los sprites a color directo de 16 bits, en "Export to C", desmarcar "Use Palette" y en "Direct Color" selecciona 15 bits. Si queréis convertir directamente esos sprites en tiles, podéis fijar la alineación a 32 bytes ahí. De esta forma exportareis los sprites como RGB5A1 y si el fondo está utilizando el color 0 (que por defecto tiene Alpha=0 en Spritegen) pues ya lo tenéis listo para funcionar. Solo os faltará crear la textura a partir de los datos del sprite para tenerlo funcionando.
  • En el ejemplo 3, podéis ver que se ha hecho una exportación de datos desde Spritegen a 8 bits con paleta ("Export to c", marcar "Alpha Enable" y "Use Palette". "Direct Color/ Palette Color" a 15 bits). En dicho ejemplo, se asigna memoria alineada para alojar los tiles de los sprites y se conserva el mapa de color original. Eso os permite no tener que alinear los datos exportados, pero la ventaja principal, es que sabiendo que el indice de color transparente es 0, podéis utilizar el mapa original para detectar colisiones de forma mas precisa: un color en el mapa original distinto a 0 representa algo "solido".
  • Como podéis observar, si exporto color directo o con paleta a 15 bits (+ 1 de Alpha! y Wii solo admite paletas de 16 bits...) en realidad, ese Alpha solo va a servir para determinar si un color es transparente o no (si se dibuja o no se dibuja, vamos): pero Wii puede usar un rango mas amplio que 16 bits (y realmente, Wii trata de forma especial los colores con el bit Alpha=0, puesto que usa un modo de color denominado RGB5A3 y por eso a RGB5A1 es conveniente que el valor global sea 0 para Alpha=0 para evitar cosas raras).
  • En realidad, vosotros podéis especificar también un color en formato RGBA8 (8 bits por componente) para dibujar superficies, o cajas desde screenlib y eso os permite hacer una especie de filtro para los usos con textura o sin textura. De esta forma, entramos en el terreno de la translucidez.
  • La translucidez es un fenómeno que consiste en poder ver objetos que están detrás de otros objetos a través de ellos. Así por ejemplo, un cristal es un material translúcido, pues permite ver lo que hay detrás aunque le cause una interferencia (por ejemplo, si ves a través de un cristal verdoso, los objetos de detrás tomarán dicha tonalidad). La diferencia real con la transparencia es que esta última, realmente no dibuja los píxeles por lo que no ocupan espacio en la ordenación Z (el layer). Así pues, para dibujar objetos translúcidos de forma correcta, debemos dibujar PRIMERO los objetos que queden detrás, pues si el objeto translúcido usa layer 0, la ordenación Z no permitirá que se dibuje nada detrás (por ejemplo, en layer 100). Y las partes transparentes, en realidad son "agujeros" en el objeto
  • La forma de interpretar esto, mas o menos es asi:
En un rango de 0 a 255: Alpha <8 -> Transparente, Alpha>=8 ->Nivel de translucidez, Alpha==255 -> Color Sólido
  • Si hablamos de color RGB5A1 seria:
Alpha==0 -> Transparente, Alpha==1 -> Color Solido

La screenlib

Inicializar

  • Para inicializar la librería, se usa esta función:
InitScreen();


  • Esto es todo lo que tienes que hacer en tu main() para que el video esté operativo usando la configuración que tengas en tu Wii.
  • Al hacerlo, SCR_WIDTH y SCR_HEIGHT te informará de la resolución que estas empleando y se explicó en el anterior capítulo, puedes determinar si el refresco es a 60Hz porque el alto sería de 480 píxeles.
  • Sin embargo, conviene recordar que la líbrería trabajará pensando en 4:3 y no en 16:9. En ese caso, te tocará a ti determinar el formato de la pantalla, ya que la resolución de salida seguiría siendo la misma y aplicar los factores de escala correspondiente.

Si quieres conocer si estas usando una configuración de 16:9 puedes inicializar así:

int is_16_9=0;

CONF_Init();

is_16_9=CONF_GetAspectRatio();

InitScreen();


  • Y a partir de ahí, hacer los ajustes pertinentes.

Cambiar el Viewport (nivel: usuario avanzado)

  • Para usos especiales, puedes cambiar el Viewport. Por ejemplo, podrías virtualizar la pantalla para tratar los 16:9 de forma proporcionada directamente, cambiando el ancho, pero no se aconseja porque por ejemplo, si ajustases para trabajar a 854x480, realmente, estarías condensando 1,333 píxeles en 1. Para ciertos gráficos quizá no sea mucho problema, pero para letras o pequeños detalles, es mejor desproporcionar un poco a que te falte resolución.
  • En el ejemplo 3 se muestra como utilizar un Viewport de 480x360 para "inventarme" una resolución de la que carece Wii de esta forma:
ChangeProjection(0, -10*(SCR_HEIGHT<=480), 479, 360+10*(SCR_HEIGHT<=480));

como se puede apreciar, se aplica una corrección para video NTSC (<=480) y espero que entiendas que esos valores son experimentales: en una TV se puede ver perfecto, pero quizás en otra no.


  • Si por alguna razón quisieras volver a los valores originales, es tan facil como usar ésta función:
RestoreProjection();


  • En el ejemplo 3 se puede observar como se cambia el valor de una variable: xclip. Esta variable se utiliza para limitar por la derecha el texto visible (función s_printf, que veremos mas adelante)

Finalizando el frame

  • La función Screen_flip(); se encarga de esperar a que se dibujen todos los gráficos, sincronizar con el video vertical, intercambiar el buffer de la pantalla y borrar el nuevo, asi como el buffer Z.
  • Es la última función que debes llamar para completar el frame de video y mientras no sea llamada, no podrás apreciar los cambios efectuados.

Dibujando caracteres

  • La librería dispone internamente, de dos sets de caracteres ANSI, los cuales son facilmente seleccionables con la función SelectFontTexture:
SelectFontTexture(0); // -> selecciona el font por defecto
SelectFontTexture(1); // -> selecciona el font extra/externo



(Nivel Avanzado)

  • La librería permite reemplazar el segundo juego de caracteres por otro que haya capturado el usuario utilizando la utilidad ttf2raw (ver utilidades recomendadas al principio). Para ello se dispone de la función:
UploadFontTextureExt(unsigned char *punt);


  • Donde punt sería el lugar de la memoria donde tenemos alojado el nuevo font.

Se puede restablecer el font original usando UploadFontTextureExt(INTERNAL_EXTFONT); Si quieres cambiar el font por defecto, pues te tocaría modificar screenlib a tu gusto

(Fin de Nivel avanzado)



  • Las variables PX y PY: son las encargadas de proporcionar las coordenadas de inicio del texto (en píxeles) que imprimiremos con la funcion s_printf
  • color: se encarga de suministrar el color de las letras en formato RGBA8 (ten cuidado de no utilizar esta variable para otra cosa).
  • bkcolor: proporciona el color del fondo tambien en formato RGBA8. Por defecto es 0.
  • autocenter: está pensada para centrar en horizontal el texto si la pones a 1. Por defecto está a 0 (desactivada).
  • sizeletter: Ccontrola los diferentes tamaños de letra, pero para vosotros sería mejor usar la función letter_size().
  • letter_size(tamx, tamy);: ajusta el tamaño de las letras a tamx, tamy. El font original es a 16x32 pero podéis ajustarlo a diferentes tamaños así.
  • xclip (avanzado): esta variable controla el límite derecho de las letras para su visualización. Si un caracter supera ese limite, el resto del texto será ignorado, pero nota que esto no equivale a hacer un recorte exacto en ese punto y si un carácter excede por ancho ese límite, será totalmente visible si es posible
  • s_printf( char *format, ...);: funciona de forma similar a la función estándar printf, que como es de sobra conocida, pues no necesita mucha explicación. La única diferencia, es que si un texto se sale de la pantalla, no se procederá a un cambio de línea automático, cosa lógica si pensais que aquí el tamaño de letra es variable. Por cierto, las letras desde aquí, se escriben en layer 0.
  • print_str(u32 x, u32 y, u32 z, u32 color, char *string,u16 tamx, u16 tamy);: si lo que queréis es imprimir una simple cadena, sin formato, etc, podéis utilizar ésta función que como podéis observar, necesita de coordenadas x,y,z (la z aqui si se aleja en sentido negativo), color (para el fondo usa bkcolor), la cadena de texto y el ancho y alto de los caracteres.

La screenlib - Texturas

  • Las texturas son un asunto que merecen un capítulo aparte dentro de ésta documentación. La screenlib ofrece soporte para texturas con paleta (soportando las 16 paletas que provee las GX) o sin paleta y también para modos de color que no son directamente soportados por las GX, pero con una pequeña adaptación son posibles. Antes de nada, tienes que saber que Wii aloja sus texturas en la memoria "normal", es decir, en otras máquinas tienes que subirlas a la VRAM, mientras que aquí bastará con que esa memoria esté alineada con 32 (recuerda memalign, que lo mencionamos al principio) y que hayamos flusheado los datos, puesto que la textura se trabaja fuera de caché por las GX. Como la screenlib no pretende ser un sustituto de las GX, si no un complemento de ellas, para trabajar, necesitarás usar los mismo objetos para manejar texturas:
GXTexObj text; // -> esta es la forma de definir un objeto de textura

GXTlutObj palette; //-> esta es la forma de definir un objeto de paleta


  • Las texturas se organizan internamente en tiles, generalmente de 4x4, pero hay otras organizaciones de datos. Por precaución, sería conveniente que tanto el ancho, como el alto de la textura, fuera divisible entre 8 para estar seguro de que nuestra textura no parte tiles (que yo no recuerdo el límite exacto y en todo caso, es un margen mucho mas generoso que usar texturas en base 2 que precisan otros sistemas)
  • Las dimensiones máximas a usar en textura son de 1024x1024, lo que implica un tamaño empleado de 1,2 o 4 MB (asi que si te parece pequeño, ya ves que no lo es tanto).

Creando Paletas

  • Internamente, screenlib utiliza una función llamada ctlut() para crear una paleta partiendo de los datos proporcionados de forma externa.
  • El formato de las paletas es de 16 bits y para trabajar con la screenlib deberias usar modos TLUT_RGB5A3, TLUT_RGB5A1, TLUT_SRGB5A1:

#define TLUT_LITTLE_ENDIAN 128

Cambia el orden de los bytes del color (util para importaciones)

#define TLUT_RGB5A3 2

Para RGB5A3 (si el bit Alpha de mas peso, es 1, el color es RGB5A1, en otro caso RGB4A3), Azul menor peso

#define TLUT_RGB5A1 2

Pseudo RGB5A1 (es RGB5A3, pero asumiendo que si Alpha==0,color==0) ,Azul menor peso

#define TLUT_RGB565 3

Este no lo deberias usar, al carecer de Alpha

#define TLUT_SRGB565 4

Este tampoco lo deberias usar

#define TLUT_SRGB5A1 5

Para RGB5A1 con R y B intercambiado, Rojo menor peso (al estilo de SpriteGen)


  • La memoria de la paleta debe estar alineada a 32 bytes y ser de tipo u16 (unsigned short). Si quieres asignar un color de paleta manualmente, partiendo desde un color RGBA8 (con Rojo como menor peso) puedes usar Color5551(x) para obtener un color en formato TLUT_SRGB5A1. Ejemplo:
Color5551(0xffff0000); //-> convierte color Solido y Azul intenso al formato SRGB5A1


  • La funcion para crear la paleta, es:
void CreatePalette(GXTlutObj *palette, int format, int pal_index, u16 *mem, int entries);


  • Donde:
  • palette: puntero a objeto de paleta.
  • format: formato de color (TLUT_RGB5A3, TLUT_RGB5A1, TLUT_SRGB5A1).
  • pal_index índice de paleta a usar dentro de las GX (0 a 15). Tambien fija una variable para relacionar paleta y textura.
  • mem memoria conteniendo las entradas de paleta (alineada a 32, recuerda). Recuerda que los datos originales serán modificados.
  • entries número de entradas de la paleta (hasta 256, aunque físicamente, hay paletas que soportan 4096 colores).

La función SetPalette()

  • SetPalette(int pal_index); se usa para fijar una variable interna con el índice de paleta (de 0 a 15) a utilizar cuando creemos una textura que haga uso de paletas. Lo normal es que primero creemos paleta, lo cual nos ajusta esa variable de forma automática y luego procediéramos a crear nuestras texturas, pero ¿qué sucede si quiero hacer una serie de cambios en la textura en cada frame? Pues que nos tocará decirle de alguna forma que paleta debe usar al crearse la textura y para eso tenemos ésta función.

Creando Texturas

  • Para eso tenemos ésta función:
void CreateTexture(GXTexObj *texture, int format, void *mem, int width, int height, int repeat);
  • texture: puntero objeto de textura.
  • format: formato de color: TILE_CI4, TILE_CI8, TILE_RGB5A1, TILE_RGB5A3, TILE_SRGB5A1, TILE_RGBA8, TILE_SRGBA8.
  • mem: memoria que contiene los datos de textura y donde se formarán los tiles (es decir, que se modifican los datos). Alineada a 32 bytes.
  • width: ancho de la textura.
  • height: alto de la textura.
  • repeat: flag que ajusta si la textura se repite en forma de mosaico si se exceden las coordenadas de textura (por defecto, usa 0).

Descripción del formato de color:

#define TILE_LITTLE_ENDIAN 128

Cambia el orden de los bytes de color (util para las importaciones)

#define TILE_CI4 0

Para CI4 (color indirecto de 4 bits, o sea, con paleta)

#define TILE_CI8 1

For CI8 (color indirecto de 8 bits, o sea, con paleta)

#define TILE_RGB5A3 2

Para RGB5A3 (si el bit Alpha de mas peso, es 1, el color es RGB5A1, en otro caso RGB4A3), Azul menor peso

#define TILE_RGB5A1 2

Pseudo RGB5A1 (es RGB5A3, pero asumiendo que si Alpha==0,color==0) ,Azul menor peso

#define TILE_RGB565 3

No deberias usarlo

#define TILE_SRGB565 4

No deberias usarlo

#define TILE_SRGB5A1 5

Para RGB5A1 con R y B intercambiados. Rojo menor peso

#define TILE_RGBA8 6

Para RGBA8. Azul menor peso

#define TILE_SRGBA8 7

Para RGBA8 con R y B intercambiados. Rojo menor peso



(Usuario avanzado)

  • Por defecto las texturas usan filtrado bilineal. Es posible cambiar a GX_NEAR (para que se vean cuadraditos si ampliamos la textura) si utilizamos antes de crear la textura la siguiente función:
UseTexLOD(GX_NEAR , GX_NEAR);


  • Y para restablecer:
UseTexLOD(GX_LINEAR , GX_LINEAR);


  • Puesto que es un uso relativamente raro, no he querido añadir un nuevo parámetro a la función de crear textura que en el fondo, va a ser un lastre.

(Fin usuario avanzado)


Seleccionando textura

  • Para seleccionar la textura en las funciones de dibujo, se utiliza ésta función:
SetTexture(GXTexObj *texture);


  • Como se puede apreciar, se le pasa un puntero al objeto de textura. Si se le pasa NULL (Valor 0), esas funciones no usarán textura y simplemente, aplicarán el color que le pasemos.
  • Existen una serie de texturas predefinidas para poder usarlas como patrones de relleno:
#define STRIPED_PATTERN &tex_pattern[0]
#define CROSS_PATTERN &tex_pattern[1]
#define SQUARE_PATTERN &tex_pattern[2]
#define MOSAIC_PATTERN &tex_pattern[3]
#define NULL_PATTERN	NULL


  • Así con: SetTexture(MOSAIC_PATTERN); Se utilizaría ese patrón de relleno con las funciones de dibujo.

La screenlib - Funciones de Dibujo

  • La screenlib se diseñó principalmente, como un conjunto de herramientas que permitiesen diseñar menús de una forma mas o menos sencilla, por lo que no es extraño que muchas de sus funciones se limiten a dibujar distintos tipos de cajas o los bordes de dichas cajas, con el fin de embellecerlas y por otro lado, marcar la diferencia entre una caja seleccionada y otra que no lo es.
  • Dentro de la librería, no hay ninguna regla que impida hacer un uso 3D o 2D diferente y no hace falta trastear mucho en los fuentes de la librería para ver de que forma utiliza las GX y de que forma pueden convivir. Desde un punto de vista 2D (o mas bien pseudo 2D) se ha tratado de facilitar ciertos usos especiales, pero además estas funciones, suponen un claro ejemplo de como se utilizan las cosas desde las GX y os da pistas sobre que es lo que deberíais alterar si por ejemplo, queremos una proyección que supere el rango de 0 a 999 en Z o usar gráficos en perspectiva y meternos de lleno en las 3D.
  • Las funciones para dibujar cajas rellenas (con un color o con una textura), son estas:
// dibuja una caja cuadrada rellena
DrawFillBox(int x,int y, int width, int height, int layer, u32 color);

// dibuja una caja con las esquinas redondeadas, rellena
DrawRoundFillBox(int x,int y, int width, int height, int layer, u32 color); 

// dibuja una caja con los lados izq-der redondeados, rellena
DrawRoundFillBox2(int x,int y, int width, int height, int layer, u32 color);
  • x, y: posición de la caja en píxeles.
  • width, height: dimensiones de la caja en píxeles.
  • layer: capa de 0 a 999, siendo 999 la capa mas alejada y 0 la mas cercana (equivale a -Z).
  • color: color en formato RGBA8 (8 bits por componente y Rojo byte de menor peso).


  • Las funciones para dibujar el borde de las cajas (con un color o con una textura), son estas:
// dibuja el borde de una caja cuadrada
DrawBox(int x,int y, int width, int height, int layer, int thickness, u32 color); 

// dibuja el borde de una caja con esquinas redondeadas
DrawRoundBox(int x,int y, int width, int height, int layer, int thickness, u32 color); 

// dibuja el borde de una caja con los lados izq-der redondeados
DrawRoundBox2(int x,int y, int width, int height, int layer, int thickness, u32 color);
  • x, y: posición de la caja en píxeles.
  • width, height: dimensiones de la caja en píxeles.
  • layer: capa de 0 a 999, siendo 999 la capa mas alejada y 0 la mas cercana (equivale a -Z).
  • thickness: grosor de la linea de 1 a lo que admita.
  • color: color en formato RGBA8 (8 bits por componente y Rojo byte de menor peso).


  • Como se puede apreciar, son funciones muy sencillitas.

Dibujando superficies curvas

  • El soporte de la librería se limita a dibujar Elipses y gráficos tipo tarta (DrawSlice).Con la función Elipse se pueden obtener círculos y circunferenvias. Y es más, teniendo en cuenta que las proporciones de la pantalla pueden hacer que un "píxel" en el Viewport no sea completamente cuadrado, resulta conveniente hacer un ajuste de los radios para que nuestros círculos o circunferencias, no se vean alargadas o achatadas, según el caso.
  • Así pues para dibujar elipses tenéis:
// dibuja una elipse con su superficie rellena
DrawFillEllipse(int x,int y, int rx, int ry, int layer, u32 color); 

// dibuja el borde de la elipse
DrawEllipse(int x,int y, int rx, int ry, int layer, int thickness, u32 color);
  • x, y: posición en píxeles.
  • rx, ry: radio en píxeles para las dimensiones X e Y.
  • layer: capa de 0 a 999, siendo 999 la capa mas alejada y 0 la mas cercana (equivale a -Z).
  • thickness: grosor de la linea de 1 a lo que admita.
  • color: color en formato RGBA8 (8 bits por componente y Rojo byte de menor peso).
  • Y para dibujar arcos de una elipse (gráficos tipo tarta o el uso que le querais dar. El objeto se dibujara entre el arco que formen degrees_start y degrees_end):
// dibuja el arco de una elipse con su superficie rellena
DrawFillSlice(int x,int y, int rx, int ry, int layer, int degrees_start, int degrees_end, u32 color); 

// dibuja el borde del arco de la elipse
DrawSlice(int x,int y, int rx, int ry, int layer, int thickness, int degrees_start, int degrees_end, u32 color);
  • x, y: posición en píxeles.
  • rx, ry: radio en píxeles para las dimensiones X e Y.
  • layer: capa de 0 a 999, siendo 999 la capa mas alejada y 0 la mas cercana (equivale a -Z).
  • thickness: grosor de la linea de 1 a lo que admita.
  • degrees_start: angulo en grados (de 0 a 360) de inicio.
  • degrees_end: angulo en grados (de 0 a 360) final.
  • color: color en formato RGBA8 (8 bits por componente y Rojo byte de menor peso).

La función de linea

  • La función DrawLine() se incluyó pensando más en ciertos usos especiales que pensando en ella como la función que "siempre deberias utilizar". La función está preparada para dibujar lineas de mas de un píxel de grosor y en realidad utiliza dos triángulos de forma interna. Pero tiene un pequeño defectillo, para dibujar lineas horizontales o verticales, tiene apoyo para poder ligar con otras lineas y formar cajas, pero si esa línea es mas gruesa de un píxel y digamos que forma "rampas", la unión no será buena. Notese también que le influye si tenemos una textura o no en activo (con SetTexture()).
  • Si queréis dibujar líneas u otros polígonos de forma rápida, deberíais utilizar GX_Begin, el cual se describe luego, mas tarde.
// dibuja lineas con o sin textura y de diferentes grosores
DrawLine(int x,int y, int x2, int y2, int layer, int thickness, u32 color);
  • x, y: posición de inicio en píxeles.
  • x2, y2: posición final en píxeles.
  • layer: capa de 0 a 999, siendo 999 la capa mas alejada y 0 la mas cercana (equivale a -Z).
  • thickness: grosor de la linea de 1 a lo que admita.
  • color: color en formato RGBA8 (8 bits por componente y Rojo byte de menor peso).

Dibujando Superficies

  • Lo que hace de ésta librería que resulte útil para hacer juegos 2D de forma directa, es sin duda, la función DrawSurface().
  • Esta función permite dibujar un objeto de textura directamente, proporcionándole unas dimensiones que no tienen porque coincidir con las de la textura utilizada. Es decir, si la textura tiene un ancho de 32x32, no hay nada que impida dibujarla como superficie mayor o menor, lo cual lleva implícito un aumento o reducción de tamaño (de ahí que nos resulte útil utilizar GX_LINEAR en el LOD de la textura al crearla).
  • Así pues DrawSurface(), se puede utilizar tanto para dibujar una ventana en la cual utilizaremos una textura como superficie donde dibujar píxeles (en el ejemplo 1 os incluyo una muestra de como utilizo una textura para dibujar) o para dibujar sprites completamente acelerados por la consola.
// dibuja un objeto de textura como superficie
DrawSurface(GXTexObj *surface,int x,int y, int width, int height, int layer, u32 color);
  • surface: puntero a objeto de textura que describe la superficie (creada con CreateTexture()). NO usar NULL aquí.
  • x, y: posición de la superficie en píxeles.
  • width, height: dimensiones visuales de la superficie en píxeles.
  • layer: capa de 0 a 999, siendo 999 la capa mas alejada y 0 la mas cercana (equivale a -Z).
  • color: color en formato RGBA8 (8 bits por componente y Rojo byte de menor peso).

Conectando con las GX: GX_Begin() [usuario avanzado]

  • Para dibujar polígonos, resulta mas adecuado usar las GX directamente, puesto que así ganamos en velocidad y realmente es muy sencillo con ayuda de unas pocas funciones de apoyo que se han añadido. Un ejemplo de uso, lo podéis ver en el ejemplo 2, donde se utiliza GX_Begin para dibujar las lineas remarcadas que forman las "montañas" (aquí se hace un uso especial, partiendo de una coordenada de origen y usando esto para dibujar 4 lineas en una configuración 2x2 para el remarcado y asi asegurar un grosor de 2 píxeles en la linea).
  • Lo primero que tenemos que hacer, es preparar lo necesario para dibujar polígonos con o sin texturas:
ConfigureForColor();

esta es la función necesaria para ajustar el uso de vértices para admitir coordenadas de posición y color:


ConfigureForTexture(int scale);

esta es la función necesaria para ajustar el uso de vértices con coordenadas de posición, color y coordenadas de textura

  • scale: este parámetro se utiliza para fijar el factor de escala para la textura, debido a que las coordenadas las especificamos como enteros de 16 bits. Para entender esto, tienes que saber que para el GX, los limites de una textura se especifican como números flotantes siendo 0.0f uno de los extremos y 1.0f el otro, aunque también es posible utilizar magnitudes negativas (en caso de crear textura con el parámetro repeat=1).
  • Asi pues, éste factor de scale es un desplazamiento cuyo valor real es 1<<scale para representar 1.0f.
  • Es decir, que si fijamos scale=10, como 1<<10==1024, un valor de 0 representaría a 0.0f mientras que 1024 representaría a 1.0f.
  • Pongamos un ejemplo: imaginaos que creamos una textura con unas dimensiones de 64x32 texels (el texel es como el píxel, pero referido a textura). A ojos del GX, las dimensiones irian desde 0.0f a 1.0f, de forma independiente a ese tamaño.

Pues bien, si especificamos un factor de scale= 10, eso significa que si damos una coordenada de textura tx=1024( en el ancho), tx sería equivalente a 1.0f pero a efectos prácticos de textura señalaría a 64, teniendo en cuenta el ancho de la textura en texels.

Sin embargo si especificamos ty=1024 (el alto), a efectos prácticos señalaría a 32 que es el alto de la textura y no a 64.

  • Puede parecer lioso pero la idea es que las coordenadas de textura no dependan exactamente del tamaño de la textura, aunque obviamente, hay una relación directa. Vosotros lo que tenéis que ver es que 1<<scale representa al límite vertical y horizontal de la textura (y el otro es 0, obviamente) y no caer en el error de pensar que como el ancho de la textura es 64 y el alto 32, hay que pasarle esos valores numéricos o 1.0f y 0.5f, porque la escala hace referencia al valor de 1.0f, ya que independientemente del tamaño real de la textura, 1.0f es el limite derecho, pero también el inferior.
  • Asi pues, si la textura es un rectángulo, las coordenadas quedarían así, para un factor escala de 10:
izq-arr = (0.0f,0.0f)= (0,0) der-arr=(1.0f,0.0f)=(1024,0)
izq-abj =(0,1.0f)=(0,1024) der-abj=(1.0f,1.0f)=(1024,1024)

Exactamente igual que si fuera una textura cuadrada.

  • Puesto que 1.0f en realidad, rebasa el límite de la textura, lo mejor es quedarse un poco corto, de forma que sería mejor usar 1023 para ese borde.

Espero que con este ejemplo, hayáis captado mejor lo de las coordenadas de textura y sus escalas (se puede especificar floats como coordenada de textura directamente, pero por cuestiones de velocidad y espacio, es mejor utilizar enteros de 16 bits y mas para un uso 2D).

GX_Begin()

  • GX_Begin es la funcion que nos permite dibujar polígonos desde las GX. Para utilizarla desde screenlib debéis hacerlo así:
GX_Begin(primitive,  GX_VTXFMT0, vertices);
  • primitive: puede ser GX_POINTS, GX_LINES, GX_LINESTRIP, GX_TRIANGLES, GX_TRIANGLESTRIP, GX_TRIANGLEFAN, GX_QUADS. Si quieres saber más, en la documentación sobre las GX, se describe este tema, pero se hace un resumen aquí:

GX_POINTS

Dibuja puntos, necesita un vértice por cada punto a dibujar

GX_LINES

Dibuja lineas, necesita dos vértices por cada linea a dibujar

GX_LINESTRIP

Dibuja lineas, necesita dos vértices para la primera linea, pero el resto de lineas utilizan el vértice de la última linea dibujada, mas uno nuevo. Es decir, que para dibujar dos lineas se necesitarían 3 vértices y las dos lineas compartirían uno de ellos

GX_TRIANGLES

Dibuja triángulos, necesita tres vértices por triángulo

GX_TRIANGLESTRIP

Dibuja triángulos enlazados de forma que el primero requiere tres vértices, pero los siguientes comparten dos vértices con el último triángulo dibujado mas uno nuevo. Resulta adecuado para crear objetos en forma de tiras o anillos (objetos torno, por ejemplo).Por ejemplo, dos trianglestrips requieren 4 vértices, mientras que tres trianglestrips requieren 5 vertices

GX_TRIANGLEFAN

Dibuja triángulos utilizando un vértice como referencia central (el primer vértice especificado), el ultimo del anterior triángulo mas uno nuevo. Obviamente, el primer triángulo precisará de trés vertices. Por ejemplo, dos trianglefan requieren 4 vertices, mientras que tres trianglefan requieren 5 vertices. Resulta adecuado para hacer graficos de tarta, cerrar figuras, para modelar una sombrilla...

GX_QUADS

Los quads son figuras que requieren 4 vértices. Los vértices requiere usar un orden horario o antihorario (es decir izq-arr, der-arr, der-abj, izq-abj por ejemplo) y se debe evitar vértices cruzados o que puedan generar dos planos. Son útiles para dibujar cuadrados/rectangulos y desde un punto de vista 3D, paredes, techos, suelos...

  • GX_VTXFMT0: esto especifica que vamos a usar el formato de vértices 0, que es el que programamos con ConfigureForColor() y ConfigureForTexture().
  • vertices: numero de vértices a usar, hasta un límite de 65535 (quizá el 0 represente 65536, pero vamos, que puedes usar un buen puñado de una sola llamada). Y por supuesto, siempre que no superes el tamaño del FIFO. Recuerda que debes darle lós vertices exactos (o se producirá un cuelgue de la muerte)

Pasando vértices a GX_Begin()

  • En screen.h tienes dos funciones definidas para pasar los parámetros necesarios a los vértices:
// para añadir vértices con posición y color
AddColorVertex(s16 x, s16 y, s16 z, unsigned color); 

// para añadir vértices con posición, color y coordenada de textura
AddTextureVertex(s16 x, s16 y, s16 z, unsigned color, s16 tx, s16 ty);


  • Procurad no equivocaros con la función utilizada si no quieres que se produzca un cuelgue.
  • Para acabar, utiliza GX_End(); que se utiliza de forma decorativa.

Ejemplos de uso de GX_Begin()

ConfigureForColor(); // usa color
GX_Begin(GX_LINE,  GX_VTXFMT0, 2); // dibuja una linea (por tanto pasamos 2 vertices)
AddColorVertex(0,0,0,0xffffffff); // primer vertice
AddColorVertex(SCR_WIDTH, SCR_HEIGHT,0,0xffffffff); //segundo vertice
GX_End();


SetTexture(&texture); // fija una textura 
ConfigureForTexture(10); // usa textura y fija escala a 10 ( 1<<10== 1024) (notese que usará siempre la textura especificada por SetTexture())
GX_Begin(GX_QUADS,  GX_VTXFMT0, 4); // dibuja un QUAD (por tanto pasamos 4 vertices)
AddTextureVertex(0,0,0,0xffffffff, 0, 0); // primer vertice
AddTextureVertex(SCR_WIDTH, 0,0,0xffffffff, 1023, 0); // segundo vertice
AddTextureVertex(SCR_WIDTH, SCR_HEIGHT,0,0xffffffff, 1023,1023); // tercer vertice
AddTextureVertex(0, SCR_HEIGHT,0,0xffffffff,0, 1023); // cuarto vertice
GX_End();


El ultimo recurso: Restaurar el entorno 2D

  • Es posible que necesitéis saltar a usar 3D o incluso que hagáis un uso mas avanzado de las 2D, cambiando la matriz mundial, etc. En ese caso, tendréis que restaurar el entorno para volver a trabajar con screenlib y para eso, se ha añadido la funcion:
Recover2DContext();


  • Esta función recupera en lo posible el contexto 2D de screenlib, pero si hacéis usos avanzados (como cambiar la matriz de transformación de texturas y cosas asi), es posible que eso lo tengáis que restablecer a mano. Obviamente, será vuestra responsabilidad cambiar a los distintos contextos y esta función no puede saber todo lo que habéis cambiado y restaurarlo vosotros.
  • En todo caso, dispones del código fuente de la librería y algunas cosas se han organizado de forma que te sea sencillo ver que es lo que precisa screenlib para trabajar.
  • Y esto es todo lo referente a la librería gráfica de Hermes.

La ASNDLIB (Introducción)

  • La ASNDLIB es la sucesora de una librería anterior, llamada SNDLIB que fue una de las primeras aportaciones de Hermes al homebrew de Wii. Se diferencian en que la primera utiliza el DSP, lo cual permite acelerar las voces, mientras que la segunda usa el procesador de la Wii para calcular las distintas voces y además soporta unas voces especiales pensadas para generar las voces de instrumentos, con efectos como ataque, sostenimiento y caída.
  • El DSP es un procesador relativamente poco potente para la tarea encomendada. Mezclar 16 voces en estéreo y con un rango de frecuencias que va desde un 1Hz a 144000Hz es algo que requiere bastantes multiplicaciones y operaciones para adaptar y mezclar las voces, no es tarea fácil para un procesador de 16 bits y que está pensado para trabajar de otra manera (baste decir que Hermes fué el primero en darle utilidad, basándose en una información con 3 años de antigüedad y resolviendo varias piezas del puzzle hasta hace bien poco desconocidas).
  • El caso es que por razones de capacidad de proceso, era imposible mantener 16 voces y añadir la posibilidad de que el DSP trabajara con los efectos de ataque, sostenimiento y caída. Se podrían añadir desde fuera y probablemente, utilizando pocos recursos y algunos trucos, pero como aparte de Hermes, creo que nadie las ha usado, pues decidió suprimirlas.
  • Por cuestiones de proceso, no es conveniente usar muchas voces por encima de 48000Hz. La Wii trabaja a 48000Hz a 16bits por sample y en estéreo y la ASNDLIB permite reproducir voces Mono o Stereo, con samples de 8 o 16 Bits (con signo) y en el rango mencionado de 1 Hz (MIN_PITCH) a 144000Hz (MAX_PITCH) lo cual significa que la librería es capaz de adaptar la frecuencia de las voces para que acaben sonando a la frecuencia especificada en las voces en esos 48000Hz. Sin embargo, es obvio que usar voces por encima de esa frecuencia, implica una pérdida de calidad de sonido y además, multiplicar la carga de trabajo al DSP. Cuanto menor sea la frecuencia a la que se reproduce una voz, mas desahogado irá el DSP, aunque es cierto que el código se ha diseñado para que el DSP cumpla bien con su trabajo incluso soportando una carga de trabajo enorme.
  • El DSP se coordina con la interrupción de audio que es llamada cada 21,333 ms aproximadamente, una vez que se interrumpe la DMA de Audio y entonces se inicia un proceso de llamadas en cascada a la DMA del DSP de hasta 17 pasos, para procesar las voces.

Por último señalar en éste apartado, que las voces de sonido a usar, deben estar alineadas a 32 bytes y usar preferentemente un padding de 32 bytes. De nuevo estamos en el mismo caso que con las texturas.

La ASNDLIB - Funciones

  • Como en muchas otras librerías, lo primero que hay que hacer es iniciarla.

Iniciando

ASND_Init();


  • Al iniciar la ASNDLIB, las voces están pausadas mediante una pausa general.

Pausa general

ASND_Pause(s32 paused);
  • paused: pon 1 para pausar y 0 para continuar.


  • Así pues, antes de trabajar con voces, necesitarás hacer:
ASND_Init();
ASND_Pause(0);


  • Tened en cuenta que en el momento de quitar la pausa y aunque no se oiga sonido, la librería estará trabajando y el contador interno general de samples, estará contando.

Finalizando

  • Para finalizar la librería, podéis usar:
ASND_End();


  • Si os habéis fijado en mis ejemplos, veréis que ésta función es llamada en fun_exit() que es fijada con atexit() para que al salir del programa, salir de algunas librerías. Esta practica es conveniente debido a que pueden haber algunas DMAs en marcha y así evitamos un posible cuelgue.
  • Bien, una vez tenemos la librería iniciada y funcionando (quitando la pausa), ahora podemos decidir que voces usar desde la 0 a la 15 (ya se ha comentado que dispones de 16).

La voz 0, se suele reservar para usar con reproductores de música.

Buscando voces libres

// devuelve la primera voz libre empezando por la voz 1
s32 ASND_GetFirstUnusedVoice();


  • La ultima voz que nos devolverá como libre es la 0. Esta función se puede utilizar para conocer la primera voz sin uso, pero no hay ninguna regla escrita que diga que no se pueden interrumpir voces en ejecución en cualquier momento.

Reproduciendo una voz

  • Para ejecutar una voz se utiliza estas funciones:
s32 ASND_SetVoice(s32 voice, s32 format, s32 pitch,s32 delay, void *snd, s32 size_snd, s32 volume_l, s32 volume_r, ASNDVoiceCallback callback);

s32 ASND_SetInfiniteVoice(s32 voice, s32 format, s32 pitch,s32 delay, void *snd, s32 size_snd, s32 volume_l, s32 volume_r);


  • La primera es el uso normal, mientras que la segunda se usa para ejecutar una voz de forma continua e ininterrumpida y que queda por tanto, sometida al control del usuario (cambio de pitch, volumen o momento de pararla).
  • Las funciones devuelven SND_OK o SND_INVALID en caso de error.
  • Parámetros:
  • voice: una desde 0 a 15 (MAX_SND_VOICES-1).
  • format: formato PCM desde VOICE_MONO_8BIT a VOICE_STEREO_16BIT (ver asndlib.h).
  • pitch: frecuencia del pitch (en Hz).
  • delay: tiempo de retardo en milisegundos (ms). Tiempo de espera antes de que se inicie la voz.
  • snd: buffer con los samples a reproducir (alineado y con padding a 32 bytes).
  • size_snd: tamaño del buffer en bytes.
  • volume_l: volumen del canal izquierdo en el rango de 0 a 255.
  • volume_r: volumen del canal derecho en el rango de 0 a 255.
  • callback: puede ser NULL, lo que indicaría que la voz se usa únicamente para reproducir los samples especificados y después quedará libre o la dirección de una función callback para trabajar a doble buffer, que será invocada desde la interrupción (cuidado con el gasto de CPU aquí) cada vez que se detecte que al menos uno de los buffers de voz queda libre y así asignar un nuevo buffer con ayuda de la función ASND_AddVoice(). En el modo doble buffer (usando callback) la voz no se libera por si misma y devuelve SND_WAITING en caso de quedarse sin samples para seguir reproduciendo. Cómo se puede observar, el modo con callback resulta mas adecuado para reproductores de audio, mientras que sin callback es mas adecuado para efectos de sonido sueltos.

La función callback de ASND_SetVoice

  • La estructura es la siguiente:
void my_callback(s32 voice){
}


  • Donde voice recibe el numero de voz por si ésta callback es compartida con otras voces. En el callback estáis en tiempo de interrupción y cuanto menos tiempo gastéis aquí, mejor. Si necesitáis hacer algo pesado, despertad un hilo desde aquí, pero no sobrecarguéis ésta función porque comprometeréis otras cosas.

La función ASND_AddVoice()

s32 ASND_AddVoice(s32 voice, void *snd, s32 size_snd);


  • Las funciones devuelven SND_OK, SND_BUSY si no es posible añadir la voz o SND_INVALID en caso de error.
  • Parámetros:
  • voice: una desde 0 a 15 (MAX_SND_VOICES-1).
  • snd: buffer con los samples a añadir (alineado y con padding a 32 bytes).
  • size_snd: tamaño del buffer en bytes.
  • Es decir: se puede observar que la función solo devolverá SND_OK en caso de tratarse de una voz correcta y encontrar uno de los bufferes libres para ser asignado. En ese caso, la voz será añadida y nosotros podremos comparar y proceder a intercambiar buffers en caso de usar la voz en un reproductor.

Lógica adicional para sistema de doble buffer

  • En un sistema de doble buffer lo usual es que un buffer se esté reproduciendo, mientras otro se esté rellenado descomprimiendo formato Ogg, por ejemplo. Sin embargo la librería ASNDLIB tiene un funcionamiento asíncrono y necesita de mecanismos de protección para evitar por ejemplo, que tengamos dos buffers añadidos en la cola de reproducción (uno en ejecución y otro en espera) y que machaquemos esos datos al descomprimir nuevos samples y volcarlos al buffer de ejecución.
  • Así pues, se necesita algo que le diga al lector-descompresor de samples que se esté quieto-parado esperando que el buffer actual de lectura quede libre al cesar la reproducción.

Para ello contamos con dos funciones que tienen resultado similar:

s32 ASND_TestPointer(s32 voice, void *pointer);
  • En esta función, debemos pasarle el inicio del buffer y ese puntero será comprobado y en caso de coincidir con el buffer en reproducción o el buffer en espera, se nos devolverá el valor 1, lo que indicaría que deberíamos esperar antes de "rellenar" el buffer con nuevos datos y 0 en caso de no estar en uso ese puntero.
  • Por tanto antes de refrescar los datos del buffer, deberíamos comprobar que ASND_TestPointer(voice, puntero)==0 y proceder a refrescar si se cumple.


s32 ASND_TestVoiceBufferReady(s32 voice);
  • Esta función devuelve 1 si uno de los dos buffers está libre. Puesto que nosotros controlamos la secuencia de intercambio con ASND_AddVoice(), es obvio que si por ejemplo, acabamos de añadir un nuevo buffer y nos queda uno libre, es que podemos leer/descomprimir sin problemas nuevos datos.
  • Así pues, deberíamos comprobar que ASND_TestVoiceBufferReady(voice)==1 antes de refrescar los datos del buffer.

Funciones de tiempo global

  • Existe un timer de sonido global asignado a todas las voces y cuya utilidad puede ser coordinar el manejo de las voces en el tiempo. Para leerlo/controlarlo, dispones de las siguientes funciones:
// devuelve el tiempo transcurrido en milisegundos (ms)
u32 ASND_GetTime();

// devuelve el numero de samples reproducidos hasta el momento en relación a 48000Hz (48000 samples = 1 segundo)
u32 ASND_GetSampleCounter();

// devuelve el numero de samples reproducidos a 48000Hz por cada interrupción
u32 ASND_GetSamplesPerTick(); 

// fija el tiempo del contador global por el especificado en milisegundos (ms)
void ASND_SetTime(u32 time);


Funciones de estado y tiempo de las voces

  • Para conocer el estado de una voz, se utiliza ésta función:
s32 ASND_StatusVoice(s32 voice);


  • Que nos devuelve:
  • SND_INVALID: voz no valida (fuera del rango 0 a 15).
  • SND_UNUSED: voz sin uso.
  • SND_WORKING: voz en uso y reproduciendo.
  • SND_WAITING: voz en uso y esperando nuevos samples (doble buffer).


  • Las voces poseen una medida de tiempo particular que es útil para determinados usos:
// devuelve el tiempo transcurrido en milisegundos, sin tener en cuenta el tiempo de delay especificado en la voz
u32 ASND_GetTimerVoice(s32 voice);

// devuelve el numero de samples reproducidos a 48000Hz (es independiente de la frecuencia de la voz)
u32 ASND_GetTickCounterVoice(s32 voice);


  • En este caso, no se puede fijar el tiempo de inicio de la voz, ya que de ello se encarga ASND_SetVoice().

La ASNDLIB - Funciones de control

Control global

  • Como funciones de control global contáis con:
// pausa general (1=pausa, 0= continua)
void ASND_Pause(s32 paused);

// retorna uno si la está activa la pausa general o 0 en otro caso
s32 ASND_Is_Paused();

// añade una callback de propósito general que será llamada cada 21,333 ms (p.e: void my_globalcallback() {})
void ASND_SetCallback(void (*callback)());

Control de Voces

// para la voz (si está en ejecución, obviamente)
s32 ASND_StopVoice(s32 voice);

// si pause=1, pausa la voz, si pause=0 continua. Está pausa se anula con ASND_SetVoice() también
s32 ASND_PauseVoice(s32 voice, s32 pause);

s32 ASND_StatusVoice(s32 voice);


  • Que nos devuelve:
  • SND_INVALID: voz no valida (fuera del rango 0 a 15).
  • SND_UNUSED: voz sin uso.
  • SND_WORKING: voz en uso y reproduciendo.
  • SND_WAITING: voz en uso y esperando nuevos samples (doble buffer).


s32 ASND_TestPointer(s32 voice, void *pointer);
  • En esta función, debemos pasarle el inicio del buffer y ese puntero será comprobado y en caso de coincidir con el buffer en reproducción o el buffer en espera, se nos devolverá el valor 1, lo que indicaría que deberíamos esperar antes de "rellenar" el buffer con nuevos datos y 0 en caso de no estar en uso ese puntero.
  • Por tanto antes de refrescar los datos del buffer, deberíamos comprobar que ASND_TestPointer(voice, puntero)==0 y proceder a refrescar si se cumple.


s32 ASND_TestVoiceBufferReady(s32 voice);
  • Esta función devuelve 1 si uno de los dos buffers está libre. Puesto que nosotros controlamos la secuencia de intercambio con ASND_AddVoice(), es obvio que si por ejemplo, acabamos de añadir un nuevo buffer y nos queda uno libre, es que podemos leer/descomprimir sin problemas nuevos datos.

Así pues, deberíamos comprobar que ASND_TestVoiceBufferReady(voice)==1 antes de refrescar los datos del buffer.

Control dinámico de Voces

  • Para voces de tipo infinito o de doble buffer, puede ser útil el uso de estas funciones:
// cambio dinámico de la frecuencia de reproducción (pitch) (rango de 1Hz a 144Khz)
s32 ASND_ChangePitchVoice(s32 voice, s32 pitch);

// cambio dinámico del volumen (recuerda que el rango es de 0 a 255)
s32 ASND_ChangeVolumeVoice(s32 voice, s32 volume_l, s32 volume_r);

A golpe de nota

  • Hay una función que nos permite utilizar voces para hacerlas sonar como si fueran notas de un instrumento, proporcionando la frecuencia a partir de los datos de nota y frecuencia base del sample:
int ANote2Freq(int note, int freq_base,int note_base);
  • note: nota codificada a reproducir. Por ejemplo: NOTE( NOTE_C,4) para nota C y octava 4.
  • freq_base: frecuencia base de los samples. Por ejemplo 8000Hz.
  • note_base: nota codificada de los samples. Por ejemplo: NOTE( NOTE_LA,3) para nota LA y octava 3 (LA 3).
  • retorna: frecuencia (en Hz).


  • Para entender esto, en asndlib.h tenéis un definido NOTE (note, octave) que compone la nota codificada a usar en esta función, asi como la enumeración de las distintas notas (NOTE_DO o NOTE_C).
  • La frecuencia base es la frecuencia a la que capturas un sonido y la nota base, es la nota que capturaste. Por ejemplo, imaginaos que capturáis la nota Do3 de un órgano a 11025 Hz, pues obviamente, para que ASNDLIB pueda simular un Do4, tiene que saber que relación hay entre la frecuencia base y las distintas notas .

Rendimiento del DSP

  • Se dispone de una curiosa función para medir el uso del DSP:
u32 ASND_GetDSP_PercentUse();


  • Devuelve el tanto por ciento usado en un determinado instante, por si queréis saber el tiempo que gasta el DSP en procesar las voces (si supera el 100%, es que al DSP le falta tiempo para procesar las voces que le estáis mandando). Digamos que es una función de debug .
  • Podéis consultar asndlib.h para mas detalles.

Lectura de Mandos: Wiimote

La librería del Wiimote

  • libwiiuse.a (-lwiiuse) contiene las funciones necesarias para leer el Wiimote y sus expansiones, tales como el Nunchuk, Mando Clasico o la Guitarra de GHIII.
  • Como podeis observar en los ejemplos de Hermes, necesitáis incluir:
#include <wiiuse/wpad.h>

que es donde se definen las funciones necesarias, aunque en wiiuse.h podeis observar tambien, que se definen ciertas estructuras necesarias.

Iniciar el Wiimote

WPAD_Init();

es todo lo necesario para iniciar la librería, pero también necesitareis configurar el modo o cosas como el tiempo que los mandos aguardarán antes de apagarse por falta de uso:

// tiempo en segundos que aguardará el mando antes de apagarse por falta de uso (p. e: 5*60 para 5 minutos)
void WPAD_SetIdleTimeout(u32 seconds);


  • El modo de operación lo podéis ajustar con:
// ajusta el modo de operación
s32 WPAD_SetDataFormat(s32 chan, s32 fmt);
  • chan: un número de 0 a 3 para cada uno de los wiimotes o mejor WPAD_CHAN_ALL para ajustar el modo de todos los mandos.
  • fmt: WPAD_FMT_BTNS (por defecto) para leer solo los botones y palancas, WPAD_FMT_BTNS_ACC para tener en cuenta además los acelerómetros y WPAD_FMT_BTNS_ACC_IR para utilizar los acelerómetros y la cámara infrarroja. Como es evidente, el gasto de baterías dependerá de la activación de estos modos, así que es conveniente activar sólo lo necesario.


// ajusta la resolución de video para cada canal, cuando se usa la cámara IR
s32 WPAD_SetVRes(s32 chan,u32 xres,u32 yres);
  • chan: un número de 0 a 3 para cada uno de los wiimotes o mejor WPAD_CHAN_ALL para ajustar el modo de todos los mandos.
  • xres: resolución horizontal, por ejemplo SCR_WIDTH si usáis screenlib.
  • yres: resolución vertical, por ejemplo SCR_HEIGHT si usas screenlib.
  • Obviamente, ésta funcion solo es necesaria si activas el modo WPAD_FMT_BTNS_ACC_IR.

Leyendo los wiimotes

s32 WPAD_ScanPads();


  • Esta función se encarga de leer todos los mandos mediante el método polling y debe ser llamada en cada frame. Devuelve el número de PAD en activo (salvo que se produzca un error, por lo que no es del todo fiable).

Leyendo los datos de un wiimote en particular

s32 WPAD_Probe(s32 chan,u32 *type);
  • Esta es la función clave que deberíais llamar para conocer si un mando está conectado o no y que tipo de datos devuelve. Retorno <0 si se produce un error, otro caso, mando conectado y datos servidos.
  • chan: un número de 0 a 3 para cada uno de los wiimotes.
  • type: puntero a una variable u32 donde podremos leer el tipo de expansión: WPAD_EXP_NONE, si no hay ninguna, WPAD_EXP_NUNCHUK, si el Nunchuk está conectado, WPAD_EXP_CLASSIC y WPAD_EXP_GUITARHERO3.


  • Bien, una vez que verificamos que WPAD_Probe() devuelve un valor >=0 y que conocemos el tipo, podemos pasar a leer todos los datos:
u32 WPAD_ButtonsHeld(int chan);
  • Devuelve todos los botones pulsados tantos del Wiimote, como de las expansiones. En wpad.h podéis encontrar las definiciones que son del tipo WPAD_BUTTON_X para los botones del wiimote, WPAD_NUNCHUK_BUTTON_X para los del Nunchuk y WPAD_CLASSIC_BUTTON_X o WPAD_GUITAR_HERO_3_BUTTON_X para el resto de las expansiones.


  • Hacer and (& ) de esta forma:
u32 buttons=WPAD_ButtonsHeld(0);
if(buttons & WPAD_BUTTON_A) { /* A pulsado */}


WPADData *WPAD_Data(int chan);
  • Esta función nos devuelve un puntero a la estructura WPADData interna (mirad en wpad.h). De esta forma, se pueden acceder a todos los datos procedentes de las expansiones, sticks analógicos o la cámara IR.


  • Existen también funciones separadas para leer cada elemento:
void WPAD_IR(int chan, struct ir_t *ir);
void WPAD_Orientation(int chan, struct orient_t *orient);
void WPAD_GForce(int chan, struct gforce_t *gforce);
void WPAD_Accel(int chan, struct vec3w_t *accel);
void WPAD_Expansion(int chan, struct expansion_t *exp);


  • Las diferentes estructuras podéis verlas definidas en wiiuse.h
  • Por ejemplo con:
WPADData * wmote_datas;
wmote_datas=WPAD_Data(0);


  • Leeríamos los datos del Wiimote 0 (el del led 1 encendido) y si tuviéramos el Nunchuk conectado, podríamos leer los datos del stick con:
// datos izquierda-derecha del stick analogico del nunchuk devuelta como u8 (128== centro aprox)
wmote_datas->exp.nunchuk.js.pos.x; 

// datos abajo-arriba del stick analogico del nunchuk devuelta como u8 (128== centro aprox)
wmote_datas->exp.nunchuk.js.pos.y;

// datos correspondientes a la posición central del stick
wmote_datas->exp.nunchuk.js.center.x;
wmote_datas->exp.nunchuk.js.center.y;

(hay mas datos como min y max, pero eso ya deberíais mirarlo vosotros).

  • O por ejemplo, en caso de activar la cámara IR y ajustar la resolución con WPAD_SetVRes() podríais leer la posición desde aquí:
// posición X de pantalla devuelta como float
wmote_datas->ir.x

// posición Y de pantalla devuelta como float
wmote_datas->ir.y 

// devuelve 1 si los datos de x e y son válidos o 0 si no lo son (apuntando fuera de la barra sensora)
wmote_datas->valid


  • Y de esta forma usar el wiimote como un puntero.
  • Se recomiendo hacer vuestras propias pruebas y buscar en wpad.h y sobre todo, en wiiuse.h la definición de las diferentes estructuras y visualizar los datos en pantalla con s_printf(), para comprender mejor que es lo que devuelven.
  • Eso si, parece ser que hay algunos fallos en relación al nunchuk y por ejemplo, el uso de los acelerómetros solo funciona en raw (por eso en el ejemplo 4 uso el Nunchuk desde exp.nunchuk.accel.x y no de forma similar a como uso el Wiimote).

El Rumble

  • El Rumble se puede activar o desactivar con:
s32 WPAD_Rumble(s32 chan, int status);


  • Simplemente haciendo status=1, pondremos el motor en marcha y lo pararemos con status=0. Si queremos simular distintas vibraciones, nos tocará ir activando y desactivando el rumble con una determinada cadencia.

Apagando mandos de forma manual

  • De eso se ocupa la función:
s32 WPAD_Disconnect(s32 chan);

Saliendo de la librería

  • Usa WPAD_Shutdown(); antes de salir del programa.

Lectura de Mandos: Gamecube Pad

Lectura de Mandos: Gamecube Pad

  • Las funciones se definen en ogc/pad.h y todo se asimila de forma interna a libogc y no necesitáis incluir librería aparte.

Iniciando la librería del Gamecube PAD

PAD_Init();

Leyendo los PADs de Gamecube

u32 PAD_ScanPads();
  • Con ésta función se leen todos los PADs conectados y devuelve una máscara que indica que mandos están conectados o no. En la versión original de pad.c, hay un fallo (que no se sabe ai se produce solo en Wii o no) que hace que si conectas en el puerto 2, sin haber un mando en el puerto 1, a veces falle la conexión con ese mando 2 (Hermes se dio cuenta desarrollando el emulador Wiiengine), por lo que vosotros disponéis de una versión modificada que evita este fallo.
  • Recordad la lectura usa método polling y que por tanto, tendrías que llamar ésta función en cada frame, antes de leer los datos del pad.
  • Ejemplo de uso:
u32 npads;
....
npads=PAD_ScanPads();

if(npads & 1) {} // mando 1 conectado
if(npads & 2) {} // mando 2 conectado
if(npads & 4) {} // mando 3 conectado
if(npads & 8) {} // mando 4 conectado

Leyendo los datos de un PAD de Gamecube en particular

  • Una vez que conocemos los mandos que tenemos conectados, podemos usar estas funciones (mirar pad.h).


u16 PAD_ButtonsHeld(int pad);
  • Devuelve todos los botones pulsados. El rango va desde PAD_BUTTON_LEFT a PAD_BUTTON_START, como podeis observar en pad.h.


s8 PAD_StickX(int pad);
s8 PAD_StickY(int pad);
  • Para conocer los valores del stick (observar que utiliza un rango de -128 a 127, siendo 0 la posición central).


s8 PAD_SubStickX(int pad);
s8 PAD_SubStickY(int pad);
  • Para conocer los valores del substick o como lo llamo yo, el pezón amarillo (observar que utiliza un rango de -128 a 127, siendo 0 la posición central).


u8 PAD_TriggerL(int pad);
u8 PAD_TriggerR(int pad);
  • Para conocer los valores analógicos de los gatillos, si se están pulsando. Dan un rango de 0 a 255.


  • Recordad que los valores analógicos son de referencia y que en el caso de los sticks, siempre es conveniente usar una zona muerta (un rango de valores que se ignora, por ejemplo, entre -32 y +32, para evitar que la posición centrada del stick sea un cachondeo y haya cierta deriva).
  • Y con esto ya sabéis todo lo imprescindible para usar éste tipo de pads

Punto de inflexión

  • Bien, hasta ahora, tenéis acceso al vídeo, al audio y a los distintos mandos. También se han proporcionado las librerías compiladas, herramientas para crear sprites e integrar las cosas en el propio programa, de forma que Hermes ha cumplido su promesa de acercaros la programación de Wii a los que añoráis los viejos ordenadores como el Spectrum y su facilidad para realizar juegos.
  • En el ejemplo 3 podéis observar la integración con los sprites, incluso podéis ver como leer el tiempo en milisegundos por mediación get_ms_clock() y así poder regular la velocidad de actualización de vuestros sprites de forma independiente al refresco de pantalla (cuidando que ese tiempo sea siempre superior o igual 20ms por frame, claro). En los ejemplos se muestran como utilizar el Modplayer e incluso en el ejemplo 3, se muestra como controlar "manualmente" el modplayer para que cuando llegue al final del MOD, cambiarlo por otro.
  • Con todo lo que sa ha explicado aquí y con lo que veis en los ejemplos, tenéis todo lo necesario para crear juegos y aprender a programarlos.
  • Alguno pensará: "Eh! No nos has contado nada sobre como acceder a la SD, ni al DVD ni a los dispositivos USB. Tampoco me has hablado de programación multihilo y cosas asi".

De algunas de esas cosas me ocuparé a continuación, pero ¿realmente lo necesitáis? Es decir, plantearos si de veras, creéis que necesitáis todo eso para realizar un juego de plataformas, un juego de marcianos y cosas así. Evidentemente, si lo que queréis es realizar un juego donde la programación pasa a un segundo plano y los gráficos ocuparán una burrada, pues es evidente que necesitareis acceder a dispositivos para cargar vuestros gráficos, pero es que una de dos:

  • O no necesitáis éste cursillo y por tanto, ya sabéis muchas de estas cosas o lo tenéis muy fácil de averiguar por vuestra cuenta. Este curso es principalmente, para principiantes.
  • O no tenéis muchos pájaros en la cabeza, pues todavía no habéis empezado a gatear y ya queréis correr.
  • Lo que se quiere que te des cuenta es que con la información que tenéis hasta ahora, los ejemplos y esas herramientas de las que disponéis, ya podéis incluso echar a correr y que si éste cursillo se parara aquí, con todo lo que tenéis, sería suficiente para hacer buenos trabajos.

LIBFAT: Acceso a los dispositivos de almacenamiento

  • NOTA: Describe la antigua LIBFAT.
  • Libfat es la librería que nos permite acceder a dispositivos del almacenamiento formateados en FAT12/FAT16 o FAT32, desde USB Gekko, el lector de SD interno y desde hace un tiempo, dispositivo USB (este último limitado a las especificaciones USB 1.1)

Para integrarla en tus programas, basta con añadir -lfat como librería en el Makefile (los ejemplos de Hermes, la incluyen) y #include <fat.h> en tus fuentes.

UTF-8 frente a ANSI char set

  • Antes de meternos en faena con la librería, conviene conocer un aspecto que puede daros alguna que otra sorpresa. Cuando los ordenadores iniciaron su andadura, se hizo necesario emplear un estándar para poder manejar texto e intercambiar la información y el estándar elegido fue el ASCII que se definió entre 1963-1966 y posteriormente, se redefinió en 1986 dando lugar a la especificación ANSI que conocemos actualmente.
  • ASCII se componía de 128 caracteres, usando 7 bits por tanto, donde se le añadía un último bit como bit de paridad en las transmisiones, dado que éste estándar se pensó inicialmente para telegrafía.
  • De esos 128 caracteres, los primeros 33 se utilizaban para códigos de control no imprimibles. Así por ejemplo, chr 8 representa un espacio atrás, chr 9 el tabulador, chr 10 avance de línea , chr 13 el retorno de carro y chr 32 el espacio. De los imprimibles, el caracter '0' es chr 48, la 'A' es chr 65, y la 'a' chr 97.
  • En algunos sistemas (LINUX/UNIX) el carácter 13 se toma como avance de línea y retorno de carro de forma simultanea y esa es la razón por las que algunas veces, si abrimos un README desde el Wordpad de Windows, el texto se apelotona (porque espera que las líneas terminen con los caracteres 10 y 13).
  • Sin embargo, pronto se hizo necesario ampliar el set de caracteres, dado que ASCII recogía los caracteres latinos USA pero por ejemplo, no recogía la ñ, ni los caracteres de acentuación que nosotros conocemos. Esto se recoge cómo una extensión que ocupa los caracteres desde el 128 al 255 (usando lo que antes era un bit de paridad) conocido como estándar ISO-8859-1 y así en solo 8 bits, se recogían todos los caracteres especiales de origen latino.
  • screenlib utiliza una captura de caracteres que contiene 224 caracteres (elimina los 32 primeros caracteres ASCII, pero conserva el carácter espacio) y que recogen esa especificación ANSI con los caracteres extendidos ISO-8859-1.
  • Puesto que estos estándares solo recogen caracteres latinos, se hizo necesario crear otros estándares que recogieran otros tipos de caracteres para soportar otros idiomas, como por ejemplo, el ruso y así nació el Unicode. Dentro del Unicode existe una especificación llamada Utf-8 (8-bit Unicode Transformation Format) que utiliza un sistema de longitud variable para que partiendo de 8 bits, se puedan codificar todos los caracteres Unicode.

Esta especificación es la que utiliza la librería libfat a la hora de trabajar con nombres de ficheros, pero ¿óomo nos afecta?.

  • Pues bien, Utf-8 utiliza el bit de mayor peso, el que actualmente recoge los caracteres ISO-8859-1 para cambiar y codificar la tabla de caracteres a utilizar. Y al ser de longitud variable, un carácter puede ocupar de 1 a 4 bytes.
  • En Unicode, los primeros 256 caracteres son los mismos que los especificados en la ISO-8859, por lo que los caracteres Utf-8 del 0 al 127, corresponderían directamente a lo que nosotros conocemos. Lo que cambia es la forma de tratar el bit de mayor peso y eso compromete los nombres de los ficheros que utilicen acentos, etc.
  • Así que tenéis dos opciones:
  • O bien procuráis utilizar nombres de ficheros/directorios que no usen caracteres fuera del rango de 128 caracteres definido por el estandar ANSI.
  • O si no os queda más remedio, tendréis que convertir de Utf-8 a ISO-8859 para imprimir o de ISO-8859 a Utf-8 para manejar nombres de ficheros/directorios que usen esos caracteres especiales.
  • Por ejemplo en la aplicación Wiireader, sólo interesa ver el nombre de los ficheros correctamente al visualizarlos en pantalla, dado que al listar ficheros desde libfat, se obtienen en Utf-8 y por tanto, al abrirlos se conservará ese Utf-8.
  • Así pues, como sabemos que los caracteres del 0 a 127 en Utf-8 son iguales y ocupan solo un byte, solo nos restaría saber como se codifican los caracteres del 128 al 255 para poder imprimirlos correctamente desde la screenlib.

En http://es.wikipedia.org/wiki/UTF-8 podemos ver que para cubrir el rango de caracteres 0x80 a 0x7ff (nosotros solo necesitamos de 0x80 a 0xff), se utilizan dos bytes:

rango: 000080 - 0007FF Unicode (UTF-16) 00000xxx xxxxxxxx Utf-8: 110xxxxx 10xxxxxx


  • Así pues, podríamos hacer una rutina de conversión de Utf-8 a ISO-8859 de esta forma:
void UTF8_To_ISO8859(u8 *src, *u8 dst) {
     while(1) {
          if(src[0]==0) {*dst=0;break;} // fin de cadena
          if((src[0] & 128)==0) { // rango de 0x0 a 0x7f
               *dst++=*src++; 
          } else if((scr[0] & 224) ==192 && (scr[1] & 192)==128) { // rango 0x80 a 0x7ff
               if(src[0] & 0x1c) { // error fuera del rango 0x80 a 0xff: caracteres no soportados
                    *dst=0;break;
               } 
               *dst++= (((src[0] & 3)<<6) | (src[1] & 63)); src+=2;
          } else { // error fuera del rango de 0x0 a 0x7ff o no usa especificación UTF-8
               *dst=0;break;
          }
     }
}


  • Con esta rutina (que por cierto, no ha sido testeada) podréis convertir cadena utf-8 como fuente, a otra ISO como destino, para visualizarla con s_printf por ejemplo (caso de que el nombre quepa en pantalla). Una sugerencia sería que ampliaseis esa función para que en caso de ser un carácter no soportado, visualizara un carácter comodín (? por ejemplo), y que trabajaseis con Utf-8 siempre y solo convirtierais para visualizar (pero allá cada uno).

LIBFAT: Inicializando

  • Libfat se inicializa con ésta función:
bool fatInit (u32 cacheSize, bool setAsDefaultDevice);


  • Lo normal es que la inicialiceis así:
fatInit(8, false);

el 8 establece una caché de 8 clusters para entradas de directorios y cosas así.


  • Libfat trata de montar una serie de dispositivos en unidades con nombres como "fat0:","fat1:",..., "fat4:"... pero permite asignar uno de esos dispoitivos cómo unidad por defecto "fat:".
  • Lo normal es que esa unidad la asignéis a la SD, de ésta forma:
fatSetDefaultInterface(PI_INTERNAL_SD);


  • En fat.h podéis ver una serie de dispositivos definidos como PI_algo, como PI_SDGECKO_A, o PI_USBSTORAGE.
  • Sin embargo, si vais a acceder a varios dispositivos fat, es preferible utilizar el nombre original de la "partición" a estar intercambiado el interface por defecto.
  • Asi pues, imaginemos que tenemos esta cadena:
char name[]="fatX:/test.txt";

podríamos especificar la unidad directamente, modificando la cadena de ésta forma: name[3]=48+PI_INTERNAL_SD; (48 es el carácter ASCII '0')

Detectando las particiones y asignando el tamaño de la caché de lectura

  • Bien, con lo que tenemos hasta ahora, podríamos funcionar sin problemas, pero por cuestiones de velocidad de lectura, puede resultar conveniente asignar un tamaño interno de caché para la lectura de ficheros. Al mismo tiempo, resulta conveniente detectar si los dispositivos están operativos, pues ciertas operaciones podrían colgar directamente la consola.
  • Para asignar el tamaño de la cache de lectura, se utiliza ésta función:
bool fatEnableReadAhead(PARTITION_INTERFACE partitionNumber, u32 numPages, u32 pageSize);


  • Para no liaros mucho:
fatEnableReadAhead(PI_INTERNAL_SD, 12, 32);

o

fatEnableReadAhead(PI_USBSTORAGE, 12, 32);

son unos buenos valores. El tamaño de página y el número de paginas, requieren un uso de memoria que si no se administra bien, se desperdicia inútilmente, por lo que no es conveniente abusar.


  • Quizá estés pensando ¿por que no se nos explica mas a fondo el uso de éstas funciones?. Pues por que el autor original de la librería, ha decidido desechar los cambios que tanto rodries cómo Hermes introdujeron y está inmerso en una tarea de despedazar la librería y migrar fuentes, no se sabe muy bien si con la idea de jugar al despiste o que. Solo se puede decir que repite viejos errores y arrastra ciertos bugs que darán problemas, sobre todo en multithread. Y que Hermes no piensa mover un dedo ni para reportarlos, ni para migrar sus cambios, ni nada de nada, puesto que para Hermes la libfat que usamos funciona bien y no le da la gana volver atrás, sobre todo en el tema de la caché. Pero obviamente, ésto es su opción y si tu "actualizas" la librería, éstas funciones de caché no las encontrarás.
  • Antes de asignar un tamaño de cache, conviene saber si el dispositivo está en funcionamiento:
{
     DIR_ITER *dir;
     char path[]="fatX:/";
     int have_device=0;

     path[3]=48+PI_INTERNAL_SD;
     dir = diropen(path);
     if (dir) {
          dirclose(dir);
          have_device|=1;
          fatEnableReadAhead(PI_INTERNAL_SD, 12, 32);
     }
}

de esta forma, podemos saber si el dispostivo SD está operativo (intentando abrir el directorio raiz) y en caso afirmativo, ajustamos un flag de presencia y activamos la caché de lectura.


  • Los dispositivos USB suelen causar problemas: a veces tardan mucho en inicializarse, no se resetean bien o se tropiezan, pero hay veces que les da la neura y eso explica cosas raras que Hermes suele hacer en sus inicializaciones.
  • Por ejemplo, en Wiireader esto es lo que Hermes usa yo para inicializar libfat tratando de pillar dispositivo SD y dispositivo USB:
fatInit(8, false);

sleep(2); // espero dos segundos, no se muy bien si para darle tiempo a que el dispositivo negocie o esté preparado para funcionar

fatSetDefaultInterface(PI_INTERNAL_SD); // usa SD como fat:

have_device=0; // bits que me indican la presencia de dispositivos

{
     path_file[3]=48+PI_INTERNAL_SD; // path_file ="fatX:/";

     for(n=0;n<5;n++) {// numero de reintentos por si la unidad no está preparada o está "enojada"   
          dir = diropen(path_file);
          if (dir) { // monta la cache
               dirclose(dir); 
               have_device|=1;
               fatEnableReadAhead(PI_INTERNAL_SD, 12, 32);
               break;
          } 
     usleep(200*1000);
     }

     path_file[3]=48+PI_USBSTORAGE; // path_file ="fatX:/";

     for(n=0;n<5;n++) { // numero de reintentos por si la unidad no está preparada o está "enojada"   
          dir = diropen(path_file);
          if (dir) { // monta la cache
               dirclose(dir); 
               have_device|=2;
               fatEnableReadAhead(PI_USBSTORAGE, 12, 32);
               break;
          } 
          usleep(200*1000);
     }
}


  • ¿Que alguno lo ve superfluo? Pues tal vez, pero cuando uno está harto de que entras desde un RESET y no te ve el "penedrive", sales y entras y ahora sí y cosas así, acabas por ver demonios por todas partes y si haciéndolo así.

LIBFAT: Trabajando con ficheros y directorios

Listando directorios

Para listar ficheros y directorios, podéis utilizar el siguiente procedimiento:

#include "sys/dir.h"
.....

DIR_ITER * dir;

static char namefile[256*4]; // reserva espacio suficiente para UTF-8

static struct stat filestat;

dir = diropen("fat:/"); // abre el directorio raiz de la unidad fat:

while(1) {
     if(dirnext(dir, namefile, &filestat)!=0) break; // si no hay mas entradas en el directorio, sal
    
     if(filestat.st_mode & S_IFDIR) { // es el nombre de un directorio
          // namefile contiene el nombre del directorio en formato UTF-8,que puede ser "." o ".." tambien
     } else {
          // namefile contiene el nombre del fichero en formato UTF-8
     }
}

dirclose(dir); // cierra el directorio

Operaciones con ficheros

  • Después de iniciar libfat y comprobar que los dispositivos están operativos, podéis utilizar las librerías estándar ANSI-C referente a dispositivos para crear/borrar/renombrar/fijar directorios.
  • Por ejemplo con:
mkdir("fat3:/jamacuco", S_IREAD | S_IWRITE);

se crearía el directorio "jamacuco" en raiz de la SD, fijando los correspondientes permisos. Quizá no sea buena idea usar fat3 así directamente, pues si algún lumbreras cambia el orden de los dispositivos mañana , fat3 quizás apunte a otra cosa (de ahi eso de usar puntero[3]=48+PI_torreo).


  • Para leer/escribir ficheros, podéis utilizar fopen/fread/fwrite/fseek/ftell/fclose...
  • Si tienes alguna duda sobre las librerías estándar, pasaos por aquí y lo miráis más a fondo.

El Desmontaje de dispositivos

  • Resulta conveniente asegurarse antes de salir de un programa de que todos los ficheros abiertos, se han cerrado y de que las cachés de escritura se han "flusheado". Resulta conveniente desmontar los dispositivos.
  • Eso se puede hacer añadiendo esto a la función de salida que definimos con at_exit():
if(have_device & 1) fatUnmount(PI_INTERNAL_SD); // desmonta la SD
if(have_device & 2) fatUnmount(PI_USBSTORAGE); // desmonta dispostivo USB


  • Estas funciones las modificó Hermes para que flushearán los datos incluso en el caso de que se detectaran ficheros abiertos antes de salir y no se pudieran desmontar los dispositivos. Si cerráis los ficheros, se flushearán los buffers modificados, pero existen ciertas operaciones como crear directorios, borrarlos, etc, que pueden dejar alguna operación pendiente de escritura desde la caché al dispositivo y por eso es aconsejable desmontarlo.
  • Y con esto , tienes todo lo necesario para trabajar con ficheros.

Gestión del Tiempo

Formas de perder el tiempo

  • En #include <unistd.h> podemos encontrar dos interesantes funciones para "perder" el tiempo:
unsigned sleep(unsigned int __seconds ); // espera X segundos durmiendo el hilo
int usleep(useconds_t __useconds); // espera X microsegundos durmiendo el hilo


  • Ejemplos:
sleep(2); // espera dos segundos
usleep(2*1000); // espera 2 milisegundos


  • La explicación correcta de ésta función sería: "Suspende la ejecución del hilo (del programa) permitiendo que otro hilo pueda tomar el control y al cabo de X tiempo, trata de continuar con la ejecución del hilo en cuanto sea posible".
  • Es decir, que al margen de la precisión que pueda tener el temporizador, el tiempo que tarda el hilo en volver a despertar, dependerá de si hay un hilo con mayor prioridad que esté funcionando en el momento de cumplirse el plazo o no.
  • Existe una función interna en la librería libogc, que no está incluida en ningún fichero de cabecera (de ahí que se denomine interna) llamada udelay, que podeis declararla así:
void udelay(int us); // retarda X microsegundos


  • Esta función pierde el tiempo de forma similar a usleep, pero no duerme el hilo. La importancia de este matiz la podréis comprender cuando se hable de la programación multithread pero os basta con saber que si un hilo no suspende (o duerme) su ejecución, no puede ser interrumpido por otro hilo salvo que ese nuevo hilo tenga una prioridad mayor. Luego queda patente que usleep() permite la ejecución de hilos que estén a la espera de inferior o igual prioridad, mientras que udelay() no.

Medida Relativa del tiempo

  • Hay una función llamada gettick() a nivel interno que nos devuelve un contador de 32 bits de tiempo. Esta función no está presente en ningún fichero de cabecera (si os interesa curiosear el fuente de las funciones, está en libogc/timesupp.c) por lo que tendréis que definirla así:
unsigned gettick();

gettick() devuelve "ticks" que hay que convertir a medidas de tiempo humanas.


  • Así podemos encontrar en /ogc/lwp_watchdog.h una serie de definiciones para realizar esa conversión:
ticks_to_secs(ticks);
ticks_to_millisecs(ticks);
ticks_to_microsecs(ticks);
ticks_to_nanosecs(ticks);


  • En el ejemplo 3 se incluyo un procedimiento para medir el tiempo en ms de ésta forma:
#define ticks_to_msecs(ticks)      ((ticks)/TB_TIMER_CLOCK)
extern unsigned gettick();

unsigned get_ms_clock() { // retorna los milisegundos transcurridos
     return ticks_to_msecs(gettick());
}


  • Así podemos medir el intervalo de tiempo entre dos medidas, haciendo la diferencia y usarlo para hacer los ajustes necesarios en el programa (por ejemplo, actualizar movimientos de nuestros sprites de forma independiente al refresco de la pantalla y cosas asi)
  • Sin embargo, existe una función que hace más o menos lo mismo que gettick(), pero trabajando con medidas de 64 bits en lugar de los 32 bits que devuelve gettick() y que se define en /ogc/lwp_watchdog.h
long long gettime();


  • Se menciona aquí por si necesitais precisión extra. Parece que gettime() gasta un tiempo en hacer la lectura).

Reloj de Tiempo Real

  • En libogc/timesupp.c hay una función:
int clock_gettime(struct timespec *tp);

esta función devuelve 0 si todo fue bien y los datos en una estructura timespec que se define así:

struct timespec {
     time_t tv_sec; /* Seconds */ // unsigned long (u32)
     long tv_nsec; /* Nanoseconds */ // (s32)
};


  • A ésta le falta un parámetro con respecto a la definición de la función en time.h y se supone que equivale a clock_gettime(CLOCK_REALTIME, &time); en otros sistemas.
  • En fin, se deja dicho para que vosotros lo sepáis. Eso si, éste tipo de funciones no están pensadas para ser llamadas en cada frame, porque suelen ser lentas al acceder al reloj de tiempo real, así que haced un uso responsable de ella.

Timers programables

  • El sistema te permite definir una serie de alarmas de tiempo que vienen muy bien para trabajar con pasos regulares. Echad un ojo a ogc/system.h
  • Hasta ahora hemos visto funciones que nos permiten perder el tiempo y otras que nos permiten medir un intervalo, pero en ninguna se puede estar seguro de que el tiempo transcurrido es el esperado, (dependiendo de la precisión del temporizador, claro).
  • Por ello las alarmas son especialmente útiles ya que te permiten programar un tiempo que una vez transcurrido, produce una interrupción que nos lleva a una callback de tratamiento donde teniendo cuidado, eso sí (recordemos que estamos en tiempo de interrupción), podemos ajustar lo que sea necesario.
  • Desde esa callback por ejemplo, podemos "despertar" hilos de una forma regular (Hermes por ejemplo emplea una alarma en su juego Guitarfun para actualizar graficos/leer pad cada 20 ms, mientras el hilo de fondo, descodifica y reproduce Ogg y de esa forma, el programa trabaja de forma independiente al refresco de frames (50/60 Hz) para cada formato de imagen).
  • Para crear una alarma, primero necesitamos dos estructuras, una para alojar la alarma:
syswd_t myalarm;


  • Y otra para alojar el tiempo de la alarma:
struct timespec alarm_time;


  • Entonces podemos proceder a crear una alarma con:
s32 SYS_CreateAlarm(syswd_t *thealarm);


  • De esta forma:
SYS_CreateAlarm(&myalarm);

la función devuelve 0 si la alarma se creó correctamente, pero ahora toca ponerla en funcionamiento con alguna de estas dos funciones:

s32 SYS_SetAlarm(syswd_t thealarm,const struct timespec *tp,alarmcallback cb); // una sola vez

s32 SYS_SetPeriodicAlarm(syswd_t thealarm,const struct timespec *tp_start,const struct timespec *tp_period,alarmcallback cb); // periódicamente

la primera ajusta la alarma para un único uso pero nada impide para que dentro de la callback programes de nuevo la alarma usando SetAlarm con el mismo u otro tiempo y repetir (o en cualquier otro punto). La segunda usa un temporizador inicial y luego otro que se utilizará para llamar de forma periódica a la callback

  • thealarm: estructura alarm inicializada con SYS_CreateAlarm().
  • tp, tp_start, tp_periodic: estructuras timespec con el tiempo a programar.
  • cb: callback que será llamada al transcurrir el tiempo.
  • Devuelven 0 si se pudo ajustar la alarma.
  • Veamos algunos ejemplos:

Ejemplo de Alarma de una sola vez:

syswd_t myalarm;
struct timespec alarm_time;

void alarm_handler() {
     // aqui llega cuando se cumpla el tiempo: recuerda que esto es una callback de interrupcion
}

......

alarm_time.tv_sec=0;
alarm_time.tv_nsec=20*1000*1000; // 20 milisegundos (notese que tv_nsec es en nanosegundos)

SYS_CreateAlarm(&myalarm);
SYS_SetAlarm(myalarm,&alarm_time,alarm_handler);


Ejemplo de Alarma Autoprogramable:

syswd_t myalarm;
struct timespec alarm_time;

int flip=0;

void alarm_handler() {
     // aqui llega cuando se cumpla el tiempo: recuerda que esto es una callback de interrupcion

     alarm_time.tv_sec=0;
     if(flip)
          alarm_time.tv_nsec=20*1000*1000; // 20 milisegundos (notese que tv_nsec es en nanosegundos)
     else
          alarm_time.tv_nsec=40*1000*1000; // 40 milisegundos (notese que tv_nsec es en nanosegundos)

     flip^=1;
     SYS_SetAlarm(myalarm,&alarm_time,alarm_handler); // reprogramamos la alarma desde la callback
}

......

alarm_time.tv_sec=0;
alarm_time.tv_nsec=20*1000*1000; // 20 milisegundos (notese que tv_nsec es en nanosegundos)

SYS_CreateAlarm(&myalarm);
SYS_SetAlarm(myalarm,&alarm_time,alarm_handler);


Ejemplo de Alarma Periódica:

syswd_t myalarm;
struct timespec alarm_time;
struct timespec alarm_time2;
int flip=0;

void alarm_handler() {
     // aqui llega cuando se cumpla el tiempo: recuerda que esto es una callback de interrupcion

     // aqui llega despues de dos segundos y luego, cada 20 milisegundos
}

......

alarm_time.tv_sec=2; // 2 segundos
alarm_time.tv_nsec=0;

alarm_time2.tv_sec=0;
alarm_time2.tv_nsec=20*1000*1000; // 20 milisegundos (notese que tv_nsec es en nanosegundos)

SYS_CreateAlarm(&myalarm);
SYS_SetPeriodicAlarm(myalarm,&alarm_time, &alarm_time2, alarm_handler); // aguarda 2 segundos y a apartir de ahí, llama la callback cada 20 ms

Liberando/Cancelando alarmas

  • Las alarmas que podemos programar, son finitas. No se sabe cual será el número exacto pero obviamente, lo podéis comprobar creando alarmas hasta que se produzca un error.
  • La función:
s32 SYS_CancelAlarm(syswd_t thealarm);

la puedes usar para cancelar alarmas ya programadas y en curso, mientras que la función:

s32 SYS_RemoveAlarm(syswd_t thealarm);

te permite liberar las alarmas creadas con SYS_CreateAlarm();


  • Y con esto ya tienes lo necesario para gestionar el tiempo de forma apropiada

Programación Multihilo

  • En Wii la programación multihilo consiste en la posibilidad de interrumpir la ejecución de un hilo para tratar otro o en la posibilidad de aprovechar los tiempos muertos de un hilo, para gestionar otras cosas desde otro hilo.
  • Los milagros no existen y la CPU sólo puede ocuparse de uno de ellos en cada momento, por lo que no se trata de ejecutar varias tareas de forma simultánea, si no de aprovechar los tiempos muertos como digo, o tratar una serie de eventos conectados a las interrupciones desde un hilo, de forma que ese hilo interrumpe a otro para realizar alguna tarea y al mismo tiempo, permite el paso de las interrupciones.
  • Los hilos tienen asignados todos un nivel de prioridad. Esta prioridad se utiliza para determinar que hilo toma el control y la regla consiste en que el hilo que tiene mayor prioridad y está en espera de ser activado, se hace con el control.
  • Una cosa que tenéis que tener en cuenta y manejar con sumo cuidado, es el tema de las reentradas en las funciones. Incluso el tema de utilizar funciones del mismo grupo dentro de una librería o que comparten recursos.
  • Por ejemplo, si desde un hilo se leen los pads con WPAD_ScanPads() y desde otro se comprueban los datos leídos del Wiimote, es posible que el hilo que lee el Wiimote tome el control en medio de una llamada a WPAD_ScanPads() y reciba datos erróneos y en otros casos, se pueden obtener una serie de paradojas.
  • Si vuestra función utiliza únicamente registros del procesador y variables locales o globales que no se modifican, no tendréis problemas en utilizarla en multihilo, puesto que cada hilo tiene su propia pila y los registros se salvan antes de cambiar de hilo. Pero si vuestra función accede a variables globales que se modifican, acceden a registros del hardware o a dispositivos, en definitiva todo lo que sea compartir recursos, entonces no estará preparada para un acceso multihilo y deberéis protegerla mediante uso de semáforos (que veremos luego).

La creación de un hilo

  • La función LWP_CreateThread() es la encargada de crear los hilos (ver ogc/lwp.h):
s32 LWP_CreateThread(lwp_t *thethread,void* (*entry)(void *),void *arg,void *stackbase,u32 stack_size,u8 prio);
  • thethread: puntero de tipo lwp_t que recibe el handle. Por ejemplo: static lwp_t h_my_thread=LWP_THREAD_NULL;
  • entry: función de entrada del hilo.
  • arg: puntero con el argumento que recibirá la función de entrada del hilo (para asignar datos privados). Puede ser NULL.
  • stackbase: puntero con la dirección base de la pila (alineada a 32). Si NULL, se asigna de forma interna.
  • stack_size: tamaño de la pila en bytes. Si 0 se asigna 8KB por defecto.
  • prio: prioridad del hilo, de 0 a 127 (maxima prioridad). Como referencia el player ogg utiliza 80 y yo suelo asignar a 40 el main() (no se conoce la prioridad de main() pero tampoco es que importe mucho).
  • La función devuelve 0 si no hubo problema alguno.

La función de entrada del hilo

  • La función de entrada sería algo así:
void * thread_main(void *arg) {
     // arg recibe el puntero arg que asignamos al crear el hilo

     return NULL; // aqui se sale del hilo
}

evidentemente, sería un hilo muy fugaz y su utilidad sería bastante baja. La cosa mejoraría añadiéndole un bucle de control y algo que permitiera oxigenar el resto de hilos:

int exit_thread_main=0;

void * thread_main(void *arg) {
     // arg recibe el puntero arg que asignamos al crear el hilo

     while(1) {
          usleep(20000); // aguarda 20 ms
          if(exit_thread_main) break;

          /* aqui haces lo que te de la gana */
     }

return NULL; // aqui se sale del hilo
}


  • Aquí observamos que este hilo ha sido pensado para tener un prioridad mayor que la de main(), para que mas o menos, cada 20 milisegundos se active (usleep() como ya se explicó, duerme el hilo y luego trata de despertarlo) y haga lo que tenga que hacer en un bucle infinito (luego la tarea sea haría cada 20 ms+ lo que tarde el resto, que puede ser una burrada de tiempo), pero que tiene una variable, exit_thread_main la cual asignándola a un valor no cero, rompe el bucle y eso haría que finalizara el hilo.
  • Ésta variable de control es muy importante puesto que este hilo por si solo no va a poder salir y eso daría lugar a un cuelgue de la máquina cuando pretendas volver al HBC por ejemplo. Así pues, antes de volver deberías asignar exit_thread_main=1; y asegurarte que el hilo está 'despierto' para que se produzca ese break.

Controlando la ejecución del hilo

  • En la función thread_main de antes, hemos observado un método que supone que el hilo va a su bola desactivándose por espacio de 20 ms y luego volviéndose activar. Sin embargo, seguramente os resultará mas útil activar el hilo de forma mas precisa, desde una callback de una alarma o cualquier otro sitio.
  • Para eso nos será muy útil el uso de colas:
static lwpq_t thread_queue=LWP_TQUEUE_NULL; // thread_queue contendrá el handler de la cola


  • Inicializamos la cola antes de crear el hilo con:
LWP_InitQueue(&thread_queue); // inicializa el queue


  • Después creamos el hilo y en la función de entrada del hilo ponemos:
int exit_thread_main=0;

void * thread_main(void *arg) {
     // arg recibe el puntero arg que asignamos al crear el hilo

     while(1) {
          if(exit_thread_main) break; // si éste es nuevo  :D 

          LWP_ThreadSleep(thread_queue);  // duerme el hilo usando una cola para poder despertarlo

          if(exit_thread_main) break;

          /* aqui haces lo que te de la gana */
     }
 
return NULL; // aqui se sale del hilo
}


  • Como se puede apreciar, ahora el hilo estará controlado por esa cola y necesitaremos decirle desde fuera que puede continuar.
  • Para ello, desde una callback podéis usar ésta función:
LWP_ThreadSignal(thread_queue);


  • Al hacerlo, en el momento en que las reglas de prioridad lo permitan, nuestro hilo despertará, hará su tarea y al completar el bucle, volverá a dormirse en espera de que llamemos a LWP_ThreadSignal() de nuevo.
  • Por poneros algunos ejemplos, en el programa Guitarfun se usa el método de las colas para actualizar los gráficos cada 20 ms mediante una alarma, los reproductores de audio esperan la activación del hilo de ésta forma para rellenar con samples los buffers, etc.
  • La técnica de usleep() que usábamos al principio, se usa en Guitarfun también, para el nuevo hilo que se encarga de rellenar los buffers de lectura de los ficheros Ogg, en lecturas de 256KB, tarea que puede consumir varios segundos y que por tanto, es despreciable la pérdida de ms del usleep y precisa un funcionamiento independiente.
  • Para finalizar una cola se usa:
void LWP_CloseQueue(lwpq_t thequeue);

Salir de los Hilos

  • Ya hemos visto que si retornamos desde la función de inicio del hilo, salimos de él, pero si queremos salir de un hilo desde el principal o vamos a salir al sistema, conviene seguir los siguientes pasos:
exit_thread_main=1; // asignamos a 1 la variable que rompe el bucle while()

LWP_ThreadSignal(thread_queue); // si estamos usando colas, forzamos que el hilo despierte en caso de estar dormido en la cola

LWP_JoinThread(h_my_thread, NULL); // pasamos el handle del hilo (lwp_t) del que esperaremos su finalización

LWP_CloseQueue(thread_queue); // cerramos la cola

si algo fallara aquí, la aplicación se colgaría, así que conviene asegurarse de que ninguna otra cosa bloqueará la salida del hilo.


  • El juego Guitarfun o la aplicación Wiireader, muestran varios ejemplos que podéis estudiar.
  • Tened mucho cuidado eso si, con usar funciones que se interfieran con el uso de varios hilos

Cambiando la prioridad

  • Cómo habéis visto, en Wii los hilos obedecen a temas prioridad a la hora de activarse, siendo el hilo de mayor prioridad el más abusón de la clase, no permitiendo la ejecución de otros hilos salvo que el mismo se libere.
  • Algunas veces, resulta interesante cambiar la prioridad de un hilo para forzar que tome el control y luego bajársela, para permitir que otro le pueda interrumpir o simplemente, fijar una prioridad muy alta para que otro hilo no pueda interrumpir al hilo actual hasta que acabe de hacer algo, momento en el cual restableceremos la prioridad.
  • Por desgracia en Wii hay un problema raro que afecta a la programación multihilo y parece ser que a los números flotantes. Dicho problema se corrigió parcialmente, pero no del todo, así que puede ser conveniente proteger ciertas secciones de ésta manera.
  • La función para cambiar la prioridad es:
void LWP_SetThreadPriority(lwp_t thethread,u32 prio);
  • thethread: handle del hilo. Si LWP_THREAD_NULL se trata del hilo actual (no confundir con NULL). También puedes usar LWP_GetSelf(); para conocer el Handle del hilo actual.
  • prio: nueva prioridad para el hilo.


  • Así por ejemplo, si dentro del main() ponemos LWP_SetThreadPriority(LWP_THREAD_NULL, 40); asignamos a main() la prioridad 40
  • No existe una función equivalente para conocer la prioridad.

Proteger secciones críticas

  • Ya hemos dicho que cambiando la prioridad, podríamos proteger una determinada sección de un hilo contra el cambio de hilos solicitado desde una callback (interrupciones), pero hay puntos donde por conveniencia es mejor deshabilitar las propias interrupciones.
  • Para ello podemos contar con dos funciones, presentes en ogc/irq.h:
u32 IRQ_Disable(); // deshabilita las interrupciones y devuelve el estado

void IRQ_Restore(u32 level); // restaura las interrupciones con el estado devuelto por IRQ_Disable()


  • Vamos que esto se usa así:
u32 stat;

stat=IRQ_Disable(); // deshabilita

/* hacer algo que no lleve mucho tiempo, ni requiera interrupciones habilitadas */

IRQ_Restore(stat); // restaura

El uso de semáforos

  • Los semáforos se utilizan para regular el paso de los hilos hacia una función o funciones que solo permiten el acceso a un hilo cada vez, debido a una serie de características propias de esa función o funciones. Por ejemplo, si se va leer desde USB, podéis programar el dispositivo para que os lea un sector y el dispositivo tardará un tiempo en "servíroslo", pero no podéis leer sectores no consecutivos de forma simultanea, ni el dispositivo puede atender mas peticiones hasta que haya finalizado con la primera (ya que solo tiene "dos manos" hablando virtualmente).
  • Así pues, se necesita un mecanismo que haga que si dos o mas hilos acceden a una función protegida, solo uno pase y el resto queden a la espera hasta que finalice el hilo que pasó primero. Y de eso se encargan los semáforos (un ejemplo lo tenéis en LIBFAT, que bloquea el acceso a todas sus funciones mediante semáforos).
  • Para crear uno, primero tenemos que crear un identificador tal que así (ver ogc/mutex.h):
mutex_t mi_semaforo=LWP_MUTEX_NULL;


  • La función para inicializarlo es ésta:
s32 LWP_MutexInit(mutex_t *mutex,boolean use_recursive);


  • Lo inicializamos así:
LWP_MutexInit(&mi_semaforo, false);


  • El semáforo creado devolverá 0 si se inició correctamente y permitirá el paso de forma inicial.
  • Cuando queramos eliminarlo, de ésta forma:
LWP_MutexDestroy(mi_semaforo);


  • Bien, ya tenemos nuestro semáforo creado, ¿y ahora, cómo se usa en la práctica?. Pues para eso tenemos éstas dos funciones:
s32 LWP_MutexLock(mutex_t mutex); // bloquea mediante el semáforo

s32 LWP_MutexUnlock(mutex_t mutex); // desbloquea el semáforo


  • Pongamos un ejemplo:
int  my_funcion(int manolo) {
     int ret=0;

     LWP_MutexLock(mi_semaforo); // bloquea el paso de hilos. De forma inicial al crear el semáforo, permite el paso de un hilo

     /* codigo protegido por el semaforo */
     ......
     ......

     LWP_MutexUnlock(mi_semaforo); // desbloquea el semaforo y permite el paso del hilo siguiente a la función

     return ret;
}


  • Como se puede apreciar, es un mecanismo muy sencillo. Aplicándolo a todas las funciones de una librería que sean accesibles, podemos evitar el acceso de otros hilos a una librería mientras esté en uso por parte de uno de ellos, pero esto no se traduce en un error de forma que la librería diga algo así "lo siento, pero estoy en uso: vuelva usted mas tarde", si no que el semáforo lo convierte en "en esté momento la función está ocupada, aguarde en la sala de espera hasta que el hilo pepito abandone la función".
  • Pero claro, es responsabilidad del hilo "pepito" informar con MutexUnlock de que otros hilos pueden acceder a ella. y también es importante que el hilo "pepito" evite volver a pasar por MutexLock, puesto que ya no dispone de tarjeta de visita y será bloqueado también.

Y ésto es todo lo que tenéis que saber sobre los semáforos, los hilos y el copón de vino para trabajar con hilos. El resto es cosa vuestra.