Asmtut 6: Live interaction

by
2013-01-24

This was originally posted on Google+, which has now been shut down. It was helpfully converted to Markdown by Robert Jacobson after which I adjusted it for reposting here. I have dated it at its original posting date, but it was posted here on 2019-09-07.


Since the last time, the nasm people have been able to fix the bugs I stumbled upon, and pushed them in a release.

Anything newer than 2.10.6 is probably also fine. Please note that you have to update if you are following this tutorial on OS X, because we will be triggering the bugs present in earlier versions.

With that out of the way, let's dive into making the game real time, so you don't have to hold down buttons on the keyboard. This should serve as a good repetition after a long hiatus, because we will only be doing more of the stuff we have already learned.

Step 13: ANSI escape codes

This part is for you, Michael!

When playing the latest version of the game we are making, we would fill the scrollback buffer in the terminal quite quickly with uninteresting prints of former game states. What we want to do instead is to print new versions of the board on top of the previous version. To do this, we will use an ANSI escape code sequence to move the cursor up to the top of the board each time we print it.

We want to use the CSI code CUU; Cursor up. The structure for CUU is ESC, '[', number of rows to move upwards, 'A'. ESC is the magic value 27 = 0x1b = 033. The number of rows we want to move is height + 2, counting the top and bottom solid lines in addition to the height number of open lines:

move_up  db  0x1b, '[27A' ; Keep number in string equal to height + 2

We can put this right above our definition of board, and then print the entire thing in one syscall. We also need the size. Let's put this calculation right under board_size:

move_up_then_board_size equ $-move_up

If we print both of move_up and board instead of just board, it will finally stand still and not scroll back:

mov rsi, move_up                 ; buf
mov rdx, move_up_then_board_size ; nbyte

In addidtion, we need to print it once without move_up before entering main_loop, so we avoid overwriting the user's terminal history. This is easily accomplished by sticking an additional syscall to SYS_write just before main_loop.

Your program should now look like part1.asm.

Step 14: Velocity

Next we will get some velocity going, so that the snake keeps moving in the direction it's going. We will do this by replacing the inc, dec, add and sub instructions in our cmp/jne block by storing the appropriate velocity in r15 with appropriate mov r15, instructions. We'll put in add r8, r15 just below .main_loop:. The additional details I'll leave as an excercise :)

Hopefully, you will end up with something like part2.asm.

Step 15: Nonblocking input

Even though we are in half-cooked (sigh) mode, we are still in blocking mode. The SYS_read call blocks until it can read at least one byte. Let's finally change that. We will do that by setting and unsetting the O_NONBLOCK flag on STDIN_FILENO with SYS_fcntl:

SYS_fcntl       equ 0x02000000 + 92

F_SETFL         equ 0x00000004
O_NONBLOCK      equ 0x00000004

mov rax, SYS_fcntl
mov rdi, STDIN_FILENO
mov rsi, F_SETFL
mov rdx, O_NONBLOCK ; 0 to unset
syscall

Unfortunately, even though we set this flag specifically on STDIN_FILENO, it magically applies to standard out as well. We don't need or want to handle nonblocking output, so we opt for the cheap workaround of setting and unsetting O_NONBLOCK before and after calls to SYS_read. We'll set the flag just before .read_more: and unset it between .done: and jmp .main_loop. Running the game now, it seems to still be kind of blocking, at least until we press one key. This is because we currently do not attempt to tell the difference between having read one byte and not. Let's fix that.

Begin by removing the "Assume exactly one byte was read"-comment. In man 2 read we see that the count of bytes read will be returned, and from the all-mighty ABI we learn that such a return value will be passed in rax. When no bytes are read because of O_NONBLOCK, SYS_read will actually return -1 and claim that the error EAGAIN has occurred. The simplest conclusion we can draw from this is that we check rax and jump to .done if it's not 1:

cmp rax, 1
jne .done

Your game should now be incredibly unplayable, and look something like part3.asm.

Step 16: Real time

At this point we need some kind of real time game loop. However, it is easier to cheat and just sleep for a bit between each frame. The price we have to pay for going the easy route is that we have to implement sleep in terms of select, since sleep is not available directly as a syscall.

SYS_select      equ 0x02000000 + 93

From man select:

int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* errorfds, struct timeval* timeout);

nfds, readfds, writefds, and errorfds can all be 0. That should fill up rdi, rsi, rdx and rcx. timeout must point to a valid timeval struct instance, its address placed in r8. Oh, nuts. Since we are already using r8, this is bad news. Let's just replace all instances of r8 with r14 to avert immediate disaster.

The declaration of struct timeval is messy and ridden with preprocessor noise, but it boils down to this nasm:

struc timeval
    ; resq: reserve quadword (64 bits)
    .tv_sec:    resq    1
    .tv_nsec:   resq    1
endstruc

Now we get to exercise nasm's syntax for defining an instance of a struct in the data section:

timeout:
    istruc timeval
        at timeval.tv_sec,  dq 0
        at timeval.tv_nsec, dq 100000
    iend

Then we're all set for adding the sleep call, right before .read_more:, so we can read any keypresses that happened during the sleep:

mov rax, SYS_select
mov rdi, 0              ; nfds
mov rsi, 0              ; readfds
mov rdx, 0              ; writefds
mov rcx, 0              ; errorfds
mov r8, timeout         ; timeout
syscall

Your code should now approximate part4.asm.

And playing the game should look kind of like this:

Next lesson: TBA

, 2013