Un poco de ensamblador...

Soporte técnico sobre los lanzamientos de MojonTwins y comentarios sobre los güegos. Ofrecemos soporte técnico con Fourspriter, te ayudamos con ZX Basic o Z88DK, te damos pistas some cómo saltarse un bicho y cosas así.

Moderador: na_th_an

Avatar de Usuario
elborra
Mensajes: 209
Registrado: Dom, 12 Ene 2014, 14:37

Un poco de ensamblador...

Mensajepor elborra » Sab, 28 Feb 2015, 01:27

Voy a ver si vuelvo poquito a poco a la carga,

Estoy intentando hacer mejoras al sistema de cargas de pantallas que tenía hecho (por motivos de rendimiento y blablabla). Para ello estoy rehaciendo una serie rutinas en c que implicaba estar continuamente paginando aún estando la mayor parte del tiempo usando la misma página de memoria (no entraré en detalles para no volveros locos con datos innecesarios).

Para evitar esto estoy 'trasladando' (no realmente porque cambian cosas) a ensamblador algunas rutinas. Una de ellas requiere la copia de bloques de bytes desde una dirección a otra; pero me estoy volviendo loco :cry:

- Una función que copie bloques de 32 bytes N veces. N es constante.
- La dirección de memoria destino se conoce y es fija. Los N bloques de 32 bytes se colocan sucesivamente
- La dirección de origen se conoce pero no es fija, hay que añadirle un offset. Dicho offset es diferente para cada bloque.

En principio los offsets vendrian representados por un array declarado en c (pero estoy abierto a otras posibilidades).

Código: Seleccionar todo

unsigned char offsets [N] = {4,10,5,6,7}

Pero a la hora de interpretar los valores contenidos en el array habría que multiplicar por 32 cada valor (si es más rápido siempre podría hacer el array de enteros y guardar el valor real necesitado desde un principio)

Código: Seleccionar todo


offset[0]=128
offset[1]=320
offset[2]=160
... etc


Ok, hasta aquí la teoría, en la práctica entiendo que puedo copiar bloques facilmente con ldir (que no se si será lo más rápido); Así, en el ejemplo siguiente se copiarian 32 bytes consecutivos desde 56000-56031 a 20000-20031:

Código: Seleccionar todo

ld hl, 56000
ld de, 20000
ld bc,31
ldir


Ok, repetir esto N veces (perdonad mi ignorancia), pero podria usar el registro a y un salto condicional. Así:

Código: Seleccionar todo

ld a, 5

ld de, 20000

.bucle
ld hl, 56000
ld bc,31
ldir

dec a
jr nz, bucle


En este caso, y al sacar ld de,20000 del bucle y aprovechar que ldir autoincrementa el registro de, conseguiria copiar 5 veces el mismo bloque de 32 bytes (56000-56031) al 20000-20159 (160 bytes)

A partir de aquí he hecho multitud de pruebas para añadir el offset a hl y obtener asi la dirección origen correcta, pero no hay manera. En pseudocódigo tendría que hacer lo siguiente:

Código: Seleccionar todo

DESTINO = 20000
PARA N VECES
   ORIGEN = 56000 + offset[N]*32
   COPIAR 32 BYTES DESDE ORIGEN A DESTINO
   DESTINO = DESTINO + 32
SIGUIENTE N

Alguna ayudita please :picha:

PD: edito para corregir alguna cosa y aprovecho para lanzar otra pregunta ya que lo que pretendo únicamente en NO paginar continuamente tal como ahora hago:

Código: Seleccionar todo

void CopiaBloque(unsigned int num) {
   ram_page [0] = 3
   ram_address [0] = 56000+(offset[num]<<5);
   ram_destination [0] = (unsigned int) (destino+(num<<5));

   #asm   
      di
      ld a, (_ram_page)
      ld b, a
      call SetRAMBank
      
      ld hl, (_ram_address)
      ld de, (_ram_destination)
      ld bc,31
      ldir

      ld b, 0
      call SetRAMBank
      ei
   #endasm
};

donde destino se declaró como (por ejemplo):

Código: Seleccionar todo

unsigned char destino[16] = {0,1,2,3,15,38,44,2,0,12,11,90,100,101,102,103}

y llamar a dicha función en un bucle:

Código: Seleccionar todo

