martes, 24 de julio de 2012

Ensamblador 8086 Buffer Doble

En este último post de la serie, explicaremos la técnica de buffer doble para pintar en la memoria de gráficos y comentaremos algunas posibles mejoras al código. Como platicábamos en el post anterior, si escribimos continuamente al área de memoria, puede suceder que versiones incompletas de nuestra imagen sean desplegadas. La técnica de buffer doble consiste en reservar un área de memoria del mismo tamaño de nuestra memoria de gráficos, en nuestro caso de 64,000 bytes (320x200); y pintar nuestros pixeles en este buffer. Una vez que tengamos nuestra imagen final lista, entonces copiamos nuestro buffer al área de memoria de gráficos. Esto minimiza el tiempo en el que estamos escribiendo directamente al área de gráficos y también el parpadeo. El siguiente video muestra el resultado final.

En el siguiente proyecto de mercurial podrán encontrar los archivos para construir el ejemplo. El archivo ensambla.bat contiene la instrucción usada para crear el ejecutable.

Reservar Memoria

Usamos la interrupción 21h con el servicio 48h para reservar memoria dinámicamente para el buffer doble, el registro bx contiene la cantidad de memoria deseada en bloques de 16 bytes (función iniciar_video en libgraf.asm). Puedes encontrar más información acerca de memoria dinámica en las siguientes referencias: referencia 1 y referencia 2. La variable vram guarda la dirección inicial del buffer doble. Ahora en el código, en lugar de escribir a la dirección A000h (memoria de gráficos), escribimos al buffer doble (vram).

Es importante notar que como reservamos memoria dinámicamente al momento de ligar los archivos (link) es necesario indicar cuanta memoria extra utilizará nuestro programa en tiempo de ejecución, es por ello que usamos la opción /link /cp:4000 en el comando ml del archivo ensambla.bat. El parámetro 4000 corresponde a la cantidad que solicitamos con la interrupción 21h, servicio 48h (4000 x 16 = 64,000 = 320 x 200).

Transparencia

Pareciera que la imagen de la pelota tiene transparencia, pero si la abrimos en un editor, el exterior es de color negro. Para lograr este efecto, en la función LoadBMP de libgraf.asm ignoramos los pixeles cuyo valor sea cero, que corresponde en este caso al color negro de nuestra paleta de colores.

Posibles Mejoras

Es notorio que la pelota se mueve lentamente, esto puede deberse a que estamos haciendo mucho procesamiento innecesario. En cada ciclo del programa (1) abrimos y leemos dos archivos (uno para la imagen de fondo y otro para la imagen de la pelota), (2) escribimos dos veces a la paleta de colores, (3) escribimos las dos imágenes al buffer y (4) copiamos el buffer doble a la memoria de gráficos. Los puntos 1 y 2 sólo son necesarios al inicio del programa.

Para el punto 1, podemos modificar el método mostrar_bmp (en libgraf.asm) para que abra el archivo, reserve memoria dinámica y guarde la imagen en esta memoria. El método mostrar_bmp arrojará como resultados el ancho, el alto y el apuntador a los pixeles de la imagen. De esta forma, en nuestro ciclo principal del programa trabajaremos con información en RAM y no tendremos que abrir continuamente archivos, lo cual es un proceso que consume mucho tiempo.

Con respecto al punto 2, estamos asumiendo que todas las imágenes usadas en el programa utilizan la misma paleta de colores. En caso de que no sea así, algunas imágenes podrían lucir extrañas. Hasta el momento, las imágenes que hemos guardado con Paint parecen tener una paleta común (usamos el programa pixelformer para observar la paleta de colores de las imágenes). Habría que modificar la función mostrar_bmp para que reciba un parámetro que indique si deseamos actualizar la paleta de colores con la paleta de colores de la imagen.

Aún así, en cada ciclo del programa estamos copiando al menos 64,000 bytes del buffer doble al área de memoria de gráficos (función copiar_buffer en libgraf.asm). Creo que resultaría interesante explorar otros modos de video, como el que menciona la siguiente página, que ya tiene soporte directo para un buffer doble y en donde es posible cambiar los buffers con pocas instrucciones.

lunes, 23 de julio de 2012

DOSBox autoexec

