Framerate control problem.

General FreeBASIC programming questions.
Post Reply
Lachie Dazdarian
Posts: 2338
Joined: May 31, 2005 9:59
Location: Croatia
Contact:

Framerate control problem.

Post by Lachie Dazdarian »

Can anybody help me with the issue of assuring that my program will run with the same speed on every PC?

I was in belief that using SCREENSYNC in every loop ensures that the game will run with the same speed on every PC but after releasing Vector X 2006 I realized I was wrong.

VSYNC placed in the Vector X 2006's main loop gave 20 FPS on my PC. The game is in 640*480 resolution and 24 bit depth graphics mode. When I released the game people reported 60 or 80 FPS. Just a note that when I turn off all the graphics in my program and just put VSYNC in a loop I still get 20 FPS. So it's not the issue of my computer's speed. At least I think so.

So I would like for someone to explain me how VSYNC is reliable for using to control the program's framerate, when it isn't and for what reasons.

Also, substitutes? SLEEP is NOT millisecond precise. I tried. TIMER? Someone tried with it?

Isn't it possible to code a millisecond perfect delay routine in ASM that would be very reliable and release it in a form of a function, FB users could use?
anonymous1337
Posts: 5494
Joined: Sep 12, 2005 20:06
Location: California

Post by anonymous1337 »

So I would like for someone to explain me how VSYNC is reliable for using to control the program's framerate, when it isn't and for what reasons.
It's not reliable. Think about this, if you VSYNC, and the amount of time for a monitor refresh has already gone by, that means you have to wait one more refresh until your graphics are updated.

Timer works more than well. Generally if you don't care about the speed of the machine you're working on and are assuming a certain machine speed, you could always do something like this

Code: Select all

dim t as double
t = timer

dim fps as integer
fps = 60

do

    do stuff

    do
    loop until timer - t >= (1/fps)
    t = timer

loop
There are of course, other methods which are much more efficient for working on differently sped machines. I believe the best is a Pixels Per Second method. I'd just browse around.

Screensync isn't used to time your program, but instead wait until the refresh is about to occur. This will cause smoother graphics, because most graphics won't be in the middle of a drawing process/skipping around at the time of flipping.

Try this for your page changing:

Code: Select all

Screenres 640, 480, 32, 2
dim shared workpage as integer

do

    screenset workpage, workpage xor 1
    
    draw stuff
    
    workpage xor = 1

loop until whatever...

Also:

Code: Select all

Line(screenx, screeny)-(screenx + width, screeny + height), RGB(255, 255, 255), bf
is faster than CLS.
Last edited by anonymous1337 on Jun 01, 2006 13:41, edited 2 times in total.
Lachie Dazdarian
Posts: 2338
Joined: May 31, 2005 9:59
Location: Croatia
Contact:

Post by Lachie Dazdarian »

Thanks. That answered most of my questions.
Stormy
Posts: 198
Joined: May 28, 2005 17:57
Location: Germany
Contact:

Post by Stormy »

If you need a faster CLS-method, then use this:

Code: Select all

DECLARE SUB FastCLS (screenptr AS ANY PTR = 0, colour AS LONG = 0)

#include once "crt.bi"

declare function memset cdecl alias "memset" (byval as any ptr, byval as integer, byval as size_t) as any ptr

SUB FastCLS (screenptr AS ANY PTR = 0, colour AS LONG = 0)
DIM w, h, depth
SCREENINFO w,h,depth
memset Screenptr, colour, (w * h) * (depth / 8)
END SUB
PlayerOne
Posts: 173
Joined: Aug 15, 2005 18:58
Location: UK

Post by PlayerOne »

Screensync is dependant on the user's refresh rate, which could be anything. It also doesn't seem very reliable - I still got tearing graphics when I tried it, and I've also had reports that it broke the Linux version.

Timer might be more accurate than sleep, but it doesn't ever surrender any execution time meaning 100% CPU usage (I'm not sure what screensync does while it's waiting). Sleep is accurate to about 15ms under Windows, I believe. Personally I would use sleep and accept some inaccuracy. It's probably not a significant problem below about 30fps.
anonymous1337
Posts: 5494
Joined: Sep 12, 2005 20:06
Location: California

Post by anonymous1337 »

PlayerOne wrote:Timer might be more accurate than sleep, but it doesn't ever surrender any execution time meaning 100% CPU usage [. . .] Sleep is accurate to about 15ms under Windows, I believe.
55ms... Generally :P

You can always use a sleep n right before you start your timer checking

Code: Select all