for(i=0;i<16;i++) {
   CopiaBloque(i);
}

¿Realmente ganaría (en ciclos de reloj, pero algo humanamente perceptible) haciendolo todo en asm (en una hipotética función CopiarBloqueS(void) {}; ) evitando paginar con SetRAMBank cada vez que llamo a CopiaBloque? Hay que tener en cuenta, para mi caso, que finalmente se copiarian entre pantalla y pantalla (de un mismo nivel) en torno a 60 bloques de 32 bytes.
Avatar de Usuario
na_th_an
Mensajes: 26412
Registrado: Vie, 09 Ene 2009, 12:18

Re: Un poco de ensamblador...

Mensajepor na_th_an » Lun, 02 Mar 2015, 10:07

A mí me pillas super espeso, pero vamos a intentarlo :lol:

elborra escribió:[...]Pero a la hora de interpretar los valores contenidos en el array habría que multiplicar por 32 cada valor (si es más rápido siempre podría hacer el array de enteros y guardar el valor real necesitado desde un principio)

Código: Seleccionar todo


offset[0]=128
offset[1]=320
offset[2]=160
... etc


En este tipo de casos la regla es muy sencilla: si puedes precalcular, precalcula. Es tontería hacer que un programa calcule todas las veces valores que van a ser constantes. Si te parece poco legible, z88dk resuelve correctamente esto precalculando en tiempo de compilación (en el binario va el resultado, así que no se pierde tiempo):

unsigned char offsets [N] = {4*32,10*32,5*32,6*32,7*32};

elborra escribió:en la práctica entiendo que puedo copiar bloques facilmente con ldir (que no se si será lo más rápido);


Es más rápido poner 32 ldi seguidos en el código. Pero luego hay que ver si merece la pena el despliegue o no. En una función que se ejecutase dentro del código del juego, yo lo haría. En algo que se ejecute fuera, donde el cambio es menos perceptible, no.

elborra escribió: Así, en el ejemplo siguiente se copiarian 32 bytes consecutivos desde 56000-56031 a 20000-20031:

Código: Seleccionar todo

ld hl, 56000
ld de, 20000
ld bc,31
ldir


Que me corrijan si me equivoco, pero para copiar 32 bytes tienes que poner un 32 en BC.

elborra escribió:A partir de aquí he hecho multitud de pruebas para añadir el offset a hl y obtener asi la dirección origen correcta, pero no hay manera. En pseudocódigo tendría que hacer lo siguiente:

Código: Seleccionar todo

DESTINO = 20000
PARA N VECES
   ORIGEN = 56000 + offset[N]*32
   COPIAR 32 BYTES DESDE ORIGEN A DESTINO
   DESTINO = DESTINO + 32
SIGUIENTE N


Mi kung fu no es demasiado bueno, pero yo lo haría así (con los offsets precalculados) (no está probado, puede estar mal)

Código: Seleccionar todo

    ld a, 5
    ld de, 20000

   
.bucle
    ; Calculamos de donde tenemos que leer
    ld h, 0
    ld l, a
    add l, a            ; hl = a*2
    add hl, _offsets    ; hl = _offsets + a*2
    ld c, (hl)
    inc hl
    ld b, (hl)          ; bc contiene el offset
    ld l, c
    ld h, b             ; hl contiene el offset
    add hl, 56000       ; hl = 56000 + offset
       
    ld bc, 32
    ldir
    dec a
    jr nz, bucle
    ret


De hecho, ya que vas a precalcular, suma el 56000 en los offsets y ahórrate el add hl, 56000.

elborra escribió:PD: edito para corregir alguna cosa y aprovecho para lanzar otra pregunta ya que lo que pretendo únicamente en NO paginar continuamente tal como ahora hago:

Código: Seleccionar todo

void CopiaBloque(unsigned int num) {
   ram_page [0] = 3
   ram_address [0] = 56000+(offset[num]<<5);
   ram_destination [0] = (unsigned int) (destino+(num<<5));

   #asm   
      di
      ld a, (_ram_page)
      ld b, a
      call SetRAMBank
      
      ld hl, (_ram_address)
      ld de, (_ram_destination)
      ld bc,31
      ldir

      ld b, 0
      call SetRAMBank
      ei
   #endasm
};

donde destino se declaró como (por ejemplo):

Código: Seleccionar todo

