Whilst developing a game I needed a delay routine capable of doing delays ranging from 0.5 sec to just a few msec. The obvious choice was to use the BIOS delay function 86h on int 15h. In a true real address mode environment it works correctly but I saw that an emulator like DOSBox messes up things. Tons of illegal reads and writes. So I had to come up with another solution.
I can't use the TimeStampCounter because DOSBox delivers garbage for it. Solutions that reprogram the PIT or use the RTC interrupt have many pitfalls and will probably not even work under DOSBox.
My solution then is to find out how many iterations a delay routine can do in the interval between 2 ticks of the standard 18.2Hz timer. Those ticks are 55 msec apart. Because sometimes measurements can be erratic I only accept the results if 2 consecutive measurements vary by less than 1%%. Finally I divide the good measurement by 55 to obtain the number of iterations per msec aka SpeedFactor. Hereafter whenever I want to pause the program I multiply the desired delay expressed in msec by this SpeedFactor and then perform that number of iterations within the delay routine.
It is necessary to use the selfsame routine to both find out the SpeedFactor and to do the actual delays. That's why this routine has 2 exit conditions.
- The elapsing of a 32 bit iteration count.
- The elapsing of a 16 bit tick count.
During program setup the iteration counter is kind of disabled by assigning it
an impossibly large value. Thus it's the tick counter criterion that will be
used.
During normal program operation it is the tick counter that is kind of disabled
by assigning it a very large value. Thus it's the iteration counter criterion
that will be used.
; IN (dx:ax,bx) OUT (dx:ax,bx)
; Do DX:AX iterations or loop until Timer did BX Ticks
ShortWait:
push ds cx si di
xchg si, ax ;'mov si, ax'
mov di, dx
xor ax, ax
cwd
mov ds, ax
.a: mov cx, [046Ch] ;BIOS Timer
.b: sub si, 1
sbb di, 0
jb .c
add ax, 1
adc dx, 0
cmp cx, [046Ch]
je .b
dec bx
jnz .a
.c: pop di si cx ds
ret
About the accompanying program I wrote.
The included demo uses many different delays ranging from 0 to 500 msec. The animation it shows is of little importance. I just want every frame to complete in 1 minute. The demo can run forever but will instantly stop if a key is pressed. I measured the time it took to show 10 frames. As expected the emulators deviate most. The real address mode (that didn't need my solution to begin with) produces a descent delay but DOSBox's results are pittyful.
Loops/msec Retries RelSpeed Runtime
----------------------------------------------------------------
Real mode
Pentium-S 133 MHz DOS6.2 22268 (0) 1.00x 10'00"
Pentium-S 166 MHz DOS6.2 27693 (0) 1.24x 10'00"
Dual T2080 1730 MHz DOS6.2 159306 (1) 7.15x 10'00"
Atom N455 1666 MHz DOS6.2 207823 (0) 9.33x 10'00"
DOS window (a)
Pentium-S 133 MHz WIN98 22097 (2) 0.99x 10'05"
Pentium-S 166 MHz WIN95 27499 (9) 1.23x 10'05"
Dual T2080 1730 MHz VISTA 342179 (9) 15.36x 9'59" (b)
Atom N455 1666 MHz WIN7 212042 (7) 9.52x 10'03" (b)
DOSBox on WIN7
Max CPU cycles, frameskip 0 1802 (9) 0.08x 9'32"
Max CPU cycles, frameskip 10 2109 (9) 0.09x 10'13"
----------------------------------------------------------------
(a) Windows apparantly always uses frameskip.
(b) A bug in Vista/Win7 messes up the first frame ???
By now I wanted to find out if delays smaller than 1 msec would be possible, thus stretching the principle of proportionality. I added a series of 10 second tests that repeat very tiny delays many times. Results are acceptable as long as delaying requires a sufficiently large number of iterations.
100 µs 10 µs 1 µs 0.1 µs
---------------------------------------------------------------
Real mode
Pentium-S 133 MHz DOS6.2 9.94" 10.08" 10.39"
Pentium-S 166 MHz DOS6.2 9.99" 10.03" 10.44"
Dual T2080 1730 MHz DOS6.2 10.03" 9.90" 9.99" 10.66"
Atom N455 1666 MHz DOS6.2 9.99" 10.03" 10.08" 10.57"
DOS window
Pentium-S 133 MHz WIN98 9.99" 10.03" 10.35"
Pentium-S 166 MHz WIN95 9.90" 9.99" 10.12"
Dual T2080 1730 MHz VISTA 9.76" 9.76" 9.81" 10.17"
Atom N455 1666 MHz WIN7 9.85" 9.85" 9.90" 10.26"
DOSBox on WIN7
Max CPU cycles, frameskip 0 9.40" 9.85"
Max CPU cycles, frameskip 10 10.08" 10.39"
---------------------------------------------------------------
Conclusion
I don't care about the exact duration of a single delaying instance.
I do care about how the user perceives the program speed be it a game or
something else.
My low tech solution seems to be adequate although it could benefit from a better speed calculation especially under emulators. I'm not looking for any high tech solution that would probably just break under said emulators.
A word about my choices
To keep this programming project interesting I challenge myself to
- Use only 8086 instructions
- Work exclusively with the BIOS api
- Use a freely available assembler, I chose FASM
- Support at least one emulator, I chose DOSBox
Questions you might help me with
What do think about my delay method? An apparent problem is that it's not synchronized with the real time meaning if I were to change my demo so it operates on e.g. a very elaborated graphical picture and didn't change the delay values I would end up with a running time longer than expected.
I tried to obtain an animation with a speedprofile of starting slowly, then moving fast, and finally arriving gently. I don't think I succeeded. Perhaps the text screen is not suitable for such a visual effect or maybe it's the limited trajectory of only 17 discrete positions that's to blame. What do you think?
What do you think about my programming in general? I'm not a regular FASM user so I still have to learn it's specifics. Especially the use of macro's is new to me.
Do you like my commenting or do you think I didn't write enough comments?
I always try to optimize for size. Can you clearly see where this was done?
The complete program
; ----------------------------------------------
format MZ
stack 100h
entry TheCode:Main
; ----------------------------------------------
segment TheCode
; ----------------------------------------------
macro SetCursorPosition
{ mov ah, 02h
int 10h }
macro GetCursorPosition
{ mov ah, 03h
int 10h }
macro TypeCharacter
{ mov ah, 0Eh
int 10h }
macro GetKeyboardKey
{ mov ah, 00h
int 16h }
macro TestKeyboardKey
{ mov ah, 01h
int 16h }
; ----------------------------------------------
; IN () OUT (dx:ax)
; Wait for the start of a new TimerTick period (54.9254 msec)
; Then measure a 4 tick period (219.7016 msec)
GetSpeedFactor: push bx cx
mov bx, 1
call .ShortWait ; -> DX:AX BX=0
mov bl, 40 ;BH=0
call .ShortWait ; -> DX:AX BX=0
mov cx, 10
xchg ax, cx
mul dx
xchg ax, cx
mov dx, 10
mul dx
add dx, cx
mov cx, 2197
xchg ax, bx ;BX=0
xchg dx, ax
div cx
xchg ax, bx
div cx
mov dx, bx
pop cx bx
ret
; - - - - - - - - - - - - - - - - - - - - - - -
.ShortWait: mov ax, -1
cwd
; --- --- --- --- --- --- --- ---
; IN (dx:ax,bx) OUT (dx:ax,bx)
; Do DX:AX iterations or loop until Timer did BX Ticks
ShortWait: push ds cx si di
xchg si, ax ;'mov si, ax'
mov di, dx
xor ax, ax
cwd
mov ds, ax
.a: mov cx, [046Ch] ;BIOS Timer
.b: sub si, 1
sbb di, 0
jb .c
add ax, 1
adc dx, 0
cmp cx, [046Ch]
je .b
dec bx
jnz .a
.c: pop di si cx ds
ret
; ----------------------------------------------
Main: mov ax, TheData
mov ds, ax
mov es, ax
cld
; Measure the number of iterations (within the ShortWait routine) per msec
; Only accept if consecutive measurements vary by less than 1%%
; If measurements remain erratic than do accept the last one
mov bp, 10 ;Max try
call GetSpeedFactor ; -> DX:AX
.a: xchg si, ax ;'mov si, ax'
mov di, dx
call GetSpeedFactor ; -> DX:AX
push ax dx ;(1)
.b: sub ax, si
sbb dx, di
jnb .c
add ax, si
adc dx, di
xchg si, ax
xchg di, dx
jmp .b
.c: mov cx, 1000
xchg ax, cx
mul dx
xchg ax, cx
mov dx, 1000
mul dx
add dx, cx
sub si, ax
sbb di, dx
pop dx ax ;(1)
cmc
dec bp
jnbe .a
mov [SpeedFactor], ax
mov [SpeedFactor+2], dx
; Display the result
mov bx, 10 ;CONST
mov si, Msg1
call .TypeString
mov ax, [SpeedFactor]
mov dx, [SpeedFactor+2]
push bx ;BX=10 Sentinel
.d: xor cx, cx
xchg ax, cx
xchg dx, ax
div bx ;BX=10
xchg ax, cx
div bx ;BX=10
xchg dx, cx
push cx
mov cx, ax
or cx, dx
jnz .d
pop ax
.e: add al, "0"
TypeCharacter
pop ax
cmp ax, bx ;BX=10 Sentinel
jb .e
; Display the number of retries when measuring was done
sub ax, bp
add al, "0"-1
mov [Msg2_], al
mov si, Msg2
call .TypeString
; Compare this computer to a (20 year old) Pentium-S at 133 MHz
mov cx, 100 ;Percentage
mov ax, [SpeedFactor+2]
mul cx
xchg cx, ax
mul [SpeedFactor]
add dx, cx
mov cx, 22268 ;Pentium
div cx
mov di, 6
sub sp, di
mov bp, sp
.f: lea si, [di-1]
.g: xor dx, dx
div bx ;BX=10
add dl, "0"
.h: dec di
mov [bp+di], dl
jz .i
mov dl, "."
cmp di, 4
je .h
test ax, ax
jnz .f
jmp .g
; Display the result with 2 decimal places
.i: cmp si, 3
jb .j
mov si, 2
.j: mov al, [bp+si]
TypeCharacter
inc si
cmp si, 6
jb .j
add sp, si
mov si, Msg3
call .TypeString
; Next comes a little demo that uses a variety of delays
; Get that stopwatch out
call .GetKey ; -> AX
; First setup the frame
.Demo: mov cx, 38
mov al, "Ú"
.k: TypeCharacter
mov al, "Ä"
loop .k
mov al, "¿"
call .TypeString__
mov bl, 3 ;BH=0
.l: mov cl, 18 ;CH=0
mov al, "Ã"
.m: TypeCharacter
mov al, "Ä"
loop .m
mov cl, 20 ;CH=0
mov al, 254
.n: TypeCharacter
loop .n
mov al, "´"
call .TypeString__
dec bx
jnz .l
mov cl, 38 ;CH=0
mov al, "À"
.o: TypeCharacter
mov al, "Ä"
loop .o
mov al, "Ù"
call .TypeString__
; Then move all the little bricks to the left
GetCursorPosition ; -> DX (AX CX)
sub dh, 4
mov dl, 1+16
mov cx, 20
.p: push cx dx ;(2)
mov si, SpeedProfile
mov di, 17
.q: mov bl,3 ;BH=0
.r: SetCursorPosition
mov al, 254
TypeCharacter
mov al, "Ä"
TypeCharacter
inc dh
dec bx
jnz .r
sub dx, 0301h
push bx dx ;(3)
mov ax, [SpeedFactor+2]
mul word [si]
xchg bx, ax ;'mov bx, ax'
lodsw
mul [SpeedFactor]
add dx, bx
mov bx, -1
call ShortWait ; -> DX:AX BX
pop dx bx ;(3)
dec di
jnz .q
pop dx cx ;(2)
inc dx ;'inc dl'
TestKeyboardKey ; -> AX ZF
jnz .EndOfDemo
loop .p
; Finally increase and display the FrameCounter
; One can only hope that 38 decimal digits are enough
mov si, CRLF
.s: dec si
mov al, "0"
xchg [si], al
cmp al, " "
je .t
cmp al, "9"
je .s
mov [si], al
.t: inc byte [si]
mov si, Msg4
call .TypeString
; Now is a good time to read your stopwatch
; The demo repeats itself
jmp .Demo
.EndOfDemo: GetKeyboardKey ; -> AX
; Next comes a series of tests with truly tiny delays
mov si, Msg5
call .TypeString ; -> SI
xor cx, cx ;CONST
; Calculate DelayCount for 0.1, 0.01, 0.001, 0.0001, and 0.00001 msec
.Test: mov di, 10
mov ax, [SpeedFactor+2]
xor dx, dx
div di
push ax
mov ax, [SpeedFactor]
div di
cmp dx, 5
pop dx
mov [SpeedFactor], ax
mov [SpeedFactor+2], dx
cmc
adc ax, cx ;CX=0
adc dx, cx ;CX=0
push dx ax ;(4)
sub ax, di ;DI=10
sbb dx, cx ;CX=0
jb .EndOfTests
; Set RepeatCount to 100K, 1M, 10M, 100M, and 1G
mov ax, [RepeatCount+2]
mul di
xchg di, ax
mul [RepeatCount]
add dx, di
mov [RepeatCount], ax
mov [RepeatCount+2], dx
sub ax, 1
sbb dx, cx ;CX=0
push dx ax ;(5)
; Get ready for START message
call .TypeString ; -> SI
call .GetKey ; -> AX
push ds ;(6)
mov ds, cx ;CX=0
mov cl, 55 ;CH=0 3 sec
.u: mov ax, [046Ch] ;BIOS Timer
.v: cmp ax, [046Ch]
je .v
loop .u
pop ds ;(6)
; The 10 second test
push si ;(7)
mov si, MsgStart
call .TypeString
pop si ;(7)
pop ax dx di bp ;(5)(4)
push bx ;(8)
nop ;'cli'
.w: mov bx, -1
call ShortWait ; -> (BX)
sub di, 1
sbb bp, cx ;CX=0
jnb .w
nop ;'sti'
pop bx ;(8)
push si ;(9)
mov si, MsgStop
call .TypeString
pop si ;(9)
cmp si, Msg6
jb .Test
; Final message before exiting
.EndOfTests: mov si, Msg6
call .TypeString
GetKeyboardKey ; -> AX
.ExitToDOS: mov ax, 4C00h
int 21h ;DOS 'Terminate'
; - - - - - - - - - - - - - - - - - - - - - - -
.GetKey: GetKeyboardKey ; -> AX
cmp al, 27
je .ExitToDOS
ret
; - - - - - - - - - - - - - - - - - - - - - - -
.TypeString__: mov si, CRLF
.TypeString_: TypeCharacter
.TypeString: lodsb
cmp al, 0
jne .TypeString_
ret
; ----------------------------------------------
segment TheData
; ----------------------------------------------
RepeatCount dw 10000,0
SpeedFactor dw 0,0
SpeedProfile dw 430,230,150,99,99,99,99,99,99,99,99,99,99,300,400,500,0
Msg1 db 'The delay routine does ',0
Msg2 db ' iterations per msec.',13,10
db 'Number of retries to obtain this result : '
Msg2_ db ' ',13,10,'This computer operates at ',0
Msg3 db ' x the speed of a Pentium-S at 133 MHz.',13,10,10
db 'The following demo completes each frame in 1 minute.'
db 13,10,'Pressing a key at any time will stop the demo.'
db 13,10,'Now press any key to start it...',13,10,10,0
Msg4 db 13,10,10,38 dup (' ')
CRLF db 13,10,0
Msg5 db 13,10,10,10,'Next comes a series of 10 second tests'
db ' using ever smaller delays.',13,10,'Use a stopwatch'
db ' on seeing messages START STOP.',13,10,10,0
db 'Press key for 0.1 msec test... ',0
db 'Press key for 0.01 msec test... ',0
db 'Press key for 0.001 msec test... ',0
db 'Press key for 0.0001 msec test... ',0
db 'Press key for 0.00001 msec test... ',0
Msg6 db 10,'Thank you...',13,10,10,0
MsgStart db 'START',0
MsgStop db ' STOP',13,10,0