sleep 5
do
loop until timer - t > (1/fps)
arenth
Posts: 511
Joined: Aug 30, 2005 6:22

Post by arenth »

Alternately if your app is supposed to run at a certain FPS, then figure out the time it should take per frame, and sleep the rest away.

Code: Select all

	dim timerStart as double
	dim timerEnd as double

	do
		timerStart = timer
		'do all your stuff here
		timerEnd = timer

		if (timerEnd - timerStart) < 1 then
			sleep ( int(  (timerEnd-TimerStart) * 100  )  

			'Sleep for the remainder * 100 to get MS.
		end if
	end loop

Thrawn89
Posts: 477
Joined: Oct 08, 2005 13:12

Post by Thrawn89 »

I usually do this...

Code: Select all


'Needs at least 2 Virtual Screen pages
'Set Screen here

'FPS
Const FPS = 30

'Timei is TIMER var
Dim As Double Timei, SecondsPerFrame

'How long each frame should take to be rendered
SecondsPerFrame = 1 / FPS

'Set up virtual vars
Dim As Integer Viewpage = 0, Workpage = 1

Screenset Workpage, Viewpage
Do
     Timei = Timer
     
     DrawScene

     ProcessInput

     UpdateScene

     Flip Workpage, Viewpage
     Do: Sleep 1: Loop While Timer - Timei <= SecondsPerFrame

     EraseScene

Loop Until SomeCondition

Works like a charm...let me explain my reasoning briefly...

-I first set the viewpage to 0 and the workpage to 1
-As you start the loop, make sure you set Timei = Timer so that you get the time it takes you to render your scene inside the Frame time calc
-Next you draw your objects to the offscreen workpage to prevent flickerage
-Process Input so that it remains drawn for the longest time possible (ya its redundant in this algo, but its a habbit after years of not double buffering)
-Update variable states as you see fit
-Flip so that you can see your workpage, meanwhile still working off screen so everything on screen is updated at the same time
-The waiting loop, IF computer rendered scene in less time than it was allowed with Seconds Per Frame, then Delay until it reaches that time
The reason I dont use just sleep, mainly is because if you input any key then it will cut it off short...I do use sleep in the loop however because it idles the program when it waits...therefore makeing you not have 100% CPU time...and if you press a key, the loop will just call another sleep
-Erase the scene however you see fit, you are still working in the offscreen buffer so you wont see the erasage...ever

Well thats it, try it out

Thrawn
Thrawn89
Posts: 477
Joined: Oct 08, 2005 13:12

Post by Thrawn89 »

But why not see for yourself?

An example I whipped up...it will keep it close to the FPS specified as possible on any computer and no flickerage whatsoever

(Try clicking, see how many balls you can make...when the FPS start to drop, it isnt cause the Algo is faulty, its cause its doing so many calcs that it cant pysically Render in the time alotted to meet the FPS)

Code: Select all


'Needs at least 2 Virtual Screen pages
Screen 19, 32, 2

DefInt A-Z
Option Explicit
Randomize Timer

'FPS
Const FPS = 30
Const SCRX = 800
Const SCRY = 600

Type Col_Type
    RR As Integer
    GG As Integer
    BB As Integer
End Type

Type _2DVector_Type
   x As Integer
   y As Integer
End Type

   Type Ball_Type
       x As Integer
       y As Integer
       OldX As Integer
       OldY As Integer
       R As Integer
       Col As Col_Type
       Vector As _2DVector_Type
   End Type

Declare Function RandomInt(High As Integer, Low As Integer)

'Note this is a basic example, I was gonna do stuff more complex, but decided not, so please dont pick at my inefficencys ;-)
ReDim Shared As Ball_Type Balls(0)

'Set Up First Ball
Balls(0).R = RandomInt(50,10)
Balls(0).x = RandomInt((SCRX - Balls(0).R), Balls(0).R)
Balls(0).y = RandomInt((SCRY - Balls(0).R), Balls(0).R)
Balls(0).Col.RR = RandomInt(255,30)
Balls(0).Col.GG = RandomInt(255,30)
Balls(0).Col.BB = RandomInt(255,30)
Balls(0).Vector.x = RandomInt(3, -3)
Balls(0).Vector.y = RandomInt(3, -3)
If Balls(0).Vector.x = 0 And Balls(0).Vector.y = 0 Then Balls(0).Vector.x = 2: Balls(0).Vector.y = RandomInt(3, -3)

'Timei is TIMER var
Dim As Double Timei, SecondsPerFrame

'How long each frame should take to be rendered
SecondsPerFrame = 1 / FPS

'Set up virtual vars
Dim As Integer Viewpage = 0, Workpage = 1

Declare Function DrawScene()
Declare Function ProcessInput()
Declare Function UpdateScene()
Declare Function EraseScene()

Declare Function AddBall()

'Calc FPS
Dim FrameCount As Integer
Dim FPSTimei As Double
FrameCount = 0

Screenset Workpage, Viewpage
FPSTimei = Timer
Do
     Timei = Timer
     
     DrawScene

     ProcessInput

     UpdateScene

     Flip Workpage, Viewpage
     Do: Sleep 1: Loop While Timer - Timei <= SecondsPerFrame

     EraseScene

     FrameCount += 1
     If Timer - FPSTimei >= 1 Then FPSTimei = Timer: WindowTitle "FPS: " & FrameCount: FrameCount = 1
Loop While Not Multikey(&h1)

Function ProcessInput()
    Dim As Integer MX, MY, MB
    GetMouse MX, MY, ,MB
    
    If MB And 1 Then AddBall()
    
End Function

Function UpdateScene()
    Dim AS Integer i
    
    For i = LBound(Balls) To UBound(Balls)
        Balls(i).x += Balls(i).Vector.x
        Balls(i).y += Balls(i).Vector.y
        
        If Balls(i).x + Balls(i).R >= SCRX Then Balls(i).x = SCRX - Balls(i).R: Balls(i).Vector.x *= -1
        If Balls(i).y + Balls(i).R >= SCRY Then Balls(i).y = SCRY - Balls(i).R: Balls(i).Vector.y *= -1
    
        If Balls(i).x - Balls(i).R <= 0 Then Balls(i).x = Balls(i).R: Balls(i).Vector.x *= -1
        If Balls(i).y - Balls(i).R <= 0 Then Balls(i).y = Balls(i).R: Balls(i).Vector.y *= -1
    Next i
    
End Function

Function DrawScene()
    Dim As Integer i
    
    For i = LBound(Balls) To UBound(Balls)
        Circle(Balls(i).x, Balls(i).y), Balls(i).R, RGB(Balls(i).Col.RR, Balls(i).Col.GG, Balls(i).Col.BB),,,,F
        
        Balls(i).OldX = Balls(i).X
        Balls(i).OldY = Balls(i).Y
    Next i
End Function

Function EraseScene()
    Dim As Integer i
    
    For i = LBound(Balls) To UBound(Balls)
        Circle(Balls(i).OldX, Balls(i).OldY), Balls(i).R, RGB(0,0,0),,,,F
    Next i
End Function

Function AddBall()
    Dim As Integer i
    
    'Preserve
    ReDim Temp_Balls(LBound(Balls) To UBound(Balls)) As Ball_Type
    For i = LBound(Balls) To Ubound(Balls)
        Temp_Balls(i) = Balls(i)
    Next i
    ReDim Balls(LBound(Temp_Balls) To (Ubound(Temp_Balls) + 1)) As Ball_Type
    For i = LBound(Temp_Balls) To UBound(Temp_Balls)
        Balls(i) = Temp_Balls(i)
    Next i

    Balls(UBound(Balls)).R = RandomInt(50,10)
    Balls(UBound(Balls)).x = RandomInt((SCRX - Balls(UBound(Balls)).R), Balls(UBound(Balls)).R)
    Balls(UBound(Balls)).y = RandomInt((SCRY - Balls(UBound(Balls)).R), Balls(UBound(Balls)).R)
    Balls(UBound(Balls)).Col.RR = RandomInt(255,30)
    Balls(UBound(Balls)).Col.GG = RandomInt(255,30)
    Balls(UBound(Balls)).Col.BB = RandomInt(255,30)
    Balls(UBound(Balls)).Vector.x = RandomInt(3, -3)
    Balls(UBound(Balls)).Vector.y = RandomInt(3, -3)
    If Balls(UBound(Balls)).Vector.x = 0 And Balls(UBound(Balls)).Vector.y = 0 Then Balls(UBound(Balls)).Vector.x = 2: Balls(UBound(Balls)).Vector.y = RandomInt(3, -3)
End Function 

Function RandomInt(High As Integer, Low As Integer)
     RandomInt = Int(Rnd * (High - Low + 1) + Low)    
End Function
Thrawn
PlayerOne
Posts: 173
Joined: Aug 15, 2005 18:58
Location: UK

Post by PlayerOne »

55ms is the DOS timer, I believe. I thought the Windows sleep didn't use that.

Basically, I'd do things the same way as Thrawn89, even down to using flip, which seems to do a better job than screensync at avoiding tearing. The issue is how long a "sleep 1" will actually sleep.
arenth
Posts: 511
Joined: Aug 30, 2005 6:22

Post by arenth »

Actually the windows timer does have a resolution of about 55ms, it has to do with an internal clock that cannot accurately represent the ticks of time, in fact its off by about 55ms, which means that if you ask for a delay of 1 ms, you could get between 1 and 56 ms of delay, depending on the state of the internal counter when you asked for the delay.
JohnK
Posts: 279
Joined: Sep 01, 2005 5:20
Location: Earth, usually
Contact:

Post by JohnK »

Why not use the windows performance timer? I have some code from rapidQ but I can't get it to work in FB (undef ref in kernal32)

Code: Select all


Declare Function QueryPerformanceCounter Lib "kernel32" (BYREF lpPerformanceCount As ULONGINT) As Long
Declare Function QueryPerformanceFrequency Lib "kernel32" (BYREF lpFrequency As ULONGINT) As Long


FUNCTION microTIMER() AS DOUBLE
    STATIC AlreadyInit AS INTEGER
    STATIC myFreq as DOUBLE
    DIM TimerFreq as ULONGINT
    DIM TimerMS as ULONGINT
    DIM myResult as ULONGINT

    IF AlreadyInit = 0 THEN                 'get our timer resolution
        QueryPerformanceFrequency(TimerFreq)
        AlreadyInit = 1
        IF TimerFreq > = 0 THEN      'do we have a timer?
            myFreq = TimerFreq
        ELSE
            RESULT = 0
            EXIT FUNCTION
        END IF
    END IF
  QueryPerformanceCounter(TimerMS)
  microTIMER = TimerMS / myFreq  'result is in relative time
END FUNCTION


'sample code
DIM a AS ULONGINT
 a = microTimer      'always call at least once to initialize
 a = microTimer      
' 'wait for 3.5 milliseconds
 DO 
 Loop until (microTimer - a) > 0.00350
Sorry UNIX dudes, this is a windows thang.
JohnK
MichaelW
Posts: 3500
Joined: May 16, 2006 22:34
Location: USA

Post by MichaelW »

This is a quick and dirty app that measures the effective resolution of various timers. The code essentially determines the mean of the step changes in the return values.

Code: Select all

#include once "windows.bi"

dim as integer i
dim as ulongint pcfreq, count1, count2
dim as double t1, t2, accum

SetPriorityClass( GetCurrentProcess(), HIGH_PRIORITY_CLASS )

'***************************************************************

for i = 1 to 1000000
  t1 = timer
  do
    t2 = timer
  loop until t2 > t1
  accum += t2 - t1
next
print using "TIMER:##.## us";accum
print

'***************************************************************
accum = 0
if QueryPerformanceFrequency(cast(PLARGE_INTEGER,@pcfreq)) then
  for i = 1 to 1000000
    QueryPerformanceCounter( cast(PLARGE_INTEGER, @count1) )
    do
      QueryPerformanceCounter( cast(PLARGE_INTEGER, @count2) )
    loop until count2 > count1
    accum += count2 - count1
  next
  print "PerformanceFrequency: ";pcfreq;" Hz"
  print  
  accum /= pcfreq
  print using "PerformanceCounter:##.## us";accum
  print
endif

'***************************************************************

accum = 0
for i = 1 to 1000
  t1 = GetTickCount
  do
    t2 = GetTickCount
  loop until t2 > t1
  accum +=  t2 - t1
next
accum /= 1000    ' Must adjust for return value in ms.
print using "TickCount:###.## ms";accum
print

'***************************************************************

accum = 0
for i = 1 to 1000
  t1 = timer
  sleep 1
  t2 = timer
  accum += t2 - t1
next
print using "SLEEP:###.## ms";accum
print

'***************************************************************

SetPriorityClass( GetCurrentProcess(), NORMAL_PRIORITY_CLASS )

sleep
Typical results under Windows 2000:

Code: Select all

TIMER: 1.79 us

PerformanceFrequency: 3579545 Hz

PerformanceCounter: 1.73 us

TickCount: 10.01 ms

SLEEP: 10.01 ms
Typical results under Windows 98 SE:

Code: Select all

TIMER: 6.38 us

PerformanceFrequency: 1193180 Hz

PerformanceCounter: 6.13 us

TickCount: 5.00 ms

SLEEP: 4.98 ms
As you can see, the effective resolution of the Performance Counter, and the TIMER function which is based on the Performance Counter, is far lower than the resolution implied by the Performance Frequency.
Post Reply