unsigned char destino[16] = {0,1,2,3,15,38,44,2,0,12,11,90,100,101,102,103}

y llamar a dicha función en un bucle:

Código: Seleccionar todo

for(i=0;i<16;i++) {
   CopiaBloque(i);
}

¿Realmente ganaría (en ciclos de reloj, pero algo humanamente perceptible) haciendolo todo en asm (en una hipotética función CopiarBloqueS(void) {}; ) evitando paginar con SetRAMBank cada vez que llamo a CopiaBloque? Hay que tener en cuenta, para mi caso, que finalmente se copiarian entre pantalla y pantalla (de un mismo nivel) en torno a 60 bloques de 32 bytes.


"Humanamente perceptible" depende de donde se ejecute este código. Si es al cambiar la pantalla, yo no me quebraría la cabeza, directamente. No creo que sea perceptible.

Ahora bien, sí que sería más rápido y quizá ahorrases bytes. Ten en cuenta que, de entrada, el bucle for(i = 0; i < 16; i ++), en especial si "i" es una variable local, ocupará mucho más espacio y tiempo que el bucle con el registro A que estás proponiendo en ensamblador.
Como diría Rorshach: "Urm..."
Avatar de Usuario
elborra
Mensajes: 209
Registrado: Dom, 12 Ene 2014, 14:37

Re: Un poco de ensamblador...

Mensajepor elborra » Mar, 03 Mar 2015, 23:26

ok, muchas gracias na_th_an. Queria responderte antes pero sólo me ha dado tiempo a hacer un par de pruebas del tema.

Hay alguna instrucción que no es válida como "add hl, 56000", si no me equivoco, ya que no acepta valores directamente (habría que cargar el valor en otro registro, p.ej BC y luego hacer la suma)

De momento no me hace falta nada más, sigo probando cuando pueda y ya te respongo correctamente. :vahka:
Avatar de Usuario
na_th_an
Mensajes: 26412
Registrado: Vie, 09 Ene 2009, 12:18

Mensajepor na_th_an » Mié, 04 Mar 2015, 09:14

Tienes razón. Es que vengo de una temporada programando para 6509 y este suele ser más flexible con esas cosas :-)
Como diría Rorshach: "Urm..."
Avatar de Usuario
na_th_an
Mensajes: 26412
Registrado: Vie, 09 Ene 2009, 12:18

Mensajepor na_th_an » Mié, 04 Mar 2015, 09:22

Si de verdad quieres ganar un montón de velocidad y espacio (sobre todo, espacio) y tienes tiempo y un buen editor de texto, hay dos cosas que puedes hacer :

1. Eliminar todas las variables locales y sustituirlas por globales. Hay muchas que se pueden reaprovechar. El acceso a una global es más rápido y ocupa menos instrucciones.

2. Cargarte todas las estructuras que puedas, por ejemplo player, y cambiarlo por globales normales. Esto lo puedes conseguir sustituyendo player.x por player_x y todas las demás. Hay algunas que no podrás eliminar, como las que escribe el conversor de mapas o el colocador, pero sólo con cambiar player y en_an, y bullets si lo utilizas, ganarás un montón.
Y si te sientes generoso podemos sacar eso como nueva versión de la churrera :-P
Como diría Rorshach: "Urm..."
Avatar de Usuario
elborra
Mensajes: 209
Registrado: Dom, 12 Ene 2014, 14:37

Re: Un poco de ensamblador...

Mensajepor elborra » Mié, 04 Mar 2015, 23:52

Ufff... antes de meterme tan de lleno tendría que ver como van los cambios e ir probando el rendimiento.

Sigo, lamentablemente, dandole vueltas al asunto, porque no hay manera. He probado tu código pero sigue sin copiar correctamente. Parece que el problema principal es que no se como ir accediendo a los datos del "array" consecutivamente.

Este código está fuera de la función, en otro lado totalmente distinto (de hecho en otro fichero .h). Estos valores pueden cambiar a lo largo del juego, pero inicialmente le damos esos valores

Código: Seleccionar todo

._ram_datos
      defw 0*32, 1*32, 7*32, 3*32, 4*32, 5*32, 6*32, 16*32;

