Episode 4: Changing Video Modes

Episode 4: Changing Video Modes

We're going to make our first foray into graphics by changing the video mode! There's lots of new stuff here – let's just jump right into it:

[cpu 8086]
[org 100h]

jmp main

%include 'formatting.asm'
%include 'stdio.mac'

main:

; Get the initial video mode and save it to [originalVideoMode]
mov ax, 0f00h                ; AH = 0x0f (get video mode)
int 10h                      ; Call INT10h fn 0x0f which will store the current video mode in AL
mov [originalVideoMode], al  ; Store it into originalVideoMode.

; Change the video mode to Mode 9 (320x200, 16 colors)
mov ax, 9h                   ; AH = 0x00 (set video mode), AL = 9 (new mode)
int 10h                      ; Call INT10h fn 0 to change the video mode

; Format originalVideoMode to a string and print it. Nothing new here!
mov ax, [originalVideoMode]
mov di, buf16
call int_to_string
print str_orgVideoMode
print buf16
print str_crlf

; Do the same thing with the new video mode (9).
mov ax, 9
mov di, buf16
call int_to_string
print str_newVideoMode
print buf16
print str_crlf

; Print 'Press any key to continue'
print str_pressAnyKey
print str_crlf

; Call INT21h fn 8 (character input without echo) to wait for a keypress
mov ah, 08h
int 21h

; Change the video mode back to whatever it was before (the value stored in
; originalVideoMode)
mov al, [originalVideoMode]
xor ah, ah
int 10h

; Exit the program
mov ax, 4c00h
int 21h

section .data

str_orgVideoMode: db 'Original video mode: $'
str_newVideoMode: db 'New video mode: $'
str_pressAnyKey: db 'Press any key to continue', 0dh, 0ah, '$'
str_crlf: db 0dh, 0ah, '$'

section .bss

originalVideoMode: resb 1
buf16: resb 16

stdio.mac contains our printing macro; nothing special there. Just make sure you add it to the MACROS variable in the Makefile. Besides that, there are a couple structural changes you should be aware of: Notice the section .data and section .bss at the bottom. An executable has (for our purposes) 3 sections: .text (or .code) which stores the instructions, .data which stores initialized data (the only type we have used so far), and .bss which stores uninitialized data. For now, you can think of initialized data as constants - labeled memory locations inside the program which contain data. Uninitialized data is exactly that -- data areas which you can freely write to and read from, but which initially have an undefined value (so you'd better write to them before you read from them!). The benefit to using uninitialized data is that it doesn't take up space in your executable - these variables will point to system memory just after the end of the program. Instead of defining our buf16 as a 16-byte array of zeroes, we declare it as 16 bytes of uninitialized space. We also declare an additional byte named originalVideoMode. In addition, we declare a bunch of new strings in our .data section, but there's nothing special about them.

The first thing we do, code-wise, is get the initial video mode and store it in our originalVideoMode variable. Looking at this list of INT 10H services, we see that if call INT 10H with a 0x0f in register AH, we'll get the current video mode in AL. We then copy that value to originalVideoMode. Before we've only referenced variables without brackets. Think of the bracket / no-bracket syntax as "reverse pointer" - referring to a memory location with brackets means to use the contents of the memory location, but referring to a variable without brackets means to use the address that that memory location points to. So un-bracketed memory references are like pointers in C, and bracketed memory references are like basic data types.

Now we're ready to change the video mode! INT 10H fn 0 will do that for us, changing to the video mode number in AL. Looking at the video modes available (taken from Vitaly Filatov's ASM page which is another great resource), we see that the PCjr supports video modes 0 through 10 (everything that's not exclusively EGA). Modes 0-7 are standard PC CGA modes, but modes 8-10 are special "CGA Plus" modes specific to the Video Gate Array graphics card that powers the PCjr and Tandy 1000. Video mode 9 seems to give us the most bang for our buck: 16 colors at 320x200. Let's go for that.

The next three blocks of code should be fairly readable to you by now. We put a number in AX, format it into a string buffer, then print the string using our macro. We're printing the original video mode then the new video mode. After that we print the string "Press any key to continue".

How are we going to know when a key is pressed? Looking back at our great resource for DOS INT 21h services, we'll use service 8, which reads a character from standard input without echoing it back to the screen. This is a blocking call; nothing more will happen until a key is pressed.

After we get a keypress, we change the video mode back to its initial value (we don't want to leave the user in 320x200x16 when we exit) then quit the program.

Let's try it out:

Beautiful! We clearly changed video modes because our text is a different color and size. Once you hit a key the program should switch back to the original 80x25 text mode and end.

We're repeating ourselves a lot. Let's see if we can simplify the code with some more macros:

; Prints the given '$'-terminated string and newline.
%macro println 1
 print %1
 print str_crlf
%endmacro

%macro intToString 1
  mov ax, %1
  mov di, buf16
  call int_to_string
%endmacro

; Waits for a key press using int 21h fn 08h (char input without echo)
%macro waitForAnyKey 0
 mov ah, 08h
 int 21h
%endmacro

Put these in stdio.mac. We've added a println macro which prints the given buffer then a newline (0xa, 0xd), a intToString which calls our int_to_string procedure after setting the registers for us, and a waitForAnyKey macro which uses INT21H fn 8 to pause the code until a key is pressed.

These macros depend on the memory locations str_crlf and buf16 to be declared, so we best note that with some comments at the top of the macros file:

; Expects the following initialized data:
; str_crlf: db 0dh, 0ah, '$'
;
; and the following uninitialized data:
; buf16: resb 16

Now we can simplify the meat of our code (printing out the original video mode through waiting for a keypress):

; Format originalVideoMode to a string and print it. Nothing new here!
print str_orgVideoMode
intToString [originalVideoMode]
println buf16

; Do the same thing with the new video mode (9).
print str_newVideoMode
intToString 9
println buf16

; Print 'Press any key to continue'
println str_pressAnyKey

; Call INT21h fn 8 (character input without echo) to wait for a keypress
waitForAnyKey

If you get confused with the edits, always remember you can check out the completed source code to this episode at:
https://github.com/josh2112/pcjr-asm-game/tree/episode-4