principleMinor
A low tech approach to measuring game speed
Viewed 0 times
techlowgamemeasuringspeedapproach
Problem
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.
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
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
Solution
New solution
At the beginning of your post, you mention having troubles with creating a timer and using the BIOS interrupts
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.
I personally have never used these interrupts, nor have I ever heard of them.
In the past, when I have had to make a subroutine for a delay, I have always thought to use the interrupt
This is really simple interrupt: when used, it will return the tick count in
Obtains the current tick-count. The tick count is stored at
in the BIOS Data Area. It is incremented about once every 55 ms by
INT 08H.
Stats
The tick-counter is updated quite frequently: about every 55 milliseconds, the counter is increased.
Approximately 18.2 ticks is equivalent to a single second. This can be scaled up and down for entire minutes or for half seconds.
Flow
Thanks to the simplicity of this interrupt, it is very easy to create a timing method. All that needs to be done is to loop and then to do some comparing of values to make sure that a certain amount of ticks have passed by.
The general flow of a subroutine for "sleeping" with this interrupt would go like this:
See? Very simple, and very straightforward.
Code
Here is what that subroutine would look like:
Note: I am not familiar with the syntax of the FASM. This code was written with my knowledge of NASM.
Of course, if needed, you may have to preserve some registers on the stack.
Reasoning
I do not like your method for timing. The main reason? Portability. There are many, many different types of processors, and (most) all are going to have different clocks.
Different clocks means different speeds, which means unexpected results from this code. If you this were running on a very fast processor, this code would be run too fast, and if this were running on a very slow processor, this code would be run too slow. Everything is blown out of proportion.
However, using this timing method that I introduced, the timing will be the same across many processors, assuming they have the same implementation of interrupt
This provides you with a very reliable source of timing, so you know that two seconds on one machine will be the same two seconds on another.
Formatting
Your formatting is little inconsistent in a few places.
Let's start with the comments.
Comments
Generally, in assembly, comments are all on the same column. This greatly enhances readability.
Why? My thoughts on it are this: as someone is reading your code, they are going to be used to seeing all of the assembler instructions on the same column. Therefore, it is better on the eyes to also have all the comments on the same line.
Here is a place that could you use some fixing:
here, you would move the comment
Labels
When it comes to formatting labels in assembly, there are generally two types of people:
and:
Looking at your code, you like to go with the first version. While this is okay, you are allowing this method to tamper with your formatting.
Throughout your code, you do a good job keeping all your instructions on the same column. The only problem lines are the instructions right after the label:
With help from the comment above, I wasn't even able to see that first instruction the first few times I read over your code.
If you are going to go with this version of formatting labels, I recommend that you shift the rest of your instructions to the right to meet the same column as the first instruction.
I, personally, use the second version for labels, because it helps keep the formatting on the entire code very consistent.
Instructions
For the most part, you are consistent when formatting your instructions that take two "arguments":
I like this: one space after the instruction and one space after the first "argument".
However, you aren't consistent with this:
Magic numbers
You have a lot of magic numbers/values in your code:
and
and
While it's good that you have c
At the beginning of your post, you mention having troubles with creating a timer and using the BIOS interrupts
86h and 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.
I personally have never used these interrupts, nor have I ever heard of them.
In the past, when I have had to make a subroutine for a delay, I have always thought to use the interrupt
1Ah 00h.This is really simple interrupt: when used, it will return the tick count in
CX:DX.Obtains the current tick-count. The tick count is stored at
0040:006Ein the BIOS Data Area. It is incremented about once every 55 ms by
INT 08H.
Stats
The tick-counter is updated quite frequently: about every 55 milliseconds, the counter is increased.
Approximately 18.2 ticks is equivalent to a single second. This can be scaled up and down for entire minutes or for half seconds.
Flow
Thanks to the simplicity of this interrupt, it is very easy to create a timing method. All that needs to be done is to loop and then to do some comparing of values to make sure that a certain amount of ticks have passed by.
The general flow of a subroutine for "sleeping" with this interrupt would go like this:
- Store the tick count
- Add to the stored tick count the amount of ticks to wait
- Get the tick count
- If the new tick count is equal to the one stored, end.
- Go to 3.
See? Very simple, and very straightforward.
Code
Here is what that subroutine would look like:
; input: bx = ticks to wait
sleep:
mov ah, 0
int 1Ah
add bx, dx ; current tick count + ticks to wait
.stall:
int 1Ah
cmp dx, bx ; if we have advanced BX ticks
jne .stall
retNote: I am not familiar with the syntax of the FASM. This code was written with my knowledge of NASM.
Of course, if needed, you may have to preserve some registers on the stack.
Reasoning
I do not like your method for timing. The main reason? Portability. There are many, many different types of processors, and (most) all are going to have different clocks.
Different clocks means different speeds, which means unexpected results from this code. If you this were running on a very fast processor, this code would be run too fast, and if this were running on a very slow processor, this code would be run too slow. Everything is blown out of proportion.
However, using this timing method that I introduced, the timing will be the same across many processors, assuming they have the same implementation of interrupt
1Ah.This provides you with a very reliable source of timing, so you know that two seconds on one machine will be the same two seconds on another.
Formatting
Your formatting is little inconsistent in a few places.
Let's start with the comments.
Comments
Generally, in assembly, comments are all on the same column. This greatly enhances readability.
Why? My thoughts on it are this: as someone is reading your code, they are going to be used to seeing all of the assembler instructions on the same column. Therefore, it is better on the eyes to also have all the comments on the same line.
Here is a place that could you use some fixing:
call .ShortWait ; -> DX:AX BX=0
mov bl, 40 ;BH=0
call .ShortWait ; -> DX:AX BX=0here, you would move the comment
;BH=0 to the right so it lines up with it's upper and lower comments.Labels
When it comes to formatting labels in assembly, there are generally two types of people:
label: instruction
instructionand:
label:
instruction
instructionLooking at your code, you like to go with the first version. While this is okay, you are allowing this method to tamper with your formatting.
Throughout your code, you do a good job keeping all your instructions on the same column. The only problem lines are the instructions right after the label:
GetSpeedFactor: push bx cx
mov bx, 1With help from the comment above, I wasn't even able to see that first instruction the first few times I read over your code.
If you are going to go with this version of formatting labels, I recommend that you shift the rest of your instructions to the right to meet the same column as the first instruction.
I, personally, use the second version for labels, because it helps keep the formatting on the entire code very consistent.
Instructions
For the most part, you are consistent when formatting your instructions that take two "arguments":
add dx, cx
mov cx, 2197I like this: one space after the instruction and one space after the first "argument".
However, you aren't consistent with this:
xchg ax, bx ;BX=0
xchg dx, axMagic numbers
You have a lot of magic numbers/values in your code:
cmp cx, [046Ch]and
mov bp, 10and
mov di, 6While it's good that you have c
Code Snippets
; input: bx = ticks to wait
sleep:
mov ah, 0
int 1Ah
add bx, dx ; current tick count + ticks to wait
.stall:
int 1Ah
cmp dx, bx ; if we have advanced BX ticks
jne .stall
retcall .ShortWait ; -> DX:AX BX=0
mov bl, 40 ;BH=0
call .ShortWait ; -> DX:AX BX=0label: instruction
instructionlabel:
instruction
instructionGetSpeedFactor: push bx cx
mov bx, 1Context
StackExchange Code Review Q#101035, answer score: 8
Revisions (0)
No revisions yet.