En la serie de posts referentes al lenguaje Ensamblador hicimos uso de DOSBox para compilar, ligar y ejecutar los programas que utilizamos como ejemplos.

DOSBox demostró ser una herramienta muy útil, sin embargo, cada vez que lo utilizamos, era necesario llevar a cabo los siguientes pasos:

  1. Montar una unidad de disco nueva que en nuestro caso, apunta al fólder donde tenemos instalado el compilador y el linker de ensamblador.
  2. Agregar al PATH de DOSBox la ruta del fólder donde se localizan nuestros códigos, ya que éste es distinto del fólder donde se encuentran el compilador y el linker.
  3. Abrir el directorio de la carpeta donde se encuentran los programas.

Después de realizar estos pasos, ya estábamos listos para comenzar a trabajar.

Ahora bien, teclear un par de veces estas instrucciones parece muy poco trabajo, pero al cabo de un rato, se vuelve tedioso, por lo que nos dimos a la tarea de automatizarlas.

Para automatizar la ejecución de un conjunto de instrucciones al abrir DOSBox en Windows, debes llevar a cabo estos sencillos pasos:

  1. Desde el botón de inicio de Windows, abre la opción de Todos los programas.
  2. Dirígete al fólder de DOSBox y abre el subfólder llamado Options (Opciones). En este fólder observarás que hay tres archivos. El que nos interesa es el archivo que termina con la palabra Options, que es un archivo de configuración para las opciones que puedes utilizar con DOSBox.
  3. Abre el archivo de opciones simplemente haciendo click sobre él.
  4. Este archivo está dividido en secciones. Reconocerás el título de una sección porque éste se encuentra escrito entre corchetes, por ejemplo: [seccion]. Debajo de cada sección, observarás que las opciones a editar se presentan escritas en forma de variable=valor y los comentarios inician con el símbolo "#".
  5. Dirígete a la última sección, llamada [autoexec]. Debajo de esta sección puedes colocar la serie de instrucciones que deseas que se ejecuten automáticamente al abrir DOSBox.

En nuestro caso, el resultado de editar la sección [autoexec] luce de la siguiente manera:

Al abrir DOSBox, observamos cómo se ejecutaron automáticamente las instrucciones que editamos en el archivo de configuración:

miércoles, 18 de julio de 2012

Ensamblador 8086 Moviendo imagen BMP

En el post anterior de esta serie mostramos cómo desplegar una imagen bmp. En este post expandiremos el código para mover la imagen alrededor de la pantalla. El siguiente video muestra el objetivo final del programa.

Lo primero que realizamos fue separar las rutinas relacionadas con desplegar la imagen, éstas fueron colocadas en el archivo libgraf.asm con su respectivo archivo libgraf.inc para poder exportar las funciones más importantes a otros módulos (el siguiente post muestra cómo exportar funciones entre módulos). La función principal para desplegar una imagen bmp (mostrar_bmp) ahora recibe los siguientes parámetros: la ruta del archivo y la posición inicial de la esquina superior izquierda de la imagen en la pantalla. De esta forma ya podemos colocar una imagen en cualquier parte de la pantalla y no sólo en la esquina superior izquierda.

La lógica para mover la imagen en la pantalla es muy sencilla (bmp01.asm):

1 Inicializar el modo de video
2 Ciclo hasta que presionemos una tecla:
   2.1 Limpiar pantalla
   2.2 Actualizar la coordenada x en la dirección actual
   2.3 Si hubo una colisión contra el límite izquierdo o derecho
      2.3.1 Ajustar coordenada x
      2.3.2 Invertir dirección en x
   2.4 Realizar pasos 2.2 y 2.3 para y
   2.5 Desplegar imagen

El código que implementa esta lógica se muestra a continuación (bmp01.asm). En el siguiente proyecto de mercurial podrás descargar los 3 archivos necesarios para construir el ejecutable. El proyecto también incluye la imagen bmp usada. Para ensamblar y ligar estos archivos basta con invocar:

ml bmp01.asm libgraf.asm

Como pueden observar en el video, la imagen parpadea notablemente, esto se debe a que estamos escribiendo al área de memoria de gráficos al mismo tiempo que esta área está siendo desplegada en la pantalla. Esto causa que frecuentemente se desplieguen en la pantalla versiones incompletas de la imagen final. En el siguiente post exploraremos una opción para mejorar esto.

.model  small
.stack 128
INCLUDE libgraf.inc
.data
; El nombre del archivo debe terminar en 0
pelotaf         db "pelota.bmp",0
pelotax         dw 100
pelotay         dw 100
pelotad         dw 40
maxx            dw 320
maxy            dw 200
deltax          dw 2
deltay          dw 2

.code
.startup
  mov ax,@data
  mov ds,ax
main proc
  ; Configurar modo grafico VGA con resolucion de 320x200
  call    InitVid
  
  ciclo1:
  call limpiar_pantalla
  ; -------------------------------------------------------
  ; Controlar posicion x de la pelota
  ; -------------------------------------------------------
  testX:
  mov ax, pelotax
  ; avanzar la pelota en la direccion actual
  add ax, deltax
  
  ; validar colision contra limite derecho
  testmaxX:
  mov cx, maxx
  sub cx, pelotad
  cmp cx, ax
  jg testminX
  ; si la pelota esta en el limite derecho
  ; alinearla al limite e invertir la direccion
  mov ax, cx
  mov deltax, -2
  
  ; validar colision contra limite izquierdo
  testminX:
  cmp ax,0
  jg testY
  ; si la pelota esta en el limite izquierdo
  ; alinearla al limite e invertir la direccion
  mov ax,0
  mov deltax, 2
  
  ; -------------------------------------------------------
  ; Controlar posicion y de la pelota
  ; -------------------------------------------------------
  testY:
  mov bx, pelotay
  ; avanzar la pelota en la direccion actual
  add bx, deltay
  
  ; validar colision contra limite inferior
  testmaxY:
  mov cx, maxy
  sub cx, pelotad
  cmp cx, bx
  jg testminY
  ; si la pelota esta en el limite inferior
  ; alinearla al limite e invertir la direccion
  mov bx, cx
  mov deltay, -2
  
  ; validar colision contra limite superior
  testminY:
  cmp bx,0
  jg dibpelota
  ; si la pelota esta en el limite superior
  ; alinearla al limite e invertir la direccion
  mov bx,0
  mov deltay, 2
  
  ; -------------------------------------------------------
  ; Actualizar posicion (x,y) y pintar pelota
  ; -------------------------------------------------------
  dibpelota:
  mov pelotax, ax
  mov pelotay, bx
  mov dx, offset pelotaf
  call mostrar_bmp

  ; Si una tecla es presionada, salimos del ciclo.
  ; Noten que esta interrupcion no espera por una tecla,
  ; solo verifica si fue presionada y si no, continua.
  mov ah,01h
  int 16h
  jnz fin

  jmp ciclo1

  fin:
  ; Regresar a modo texto
  mov ax,0003h
  int 10h
  
  ; Finalizar el programa
  mov ax,4c00h
  int 21h
  ret
main endp
end

domingo, 8 de julio de 2012

Ensamblador 8086 Creando módulos

Conforme desarrollamos ejemplos más elaborados, el tamaño de nuestros archivos aumenta y muchas veces copiamos el mismo código en varios programas (por ejemplo, el código que despliega una imagen bmp). En este post, veremos una forma de cómo reusar funciones que están en un módulo (archivo) independiente.

Tomemos como ejemplo el programa de este post. El programa convierte dos cadenas a enteros usando la función atoi (ascii to integer), los suma y convierte el resultado a cadena usando itoa (integer to ascii) para poder desplegarlo. Las funciones itoa/atoi son funciones que podemos reusar en varios programas, por lo que es buena idea ponerlas en un archivo independiente.

Comencemos por colocar estas funciones en un archivo llamado libcad.asm. El segundo paso es crear un archivo que contenga los prototipos de las funciones que queremos usar en otros archivos. Llamemos a este archivo libcad.inc. El contenido del archivo libcad.inc es el siguiente:

.CODE
EXTERNDEF atoi:NEAR
EXTERNDEF itoa:NEAR
@CurSeg ENDS

Noten que debemos especificar el segmento en donde se encuentra lo que queremos exportar. En este caso nuestras funciones se encuentran en el segmento .CODE; además, es recomendable cerrar el segmento ya que incluiremos este archivo en otros archivos. La palabra clave EXTERNDEF declara lo que queremos exportar, en este caso es seguida por el nombre de la función y el tipo de la función (NEAR, porque estamos usando .MODEL SMALL en nuestros programas).

