mentalthink escribió:Jaja!!! al estraperlo por si acaso!!!
Lo que comentaste en el programa de los sprites en memoria por el tema de rotarlos y algo que no capte de un programa Compilado en tiempo real.. realmente me dejo bastante anonadado...
Lo de tener los sprites prerrotados en memoria es para evitar tener que desplazara X bits a la derecha cada scan del sprite original. Esto es necesario para poder imprimir un sprite en una posición de pixel en una pantalla diseñada para imprimir en celdas de caracteres. Las instrucciones de desplazamiento en el Z80 sólo desplazan un1 bit cada vez, así que en el peor caso, desplazar 7 bits a la derecha supone repetir 7 veces esta operación, y además, aplicarla a cada scan. Un sprite de 16x16 necesitaría 32 operaciones de desplazamiento para 1 bit, así que para 7, 224 operaciones, a 4 ciclos de reloj en el mejor de los casos, 896 ciclos de reloj. Y esto sólo para preparar el sprite y suponiendo que no tenga máscara (si no, repite las mismas operaciones con la máscara). Hay por supuesto "atajos" para no tener que repetir esto 7 veces, tales como usar la instrucción RLD, que usa la memoria como paso intermedio pero permite desplazar 4 bits de golpe.
En definitiva, que si uno quiere ahorrarse todas estas operaciones, lo que hace es guardar la versión ya desplazada del sprite 1 bit, 2 bits, etc, en memoria. A la hora de desplazar, simplemente coge la versión ya desplazada, que será más rápido que hacer todos los desplazamientos en código.
Sobre los sprites compilados, pues la idea es que la operación de pintar un sprite se divide realmente en dos operaciones:
- Primero coges la definición del sprite y la pasas por una rutina que hace todo lo que normalmente haría una rutina de impresión de sprites normal, es decir: recorrer todos los scans del sprite, preparar cada scan desplazándolo lo que sea necesario, hacer lo mismo con la máscara, leer lo que hay en pantalla en ese momento, y por último, combinar máscara, pantalla y sprite para obtener el nuevo valor que se ha de escribir en pantalla. Cuando se tienen ya los dos datos importantes (qué se ha de escribir en pantalla, y dónde ha de escribirse) lo que hace la rutina no es escribir esos datos a pantalla, sino que genera un par de instrucciones en código máquina del estilo:
O alguna otra secuencia equivalente. Un "puntero de instrucciones" va apuntando a una zona de memoria donde se van escribiendo estas instrucciones, de forma que al final tienes una secuencia, más o menos larga de LD A,N / LD (NNNN),A / LD A,N / LD (NNNN),A / LD A,N / LD (NNNN),A , etc... Cuando la primera rutina termina con el sprite, añade una instrucción RET
Esta operación la puedes hacer en memoria no contenida, y mientras que la ULA está pintando la pantalla actual. Tu única restricción es terminar todo esto antes de que la ULA termine de escribir la pantalla. Tienes por tanto bastante tiempo.
- La segunda operación, que es la crítica en velocidad, y que debe hacerse justo cuando la ULA ha interrumpido al micro y por tanto estamos en retrazo vertical, es ir llamando a todas las rutinas que se han pregenerado en el paso anterior (una por sprite, o una para todos los sprites, depende de cómo lo hayamos hecho). Estas rutinas simplemente escriben valores a memoria, no hacen nada más, con lo que son mucho más rápidas que la impresión "general" de sprites.
Para que te hagas una idea, una rutina que imprima un sprite de 8x8 (para no hacer el ejemplo muy largo) en cualquier posición de pixel, y en un caso malo (que la impresión caiga en una coordenada X que no sea múltiplo de 8 y que la coordenada Y sea múltiplo de 8 + 4, para que la impresión caiga entre dos celdas consecutivas verticalmente de caracteres) generará una secuencia tal como ésta:
Código: Seleccionar todo
ld hl,dato1scan1*256+dato2scan1 ;10
ld (scan1),hl ;16
ld hl,dato1scan2*256+dato2scan2 ;10
ld (scan2),hl ;16
ld hl,dato1scan3*256+dato2scan3 ;10
ld (scan3),hl ;16
ld hl,dato1scan4*256+dato2scan4 ;10
ld (scan4),hl ;16
ld hl,dato1scan5*256+dato2scan5 ;10
ld (scan5),hl ;16
ld hl,dato1scan6*256+dato2scan6 ;10
ld (scan6),hl ;16
ld hl,dato1scan7*256+dato2scan7 ;10
ld (scan7),hl ;16
ld hl,dato1scan8*256+dato2scan8 ;10
ld (scan8),hl ;16
ret ;10
Sumándolo todo: 26*8+10=218 ciclos de reloj. En los 14336 ciclos de reloj que hay entre el momento en que se produce la interrupción, y la ULA comienza a escribir en pantalla y por tanto comienza el periodo de contienda, te da tiempo a pintar 14336/218 = 65 sprites de 8x8 píxeles. Por supuesto son menos, ya que para pintar un sprite, primero hay que haber borrado ese mismo sprite en su posición anterior, y que aunque te dé tiempo a pintar 65 sprites en pantalla, a lo mejor no te da tiempo a compilar esos 65 sprites en el tiempo que dura el redibujado de pantalla (la primera operación). Ponte que en lugar de 65 fueran la mitad. Aún así es una buena cifra: de hecho, sería el mismo número de sprites (32) que es capaz de pintar el MSX con su gestión por hardware
Te paso un ejemplo (el primer experimento que hice sobre este tema). Es una rutina que pinta 50 pequeños sprites de 2x2 píxeles y los va animando por pantalla. El borde de la misma presenta varios colores en sucesión, que se corresponden a los tiempos que tarda cada proceso en realizarse.
En azul: el tiempo que tarda en ejecutarse la rutina de borrado de sprites (la que se compiló en el paso anterior). De tan poco tiempo que dura, no llega a verse el azul en pantalla.
En rojo: el tiempo que tarda en ejecutarse la rutina de pintado de sprites (también compilada en el paso anterior)
En magenta: el tiempo que se tarda en actualizar la posición de todos los sprites (incluyendo el control del "chocado" contra los bordes de la pantalla y esas cosas)
En verde: el tiempo que se tarda en compilar las dos rutinas que se usarán en el próximo paso: la primera para borrar los sprites que se acaban de pintar, y la segunda para pintar los sprites en la nueva posición actualizada.
En negro: tiempo ocioso hasta llegar a la siguiente interrupción.
Para que no haya parpadeo en pantalla, el tiempo de borrado más el tiempo de pintado no debe superar la posición de la primera línea de pantalla. Como puedes observar, esto se cumple con creces en el ejemplo (el borde rojo termina mucho antes de que se llegue a la primera línea de pantalla). Si usara una rutina "convencional", ésta tardaría todo lo que ocupa la parte verde más la parte azul más la roja.
Con el depurador puedes ver las rutinas de borrado y pintado que se generan automáticamente en cada retrazo vertical. La de borrado está a partir de la posición ECEB, y la de pintado a partir de EF44.