HiveBrain v1.2.0
Get Started
← Back to all entries
principleMinor

A low tech approach to measuring game speed

Submitted by: @import:stackexchange-codereview··
0
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.

  • 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 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:006E
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:



  • 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

    ret


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 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=0


here, 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
       instruction


and:

label:
    instruction
    instruction


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:

GetSpeedFactor: push    bx cx
    mov bx, 1


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":

add dx, cx
mov cx, 2197


I 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, ax


Magic numbers

You have a lot of magic numbers/values in your code:

cmp cx, [046Ch]


and

mov bp, 10


and

mov di, 6


While 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

    ret
call    .ShortWait      ; -> DX:AX BX=0
mov bl, 40          ;BH=0
call    .ShortWait      ; -> DX:AX BX=0
label: instruction
       instruction
label:
    instruction
    instruction
GetSpeedFactor: push    bx cx
    mov bx, 1

Context

StackExchange Code Review Q#101035, answer score: 8

Revisions (0)

No revisions yet.