• Please review our updated Terms and Rules here

Variable FPS Animation with DDA

neilobremski

Experienced Member
Joined
Oct 9, 2016
Messages
55
Location
Seattle, USA
I just finished writing some code to animate movement and turning. When initially planning to tackled this task I was going to make a lot of assumptions and fix the framerate regardless of the computer speed. This would mean that on fast-enough computers the animation would run as expected but on slower computers it would slow down. Of course, once I started down this path I immediately didn't like it and decided to delve into writing something that would adjust the FPS [SUP][1][/SUP] depending on how much time was being taken to draw each frame.

Firstly, I am using the BIOS tick count at 0000:046C for timing which provides a granularity of 18.2 changes per second [SUP][2][/SUP]. I use this as the minimum basis for delay after drawing a frame. That is, the fastest a frame can be drawn is one tick or 1/18.2 seconds (55 milliseconds). To do this I only read the first byte of the 32-bit tick count, because it is stored in little endian byte order so the first byte is the only one that changes every tick.

Code:
XOR	BX, BX	; 9CF
MOV	DS, BX	; 9D1
CLD		; 9D3
MOV	SI, 046C; 9D4 BIOS timer = 0:046C - 0:046F
		;
LODSB		; 9D7						:PLAY_DRAW_TCK
DEC	SI	; 9D8
CMP	AL, AH	; 9D9
JZ	39D7	; 9DB if (At == BIOS Tick Byte) >PLAY_DRAW_TCK

The tick count prior to drawing the frame is stored in AH so while the minimum delay is 1 tick, the maximum can be up to 127 ticks or about 7 seconds. Calculating the difference between AH and AL results in the number of ticks required to draw a frame; a value I call the Animation Rate (Ar). [SUP][3][/SUP] On a fast enough computer the Animation Rate will always be 1 which means no frames are "skipped".

Now that I have a rate I have to animate something. The thing I animate is a byte value that is animated towards zero. For example, let's say the Animated Value (Av) is 10 and I draw 5 frames: 8, 6, 4, 2, 0. Now how fast this is animated is based on the Animation Base Rate (Ab) which I set to 9 (about half of 18.2) because a second feels like an eternity when moving or turning and a half second is much better. The algorithm, then, for determining the delta of the Animated Value is:

delta = Value * Rate / Base​

