es/Dynamic Xtreme internal workings
| English | Português | Español | 日本語 |
En este artículo se explica cómo funciona Dynamic Xtreme, desde la preparación de los recursos al instalarlo hasta la carga y el dibujo de una pose durante el juego.
Qué prepara la instalación
Antes de que un sprite pueda usar sus gráficos o una paleta, Dynamic Xtreme tiene que guardarlos en la ROM y preparar la información para encontrarlos. Lo mismo ocurre con cualquier otro recurso dinámico que hayas añadido.
Para hacerlo, la herramienta lee los DynamicInfo, DrawInfo y PaletteEffect. Cada archivo aporta una parte distinta: qué recursos se van a cargar, cómo se dibujan las poses y qué cambios de color se pueden aplicar.
Gracias a esta preparación se generan defines en DXDefines.asm que permiten utilizar los recursos y ejecutar fácilmente las rutinas necesarias.
DynamicInfo: recursos y datos de carga
Un DynamicInfo reúne los gráficos de poses, las paletas y los recursos generales que necesita un sprite u otro código. Si incluye gráficos de poses dinámicas, la herramienta los organiza para que puedan cargarse con la distribución de tiles que espera el sistema.
Por cada pose guarda varios datos en tablas separadas. Todas mantienen el mismo orden: la entrada de una pose en una tabla corresponde a esa misma pose en las demás. En la tabla siguiente, N es la cantidad de poses dinámicas instaladas.
| Tabla | Bytes totales | Bytes por valor | Qué guarda y para qué sirve |
|---|---|---|---|
PoseSize |
2 × N | 2 | La cantidad de datos que utiliza cada pose, en bytes. Se utiliza para comprobar si su carga cabe en el presupuesto del frame. |
Pose16x16Blocks |
N | 1 | Cuántos bloques de 16×16 hay que reservar en VRAM para la pose. No siempre coincide exactamente con los bytes que se copian, porque una pieza incompleta también puede necesitar un bloque de espacio. |
PoseResource |
3 × N | 3 | La dirección de los gráficos de cada pose en ROM. Incluye el banco, de modo que el código de carga pueda encontrar su origen completo. |
PoseResourceSizePerLine |
4 × N | 2; dos valores por pose | Dos tamaños por pose, correspondientes a los dos tramos en que pueden dividirse sus gráficos. La rutina de carga utiliza esos tamaños para repartirlos correctamente en VRAM. |
PoseSecondLineOffset |
2 × N | 2 | El desplazamiento en VRAM desde el comienzo del primer tramo hasta el comienzo del segundo. Permite colocar el segundo tramo donde corresponde, aunque el primero termine con una fila incompleta. |
Estas tablas describen todas las poses instaladas, no solo las que están en pantalla. Más adelante, la caché guarda por separado cuáles están cargadas y dónde quedaron.
Por ejemplo, si se solicita una pose que todavía no está en VRAM, el sistema consulta cuántos bytes necesita cargar, busca espacio para sus bloques y utiliza la dirección y los datos de sus tramos para preparar la carga.
Tablas de paletas
Las paletas declaradas en los DynamicInfo se reúnen en PaletteTable. Si P es la cantidad de entradas de las listas de paletas de los DynamicInfo:
| Tabla | Bytes totales | Bytes por valor | Contenido |
|---|---|---|---|
PaletteTable.IDs |
2 × P | 2 | ID de cada paleta. |
PaletteTable.Addresses |
3 × P | 3 | Dirección de sus colores en ROM. |
P cuenta las entradas de las listas, no solo las paletas distintas: una paleta compartida puede aparecer en más de un DynamicInfo.
Las dos listas siguen el orden de las paletas de los DynamicInfo. El define de offset de cada DynamicInfo indica dónde comienza su lista dentro de ellas. Al elegir una versión, el sistema suma ese índice al offset y obtiene tanto el ID como la dirección de los colores elegidos.
El ID permite reconocer y compartir una paleta; la dirección permite cargarla. No son el número de paleta de pantalla: una misma paleta instalada puede terminar, por ejemplo, en la paleta 9 o en la C según la asignación.
Recursos generales y defines
Los archivos declarados en Resources se guardan en ROM. Su dirección y la cantidad de bytes que ocupan pueden consultarse mediante los defines generados en DXDefines.asm. Por ejemplo, para my_resource.bin se generan:
!ResourceMyResource: dirección del recurso en ROM.!ResourceMyResourceSize: cantidad de bytes que ocupa.!ResourceIDMyResource: ID del recurso.
El nombre se normaliza en PascalCase: se eliminan espacios, guiones y guiones bajos, y se pone en mayúscula la inicial de cada palabra.
DrawInfo: datos y rutinas de dibujo
El DrawInfo describe cómo está formada una pose: qué tiles tiene, dónde se coloca cada uno, qué tamaño utiliza y cómo cambia al voltearse.
Al instalarlo, Dynamic Xtreme genera rutinas de dibujo optimizadas según los parámetros que utilizan los tiles. Tiene en cuenta, por ejemplo, si necesitan desplazamientos X e Y, si comparten el mismo tamaño, si utilizan propiedades comunes y si requieren volteos.
De esta forma, la rutina realiza el trabajo que necesita ese dibujo sin añadir el manejo de opciones que no utiliza. Varias poses pueden compartirla si necesitan el mismo tratamiento; eso no significa que compartan también sus gráficos o su ID.
La relación entre una pose y su dibujo se guarda en estas tablas. D es la cantidad de poses de dibujo y R la cantidad de rutinas generadas:
| Tabla | Bytes totales | Bytes por valor | Qué guarda y para qué sirve |
|---|---|---|---|
TableOffset |
2 × D | 2 | Dónde comienzan los datos de la pose dentro de las tablas de su rutina de dibujo. Así se localizan sus tiles y desplazamientos sin recorrer las poses anteriores. |
NumberOfTilesMinus1 |
D | 1 | La cantidad de tiles de la pose menos uno. Una pose de cuatro tiles guarda 3; la rutina utiliza ese valor para recorrer sus piezas. |
GraphicRoutineIDs |
D | 1 | Qué rutina de dibujo utiliza cada pose. Las poses con características compatibles pueden apuntar al mismo ID de rutina. |
GraphicRoutine |
3 × R | 3 por rutina | Las direcciones de las rutinas de dibujo. El ID de rutina permite encontrar aquí el código que debe ejecutarse. |
Cada rutina tiene, además, las tablas que necesitan sus poses. Aquí T es la cantidad de entradas de cada tabla de la rutina:
| Datos | Bytes totales | Bytes por valor | Qué describen |
|---|---|---|---|
Tiles |
T, si se genera | 1 | Los números de tile que forman la pose. |
Properties |
T, si se genera | 1 | Las propiedades de cada pieza, como los volteos y su prioridad respecto a las capas. |
XDisplacements y YDisplacements |
T por tabla, si se genera | 1 | La posición de cada tile respecto al punto de origen de la pose. |
| Tablas de desplazamientos para los volteos | T por tabla, si se genera | 1 | Las posiciones que deben utilizarse al voltear la pose cuando necesita una distribución distinta. |
Sizes |
T, si se genera | 1 | El tamaño de cada tile: 8×8 o 16×16. |
No todas estas tablas tienen que aparecer en todas las rutinas. Si un dato es igual para todas las piezas o puede obtenerse de un valor común, la herramienta puede generar una rutina que lo utilice directamente.
La optimización se hace durante la instalación. Si todos los tiles tienen el mismo tamaño, por ejemplo, la rutina puede utilizar ese valor común sin leer un tamaño distinto para cada pieza.
PaletteEffect: datos de los efectos
Un archivo PaletteEffect contiene una lista de cambios de color. La instalación les asigna IDs y guarda sus datos en PaletteEffectsTable.
Como en las tablas de poses, los datos de cada efecto mantienen la misma posición en las distintas listas. Los IDs de efectos comienzan en 1; el ID 0 se utiliza para indicar que no hay efecto global. En la tabla, E es la cantidad de efectos instalados; el conjunto ocupa 2 + 7 × E bytes.
| Parte de la tabla | Bytes totales | Bytes por valor | Qué guarda |
|---|---|---|---|
Length |
2 | 2 | La cantidad de efectos instalados. |
Types |
E | 1 | El tipo de cada efecto, por ejemplo MixH, MixS o PalTransitionRGB. Indica qué cálculo debe realizarse.
|
Value1, Value2 y Value3 |
E por tabla; 3 × E entre las tres | 1 por tabla | Los valores de los canales que utiliza el efecto. En una transición, estos tres bytes guardan la dirección de la paleta de destino, no valores de canales. |
Ratio1, Ratio2 y Ratio3 |
E por tabla; 3 × E entre las tres | 1 por tabla | Cuánto se aplica el cambio en cada componente. Van de 0 a 127. |
Si el efecto es una transición hacia otra paleta, la herramienta también prepara esa paleta. Convierte sus colores al formato RGB o HSL que necesita la transición y los guarda en ROM. Durante el juego se puede utilizar ese destino sin tener que convertirlo de nuevo en cada llamada.
Estas paletas de transición son distintas de las paletas normales que se cargan a CGRAM: contienen tres componentes por color y se utilizan para calcular el efecto.
Cómo encuentra el código los recursos instalados
DXDefines.asm publica los IDs, direcciones, tamaños y offsets que necesitan los recursos. Por ejemplo, !DynamicPoseIDMySpriteIdle identifica una pose y !PaletteEffectIDMyEffect0 identifica el primer efecto de un archivo.
Los nombres se normalizan en PascalCase: se eliminan separadores como espacios, guiones y guiones bajos, y se pone en mayúscula la inicial de cada palabra. my_sprite, por ejemplo, pasa a MySprite.
También se genera BufferTable, que guarda las direcciones de los bloques donde se insertaron los recursos. Cada dirección ocupa 3 bytes y se añade un valor final $FFFFFF de otros 3 bytes. Para B bloques, ocupa 3 × (B + 1) bytes. La herramienta utiliza esa información para localizar los datos de una instalación anterior al reinstalar.
Una reinstalación puede cambiar IDs y direcciones. Por eso hay que volver a insertar los sprites, bloques y códigos UberASM que utilizan los defines generados.
El trabajo de cada frame
En la lógica principal se ejecutan el jugador, los sprites y los demás códigos del modo de juego. Los que utilizan Dynamic Xtreme solicitan las poses y paletas que necesitan, preparan los efectos locales y llaman a las rutinas de dibujo. Las cargas que se aceptan quedan anotadas para transferir sus datos durante NMI.
Al terminar esa lógica, el código de game mode de Dynamic Xtreme realiza las tareas que correspondan a las opciones habilitadas: prepara las actualizaciones de VRAM de la optimización de Kevin, mantiene el estado de las paletas y calcula los efectos globales pendientes.
Después, durante NMI, Dynamic Xtreme realiza estas tareas en orden, según las funcionalidades habilitadas:
- Pone
DX_Dynamic_CurrentDataSenden 0. - Incrementa
DX_Timer. - Incrementa
DX_SyncTimersi la acción no está congelada ni el juego está en pausa. - Ejecuta las transferencias de gráficos a VRAM.
- Ejecuta las transferencias de colores a CGRAM.
- Actualiza el color 0,0 si el sistema de efectos tiene un resultado pendiente.
El orden que seguiremos para explicarlo es, por tanto, lógica principal → tareas al terminar el game mode → transferencias durante NMI.
Lógica principal
Cada recurso decide qué necesita. Un sprite puede cambiar de pose, una animación puede pedir nuevos gráficos y un código UberASM puede cargar una paleta o seleccionar un efecto.
Si la pose solicitada ya está cargada, Dynamic Xtreme actualiza su tiempo de último uso para protegerla del reemplazo y no vuelve a cargar sus gráficos. Si no está cargada, comprueba el presupuesto de datos del frame y busca espacio disponible en VRAM antes de preparar su carga.
El dibujo también se prepara en esta parte: las rutinas escriben en los buffers OAM la posición, los tiles y las propiedades de las piezas que se mostrarán.
Una solicitud puede rechazarse, por ejemplo si no queda presupuesto para sus datos. El recurso que la pidió debe comprobar el resultado y decidir si conserva lo anterior o vuelve a intentarlo después. Una carga rechazada no queda guardada automáticamente para otro frame.
Tareas de game mode
En esta parte se realizan las siguientes tareas, según las funcionalidades habilitadas.
Antes de ejecutar la lógica del juego:
- Si está habilitado
ControllerOptimization, se ejecuta la rutina del control aquí en lugar de hacerlo en NMI. - Se comprueba si Dynamic Xtreme debe funcionar en el game mode actual. Si no debe hacerlo, se reinician sus variables. Esto ocurre cada frame que se permanezca en un modo no permitido.
Después se ejecuta la lógica normal del juego, incluidos los sprites y los códigos que solicitan cargas o cambios de paleta.
Cuando termina esa lógica:
- Si está habilitada la optimización de VRAM de Kevin, se ajustan los destinos de las actualizaciones de Lunar Magic antes de que se transfieran.
- Se ponen en 0 los indicadores de actualización de las paletas y se resta 1 a sus temporizadores de protección si todavía son mayores que 0. Esta parte se omite durante la pausa y en los modos no permitidos.
- Se aplican los efectos globales a las paletas que necesitan actualizarse y se preparan las transferencias de sus colores. Esta parte también se omite si la comprobación anterior impide continuar.
Las copias a VRAM y CGRAM se realizan después, durante NMI. Aquí se deja preparado lo que se va a copiar.
Game modes permitidos
Dynamic Xtreme utiliza dos comprobaciones de game mode:
- Sistema general:
Routines_AllowedGameMode, publicado como!AllowedGameMode. Se utiliza para decidir si se reinicializa el sistema, si se procesa su NMI y si se realizan las tareas de paletas. Las macros de asignación de paletas también consultan esta comprobación cuando necesitan procesar una carga del DynamicInfo. - Rutinas de sprites:
DXAllowedGameModeutiliza una tabla distinta. La carga de poses mediante!Uploady sus macros generales tiene una tabla con los mismos valores para decidir si procesa una solicitud nueva.
Por tanto, que el sistema general esté habilitado no significa que todas las funciones de sprites acepten cargas nuevas en ese modo. Esta tabla muestra las comprobaciones actuales; los IDs están en hexadecimal:
| Game mode | Pantalla o etapa de SMW | Sistema general | DXAllowedGameMode / solicitudes nuevas de Upload |
|---|---|---|---|
$06 |
Apertura del foco de la pantalla de título. | Sí | No |
$07 |
Pantalla de título. | Sí | Sí |
$08 |
Selección de partida. | Sí | No |
$09 |
Borrado de partida. | Sí | No |
$0A |
Selección de uno o dos jugadores. | Sí | No |
$0C |
Carga del overworld. | Sí | No |
$0D |
Aparición gradual del overworld. | Sí | No |
$0E |
Overworld. | Sí | Sí |
$12 |
Preparación del nivel. | Sí | No |
$13 |
Aparición gradual del nivel. | Sí | No |
$14 |
Juego dentro del nivel. | Sí | Sí |
$15 |
Desvanecimiento hacia Game Over / Time Up. | Sí | No |
$1B |
Escena de castillo o créditos. | No | Sí |
$1C |
Desvanecimiento hacia la casa de Yoshi de los créditos. | Sí | No |
$1D |
Carga de la casa de Yoshi de los créditos. | Sí | No |
$1E |
Aparición gradual de la casa de Yoshi de los créditos. | Sí | No |
$1F |
Casa de Yoshi de los créditos. | Sí | Sí |
$24 |
Aparición gradual de la presentación de enemigos. | Sí | No |
$25 |
Presentación de enemigos de los créditos. | Sí | Sí |
$29 |
Pantalla «The End». | No | Sí |
Los demás valores entre $00 y $2F. |
— | No | No |
Los nombres de las etapas corresponden a los game modes de SMW documentados en SMWDisX; los permisos se obtienen de las tablas de Dynamic Xtreme.
Cuando Upload no permite una solicitud nueva, comprueba si el recurso conserva una pose anterior. Si existe, actualiza su tiempo de último uso y devuelve carry activado; si no existe, devuelve carry desactivado. No solicita otra pose en ese camino.
Los modos $1B y $29 muestran por qué deben distinguirse ambas columnas: pasar la comprobación de sprites no habilita el NMI del sistema general ni evita que el game mode reinicialice sus variables. La columna de sprites no debe interpretarse como una garantía de funcionamiento completo en esos modos.
Transferencias durante NMI
NMI es una interrupción que SNES puede generar al comenzar VBlank, después de dibujar la parte visible de la pantalla. El juego aprovecha ese intervalo para actualizar gráficos, colores y otros datos de video.
La CPU ejecuta tanto la lógica del juego como la rutina NMI. Durante la lógica del juego, mueve sprites y prepara las solicitudes de carga. Al comenzar VBlank, una interrupción NMI puede detener temporalmente esa lógica para ejecutar las actualizaciones de video. Cuando termina la rutina NMI, la CPU retoma el código que había interrumpido.
Este es el recorrido habitual de un frame:
| Momento | Qué hace la CPU | Qué ocurre con los gráficos |
|---|---|---|
| Lógica del juego | Mueve sprites, selecciona poses, calcula efectos y prepara el dibujo. | Las solicitudes aceptadas se guardan en las listas de transferencias. |
| VBlank, durante NMI | Ejecuta la rutina NMI y configura las transferencias DMA. | Se copian los gráficos a VRAM, los colores a CGRAM y los datos de OAM a su memoria correspondiente. |
| Al terminar NMI | Retoma el código interrumpido. | Los datos actualizados se utilizan al dibujar la siguiente imagen. |
La tabla resume el recorrido habitual; durante slowdown, SMW puede ejecutar un NMI reducido, como se explica en el apartado de DX_Timer.
En los modos permitidos, la rutina de Dynamic Xtreme realiza estas tareas en orden, según las funcionalidades habilitadas:
- Pone
DX_Dynamic_CurrentDataSenden 0 para contar las solicitudes del siguiente ciclo de lógica. Las transferencias ya preparadas siguen guardadas en sus listas. - Incrementa
DX_Timer. - Comprueba
$9Dy$13D4|!addr. Si ambos están en 0, incrementaDX_SyncTimer. - Ejecuta las transferencias pendientes a VRAM, si está habilitado
GraphicsChange. - Ejecuta las transferencias pendientes a CGRAM, si está habilitado
PalettesChange. - Si está habilitado
PalettesEffects, ejecuta la actualización del color 0,0 cuando hay un resultado pendiente.
Para realizar las copias se utiliza DMA. NMI lee cada carga de las listas, configura desde dónde se copiarán los datos, cuántos bytes se copiarán y dónde se colocarán, y ejecuta la transferencia.
El tiempo de VBlank es limitado y SMW también lo necesita para sus actualizaciones de pantalla. Para limitar la cantidad de datos enviados en un frame y evitar flickering, Dynamic Xtreme utiliza DX_Dynamic_CurrentDataSend, que cuenta los bytes de las cargas solicitadas, y DX_Dynamic_MaxDataPerFrame, que establece el límite permitido. Antes de aceptar una carga normal, se comprueba que sus bytes no hagan superar ese límite.
Las listas de transferencias
Cada transferencia guarda el origen de los datos, su banco, la cantidad de bytes y el destino en VRAM o CGRAM.
Al realizar las copias, NMI recorre las listas desde la última entrada hacia la primera y después las vacía. Esto no borra los gráficos o colores cargados: permanecen en su destino hasta que otro código los cambie.
La lista de VRAM utiliza 0 cuando no tiene entradas. La de CGRAM utiliza $FF, porque guarda el índice de la última entrada en lugar de guardar directamente su cantidad.
Si dos códigos escriben sobre el mismo destino, deben coordinar sus cargas. El orden de las solicitudes no debe utilizarse como sustituto de decidir qué recurso puede ocupar cada espacio.
Normalmente, el recurso se lee desde ROM. Si el recurso se encuentra en un buffer en RAM, el usuario debe conservar su contenido hasta que NMI realice la transferencia. Por ejemplo, si prepara una paleta en RAM, no debe modificar ni reutilizar ese buffer antes de que se copie. La lista guarda su dirección, no una copia de sus datos; Dynamic Xtreme no protege el buffer contra cambios realizados por otros códigos.
Presupuesto de datos
Las macros de transferencia, como %TransferToVRAM() y %TransferToCGRAM(), comprueban cuántos bytes se han solicitado mediante DX_Dynamic_CurrentDataSend. Si al añadir la nueva carga se supera DX_Dynamic_MaxDataPerFrame, la rechazan. Si la aceptan, suman sus bytes al contador.
El límite predeterminado es $0800, equivalente a 2048 bytes. Su objetivo es evitar flickering por enviar demasiados datos en un mismo frame. Es un límite de cantidad de datos, no una medida exacta del tiempo que tarda cada transferencia.
DX_Dynamic_CurrentDataSend lleva la cuenta de los bytes solicitados. La comprobación CurrentDataSend + bytes del recurso > MaxDataPerFrame impide aceptar una carga normal que supere el presupuesto y limita así los datos que se transferirán durante VBlank.
En las solicitudes de poses dinámicas, la falta de presupuesto y la falta de espacio utilizable en VRAM devuelven el mismo resultado de fallo. La llamada no indica cuál de las dos causas impidió la carga. Un fallo tampoco significa que se haya sobrecargado VBlank: si se debe al presupuesto, la carga se rechazó precisamente para evitar esa sobrecarga.
Estas macros tienen variantes como %ForcedTransferToVRAM() y %ForcedTransferToCGRAM(), que aceptan la carga sin comprobar si supera el límite. Sus bytes también se suman a DX_Dynamic_CurrentDataSend, por lo que reducen lo que queda disponible para las siguientes cargas normales. Se utilizan para cargas prioritarias, como las del jugador; usarlas no evita el flickering si se solicita una cantidad excesiva de datos.
Para consultar la diferencia entre el presupuesto de bytes y la capacidad de las listas, las restricciones de las transferencias y las comprobaciones que debe realizar tu código, consulta PPU en la API ASM.
Los dos relojes
DX_Timer: protección de las solicitudes
DX_Timer avanza cuando se ejecuta la parte de NMI de Dynamic Xtreme. Además de medir cuánto lleva una pose sin utilizarse, sirve para evitar que un recurso repita sus solicitudes antes de que se procesen las anteriores.
No siempre hay un ciclo de lógica por cada ejecución completa de NMI. Durante slowdown, SMW puede ejecutar una versión reducida de NMI, y puede haber dos ciclos de lógica sin que entre ellos se hayan procesado las listas de transferencias de Dynamic Xtreme.
Sin una protección, un recurso podría añadir otra vez sus cargas a esas listas. Las solicitudes repetidas ocuparían más espacio y podrían provocar flickering.
Para evitarlo, las rutinas de carga de poses para sprites comparan DX_Timer con el valor de dos bytes guardado por ese sprite, normalmente llamado SafeFrame. En los aliases de sprites se utiliza !SafeFrameLowByte,x y !SafeFrameHighByte,x.
El proceso es el siguiente:
- Compara los relojes. Si
SafeFramecoincide conDX_Timer, ese sprite ya procesó su carga de poses con ese valor del reloj. La rutina omite una nueva solicitud. - Guarda el intento antes de procesarlo. Si los valores son distintos, copia
DX_TimeraSafeFramey después comprueba si puede reutilizar la pose o necesita solicitar otra. - Comprueba el resultado. Si necesita una pose nueva, la solicitud puede aceptarse o rechazarse. Aunque sea rechazada, el valor guardado en
SafeFrameno se deshace.
Por tanto, SafeFrame indica cuándo se procesó el último intento, no cuándo se consiguió cargar una pose. Si la carga falla, volver a ejecutar la rutina con el mismo DX_Timer no permite intentarla otra vez, aunque entre ambas llamadas haya cambiado la pose deseada o el espacio disponible.
En !Upload y !UploadWithoutPoseOffset, tanto una solicitud rechazada como una llamada omitida por esta protección utilizan el mismo camino de respaldo:
- Si
!LastPoseHashIndex,xes distinto de$FF, actualizan el tiempo de último uso de esa pose para protegerla del reemplazo y devuelven carry activado. El sprite puede seguir dibujando la última pose disponible. - Si contiene
$FF, no existe una pose de respaldo y devuelven carry desactivado.
Por ejemplo, durante slowdown puede ocurrir esta secuencia:
DX_Timer = $0030. El sprite guarda$0030en suSafeFramee intenta obtener una pose nueva. No queda presupuesto de datos y la solicitud falla; conserva la pose anterior si tiene una.- SMW ejecuta otro ciclo de lógica sin que se haya procesado el NMI de Dynamic Xtreme. El reloj sigue en
$0030. La siguiente llamada del mismo sprite detecta la coincidencia y no repite la solicitud. - Cuando se ejecuta el NMI de Dynamic Xtreme,
DX_Timeravanza a$0031. En la siguiente llamada, los relojes ya no coinciden y el sprite puede volver a intentarlo. La solicitud rechazada no se reintenta sola: el código del sprite debe volver a llamar a la rutina.
Esta protección está activada en los aliases de carga de poses para sprites. En GeneralUpload y Upload (macro general) se controla con safeCheck; el respaldo de la pose anterior se controla por separado con checkLast. TakeDynamicRequest y las macros de transferencia PPU no realizan esta comprobación de SafeFrame por ti.
DX_Timer continúa avanzando durante la pausa cuando se ejecuta esa parte de NMI.
DX_SyncTimer: animaciones sincronizadas
La función principal de DX_SyncTimer es dar a los sprites un reloj común para sus animaciones. Por ejemplo, varios sprites que utilizan las mismas poses pueden calcular con este reloj qué pose les corresponde cada cuatro frames.
Así todos cambian al mismo tiempo y solicitan la misma pose. La primera solicitud puede necesitar cargarla; las demás la reutilizan sin programar otra carga de sus gráficos. Esto ahorra transferencias DMA y datos enviados por frame.
Si cada sprite llevara un reloj independiente, podrían terminar usando poses distintas de la misma animación al mismo tiempo y necesitar más cargas y espacio de VRAM.
DX_SyncTimer se detiene durante la pausa y cuando se congela la acción, para que las animaciones sincronizadas también se detengan.
Carga de gráficos: GraphicsChange
GraphicsChange guarda en RAM una lista de solicitudes de carga. Durante NMI se recorren esas solicitudes y se copian sus datos a VRAM.
Variables de la lista
En esta tabla, V es el valor de !VRAMTransferSize: 64 por defecto. Las cuatro tablas de solicitudes reservan V bytes cada una; con entradas de 2 bytes, ese valor deja espacio para 32 solicitudes. Junto con el contador, reservan 1 + 4 × V bytes: 257 por defecto.
| Variable o tabla | Bytes totales | Bytes por valor | Qué guarda |
|---|---|---|---|
DX_PPU_VRAM_Transfer_Length |
1 | 1 | Cantidad de solicitudes pendientes. El valor 0 indica que la lista está vacía. |
DX_PPU_VRAM_Transfer_Source |
V; 64 por defecto | 2 | Dirección de origen de cada solicitud, sin el banco. |
DX_PPU_VRAM_Transfer_SourceBNK |
V; 64 por defecto | 2 reservados; banco de 1 byte | Banco del origen. |
DX_PPU_VRAM_Transfer_SourceLength |
V; 64 por defecto | 2 | Cantidad de bytes que se copiarán. |
DX_PPU_VRAM_Transfer_Offset |
V; 64 por defecto | 2 | Dirección de destino en VRAM. |
Las tablas mantienen el mismo orden: la primera entrada de cada tabla pertenece a la primera solicitud, la segunda a la segunda, etc. Cada entrada de estas tablas ocupa 2 bytes, por lo que se avanza con desplazamientos de $00, $02, $04, etc.
Estos nombres representan direcciones de RAM calculadas en DXDefines.asm. Sus direcciones pueden cambiar según la configuración y las funcionalidades habilitadas.
Añadir una solicitud
La macro %TransferToVRAM() realiza los siguientes pasos:
- Calcula cuántos bytes habría en total al añadir la nueva carga a
DX_Dynamic_CurrentDataSend. - Compara el resultado con
DX_Dynamic_MaxDataPerFrame. Si lo supera, devuelve el carry desactivado y no añade la solicitud. - Utiliza
DX_PPU_VRAM_Transfer_Lengthpara encontrar la siguiente entrada y aumenta ese contador en 1. - Guarda el origen, banco, cantidad de bytes y destino en las tablas.
- Actualiza
DX_Dynamic_CurrentDataSendcon los bytes de la carga y devuelve el carry activado.
%ForcedTransferToVRAM() guarda los mismos datos, pero no comprueba el límite de bytes. Las variantes FromAddr y Resource facilitan obtener el origen desde una dirección completa o desde los defines de un recurso.
Añadir una solicitud no copia los gráficos inmediatamente: deja la información preparada para NMI.
Ejecutar las solicitudes durante NMI
La rutina VRAMDMA:
- Lee
DX_PPU_VRAM_Transfer_Length. Si es 0, termina porque no hay solicitudes. - Calcula el desplazamiento de la última entrada:
(Length - 1) × 2. - Lee su origen, banco y cantidad de bytes, y los coloca en los registros de DMA.
- Escribe el destino de VRAM en
$2116-$2117e inicia la transferencia mediante$420B. - Retrocede 2 bytes para procesar la solicitud anterior. Repite hasta completar la primera.
- Escribe 0 en
DX_PPU_VRAM_Transfer_Lengthpara indicar que la lista está vacía.
La lista se ejecuta del final al inicio. No hace falta borrar todos sus datos al terminar: al quedar su largo en 0, la próxima solicitud volverá a utilizar la primera entrada.
Animaciones con un destino fijo
El código de una animación debe:
- Elegir la imagen que corresponde al paso actual.
- Calcular su dirección de origen.
- Solicitar la copia al destino de VRAM de la animación.
- Comprobar el carry. Si se rechaza la carga, conservar la imagen anterior y volver a intentarlo.
El destino puede ser siempre el mismo. Lo que cambia es la imagen que se copia allí; la lista de transferencias no decide cuándo debe avanzar la animación.
Puedes ver un código completo en Crear animaciones de gráficos de la guía de UberASM.
Carga de una pose dinámica
La carga de una pose utiliza la caché para no repetir transferencias de gráficos que ya están disponibles:
- Busca el ID en la tabla hash.
- Si ya está registrado, actualiza su tiempo de uso y no añade otra carga.
- Si no está, comprueba la cantidad de bytes y busca un espacio adecuado en el mapa de VRAM.
- Elige qué poses pueden reemplazarse, teniendo en cuenta su tiempo sin uso y el espacio que necesita la nueva.
- Actualiza las tablas de la caché y el mapa.
DynamicRoutinedivide los datos de la pose en las transferencias necesarias.- NMI ejecuta esas transferencias.
Cada paso se desarrolla en Sprites dinámicos: DynamicPoses, incluyendo la búsqueda por hash, el mapa, la política de reemplazo y la división de la carga en filas.
Carga y asignación de paletas: PalettesChange
El sistema distingue dos valores:
- ID de paleta: identifica los colores guardados en ROM.
- Color de destino: indica desde qué color de la paleta de colores de CGRAM se colocarán los colores que se carguen. Por ejemplo,
$91comienza en el color 1 de la paleta 9.
La asignación automática permite compartir colores entre sprites sin exigir un destino fijo para cada uno.
Lista de cargas a CGRAM
En esta tabla, C es !CGRAMTransferSize: 64 por defecto. El conjunto reserva 1 + 7 × C bytes, es decir, 449 con ese valor. Aunque SourceBNK reserva 2 × C bytes, el código utiliza un byte de banco por solicitud.
| Variable o tabla | Bytes totales | Bytes por valor | Qué guarda |
|---|---|---|---|
DX_PPU_CGRAM_Transfer_Length |
1 | 1 | Índice de la última solicitud pendiente. $FF indica una lista vacía; 0 indica que hay una solicitud.
|
DX_PPU_CGRAM_Transfer_Source |
2 × C; 128 por defecto | 2 | Dirección de origen, sin el banco. |
DX_PPU_CGRAM_Transfer_SourceBNK |
2 × C; 128 reservados por defecto | 1 utilizado por solicitud | Banco del origen. |
DX_PPU_CGRAM_Transfer_SourceLength |
2 × C; 128 por defecto | 2 | Cantidad de bytes que se copiarán. Cada color ocupa 2 bytes. |
DX_PPU_CGRAM_Transfer_Offset |
C; 64 por defecto | 1 | Primer color de destino en CGRAM. |
Durante NMI, CGRAMDMA:
- Comienza por la última solicitud pendiente.
- Configura DMA con el origen y la cantidad de bytes de esa solicitud.
- Selecciona el primer color mediante
$2121y realiza la copia. - Continúa con las solicitudes anteriores.
- Al terminar, escribe
$FFenDX_PPU_CGRAM_Transfer_Length.
Las macros TransferPalette también actualizan las copias de colores y el estado de efectos:
- Sin efecto global: solicitan la carga y actualizan los buffers de SMW. Así el juego no vuelve a utilizar colores anteriores durante la animación de victoria del goal.
- Con efecto global: guardan los colores originales y preparan el estado necesario para aplicarles el efecto antes de enviarlos a CGRAM.
Asignación automática
| Variable o tabla | Bytes totales | Bytes por valor | Para qué sirve |
|---|---|---|---|
DX_Dynamic_Palettes_ID |
32 | 2 | Guarda el ID asignado a cada destino. $FFFF indica una entrada libre y $FFFE una reservada.
|
DX_Dynamic_Palettes_DisableTimer |
16 | 1 | Protege cada destino del reemplazo automático mientras está en uso. |
DX_Dynamic_Palettes_EffectEnabled |
1 | 1; un bit por paleta de sprites | Indica qué paletas de sprites están asignadas con efecto global habilitado. Esta condición también se compara al compartir una asignación. |
La búsqueda sigue este orden:
- Reutilizar una asignación con el mismo ID y una condición de efecto compatible.
- Si no existe, utilizar una entrada libre.
- Si no hay entradas libres, reemplazar una que no esté reservada y cuyo temporizador sea 0.
- Si no encuentra ninguna, indicar que la asignación falló.
Mientras el recurso utiliza la paleta, mantiene su temporizador en 2. Llegar a 0 no borra los colores: la asignación puede reutilizarse si otro recurso todavía no la reemplazó.
Asignación manual y versión
- Manual con carga: el código elige el destino y solicita sus colores.
- Manual sin carga: el código elige el destino y utiliza los colores que ya estaban preparados.
- Versión: selecciona una entrada de la lista de paletas del DynamicInfo. La última versión procesada permite comprobar si esa selección cambió.
Dos recursos que elijan el mismo destino con colores distintos deben coordinarse. Mantener la protección de una paleta no significa volver a cargar sus colores.
Dibujo de poses: DrawingSystem
El dibujo prepara las piezas de una pose en OAM: qué tiles se muestran, dónde se colocan y qué propiedades utilizan. Los datos gráficos deben estar en VRAM.
Pasos del dibujo
- Busca la rutina y los datos de la pose mediante
GraphicRoutineIDs,GraphicRoutineyTableOffset. - Obtiene la cantidad de piezas desde
NumberOfTilesMinus1. - Calcula la posición de cada tile usando la posición del sprite, sus desplazamientos y los volteos que correspondan.
- Comprueba si la pieza puede verse y si queda espacio de OAM.
- Escribe su posición, número de tile, propiedades y tamaño en los buffers OAM.
El número de tile depende del dibujo:
| Caso | Cómo se obtiene el tile |
|---|---|
| Pose dinámica | Se ajusta al lugar de VRAM donde quedaron cargados sus gráficos. |
| Pose estática | Se utiliza su número de tile fijo. |
| Pose que utiliza un tile común | El código debe preparar !Tile antes de llamar al dibujo.
|
Prioridad y espacio disponible
- En LoROM, el sistema lleva su propio control de entradas OAM disponibles.
- En SA-1, utiliza la integración con MaxTile.
- La prioridad de dibujo organiza las piezas de sprites. No es la misma propiedad que determina si un tile queda delante o detrás de las capas.
- Los límites por línea de pantalla de SNES siguen existiendo. Tener una pose cargada no garantiza que puedan verse todas sus piezas.
Jugador y Yoshi
PlayerFeatures
El jugador original tiene rutinas propias para elegir y cargar las piezas de sus poses. PlayerFeatures permite integrar ese trabajo con las cargas de Dynamic Xtreme y cambiar el origen de sus gráficos y paletas.
Con el formato original, el juego sigue eligiendo las piezas de la pose. La rutina obtiene sus direcciones desde el origen configurado y compara esas direcciones con las anteriores para cargar las que hayan cambiado.
Estas cargas utilizan transferencias forzadas y sus bytes también cuentan para las solicitudes normales posteriores. Así conservan su prioridad sin quedar fuera de la contabilidad del sistema.
La paleta del jugador tiene un tratamiento especial: combina colores y selecciones del propio juego con el origen personalizado. No funciona simplemente como sustituir una paleta completa de un sprite cualquiera.
El soporte para jugadores personalizados permite que otros sistemas administren de otra forma sus poses y dibujo. No convierte automáticamente cualquier spritesheet al formato del jugador original.
YoshiFeatures
Yoshi también carga sus gráficos por piezas. YoshiFeatures permite cambiar su origen manteniendo la organización que esperan sus rutinas.
El código calcula qué partes necesita y prepara sus cargas a los destinos correspondientes de VRAM. Como las del jugador, estas cargas tienen un tratamiento prioritario.
Esta opción necesita PlayerFeatures y comparte sus restricciones de compatibilidad. No permite evitar un conflicto del sistema del jugador habilitando únicamente las funciones de Yoshi.
Diferencias entre LoROM y SA-1
Las principales diferencias de Dynamic Xtreme entre ambas configuraciones están en la memoria utilizada para sus variables y en el sistema de dibujo:
| Aspecto | LoROM | SA-1 |
|---|---|---|
| Memoria para las variables | Se suele utilizar Free7E2000 para aprovechar la RAM recuperada como espacio para las variables de Dynamic Xtreme. Si esta opción provoca problemas de compatibilidad, puede deshabilitarse y utilizar la configuración alternativa, con el espacio de RAM más ajustado.
|
No utiliza Free7E2000. Las áreas principales de variables se encuentran en BW-RAM, en los bancos $40 y $41.
|
| Prioridades de dibujo | Utiliza el sistema propio de Dynamic Xtreme, con dos prioridades de dibujo. | Utiliza MaxTile, con cuatro prioridades de dibujo para los tiles. |
Las dos prioridades de LoROM no permiten mantener todas las distinciones de orden de las cuatro prioridades de SA-1. Por eso, algunos recursos que se dibujan correctamente en SA-1 pueden presentar un orden de dibujo diferente o incorrecto en LoROM. Estas prioridades organizan los tiles de sprites; no son la prioridad que determina si aparecen delante o detrás de las capas.
Los rangos de RAM de cada configuración se detallan en Espacios de RAM utilizados.
Memoria y optimizaciones
Free7E2000
SMW mantiene en RAM gráficos originales del jugador, Yoshi y ciertas animaciones. En ROMs sin SA-1, esta opción adapta las cargas para leer esos datos desde ROM.
Dynamic Xtreme aprovecha la RAM recuperada para sus variables internas. No es un espacio que quede libre para que cualquier otro código escriba encima.
SA-1 utiliza otras direcciones y desactiva esta opción. Los rangos están en Espacios de RAM utilizados.
ControllerOptimization
Esta opción mueve el procesamiento del control desde NMI a la parte de game mode. El objetivo es dejar más tiempo para las actualizaciones de pantalla, no añadir un frame de retraso ni cambiar los botones que recibe el juego.
FixedColorOptimization
El color fijo se utiliza en las mezclas de color de la consola. Para actualizarlo, el juego prepara sus componentes y los envía al registro correspondiente.
La optimización reemplaza esa preparación por una secuencia más corta. El color sigue enviándose a la consola, pero necesita menos trabajo durante NMI.
KevinLMVRAMOptimization
Lunar Magic utiliza listas llamadas stripe images para actualizar partes de los mapas de tiles. Esas listas incluyen tanto los datos como la información sobre sus destinos.
La versión integrada de la optimización de Kevin prepara fuera de NMI parte del ajuste de esos destinos. Así deja menos trabajo de preparación para el momento de la transferencia.
Esta opción sustituye el uso del parche original por separado. Además, no debe desinstalarse después de instalarla; hay que tenerlo en cuenta antes de activarla.
StatusBarOptimization
La barra de estado original actualiza zonas concretas de VRAM. Esta opción prepara sus transferencias de forma más directa, reduciendo la configuración de DMA que necesita hacer durante NMI.
No añade una barra nueva. Modifica la actualización de la original y, por eso, entra en conflicto con recursos que la reemplazan, modifican o eliminan.
Sprites dinámicos: DynamicPoses
El sistema funciona como una caché de poses en VRAM. No reserva una copia de los gráficos para cada sprite: guarda la pose y permite que la reutilicen todos los recursos que la necesiten.
- Una pose se carga una sola vez mientras permanezca en la caché.
- Las siguientes solicitudes actualizan su tiempo de último uso, sin añadir otra carga de sus gráficos.
- La pose no se borra cuando un recurso deja de utilizarla. Permanece allí hasta que se necesite su espacio y se elija reemplazarla.
- Si fue reemplazada y vuelve a solicitarse, tendrá que cargarse otra vez.
La elección no depende de cuánto tiempo lleva la pose desde que se cargó, sino de cuánto tiempo lleva sin utilizarse y de si su espacio sirve para la pose nueva.
Tres valores distintos
| Valor | Qué identifica |
|---|---|
| ID de pose | La pose instalada. Tiene 16 bits y permite consultar sus datos en ROM. |
| Índice de la tabla hash | La entrada de RAM donde se registró la pose. Va de 0 a 127. |
| Bloque de VRAM | ID de una de las 128 posiciones para tiles de 16×16 que caben en el espacio de VRAM de sprites. Se numeran de izquierda a derecha y de arriba abajo, desde 0 hasta 127. Para una pose que ocupa varios bloques, se guarda el ID de su primer bloque. |
El índice de la tabla hash y el bloque de VRAM no tienen por qué coincidir. Una entrada de la tabla puede registrar una pose que empieza en cualquier bloque permitido.
Tablas de las poses registradas
| Variable o tabla | Bytes totales | Bytes por valor | Qué guarda |
|---|---|---|---|
DX_Dynamic_Pose_Length |
1 | 1 | Cantidad de poses registradas. |
DX_Dynamic_Pose_HashSize |
128 | 1 | Cantidad de poses registradas para cada hash. |
DX_Dynamic_Pose_ID |
256 | 2 | ID completo de la pose de cada entrada. $FFFF indica una entrada libre.
|
DX_Dynamic_Pose_Offset |
128 | 1 | Bloque de VRAM donde comienza cada pose. |
DX_Dynamic_Pose_TimeLastUse |
256 | 2 | Valor de DX_Timer de su último uso.
|
Estas tablas ocupan 769 bytes en RAM. Guardan las poses registradas durante el juego; las tablas generadas desde el DynamicInfo describen, en cambio, todas las poses instaladas.
Cómo se busca una pose
La rutina DynamicPoseHashmap_FindPose utiliza una tabla hash de 128 entradas. El hash permite elegir dónde empezar a buscar:
hash = ID de pose AND $007F
Dos IDs diferentes pueden tener el mismo hash. Por ejemplo, $0005 y $0085 producen $05. Por eso el hash no basta para confirmar que la pose está cargada: también se compara el ID completo.
La búsqueda sigue estos pasos:
- Guarda el ID solicitado y calcula su hash.
- Si
DX_Dynamic_Pose_Lengthes 0, no hay poses registradas y la búsqueda termina. - Consulta
DX_Dynamic_Pose_HashSize[hash]. Si es 0, ninguna pose registrada tiene ese hash y también termina. - Comienza por la entrada cuyo índice coincide con el hash.
- Lee el ID de esa entrada. Si coincide con el solicitado, encontró la pose.
- Si no coincide, continúa con la siguiente entrada de la secuencia de búsqueda.
La secuencia avanza 15 entradas por intento y vuelve al comienzo al superar 127:
siguiente índice = (índice actual + 15) AND $7F
Con 128 entradas, ese paso permite visitar todas las posiciones antes de repetir una. Para el hash $05, el comienzo de la secuencia es:
$05 → $14 → $23 → $32 → $41 → ...
El contador de poses de cada hash
Cada hash tiene su propio contador en DX_Dynamic_Pose_HashSize. Por ejemplo, si hay dos poses registradas cuyo ID produce el hash $05, la entrada DX_Dynamic_Pose_HashSize+$05 contiene 2.
Ese número indica cuántas poses con ese hash existen, no cuántas posiciones hay que recorrer. La búsqueda utiliza una copia temporal del contador:
- Comienza con la cantidad de poses registrada para el hash solicitado.
- Recorre las posiciones de la secuencia hasta encontrar el ID solicitado o haber comprobado esa cantidad de poses con el mismo hash.
- Cada vez que encuentra una pose con ese hash, pero con otro ID, resta 1 al contador temporal.
- Las posiciones libres y las poses de otros hashes no descuentan nada: continúa buscando.
- Si el contador temporal llega a 0 sin encontrar el ID, la pose solicitada no está registrada y la búsqueda termina.
El contador de DX_Dynamic_Pose_HashSize no se modifica durante esta búsqueda. Solo se actualiza al añadir o retirar poses de la caché.
Por ejemplo, se busca $0105, que produce el hash $05, y el contador de ese hash vale 2:
| Posición visitada | ID guardado | Hash del ID | Poses del hash $05 que faltan por comprobar |
|---|---|---|---|
$05 |
$0005 |
$05 |
1: tiene el mismo hash, pero no es el ID solicitado. |
$14 |
$0014 |
$14 |
1: es otro hash, así que no se descuenta. |
$23 |
$0085 |
$05 |
0: ya se comprobaron las dos poses con ese hash. $0105 no está registrada.
|
Lo habitual es que haya una o dos poses por hash. Esto permite resolver la búsqueda con pocas comprobaciones y un coste normalmente cercano a O(1). No significa que siempre se visiten solo una o dos posiciones: puede haber entradas de otros hashes entre ellas. Con muchas colisiones, el recorrido puede ser más largo.
Una entrada libre no termina la búsqueda. Puede haber quedado vacía al reemplazar una pose y existir otra del mismo hash más adelante. El contador permite saber cuándo ya se comprobaron todas las poses de ese hash, incluso si hay huecos entre ellas.
Cuando encuentra la pose
TakeDynamicRequest:
- Obtiene el índice de la entrada encontrada.
- Escribe el valor actual de
DX_TimerenDX_Dynamic_Pose_TimeLastUsepara esa entrada. - Devuelve el éxito de la solicitud, sin volver a programar la carga de sus gráficos.
Así todos los recursos que solicitan ese ID utilizan la misma pose y la mantienen protegida. No importa si el recurso es un sprite normal, cluster, extended u otro código que utiliza el sistema.
Esto también evita duplicar una carga dentro del mismo ciclo: cuando la primera solicitud se acepta, la pose ya queda registrada aunque la transferencia física se vaya a realizar durante NMI.
Cuando la pose no está registrada
TakeDynamicRequest continúa con la preparación de una carga nueva:
- Consulta
PoseSizey calculaDX_Dynamic_CurrentDataSend + PoseSize. - Si supera
DX_Dynamic_MaxDataPerFrame, termina con fallo, sin cambiar las poses existentes. - Consulta
Pose16x16Blockspara saber cuántos bloques necesita. - Llama a
VRAMMap_GetBestSlotpara buscar un espacio adecuado. - Si no existe un espacio utilizable, termina con fallo.
- Retira del mapa y de la tabla hash las poses que ocupen el espacio elegido.
- Busca una entrada libre de la tabla hash y registra la pose nueva.
- Actualiza el mapa de VRAM y el contador de datos del frame.
- Llama a
DynamicRoutinepara añadir las transferencias.
El presupuesto se comprueba antes de reemplazar poses. Si la nueva carga no cabe en el frame, no se libera espacio para una solicitud que no se puede atender.
Añadir y quitar entradas de la tabla hash
DynamicPoseHashmap_FindFreeSpacebusca una entrada cuyo ID sea$FFFF, siguiendo la misma secuencia de pasos de 15.DynamicPoseHashmap_Addguarda el ID y su tiempo de uso, aumentaDX_Dynamic_Pose_Lengthy aumenta el contador de su hash.DynamicPoseHashmap_Removeescribe$FFFFen el ID retirado y reduce ambos contadores.
Retirar una entrada no borra por sí solo los píxeles de VRAM. Indica que esa pose dejó de estar disponible en la caché; las nuevas transferencias sobrescribirán el espacio elegido.
Cómo se representa el mapa de VRAM
El mapa representa 128 posiciones de tiles de 16×16 en la VRAM de sprites, numeradas de izquierda a derecha y de arriba abajo. Cada bloque es una de esas posiciones, no un tramo de 128 bytes contiguos.
Un tile de 16×16 está formado por cuatro tiles de 8×8 distribuidos en dos filas. El mapa administra las posiciones que ocupa la pose; la distribución de sus datos para cargarlos se resuelve después en DynamicRoutine.
| Tabla | Bytes totales | Bytes por valor | Contenido |
|---|---|---|---|
DX_Dynamic_Tile_Offset |
128 | 1 | Posición donde comienza la sección. El bit $80 indica que está restringida: ese espacio no se utiliza para poses dinámicas.
|
DX_Dynamic_Tile_Size, también llamada DX_Dynamic_Tile_Pose |
128 | 1 | Si el bit $80 está activo, los otros bits guardan la cantidad de posiciones de la sección menos uno. Si está desactivado, guardan el índice hash de la pose que ocupa la sección. Para saber si el espacio está restringido, se consulta Offset.
|
Estas dos tablas ocupan 256 bytes. Junto con las tablas de poses, el administrador utiliza 1025 bytes de RAM, sin contar las listas de transferencias ni las scratch de trabajo.
Secciones del mapa
Una sección agrupa posiciones consecutivas del mapa que están restringidas, libres o utilizadas por una pose. Su información se guarda en la primera y la última posición. Esto permite saltar de una sección a la anterior sin revisar todas sus posiciones intermedias.
Los siguientes ejemplos muestran tres estados posibles para una sección de 6 posiciones, de $20 a $25. Son casos alternativos, no tres secciones que ocupen el mismo espacio a la vez.
Ejemplo 1: sección restringida
| Entradas de las tablas | Valor | Significado |
|---|---|---|
DX_Dynamic_Tile_Offset+$20 y +$25 |
$A0 |
$80 indica restringida y $20 indica dónde comienza.
|
DX_Dynamic_Tile_Size+$20 y +$25 |
$85 |
La sección tiene 6 posiciones: ($85 AND $7F) + 1 = 6.
|
Aunque el bit alto de Size esté activo, no es espacio libre para poses dinámicas: el bit de restricción en Offset hace que se descarte. No se evalúa como una pose que pueda reemplazarse.
DynamicPoseSpaceConfig utiliza este tipo de sección para excluir las partes de SP1-SP4 que no deben utilizar los sprites dinámicos.
Ejemplo 2: sección libre
| Entradas de las tablas | Valor | Significado |
|---|---|---|
DX_Dynamic_Tile_Offset+$20 y +$25 |
$20 |
Comienza en $20 y no está restringida.
|
DX_Dynamic_Tile_Size+$20 y +$25 |
$85 |
El bit $80 indica el tamaño de una sección libre: $05 + 1 = 6 posiciones.
|
Esta sección sí está disponible. Puede alojar una pose que necesite hasta 6 posiciones; si se coloca una de 4, las otras 2 quedan como otra sección libre.
Ejemplo 3: sección utilizada por una pose
Supongamos que una pose de 6 bloques está registrada en la entrada hash $07 y ocupa las posiciones $20 a $25:
| Entradas de las tablas | Valor | Significado |
|---|---|---|
DX_Dynamic_Tile_Offset+$20 y +$25 |
$20 |
La sección comienza en $20 y no está restringida.
|
DX_Dynamic_Tile_Size+$20 y +$25 |
$07 |
El bit $80 está desactivado: $07 es el índice hash de la pose, no su cantidad de bloques.
|
Para conocer la cantidad de posiciones y el tiempo sin uso:
- Se lee el ID de la pose en
DX_Dynamic_Pose_ID+$0E. Se utiliza$0Eporque cada ID ocupa 2 bytes y$07 × 2 = $0E. - Con ese ID se consulta
Pose16x16Blocks, que en este ejemplo contiene 6. - Se consulta
DX_Dynamic_Pose_TimeLastUse+$0Epara calcular cuánto lleva sin utilizarse.
La entrada DX_Dynamic_Pose_Offset+$07 guarda $20, relacionando esa pose registrada con su posición en VRAM.
Recorrido y búsqueda de espacio
VRAMMap_GetBestSlot comienza por la posición de tile de 16×16 $7F. Para recorrer el mapa:
- Lee
DX_Dynamic_Tile_Offsetde la posición actual. - Obtiene el comienzo de la sección con
Offset AND $7F. - Evalúa esa sección y los espacios que pueda formar con otras secciones contiguas.
- Pasa a la posición
comienzo de la sección - 1, que es la última posición de la sección anterior. - Repite hasta llegar al comienzo del mapa.
Por ejemplo, si las últimas secciones comienzan en $70, $60 y $50:
| Posición visitada | Comienzo leído de Offset | Próxima posición |
|---|---|---|
$7F |
$70 |
$6F
|
$6F |
$60 |
$5F
|
$5F |
$50 |
$4F
|
Así recorre secciones completas, no cada tile intermedio.
Qué se evalúa durante el recorrido
El espacio candidato puede ser una sección o varias secciones contiguas. En cada paso se comprueba:
- Si hay una sección restringida, no se utiliza ni se combina a través de ella.
- Si hay una pose protegida por su uso reciente, tampoco puede formar parte del espacio.
- Si el candidato todavía es pequeño, se puede continuar con una sección contigua para completar la cantidad de bloques necesaria.
- Si ya cabe la pose, se compara el candidato con el mejor encontrado: primero por su score de tiempo sin uso y después por el espacio suficiente más pequeño.
No se elige una sección solo por contener la pose con más tiempo sin uso. El espacio completo debe permitir cargar la pose nueva. La puntuación de varias secciones combinadas depende de la pose utilizada más recientemente dentro de ese espacio.
Durante la búsqueda se conservan estos datos temporales:
| Datos de trabajo | Bytes totales | Bytes por valor | Para qué se usan |
|---|---|---|---|
VRAMMapCurrentSpace |
3 | 1 por campo | Comienzo, score y cantidad de bloques del espacio candidato actual. |
VRAMMapBestSpace |
3 | 1 por campo | Los mismos campos para el mejor espacio encontrado. |
VRAMMapTMP_Size |
1 | 1 | Cantidad de bloques que necesita la pose nueva. |
Estos nombres utilizan scratch de trabajo, no RAM persistente adicional.
Política de reemplazo: tiempo sin uso y espacio suficiente
El sistema no elige simplemente la pose que se cargó primero. Utiliza el tiempo transcurrido desde su último uso:
tiempo sin uso = DX_Timer - TimeLastUse de la pose
VRAMMapSlot_GetSizeAndScore obtiene el tamaño y una puntuación para cada región:
| Caso | Puntuación y tratamiento |
|---|---|
| Región libre | $FF. Se favorece frente a regiones ocupadas.
|
| Región ocupada | Tiempo sin uso, limitado a $FE.
|
| Tiempo sin uso de 0 o 1 | No se puede reemplazar: la pose sigue protegida. |
| Región restringida | Se descarta: ese espacio no se utiliza para poses dinámicas. |
La protección exige al menos dos avances de DX_Timer sin utilizar la pose. Cada recurso que vuelve a solicitarla actualiza su tiempo de uso y vuelve a protegerla.
Por qué no siempre se reemplaza la pose con más tiempo sin uso
La pose nueva necesita espacio suficiente y contiguo. Un espacio pequeño no sirve por sí solo, aunque lleve mucho tiempo sin utilizarse.
Por ejemplo, para una pose nueva de 4 bloques:
| Espacio candidato | Tiempo sin uso | ¿Sirve? |
|---|---|---|
| Pose A: 2 bloques, sin espacio contiguo utilizable | 20 | No: solo deja 2 bloques. |
| Pose B: 4 bloques | 8 | Sí: puede alojar la pose nueva. |
En este caso se puede reemplazar B y conservar A. A lleva más tiempo sin uso, pero su espacio no permite atender la solicitud.
Si dos o más regiones contiguas pueden reemplazarse, el sistema puede considerarlas juntas. Para esa combinación:
- Suma sus cantidades de bloques.
- Utiliza la menor puntuación de las regiones combinadas, es decir, el menor tiempo sin uso.
- Si una está restringida o protegida, no puede utilizarla para completar el espacio.
Así una pose usada recientemente no puede quedar sobrescrita por combinarla con otra que lleva mucho tiempo sin uso.
Elegir entre candidatos válidos
- El candidato debe tener al menos la cantidad de bloques solicitada.
- Se prefiere la puntuación más alta: espacio libre o mayor tiempo sin uso.
- Si las puntuaciones son iguales, se prefiere el candidato con menos bloques, siempre que la pose quepa.
- Si puntuación y tamaño también coinciden, se conserva el candidato que ya se había elegido.
Las regiones se combinan durante la búsqueda cuando el candidato todavía es demasiado pequeño. No se juntan todas las regiones reemplazables sin tener en cuenta el tamaño solicitado.
Reemplazar y dividir la región elegida
Una vez elegido el espacio:
VRAMMap_RemovePosesInSpacerecorre sus regiones y retira de la tabla hash las poses que haya que reemplazar.VRAMMap_RemoveSpacerepresenta el espacio elegido como una región libre, actualizando sus extremos.- Se busca una entrada hash libre y se guarda el bloque de comienzo en
DX_Dynamic_Pose_Offset. DynamicPoseHashmap_Addregistra la pose.VRAMMap_AddPoseInSpaceescribe el índice hash de la pose y el comienzo de su región en el primer y último bloque.- Si sobran bloques, crea otra región libre con el resto.
Si una región de 6 bloques aloja una pose de 4, quedan una región ocupada de 4 y una libre de 2. Dynamic Xtreme no tiene un sistema de desfragmentación de VRAM: no desplaza las poses cargadas para reunir los espacios libres.
DynamicRoutine: preparar las transferencias de la pose
En este punto ya se eligió el espacio y se registró la pose. DynamicRoutine convierte esa asignación en solicitudes de transferencia.
Utiliza estos datos:
| Dato | Cantidad consultada por pose | Para qué sirve |
|---|---|---|
| Bloque de comienzo | 1 byte | Lugar elegido por el mapa de VRAM. |
OffsetToVRAMOffset |
2 bytes de una tabla de 256 bytes | Convierte uno de los 128 bloques en un desplazamiento de VRAM. |
PoseResource |
3 bytes | Dirección completa de los datos de la pose en ROM. |
PoseResourceSizePerLine |
4 bytes: dos valores de 2 | Cantidad de bytes del primer y del segundo tramo. |
PoseSecondLineOffset |
2 bytes | Desplazamiento de VRAM para el segundo tramo. |
Convertir el bloque en una dirección de VRAM
Los bloques de 16×16 no se convierten simplemente en un desplazamiento lineal de bytes. Cada uno utiliza tiles de 8×8 distribuidos en las filas de VRAM.
OffsetToVRAMOffset contempla esa distribución. Por ejemplo:
| Bloque | Desplazamiento | Dirección de VRAM |
|---|---|---|
$00 |
$0000 |
$6000
|
$01 |
$0020 |
$6020
|
$08 |
$0200 |
$6200
|
Estas direcciones de VRAM están expresadas en palabras de 2 bytes, no en bytes. La rutina suma la base $6000 utilizada para los gráficos de sprites.
Procesar los tramos y sus filas
- Obtiene el origen desde
PoseResource. - Procesa los bytes del primer tramo mediante
DynamicRoutineLine. - Si el segundo tramo tiene tamaño 0, termina.
- Si existe, adelanta el origen la cantidad de bytes del primer tramo.
- Suma
PoseSecondLineOffsetal desplazamiento de VRAM y procesa el segundo tramo.
DynamicRoutineLine divide cada tramo según las filas y el punto de comienzo. Para un destino V expresado en palabras de VRAM, prepara:
Final de la primera fila = (V + $0100) AND $FF00 Bytes hasta ese final = (final de la primera fila - V) × 2 Destino de la segunda parte = final de la primera fila + $0100 Bytes de la segunda parte de un grupo = $0200 - bytes de la primera
Los tamaños son cantidades de bytes, mientras que los destinos de VRAM están expresados en palabras. Por eso se multiplica por 2 al calcular cuántos datos caben hasta el final de una fila.
La rutina utiliza tres recorridos:
.EndLine: añade una solicitud final con los bytes restantes..TwoLinesWithEndLine: añade dos solicitudes para los datos restantes, cada una con su destino y cantidad de bytes..TwoLines: añade dos solicitudes que suman$0200bytes, avanza el origen y los destinos y repite con los datos que faltan.
Hay un caso directo antes del recorrido general: si el destino comienza en un límite de $0100 palabras y el tramo tiene al menos $0200 bytes, la rutina utiliza .EndLine para preparar la copia del tramo completo en una solicitud.
Por ejemplo, un tramo de $0200 bytes que comienza en $6020 se divide así:
| Solicitud | Origen | Destino VRAM | Bytes |
|---|---|---|---|
| Primera | Comienzo del tramo | $6020 |
$01C0: 448
|
| Segunda | Comienzo del tramo + $01C0 |
$6200 |
$0040: 64
|
Las dos solicitudes copian los 512 bytes del tramo, respetando los destinos que calcula la distribución de filas. No son dos copias completas de la pose.
Cada solicitud utiliza estas tablas:
DX_PPU_VRAM_Transfer_SourceDX_PPU_VRAM_Transfer_SourceBNKDX_PPU_VRAM_Transfer_SourceLengthDX_PPU_VRAM_Transfer_Offset
Por cada solicitud añadida aumenta DX_PPU_VRAM_Transfer_Length.
La cantidad total de bytes de la pose ya se comprobó y se añadió a DX_Dynamic_CurrentDataSend en TakeDynamicRequest. Esta parte divide esa carga en transferencias; no vuelve a contar la pose completa por cada solicitud.
DynamicRoutine no ejecuta DMA. Deja las solicitudes preparadas para que VRAMDMA las copie durante NMI.
Qué conserva el sprite
Las rutinas de sprites guardan el índice hash de la pose utilizable, no una copia independiente de sus gráficos. Ese índice permite consultar su bloque de VRAM y mantener actualizado su tiempo de uso.
Si una pose nueva no puede cargarse:
- Si existe una pose anterior válida, se mantiene y se actualiza su tiempo de uso para protegerla.
- Si nunca hubo una pose válida, no se debe dibujar como si la solicitud hubiera tenido éxito.
- En las llamadas con soporte para mantener la anterior, el carry puede indicar que todavía hay una pose utilizable aunque no se haya completado el cambio.
La animación y el movimiento siguen siendo responsabilidad del sprite. La caché se ocupa de reutilizar o preparar los gráficos solicitados.
Ejemplo: dos sprites solicitan la misma pose
Supongamos que dos sprites visibles, A y B, necesitan la pose dinámica $0005. Sus paletas ya están listas, la pose todavía no está en VRAM y hay espacio y presupuesto para cargarla.
- El sprite A solicita la pose. La búsqueda no encuentra
$0005en la caché. El sistema elige un espacio, registra la pose y prepara las transferencias de sus gráficos. A guarda el índice de esa entrada en!LastPoseHashIndex,x. Los gráficos aún no se han copiado a VRAM. - A prepara su dibujo. La rutina de dibujo utiliza la entrada registrada para calcular los tiles que corresponden a ese espacio de VRAM y escribe sus posiciones y propiedades en los buffers OAM.
- El sprite B solicita la misma pose. La búsqueda ya encuentra
$0005registrada, aunque su transferencia siga pendiente. Actualiza su tiempo de último uso y no programa otra carga. B guarda el mismo índice de caché en su propia!LastPoseHashIndex,x. - B prepara su dibujo. Escribe sus propias piezas en los buffers OAM, utilizando los mismos gráficos que A, pero con su posición, paleta y volteo.
- Durante NMI se actualizan los datos de video. Se realizan las transferencias pendientes de la pose a VRAM y el juego transfiere los buffers OAM. En la siguiente imagen, los dos sprites utilizan los mismos gráficos cargados.
Se prepara una sola carga de la pose, aunque dos sprites la utilicen. Esa carga puede necesitar varias transferencias DMA según cómo se distribuyan sus gráficos en VRAM.
En este ejemplo, que la pose esté disponible durante la lógica significa que ya tiene una entrada y un espacio asignados, con su carga preparada para NMI. Si sus gráficos ya estaban en VRAM, se reutilizan directamente y no hay que preparar otra transferencia.
Si la solicitud de A fuera rechazada, no se registraría la pose nueva ni quedaría su carga pendiente. A utilizaría su pose anterior si tiene una; de lo contrario, no dispondría de una pose para dibujar. B tendría que resolver su propia solicitud y no encontraría una entrada creada por el intento rechazado de A.
Efectos de paleta: PalettesEffects
El sistema conserva los colores originales para poder cambiar de efecto, combinarlo con otro o volver a la paleta sin efecto. No debe utilizar el resultado ya afectado como si fuera la paleta original.
Buffers de colores
| Variable o buffer | Bytes totales | Bytes por valor | Contenido |
|---|---|---|---|
DX_PPU_CGRAM_PaletteCopy |
512 | 2 por color | Colores de base en formato SNES, con 2 bytes por color. |
DX_PPU_CGRAM_BaseRGBPalette |
768 | 3 por color | Base separada en rojo, verde y azul, con 3 bytes por color. |
DX_PPU_CGRAM_BaseHSLPalette |
768 | 3 por color | Base separada en tono, saturación y luminosidad, con 3 bytes por color. |
DX_PPU_CGRAM_PaletteWriteMirror |
512 | 2 por color | Resultado en formato SNES, listo para solicitar su copia a CGRAM. Corresponde al buffer de paletas del juego. |
Indicadores de cada paleta
Cada bit de estos indicadores corresponde a una paleta. Las variables BG cubren 0-7 y las SP cubren 8-F.
| Variables | Bytes totales | Bytes por valor | Qué indican |
|---|---|---|---|
DX_PPU_CGRAM_BGBaseLoaded y DX_PPU_CGRAM_SPBaseLoaded |
1 por variable; 2 entre ambas | 1; un bit por paleta | Si ya se preparó la base de colores en formato SNES. |
DX_PPU_CGRAM_BGBaseRGBPaletteLoaded y DX_PPU_CGRAM_SPBaseRGBPaletteLoaded |
1 por variable; 2 entre ambas | 1; un bit por paleta | Si está preparada la conversión de la base a RGB. |
DX_PPU_CGRAM_BGBaseHSLPaletteLoaded y DX_PPU_CGRAM_SPBaseHSLPaletteLoaded |
1 por variable; 2 entre ambas | 1; un bit por paleta | Si está preparada la conversión de la base a HSL. |
DX_PPU_CGRAM_BGEffectLoaded y DX_PPU_CGRAM_SPEffectLoaded |
1 por variable; 2 entre ambas | 1; un bit por paleta | Si está preparado el resultado del efecto global. |
Estos indicadores evitan repetir conversiones y cálculos que siguen siendo válidos.
Preparar una base
- Se comprueba si la paleta ya tiene una base registrada.
- Si no la tiene, se obtienen los colores del buffer del juego. Una carga explícita, como
TransferPalette, puede proporcionar esa base directamente. - Se comprueba si el efecto necesita RGB o HSL.
- Si falta esa conversión, se prepara y se marca como disponible.
Si solo cambia el efecto, la base puede seguir siendo válida. Si cambian los colores originales, las llamadas de carga actualizan la base e invalidan las conversiones y resultados que deben calcularse otra vez.
Datos de un efecto
El ID permite consultar las tablas generadas durante la instalación:
Types: cálculo que debe realizarse.Value1,Value2yValue3: valores de los canales o dirección de la paleta de destino de una transición.Ratio1,Ratio2yRatio3: cuánto se aplica cada cambio.
Los canales utilizan valores de 0 a 31. Los ratios van de 0 a 127: 0 conserva el origen y 127 aplica por completo el cambio elegido.
Los cálculos se realizan fuera de NMI. NMI realiza la transferencia de los colores resultantes.
Efecto global
| Variable | Bytes totales | Bytes por valor | Para qué sirve |
|---|---|---|---|
DX_Dynamic_Palettes_GlobalEffectID |
2 | 2 | Selecciona el efecto global. El valor 0 permite restaurar los colores de base. |
DX_Dynamic_Palettes_GlobalBGEnable |
1 | 1; un bit por paleta | Selecciona las paletas 0-7 que reciben el efecto. |
DX_Dynamic_Palettes_GlobalSPEnable |
1 | 1; un bit por paleta | Selecciona las paletas 8-F que reciben el efecto. |
DX_Dynamic_Palettes_LastGlobalEffectIDPerPal |
32 | 2 | Guarda el último ID global procesado por cada paleta. |
DX_PPU_ColorEffect_Enable |
1 | 1 | Controla la participación del color fijo y el color 0,0. |
Para cada paleta habilitada, el administrador:
- Comprueba si necesita actualizar el resultado del efecto.
- Prepara la base RGB o HSL si todavía falta.
- Calcula el efecto si el resultado anterior ya no sirve.
- Prepara los colores en formato SNES y solicita su transferencia.
- Confirma el ID procesado cuando la carga correspondiente se acepta.
Si se mantiene el mismo efecto y no cambió la base, no hace falta calcularlo otra vez. Al cambiar el ID global se invalidan los resultados anteriores, no necesariamente las bases RGB y HSL.
El ID global 0 también requiere una actualización: el sistema debe cargar de nuevo los colores de base para quitar el efecto anterior.
Efecto local y concatenación
Un recurso puede tener un efecto propio y recibir también el global. Por ejemplo, un sprite puede cambiar de tono y después oscurecerse junto con el nivel.
El orden es:
- Preparar el resultado del efecto local.
- Utilizarlo como base para el efecto global mediante la llamada de concatenación.
- Calcular el efecto global sobre esa base.
- Cargar el resultado combinado, sin mostrar antes la versión que solo tiene el efecto local.
- La concatenación RGB se utiliza con efectos RGB.
- La concatenación HSL se utiliza con efectos HSL.
- Los recursos que comparten un destino de CGRAM comparten sus colores. Para mostrar dos efectos locales distintos, necesitan destinos que permitan mantener ambos resultados.
Si no se acepta la transferencia
- Calcular el efecto no significa que ya esté en CGRAM.
- Si la transferencia no se acepta, el administrador puede volver a intentar la actualización.
- Cuando corresponde, se mantiene el resultado anterior utilizable hasta poder sustituirlo.
- No se deben cargar primero los colores originales sin efecto: podrían verse así durante un frame antes del resultado correcto.
Cómo trabajan juntos
El recorrido de un sprite dinámico reúne estas tareas:
- Su lógica elige la pose y la versión de paleta.
- La asignación busca una paleta utilizable y prepara los colores si hace falta.
- La caché protege la pose ya cargada o prepara la carga de una nueva.
- El dibujo escribe los datos de sus piezas en los buffers OAM.
- El administrador de efectos prepara los cambios globales pendientes.
- NMI realiza las transferencias de gráficos y colores solicitadas.
Compartir gráficos, compartir una paleta y compartir una rutina de dibujo son tres cosas distintas. Dynamic Xtreme puede aprovechar cada una sin obligar a que los recursos compartan también las demás.