Dentro ya de la función propiemente dicha no encuentro la manera de que a cada iteracion del bucle (que realizamos con el registro A) pueda acceder consecutivamente a:
ram_datos[0] -> ram_datos -> 0*32
ram_datos[1] -> ram_datos+2? -> 1*32
ram_datos[2] -> ram_datos+4? -> 7*32
...
ram_datos[7] -> ram_datos+14? -> 16*32
y poder sumarle dicho valor como offset para calcular el origen de los datos a copiar (con ldi o ldir) dejando HL como 56000+offset

Por cierto, "ret" realmente que hace ¿es necesario?, porque en algunas ocasiones dejar esta linea hace que se cuelgue el juego en cuando ejecuta la funcion de copiar bloques; y quitándola vuelve a funcionar (independientemente de que no hace lo que quiero xD )
Avatar de Usuario
na_th_an
Mensajes: 26412
Registrado: Vie, 09 Ene 2009, 12:18

Re: Un poco de ensamblador...

Mensajepor na_th_an » Jue, 05 Mar 2015, 08:15

La forma de hacerlo que yo sé es la que te he puesto que, por supuesto, puede tener algún error. ¿Has mirado con el debugger de Spectaculator (CTRL+ENTER) cómo se va ejecutando el tema? Cuando empiezas con el ensamblador tienes que tener a mano el debugger.

Vale, no sé cual es mi problema, así que voy a orientarlo de otra forma :lol: . De entrada, 'a' es el acumulador, es el registro más versátil, no deberías desperdiciarlo en un iterador - para eso puedes usar B que además te sirve para terminar el bucle con un DJNZ. Prueba así. Seguro que viene alguien a reñirme por usar la pila. Que venga, te dará un código mejor que el mío. Y a lo mejor funciona y todo :lol:

Código: Seleccionar todo

    ld b, 5             ; Contamos con b, mejor.
    ld de, 20000
    ld hl, _offsets     ; Vamos a ir leyendo de aquí los offsets
   
.bucle
    ; Salvamos el contador
    push bc
   
    ; Leemos un nuevo offset
    ld c, (hl)
    inc hl
    ld b, (hl)
    inc hl              ; Siguiente offset
   
    ; Offset en bc, sumamos 56000.
    push hl             ; Antes salvamos hl
    ld hl, 56000
    add hl, bc

    ; Copiamos 32 bytes   
    ld bc, 32
    ldir
   
    ; Recuperamos nuestro puntero a _offsets
    pop hl
   
    ; Recuperamos el contador y cerramos el bucle
    pop bc
    djnz bucle          ; Decrementa b y salta si es != 0


Lo del ret, depende. Si has llegado al bloque #asm porque viene a continuación de código C, de ninguna manera. Si has llegado con un call desde ensamblador, es necesario para poder volver.

Sobre los otros cambios que te propongo, es cuestión de find & replace con un buen editor de textos. Yo lo hice usando code::blocks, que me permitía cargar todos los archivos y hacer cambios globales. Y sí, te aseguro que así ocupa menos y se ejecuta más rápido. De hecho, fue la base de MK2. Lo primero que hicimos fue optimizar así la Churrera. Luego ya empezamos a reescribir rutinas.

A lo mejor te interesaría portar a MK2, en realidad...
Como diría Rorshach: "Urm..."
Avatar de Usuario
elborra
Mensajes: 209
Registrado: Dom, 12 Ene 2014, 14:37

Re: Un poco de ensamblador...

Mensajepor elborra » Jue, 05 Mar 2015, 13:44

na_th_an escribió:A lo mejor te interesaría portar a MK2, en realidad...

Pues no te creas que le eché un vistazo al código de la 4 parte de Leovigildo; el problema era meterle todas las modificaciones (mapa comprimido de Antonio, cambio tamaño zona juego, plataformas en "genital", los bloques arrastrables, scripts en memoria extendida, personajes con textos animados, puentes, set de tiles..) que ya ni me acuerdo de muchas de ellas U_U y tengo que mirarmelo 20 veces para saber que es lo que estaba haciendo, como y el porque XD. Vamos que le metí un repaso a las estructuras principales para adecuarlo a mis necesidades, obviado el generador de niveles de 128k y haciendo mi propio "crearNiveles" XD.