But this is imperfect because, even if I were to use floating point (which I'm not) then the delta may not add up to the original value. For example, if the Animation Value is 127, the Rate is 1 (fastest), and the Base is 9 then the result is 14.11111111111111. No matter how many 1's you have there, this will never correctly add back up to 127 and I could end up in an infinite loop.

This is where the lovely DDA comes in. The integer division (IDIV) instruction provides both the quotient and the remainder which provides me with Aq and Am (Animation Quotient and Animation Modulus):

Aq = Value * Rate / Base
Am = ABS(Value * Rate % Base)​

On each animation frame, I'll add the integer quotient to the animation value (Av += Aq). Then to handle the fractional portion, I add the absolute modulus to a signed error integer. When this error is greater than zero it means the fraction has overflowed: the animation value is incremented (or decremented, depending on sign) by 1 and the error is reduced by the base value:

Av += sign(Aq)
Ae -= Ab​

To see how this works, consider again the 127 * 1 / 9 which happens to be my movement animation value. There are going to be 9 frames in increments of at least 14. The error accumulator overflows in the first frame such that the resulting animation deltas are (and resulting error): 15 (-8), 14 (-7), 14 (-6), 14 (-5), 14 (-4), 14 (-3), 14 (-2), 14 (-1), 14 (0). On the final frame, the error is calculated to zero which is still not enough to overflow and thus the result is a perfect 127. The only downside is that the overflow occurs in the first frame instead of in the middle frame but this is an acceptable loss.

And now without further ado, here's the main block of animation code ...

Code:
MOV	SI, 27C2; 91F						:PLAYMAP_LOOP
XOR	AX, AX	; 922
MOV [SI+1], AL	; 924 Ae = 0
MOV [SI+2], AX	; 927 Am = 0, Aq = 0
MOV	AX, 0909; 92A
MOV [SI+6], AX	; 92D Ab = 9, Af = 9
CLD		; 930
LODSB		; 931 SI @ 27C3
INC	SI	; 932 SI @ 27C4
OR	AL, AL	; 933
JZ	3992	; 935 if (!Av) >PLAYMAP_DRAW
		; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
		; Animate
		;
NOP		; 937 debugging breakpoint			:PLAYMAP_ANI
MOV	BL, AL	; 938 BL=Av
MOV BH, [SI+4]	; 93A BH=Af
LODSW		; 93D AH=Am, AL=Aq (SI @ 27C6)
OR	AX, AX	; 93E
JZ	3969	; 940 if (0 == Am || 0 == Aq) >PLAY_ANI_CALC
		;
SUB BH, [SI-5]	; 942 Af -= Ar (@27C6 - 5 = 27C1)		:PLAY_ANI_NEXT
JNS	3949	; 945 if (Af - Ar >= 0) >PLAY_ANI_UPD
XOR	BH, BH	; 947
		;
MOV DI, [SI]	; 949 DI = Aa (SI @ 27C6)			:PLAY_ANI_UPD
ADD	[DI], AL; 94B (*Aa) += Aq (*Address + Quotient)
MOV [SI+2], BH	; 94D Af				(@27C6 + 2 = 27C8)
SUB [SI-4], AL	; 950 Av -= Aq (Value - Quotient)	(@27C6 - 4 = 27C2)
ADD [SI-3], AH	; 953 Ae += Am (Err + Remainder)	(@27C6 - 3 = 27C3)
JLE	3992	; 956 if (Ae <= 0) >PLAYMAP_DRAW
		;
MOV BH, [SI+3]	; 958 BH = Ab (@27C6 + 3 = 27C9)		:PLAY_ANI_DDA
SUB [SI-3], BH	; 95B Ae -= Af (@27C6 - 3 = 27C3)
CBW		; 95E AH = FFFF or 0000
OR	AH, 01	; 95F if (!AH) then AH = 1 else AH = -1
SUB [SI-4], AH	; 962 Av -= sgn(Aq)			(@27C6 - 4 = 27C2)
ADD	[DI], AH; 965 (*Aa) += sgn(Aq)
JMP	3992	; 967 >PLAYMAP_DRAW
		;
MOV	AX, BX	; 969						:PLAY_ANI_CALC
XOR	AH, AH	; 96B
MOV [SI+3], BH	; 96D Ab = Af				(@27C6 + 3 = 27C9)
MOV CH, [SI-5]	; 970					(@27C6 - 5 = 27C1)
OR	CH, CH	; 973
JZ	3988	; 975 if (!Ar) >PLAY_ANI_SAVE
CMP	CH, BH	; 977
JAE	3988	; 979 if (Ar >= Af) >PLAY_ANI_SAVE
IMUL	CH	; 97B AX = Av * Ar = AL * CH
OR	BH, BH	; 97D
JZ	3988	; 97F if (!Af) >PLAY_ANI_SAVE
IDIV	BH	; 981 Aq = (Az * Ar) / Af
CWD		; 983 DX = FFFF or 0000
XOR	AH, DH	; 984
SUB	AH, DH	; 986 Am = ABS((Az * Ar) % 18)
MOV [SI-2], AX	; 988 save [Aq][Am]				:PLAY_ANI_SAVE
XOR	DX, DX	; 98B
MOV [SI-3], DL	; 98D save [Ae] = 0
JMP	3942	; 990 >PLAY_ANI_NEXT

The memory paragraph at 27C0 is defined:

Code:
; ANIM	[00][01][02][03] [04][05][06][07] [08][09][0A][0B] [0C][0D][0E][0F]
; 27C0: [At][Ar][Av][Ae] [Aq][Am][  Aa  ] [Af][Ab]

Finally, in order to achieve variable FPS I need to only update the Animation Rate and reset the delta data:

Code:
INC	SI	; 9EB SI @ 27C1					:PLAY_DRAW_ADJ
CMP	[SI], AL; 9EC
JE	39FA	; 9EE if (Ar == NewRate) >PLAY_DRAW_ANI
MOV	[SI], AL; 9F0 Ar = NewRate
XOR	AX, AX	; 9F2 when rate changes Ae,Am,Aq must be recalculated
MOV [SI+2], AL	; 9F4 Ae = 0
MOV [SI+3], AX	; 9F7 Am = 0, Aq = 0

Then upon the next frame, the animation delta will be recalculated and proceed as before. This means that really big scenes may slow things down only temporarily but things will speed up as the rendering simplifies. In an emulator you can fake this complexity by dedicating more or fewer cycles to the program. Voila, variable FPS!

Footnotes:
[SUP][1][/SUP]. Frames Per Second where a frame is a fully rendered screen of pixels.

[SUP][2][/SUP]. This assumes the PIT timer 0 hasn't been jacked to run faster; in that case the animation will necessarily run faster as well.

[SUP][3][/SUP]. If the drawing time exceeds the maximum ticks supported then the animation speed will be incorrect since the calculation for the ticks per frame will be incorrect.
 
Back
Top