0x0B Ensamblador ZX Spectrum Pong – Optimización parte 2

Esta entrega de Ensamblador ZX Spectrum Pong surge a raíz de una serie de comentarios realizados por Spirax en el grupo de Ensamblador Z80 de Telegram, una vez terminadas las sesiones que se realizaron en colaboración con Retro Parla, y cuyos vídeos he ido añadiendo al final de los artículos. Estamos ante una entrega completamente nueva con respecto de la publicación original.

Ensamblador ZX Spectrum Pong – Optimización parte 2

Vamos a realizar dos nuevas optimizaciones, que van a permitir reducir el número de bytes y ciclos de reloj de nuestro Pong.

Como viene siendo costumbre, creamos una carpeta que se llame Paso11 y copiamos todos los ficheros .asm desde la carpeta Paso10, y nos ponemos manos a la obra.

Optimización de PlaySound

La primera optimización la vamos a realizar en la rutina de sonido, siguiendo los comentarios de Spirax, en concreto en la manera en la que evaluamos el sonido que tenemos que emitir.

Abrimos el archivo Sound.asm y localizamos la etiqueta PlaySound, cuyas primeras líneas son.

PlaySound:
; Preserva el valor de los registros
push	de
push	hl

cp		$01					; Evalúa si se emite el sonido de Punto						
jr		z, playSound_point	; Si el resultado es 0, el valor de A era 1 y emite el
							; sonido del punto
cp		$02					; Evalúa si se emite el sonido de Pala						
jr		z, playSound_paddle	; Si el resultado es 0, el valor de A era 2 y emite el
							; sonido de choque con la pala

En esta rutina utilizamos CP $01 y CP $02 para comprobar que sonido hay que emitir. Cada instrucción CP ocupa 2 bytes y tarda 7 ciclos de reloj; vamos a sustituir estas instrucciones por DEC A, que ocupa 1 byte y tarda 4 ciclos de reloj, por lo que nos vamos a ahorrar 2 bytes y 6 ciclos de reloj. Al contrario de CP, DEC si altera el valor del registro A, pero dado que lo que tenemos en A es el tipo de sonido a emitir, no nos afecta.

Veamos como queda el inicio de la rutina.

PlaySound:
; Preserva el valor de los registros
push	de
push	hl

; Spirax
dec		a					; Evalúa si se emite el sonido de Punto
jr		z, playSound_point	; Si el resultado es 0, el valor de A era 1 y emite el
							; sonido del punto
; Spirax
dec	a						; Evalúa si se emite el sonido de Pala		
jr	z, playSound_paddle 	; Si el resultado es 0, el valor de A era 2 y emite el
							; sonido de choque con la pala

Primero preserva el valor de DE, PUSH DE, luego el de HL, PUSH HL, y a continuación decrementa A, DEC A. Si A era 1, el resultado de la operación es 0 y salta a emitir el sonido, JR Z, PlaySound_point.

Si A no era uno, seguimos con las comprobaciones; decrementa A, DEC A, y si el resultado de la operación es 0 salta a emitir el sonido, JR Z, PlaySound_paddle. Si salta a reproducir el sonido es porque inicialmente A valía 2, con el primer decremento vale 1 y con este segundo decremento vale 0.

Si no ha saltado, la rutina sigue tal y como estaba y emite el sonido del punto.

Este es el momento de compilar, cargar en el emulador y comprobar que todo sigue funcionando.

Optimización de ReprintPoints

Esta rutina la implementamos de nuevo en la entrega anterior, la optimizamos y ahora volvemos a optimizarla.

Con esta optimización vamos a ahorrar 20 bytes y 107 ciclos de reloj. Para lograr este ahorro vamos a aplicar el mismo método que aplicamos, en la entrega anterior, siguiendo los comentarios de Spirax; vamos a seguir por el camino que nos marcó.

Como recordaréis, pusimos una etiqueta para que el pintado del marcador del jugador 2 se pudiera llamar de manera independiente; vamos a hacer los mismo con el marcador del jugador 1. Con esta modificación, los marcadores van a tardar algo más en pintarse (solo se pintan al inicio de la partida y al marcar un punto), pero vamos a simplificar la rutina ReprintPoints, ahorrando bytes y ciclos de reloj, y eliminando código redundante.

Vamos a empezar modificando la rutina PrintPoints para que se pueda llamar de manera independiente al pintado de los marcadores de ambos jugadores.

Abrimos el archivo Video.asm y localizamos la etiqueta PrintPoints; justo debajo de ella agregamos otra etiqueta; es la que vamos a llamar para pintar el marcador del jugador 1.

printPoint_1_print:

Entre las etiquetas PrintPoints y printPoint_1_print vamos a añadir las llamadas a pintar el marcador de cada jugador.

call	printPoint_1_print	; Pinta el marcador del jugador 1
jr		printPoint_2_print	; Pinta el marcador del jugador 2

Lo primero que hacemos es llamar a pintar el marcador del jugador 1, CALL printPoint_1_print, y luego saltar a pintar el marcador del jugador 2, JR printPoint_1_print.

Ya solo queda un cambio en PrintPoints, hay que añadir RET justo antes de la etiqueta printPoint_2_print, para que CALL printPoint_1_print salga correctamente; recordad que el resto de saltos salen por el RET de PrintPoint.

Añadimos RET antes de printPoint_2_print.

ret

printPoint_2_print:

Con esto hemos acabado con las modificaciones necesarias en PrintPoints, y como vemos no hemos ahorrado nada, al contrario, hemos añadido código, añadiendo bytes y ciclos de reloj.

Vamos ahora con el ahorro, para ello localizamos la etiqueta reprintPoint_1_print y la borramos. También borramos las líneas que la siguen hasta llegar a la etiqueta reprintPoint_2, está ultima etiqueta no la borramos.

Localizamos la etiqueta ReprintPoints y nueve líneas más abajo encontramos la instrucción JR Z, reprintPoint_1_print. Dado que esta etiqueta ya no existe, hay que cambiar esta línea y dejarla como sigue.

jr		z, printPoint_1_print

Ahora Localizamos la etiqueta reprintPoint_1 y vamos a terminar con las modificaciones. El código actual de esta etiqueta, una vez borrada toda la parte de reprintPoint_1_print es el siguiente.

reprintPoint_1:
cp		POINTS_X1_R				; Lo compara con el límite derecho de marcador 1
jr		c, reprintPoint_1_print	; Si hay acarreo, pasa por el marcador 1 
 								; y salta para pintar
jr		nz, reprintPoint_2		; Si no es cero, pasa por la derecha 
 								; y salta para comprobar paso por marcador 2

Tenemos que cambiar la doble comprobación; dado que la etiqueta reprintPoint_2 esta ahora justo debajo de la línea JR NZ, reprintPoint_2, ese salto ya no es necesario, pero si que es necesario comprobar si es cero, en cuyo caso hay que pintar el marcador del jugador 1, JR Z, printPoint_1_print, y cambiar el salto de JR C, reprintPoint_1_print por JR C, printPoint_1_print, por lo tanto, el código quedaría así.

reprintPoint_1:
cp		POINTS_X1_R				; Lo compara con el límite derecho de marcador 1
jr		z, printPoint_1_print
jr		c, printPoint_1_print	; Si es 0 o hay acarreo, pasa por el marcador 1 
 								; y salta para pintar

El aspecto final de las rutinas PrintPoints y ReprintPoints es el siguiente.

; -----------------------------------------------------------------------------
; Pinta el marcador.
; Cada número consta de 1 byte de ancho por 16 de alto.
; Altera el valor de los registros AF, BC, DE y HL.
; -----------------------------------------------------------------------------
PrintPoints:
call	printPoint_1_print		; Pinta el marcador del jugador 1
jr		printPoint_2_print		; Pinta el marcador del jugador 2

printPoint_1_print:				
ld		a, (p1points)			; Carga en A los puntos del jugador 1
call	GetPointSprite			; Obtiene el sprite a pintar en el marcador

; 1er dígito del jugador 1
ld		e, (hl)					; Carga en E la parte baja de la dirección 
								; donde está el primer dígito
inc		hl						; Apunta HL a la parte alta de la dirección 
								; donde está el primer dígito
ld		d, (hl)					; y la carga en D
push	hl						; Preserva el valor de HL
ld		hl, POINTS_P1			; Carga en HL la dirección de memoria donde se pintan
								; los puntos del jugador 1								
call	PrintPoint				; Pinta el primer dígito del marcador del jugador 1
pop		hl						; Recupera el valor de HL

; 2º dígito del jugador 1	
inc		hl						; Apunta HL a la parte baja de la dirección 
								; donde está el segundo dígito
ld		e, (hl)					; y la carga en E
inc		hl						; Apunta HL a la parte alta de la dirección 
								; donde está el segundo dígito
ld		d, (hl)					; y la carga en D
; Spirax
ld		hl, POINTS_P1 + 1		; Carga en HL la dirección de memoria donde se pinta
								; el segundo dígito de los puntos del jugador 1
call	PrintPoint				; Pinta el segundo dígito del marcador del jugador 1
ret