Sip, esto quiere decir que a cada carga de pantalla se ve cambiaban tiles y atributos (descomprimiendo), sprites de enemigos (descomprimiendo), scripts para esa pantalla concreta, enemigos para esa pantalla concreta, y alguna cosa más.. ¡y oye! ¡que la transición entre pantallas era bastante buena! :shock: (teniendo en cuenta que no necesitas la rapidez de cambio de un plataformas), pero lo suficientemente "lenta" como para que se quedara pillada la música en alguna nota durante ese tiempo (hasta volver a RAM0 y habilitar las interrupciones)

De ahí que al retomarlo este empezando de "0" a ver si con lo aprendido podía distribuir mejor todo pero al menos con la misma "base". Es cierto que a simple vista el código de MK2 se ve todo más ordenado y distribuido en más ficheros, pero claro el haber estado buceando tanto en la churrera me ha pasado factura, y no se por donde empezar a meterle mano xD.
Avatar de Usuario
na_th_an
Mensajes: 26412
Registrado: Vie, 09 Ene 2009, 12:18

Re: Un poco de ensamblador...

Mensajepor na_th_an » Jue, 05 Mar 2015, 18:55

Sobre los tirones, mira el hilo de pentacorn. Creo que los dejamos al mínimo con un par de historias. Si consigues que el tiempo que están las interrupciones deshabilitadas sea menos de un frame, con un halt en el sitio correcto lo solucionas.
Como diría Rorshach: "Urm..."
Avatar de Usuario
elborra
Mensajes: 209
Registrado: Dom, 12 Ene 2014, 14:37

Re: Un poco de ensamblador...

Mensajepor elborra » Vie, 06 Mar 2015, 18:02

Lo has clavado de esta otra forma na_ta_an, yo ya me hebía metido con push y pop pero no daba con un código correcto (Algo estaría haciendo mal U_U )

Como emulador estaba usando el Fuse pero el debugger es muy simple y sólo puedor hacer paso a paso (no si se puede poner algún punto de ruptura y tampoco lo miré mucho), me pasaré al Spectacular a ver si puedo controlarlo medianamente.

na_th_an escribió:La forma de hacerlo que yo sé es la que te he puesto que, por supuesto, puede tener algún error. ¿Has mirado con el debugger de Spectaculator (CTRL+ENTER) cómo se va ejecutando el tema? Cuando empiezas con el ensamblador tienes que tener a mano el debugger.

Vale, no sé cual es mi problema, así que voy a orientarlo de otra forma :lol: . De entrada, 'a' es el acumulador, es el registro más versátil, no deberías desperdiciarlo en un iterador - para eso puedes usar B que además te sirve para terminar el bucle con un DJNZ. Prueba así. Seguro que viene alguien a reñirme por usar la pila. Que venga, te dará un código mejor que el mío. Y a lo mejor funciona y todo :lol:

Código: Seleccionar todo

    ld b, 5             ; Contamos con b, mejor.
    ld de, 20000
    ld hl, _offsets     ; Vamos a ir leyendo de aquí los offsets
   
.bucle
    ; Salvamos el contador
    push bc
   
    ; Leemos un nuevo offset
    ld c, (hl)
    inc hl
    ld b, (hl)
    inc hl              ; Siguiente offset
   
    ; Offset en bc, sumamos 56000.
    push hl             ; Antes salvamos hl
    ld hl, 56000
    add hl, bc

    ; Copiamos 32 bytes   
    ld bc, 32
    ldir
   
    ; Recuperamos nuestro puntero a _offsets
    pop hl
   
    ; Recuperamos el contador y cerramos el bucle
    pop bc
    djnz bucle          ; Decrementa b y salta si es != 0


Lo del ret, depende. Si has llegado al bloque #asm porque viene a continuación de código C, de ninguna manera. Si has llegado con un call desde ensamblador, es necesario para poder volver.

Sobre los otros cambios que te propongo, es cuestión de find & replace con un buen editor de textos. Yo lo hice usando code::blocks, que me permitía cargar todos los archivos y hacer cambios globales. Y sí, te aseguro que así ocupa menos y se ejecuta más rápido. De hecho, fue la base de MK2. Lo primero que hicimos fue optimizar así la Churrera. Luego ya empezamos a reescribir rutinas.

A lo mejor te interesaría portar a MK2, en realidad...

Volver a “Ayuda”

¿Quién está conectado?

Usuarios navegando por este Foro: No hay usuarios registrados visitando el Foro y 1 invitado