El archivo libcad.inc se debe incluir tanto en el archivo que define las funciones como en los archivos que las usan. El archivo libcad.asm queda de la siguiente forma:

.model SMALL
.STACK 128
INCLUDE libcad.inc
.CODE
; ========= Convertir cadena a numero =====================
; Parametros
; si: offset inicial de la cadena con respecto a DS
; Retorna
; bx: valor
atoi proc NEAR
  xor bx,bx   ;BX = 0

  atoi_1:
  lodsb       ;carga byte apuntado por SI en AL
              ;e incrementa si
  cmp al,'0'  ;es numero ascii? [0-9]
  jb noascii  ;no, salir
  cmp al,'9'
  ja noascii  ;no, salir

  sub al,30h  ;ascii '0'=30h, ascii '1'=31h...etc.
  cbw         ;byte a word
  push ax
  mov ax,bx   ;BX tendra el valor final
  mov cx,10
  mul cx      ;AX=AX*10
  mov bx,ax
  pop ax
  add bx,ax
  jmp atoi_1  ;seguir mientras SI apunte a un numero ascii
  noascii:
  ret         ;BX tiene el valor final
atoi endp

; =============== Convertir numero a cadena ===============
; Parametros
; ax: valor
; bx: donde guardar la cadena final
; Retorna
; cadena
itoa proc NEAR
  xor cx,cx  ;CX = 0

  itoa_1:
  cmp ax,0   ; El ciclo itoa_1 extrae los digitos del
  je itoa_2  ; menos al mas significativo de AX y los
             ; guarda en el stack. Al finalizar el 
  xor dx,dx  ; ciclo el digito mas significativo esta
  push bx    ; arriba del stack.
  mov bx,10  ; CX contiene el numero de digitos
  div bx
  pop bx
  push dx
  inc cx
  jmp itoa_1

  itoa_2:
  cmp cx,0    ; Esta seccion maneja el caso cuando
  ja itoa_3   ; el numero a convertir (AX) es 0.
  mov ax,'0'  ; En este caso, el ciclo anterior
  mov [bx],ax ; no guarda valores en el stack y
  inc bx      ; CX tiene el valor 0
  jmp itoa_4

  itoa_3:
  pop ax      ; Extraemos los numero del stack
  add ax,30h  ; lo pasamos a su valor ascii
  mov [bx],ax ; lo guardamos en la cadena final
  inc bx
  loop itoa_3

  itoa_4:
  mov ax,'$'  ; terminar cadena con '$' para 
  mov [bx],ax ; imprimirla con la INT21h/AH=9
  ret
itoa endp

end

Y el archivo de ejemplo que usa las funciones es el siguiente (ejem01.asm):

.model SMALL
.STACK 128
INCLUDE libcad.inc
.DATA
  cadena1 db '123$'
  cadena2 db '444$'
  cadena3 db 6 DUP(?)
  op1 dw ?
  op2 dw ?

.CODE
  .STARTUP
  mov ax,@data
  mov ds,ax

main proc
  ; SI parametro
  mov si, offset cadena1
  call atoi
  mov op1,bx
   
  ; SI parametro
  mov si, offset cadena2
  call atoi
  mov op2,bx
   
  ; sumar
  mov ax, op1
  add ax, op2
  mov bx, offset cadena3
  call itoa
   
  mov dx, offset cadena3
  call desplegar
   
  ; INT 21h / AH=4Ch retorna el control al sistema operativo
  ;                  termina el programa
  salir:
  mov ax,4c00h
  int 21h
  ret
main endp

; ============ Proc: Desplegar mensaje ====================
; Parametros
; dx: offset de cadena terminada por $ con respecto a DS
desplegar proc
   ; INT 21h / AH=9 - despliega cadena apuntada por DS:DX. 
   ; la cadena debe estar terminada por '$'.
   mov ah,09h
   ;mov dx, offset cad
   int 21h
   ret
desplegar endp

end

Para ensamblar y ligar estos archivos basta con invocar

ml EJEM01.ASM LIBCAD.ASM