printPoint_2_print:	
; 1er dígito del jugador 2
ld		a, (p2points)			; Carga en A los puntos del jugador 2
call	GetPointSprite			; Obtiene el sprite a pintar en el marcador
ld		e, (hl)					; Carga en E la parte baja de la dirección
								; donde está el primer dígito
inc		hl						; Apunta HL a la parte alta de la dirección
								; donde está el primer dígito
ld		d, (hl)					; y la carga en D
push	hl						; Preserva el valor de HL
ld		hl, POINTS_P2			; Carga en HL la dirección de memoria donde se pintan
								; los puntos del jugador 2
call	PrintPoint				; Pinta el primer dígito del marcador del jugador 2
pop		hl						; Recupera el valor de HL

; 2º dígito del jugador 2	
inc		hl						; Apunta HL a la parte baja de la dirección
								; donde está el segundo dígito
ld		e, (hl)					; y la carga en E
inc		hl						; Apunta HL a la parte alta de la dirección
								; donde está el segundo dígito
ld		d, (hl)					; y la carga en D
; Spirax
ld		hl, POINTS_P2 + 1		; Carga en HL la dirección de memoria donde se pinta
								; el segundo dígito de los puntos del jugador 2
; Pinta el segundo dígito del marcador del jugador 2

PrintPoint:
ld		b, $10					; Cada dígito son 1 byte por 16 (scanlines)

printPoint_printLoop:
ld		a, (de)					; Carga en A el byte a pintar
ld		(hl), a					; Pinta el byte
inc		de						; Apunta DE al siguiente byte
call	NextScan				; Apunta HL al siguiente scanline
djnz	printPoint_printLoop	; Hasta que B = 0
ret

; -----------------------------------------------------------------------------
; Repinta el marcador.
; Cada número consta de 1 byte de ancho por 16 de alto.
; Altera el valor de los registros AF, BC, DE y HL.
; -----------------------------------------------------------------------------
ReprintPoints:
ld	hl, (ballPos)				; Carga en HL la posición de la bola
call	GetPtrY					; Obtiene tercio, línea y scanline de la posición 
 								; de la bola
cp		POINTS_Y_B				; Lo compara con el límite inferior del marcador
ret		nc						; Si no hay acarreo, pasa por debajo y sale
ld		a, l					; Carga en A la línea y columna de la posición de la bola
and		$1f						; Se queda con la columna
cp		POINTS_X1_L				; Lo compara con el límite izquierdo del marcador 1
ret		c						; Si hay acarreo, pasa por la izquierda y sale
jr		z, printPoint_1_print	; Si es 0, está justo en el margen izquierdo 
 								; y salta para pintar

cp		POINTS_X2_R				; Lo compara con el límite derecho de marcador 2
jr		z, printPoint_2_print	; Si es 0, está justo en el margen derecho 
 								; y salta para pintar
ret		nc						; Si no hay acarreo, pasa por la derecha y sale

reprintPoint_1:
cp	POINTS_X1_R					; Lo compara con el límite derecho de marcador 1
jr		z, printPoint_1_print	
jr		c, printPoint_1_print	; Si es 0 o hay acarreo, pasa por el marcador 1 
 								; y salta para pintar 
reprintPoint_2:					
cp		POINTS_X2_L				; Lo compara con el límite derecho de marcador 2
ret		c						; Si hay acarreo, pasa por la izquierda y sale
; Spirax
jr		printPoint_2_print		; Pinta el marcador del jugador 2

Si comparamos la implementación de ReprintPoints con la implementación que hicimos de esta rutina en el capítulo anterior, podemos observar que la rutina se ha simplificado significativamente, quedando prácticamente reducida a las comprobaciones que incluimos para que el marcador solo se repintase cuando es necesario.

Ahora ya solo queda compilar, cargar en el emulador y comprobar que todo sigue funcionando.

Hemos terminado. ¿O queréis añadir una pantalla de carga?

Podéis descargar todo el código que hemos generado.

Enlaces de interés

Vídeo

Si lo prefieres, puedes ver el vídeo que grabamos de esta sesión.

Ensamblador ZX Spectrum, optimización parte 2

Ensamblador para ZX Spectrum PONG por Juan Antonio Rubio García.
Comentarios al código por Spirax.
Correcciones al texto original realizadas por Joaquín Ferrero.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartitIgual 4.0 Internacional License.
Este tutorial ha sido publicado con anterioridad en AUA y se han grabado vídeos que están publicados a través de Retro Parla.

No olvides visitar las webs amigas.

AUA

Aquí puedes ver más cosas que he desarrollado para .Net, y aquí las desarrolladas en ensamblador para Z80.

Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies, pinche el enlace para mayor información.plugin cookies

ACEPTAR
Aviso de cookies
A %d blogueros les gusta esto: