Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to map 320x200 pixels to VGA video memory in 16 bit assembly

For a bonus assignment at college, we are required to make a game in assembly. My idea was to recreate the first level of mario NES, but while coding I could not make the emulator I am using (qemu) display pixels on screen.

I am coding on a Linux Debian distribution and compiling my code with NASM. I am using this bootloader: ( https://github.com/Pusty/realmode-assembly/blob/master/part6/bootloader.asm ) to make the game bootable and am coding the kernel myself.
To draw to the screen I am using 320x200 VGA mode (int 0x10, 0x13) and use a double buffer method to write to screen to make it run smoother.

I am currently using this code to draw the double buffer

; si = image to draw, ax = x location offset of image, bx = y location offset of image
drawBuffer:
pusha
push es

xor di, di
imul di, bx, 320     ; translate the y offset to y position in buffer
add di, ax           ; adds the x offset to buffer to obtain (x,y) of image in buffer
mov es, word [bufferPos]  ; moves the address of the buffer to es
mov bp, ax           ; saves the x offset for later use

; image is in binary and first two words contain width and height of image
xor ax, ax
lodsb
mov cx, ax           ; first byte of img is the width of the image
lodsb
mov dx, ax           ; second byte of img is height of the image

    .forY:
     ; check if within screen box
     mov bx, di
     add bx, cx      ; adds length of image to offset to get top right pixel
     cmp bx, 0       ; if top right pixel is less than 0 it is outside top screen
     jl .skipX       ; if less then 0 skip the x pixels row
     sub bx, cx
     sub bx, 320*200 ; subtracts total screen pixels off of image to get left botom pixel
      jge .skipX     ; if greater then 0 it is outside of bottom of screen
      xor bx, bx

      .forX:
           ; check if within left and right of screen
           cmp bx, 320
           jg .skip
           sub bx, bp
           cmp bx, 0
           jl .skip
           ; if more bx (x position) is more >320 or <0 it is outside of screen           

           mov al, byte [si + bx]   ; moves the color of the next pixel to al
           test al, al              ; if al is 0x0 it is a 'transparant' pixel
           jz .skip
           mov byte [es:di], al     ; move the pixel to the buffer
           .skip:
           inc bx                   ; move to next pixel in image
           cmp bx, cx               ; check if all pixels in x row of image are done
           jl .forX                 ; if not all images are done repeat forX
     .skipX:
     add di, 320             ; if forX is done move to next y position
     add si, cx              ; moves to the next y row in image
     dec dx                  ; decrements yloop counter
     jnz .forY
pop es
popa
ret


bufferPos dw 0x7E0      ; address at which to store the second buffer

This code is stored in a separate file called buffer.s and is included in the kernel.s using

%include buffer.s

The kernel is located at 0x8000 and all it does is contain the image to draw with the x and y offset and it calls the double buffer

org 0x8000
bits 16

setup:
   mov ah, 0
   mov al, 0x13
   int 0x10

main:
   call resetBuffer     ; method to set all pixels in buffer to light blue
   mov ax, 10           ; x position of image
   mov bx, 100          ; y position of image
   mov si, [mario]      ; moves the image location to si
   call drawBuffer

   call writeVideoMem   ; simply stores all bytes in the buffer to the videoMemory
   jmp main

jmp $

mario
   dw mario_0

mario_0 incbin "images/mario_right_0.bin"

times (512*16)-($-$$) db 0

What I would expect is for it to draw mario, but when I run it on qemu using

nasm -fbin bootloader.s -o bootloader.bin
nasm -fbin kernel.s -o kernel.bin
cat bootloader.bin kernel.bin > game.bin

qemu-system-i386 game.bin

it just shows a black screen and nothing is drawn.

The only thing I can think of is that to access all the pixels 16 bit registers don't have enough bits and that is why it is not working.

PS. If any more information is needed, I'll be happy to provide

like image 394
tbreugelmans Avatar asked Sep 18 '25 22:09

tbreugelmans


1 Answers

bufferPos dw 0x7E0 

Writing to the buffer overwrites the kernel!

The bootloader that you use stores the kernel at linear address 0x00008000. For loading it sets ES:BX == 0x0000:0x8000. And you've correctly placed an ORG 0x8000 on top of the source for your kernel.

But you've defined your double buffer at linear address 0x00007E00 (ES * 16). You've set ES == 0x07E0.

A decent buffer for the 320x200x8 video mode should have 64000 bytes. Therefore setup your double buffer at e.g. linear address 0x00010000 which would require you to write:

bufferPos dw 0x1000

This leaves room for a 32KB kernel. Currently you're using only 8KB for the kernel.


The code that you use to check if the picture stays within the screen/buffer looks very bogus. I suggest you remove it for now.
The inner loop that processes 1 horizontal line forgets to increment the DI register!

.forY:
    PUSH DI              <******************
    xor bx, bx
    .forX:
        mov al, byte [si + bx]
        test al, al           ; if al is 0x0 it is a 'transparant' pixel
        jz .skip
        mov byte [es:di], al  ; move the pixel to the buffer
      .skip:
        INC DI           <******************
        inc bx                ; move to next pixel in image
        cmp bx, cx            ; check if all pixels in row are done
        jl .forX                 ; if not all images are done repeat forX
    .skipX:
    POP DI               <******************
    add di, 320             ; if forX is done move to next y position
    add si, cx              ; moves to the next y row in image
    dec dx                  ; decrements yloop counter
    jnz .forY

Try this simplified code, and once it works re-think how to check the limits. IMO that should not involve the address in DI.

An small improvement to the above snippet:

.forY:
    xor bx, bx
    .forX:
        mov al, byte [si + bx]
        test al, al           ; if al is 0x0 it is a 'transparant' pixel
        jz .skip
        mov byte [es:di+BX], al  ; move the pixel to the buffer
      .skip:
        inc bx                ; move to next pixel in image
        cmp bx, cx            ; check if all pixels in row are done
        jl .forX                 ; if not all images are done repeat forX
    .skipX:
    add di, 320             ; if forX is done move to next y position
    add si, cx              ; moves to the next y row in image
    dec dx                  ; decrements yloop counter
    jnz .forY
like image 134
Sep Roland Avatar answered Sep 23 '25 10:09

Sep Roland