4

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

Ross Ridge
  • 38,414
  • 7
  • 81
  • 112
  • That is not a [mcve]. Note, if you don't even get the light blue background then don't focus on the mario image because you have deeper problems. Also, learn to use a debugger. – Jester Oct 15 '19 at 22:19
  • 320x200x8bpp VGA mode you using is 8bit per pixel not 16 !!! Also I do not see any target segment 0xA000 so I hope that is where you are copying the screen buffer. Take a look at this old MS-DOS game of mine [What is the best way to move an object on the screen?](https://stackoverflow.com/a/29579522/2521214) it might help a bit with your project ... also this [Graphics mode in assembly 8086](https://stackoverflow.com/a/48664419/2521214) – Spektre Oct 16 '19 at 07:49
  • If I am right, I am using 8 bits per pixel as I am moving the data in register al to the memory location in the buffer. And this is just the double buffer to write it to screen I am indeed copying it to the 0xA000 segment. Should I also provide the code I use for copying the buffer to video memory to make it more clear? – tbreugelmans Oct 16 '19 at 10:51
  • @LazyL0tus no need if you're sure its working ... there might be more issues causing this I can think of. 1. I am not familiar with qemu nor debian but its possible your emulator expect certain VGA access style to refresh screen like accessing some VGA register or calling specific BIOS call. 2. Another thing is as your stuff is bootable is your executable really 16 bit x86 or 32/64 bit ? IIRC The latter can not access BIOS functions ... 3. do you have LCD? some can not handle old VGA low resolutions correctly and are out of sync... 4. is your sprite really loaded into memory? – Spektre Oct 16 '19 at 11:44
  • try to render some pattern instead like `pixel[y*320+x]=x+y;` to check your rendering works... something like this [No Signal](https://stackoverflow.com/a/29296619/2521214) that would rule out the 1,2,3 problems if working – Spektre Oct 16 '19 at 11:46
  • Yes, my stuff is actually 16 bits, I could put pixels to the screen using BIOS interrupts. Also qemu can display graphics to the screen (I looked at some tutorials for games in qemu). As for rendering a pixel to the screen I can directly draw a pixel in video memory. But when I first draw it in the buffer and then copy it to the VGA memory it does not display it to the screen. – tbreugelmans Oct 16 '19 at 13:06

1 Answers1

4
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
Sep Roland
  • 33,889
  • 7
  • 43
  • 76
  • 1
    This fixed at least one problem, my screen now shows a full light blue screen! But still no sprite gets loaded so I'll still have to look into that – tbreugelmans Oct 16 '19 at 12:57
  • @LazyL0tus Do remove the `jmp main` from your code. Only keep the `jmp $`. You're erasing the picture immediately after drawing it. Maybe that's why you only see the blue background! – Sep Roland Oct 16 '19 at 13:09
  • After removing the `jmp main` it still only shows a blue screen which is expected as the reset buffer only resets all pixels in the buffer and after that it redraws the sprite to the buffer. The problem lies with the buffer as it is either not drawing the sprite or it is not copied over correctly – tbreugelmans Oct 16 '19 at 13:22