PDA

View Full Version : Serious problems getting LockFPS to work.



jasonf
05-11-2006, 04:22 PM
I'm using a modified version of LockFPS and it only ever works properly while I'm stepping through the code.. as soon as I remove the break points, it stop working properly.

{$OPTIMIZATION OFF}
function TSDLTicks.LockFPS(targetFPS: Byte): single;
var
currentTime : UInt32;
TargetTime : UInt32;
begin
if (targetFPS = 0) then
targetFPS := 1;


// delay to maintain a constant frame rate
currentTime :=SDL_GetTicks();
if currentTime >m_FrameStartTime then
begin

TargetTime := m_FrameStartTime + (targetFPS *m_ticksPerSecond ) ;

while true do
begin

currentTime :=SDL_GetTicks();

if m_FrameStartTime - currentTime = 0 then
begin
SDL_Delay(1);
end;

if (currentTime<m_FrameStartTime) then
begin
result := 1;
Break;
end;

result := currentTime / m_ticksPerSecond;
if (TargetTime < currentTime) then
begin
result := 2;
Break;
end;
end;
end;

m_FrameStartTime := currentTime;
result := 0;
end;

I've been working on this all bloody day and I can't get it to work.

FrameStartTime is set at the beginning of each cycle by calling a funciton on the class containing the LockFPS function.

The LockFPS function is called at the end to eat up any spare CPU cycles and keep the game running at 30fps.

I don't know WTF is happening and why it works perfectly when I step through, yet as soon as I remove the breakpoint and continue, it never exits the loop.
I thought perhaps it was an optimization issue so I put the Optimization Off directive at the top of the unit and then at the top of the function, but it doesn't make any difference.

This is also a problem when this is compiled using FPC.

Any clues?

Clootie
05-11-2006, 06:48 PM
May be
(targetFPS *m_ticksPerSecond)
should be replaced with
(m_ticksPerSecond / targetFPS) ???
:?

tanffn
06-11-2006, 07:28 AM
The code looks fine.. :?
Maybe, just to make the code ‘safer’ instead using while True, make it as while Waiting and instead Break set Waiting:= false.

jasonf
06-11-2006, 10:48 AM
I've tried loads of combinations of this.. having the expression in the While loop itself, the original Until statement in the Jedi-SDL version does the same.

All versions I try work when you step through and fail utterly when you don't

I spent a long time banging my head against a brick wall on this.
At first I thought it was being optimized to death.. but even with optimizing turned off, it still fails.

I've tried putting SDL_Delay(1000) in there, nothing.. doesn't even seem to get called. I think Delphi is looking at the code and deciding that there's nothing to do in there and optimizing it out.. but when it steps through in the debugger, it has to follow each line. I dunno, I'm clutching at straws here.

jasonf
06-11-2006, 12:57 PM
Dr Bob (http://www.drbob42.com/articles/index_d.htm) says this..

This is a Win32 Sleep function for a regular App.


procedure Sleep(MilliSecondsToWait: LongInt);
var StartTime: LongInt;
begin
StartTime := GetTickCount;
while (GetTickCount >= StartTime) and
(GetTickCount < (StartTime + MilliSecondsToWait)) do
Application.ProcessMessages
end {Sleep};


Unfortunately, there is no XPlatform ProcessMesseges function for SDL. The SDL_Delay might work, but it doesn't appear to be called..

I'm going to try this with the GetTickCount function instead of the SDL_GetTicks

jdarling
06-11-2006, 01:44 PM
If your running on newish hardware also make sure that you are setting the thread affinity mask to 1 in your startup code. You may be running into the multiprocessor issues related to timmers and processes.

Actually you should be doing this anyways, unless you are specifying the target processor for your timers. Another thing would be to do away with the locked FPS all together and make the application frame independent. Utilizing a rendering thread and a processing thread. You can then scale properly.

Thread afinity mask code:
{$IFDEF WIN32}
SetThreadAffinityMask(GetCurrentThread(), 1);
{$ENDIF}


Of final note, I've had problems with the SDL functions to get tick counts on some systems. To aliviate this I've created my own:
function SystemGetTickCount : Cardinal;
begin
result := -1;
{$IFDEF WIN32}
result := Windows.GetTickCount();
{$ENDIF}
{$IFDEF UNIX}
result := clock_gettime(CLOCK_MONOTONIC);
{$ENDIF}
end;

Well, ok, I didn't create my own, but I did wrap up the *NIX and Windows default calls so that one call would work on either OS. From testing the usage doesn't change. Still need to do the Mac version, but damned if I can find the right calls.

jasonf
06-11-2006, 02:47 PM
AAAARRRGGGHHHHHhhh Threads!!! (goes and hides under a box in the corner)

I've not had much luck with Threads in the past, but that's probably because I've not had to use them much.

Also, I've not written the engine to be thread safe so it'll probably fall over in a heap if I try to seperate the Logic and the rendering, Actually, they are quite seperate.. I call Engine.Tick( no#ticks ) followed by Engine.Render
The Render looks through a list of renderable objects which are within the scope of the camera and draws them. ) but I think the change would be quite traumatic for the engine.

For the time being, I'm going to press on with the FrameLocking style of coding.

I am starting to look into more frame independant technique however as it works well on slower machines.. at the moment it's a crude multiply by missed ticks technique, but it could be improved.


Also, I'm working on a fairly old 800 Athlon machine.. single processor. But I'll look into that Thread/processor issue.

savage
06-11-2006, 03:02 PM
The following code should ( tested in a console app ) lock your FPS to a TargetFPS.


procedure TSDLTicks.LockFPS(targetFPS: Byte);
var
currentTime : UInt32;
targetTime : single;
begin
if (targetFPS = 0) then
targetFPS := 1;

targetTime := m_ticksPerSecond / targetFPS;

// delay to maintain a constant frame rate
repeat
currentTime := SDL_GetTicks;
until ( (currentTime - s_lastTime ) > targetTime );

// reset the timer
s_lastTime := currentTime;
end;

jasonf
06-11-2006, 03:29 PM
Cheers Dom, I'll give this a test later.

One thing though, the LockFPS function has to be capable of withstanding the rollover (Apparently the SDL_GetTicks rolls back after 40 odd days) if this isn't taken into account, when the rollover happens, the game will lock up.

A simple test to see if the CurrentTime < s_LastTime will do.

savage
06-11-2006, 03:36 PM
According to the source code...


Get the number of milliseconds since the SDL library initialization.
Note that this value wraps if the program runs for more than ~49 days.

My understanding is that, this is essentially since the game/app started.
If someone is still playing your games after 49 days straight, it's either the best game ever and/or the gamer does not have much of a social life :)

jasonf
06-11-2006, 05:14 PM
LOL.. we'd be doing them a favour by crashing... "Now get outside and get some sunshine!" :lol:

For something like a dedicated server though it could be an issue, they can run for months at a time. (I'm guessing)

And what do you mean, *unless* it's the best game ever :roll: :wink: :lol:

jdarling
06-11-2006, 06:44 PM
For a dedicated server, the solution is quite simple. Test for value b < value a and the calculate as appropriate. Of course this has already been stated and known :).

PS: The longest I've seen anyone ever play a game was 27 days before he fell asleep at the keyboard. Poor fool only had to play 3 more days to win the bet.

savage
06-11-2006, 06:52 PM
For something like a dedicated server though it could be an issue, they can run for months at a time. (I'm guessing)

I'm not sure someone would write a dedicated server using SDL, but I suppose it's possible.

PS. I'm having a mental block of how to work out FPS, without actually having an FPS counter variable. I'd like to get the GetFPS function working properly as well.

jdarling
06-11-2006, 08:49 PM
SlicesPerDeviation * FrameTime = FPS

So standard: 1000 * FrameTime = ~FPS (due to rounding errors)

Or maybe it was Slices/FrameTime can't remember whitch offhand but its one or the other.

jasonf
07-11-2006, 10:31 AM
The Current GetFPS interferes with the LockFPS as it resets the starting time.

I think there should be a function at the beginning of the frame which sets the start time, then both GetFPS and LockFPS use this value as a basis for the calculations.

Having the functions completely independant, whilst being a nice idea is flawed if they use the same variables. So they either need to use seperate variables or have the reset done elsewhere.

I think the LockFPS is working at the moment, but I can't seem to get the GetFPS working properly. I'll take another look later. It would be nice to see a nice, unchanging 30 on the GetFPS function due to the LockFPS limiting to 30.

Also, we are assuming 1000ms for each second.. can we guarantee this for SDL_GetTicks? (may sound like a stupid question, but why have a variable which we can set and not a constant if otherwise?)

jasonf
07-11-2006, 10:42 AM
OK.. I just played with some numbers and I'm fairly sure it is..

fps = TicksPerSecond / ( CurrentTime - LastTime )

Becuase if TicksPerSecond = 1000, LastTime = 12000 and CurrentTime = 13000

1000 / ( 13000 - 12000 = 1000 ) = 1

If the frame takes 1000 Ticks, then we are running at 1 frame per second.

savage
07-11-2006, 11:04 AM
OK.. I just played with some numbers and I'm fairly sure it is..

fps = TicksPerSecond / ( CurrentTime - LastTime )

Becuase if TicksPerSecond = 1000, LastTime = 12000 and CurrentTime = 13000

1000 / ( 13000 - 12000 = 1000 ) = 1

If the frame takes 1000 Ticks, then we are running at 1 frame per second.

Will need to add a check for ( CurrentTime - LastTime ) = 0 other wise it will blow up. Ok I might give that a go.

jasonf
07-11-2006, 11:26 AM
Nah, leave the check out... as a punishment for those with really fast machines :twisted:

jdarling
07-11-2006, 01:42 PM
OK.. I just played with some numbers and I'm fairly sure it is..
fps = TicksPerSecond / ( CurrentTime - LastTime )
This is correct, the common function is: FPS = Slices/FrameTime whitch drops down to 1000 / (CurrentTime-LastTime) in most cases. Here is an implementation that we played with for our stuff:

var
LastFrame : Cardinal;

function SystemGetTickCount() : Cardinal;
begin
result := -1;
{$IFDEF WIN32}
result := Windows.GetTickCount();
{$ENDIF}
{$IFDEF UNIX}
result := clock_gettime(CLOCK_MONOTONIC);
{$ENDIF}
end;

function GetFPS() : Cardinal;
var
CurrTime, FrameTime : Cardinal;
begin
CurrTime := SystemGetTickCount;
FrameTime := CurrTime-LastFrame;
if FrameTime < 0 then
FrameTime := (MaxCardinal-CurrTime)+LastFrame;
GetFPS := 1000 / FrameTime;
end;

// The render loop
begin
LastFrame := SystemGetTickCount;
// Setup stuff here
while GameRunning do
begin
// Rendering code goes here.
WriteLn('FPS: ', GetFPS);
LastFrame := SystemGetTickCount;
end;
end.

This seems to work fairly well, with the largest problem being that the first itteration is off, due to setup times and etc... Of course, I never left it up and running for the 49 days that it would take to find out if I screwed something up with the rollover stuff :).

jasonf
07-11-2006, 02:08 PM
I've just spotted this mail from 1999 about GetTickCount and QueryPerformanceCounter

http://www.libsdl.org/pipermail/sdl/1999-September/023531.html

savage
07-11-2006, 02:23 PM
Latest version of sdlticks...


interface

uses
sdl;

type
TSDLTicks = class
private
FStartTime : UInt32;
FTicksPerSecond : UInt32;
FElapsedLastTime : UInt32;
FFPSLastTime : UInt32;
FLockFPSLastTime : UInt32;
public
constructor Create;
destructor Destroy; override; // destructor

{************************************************* ****************************
Init
If the hi-res timer is present, the tick rate is stored and the function
returns true. Otherwise, the function returns false, and the timer should
not be used.
************************************************** ***************************}
function Init : boolean;

{************************************************* **************************
GetGetElapsedSeconds
Returns the Elapsed time, since the function was last called.
************************************************** *************************}
function GetElapsedSeconds : Single;

{************************************************* **************************
GetFPS
Returns the average frames per second.
If this is not called every frame, the client should track the number
of frames itself, and reset the value after this is called.
************************************************** *************************}
function GetFPS : single;

{************************************************* **************************
LockFPS
Used to lock the frame rate to a set amount. This will block until enough
time has passed to ensure that the fps won't go over the requested amount.
Note that this can only keep the fps from going above the specified level;
it can still drop below it. It is assumed that if used, this function will
be called every frame. The value returned is the instantaneous fps, which
will be less than or equal to the targetFPS.
************************************************** *************************}
procedure LockFPS( targetFPS : Byte );
end;

implementation

{ TSDLTicks }
constructor TSDLTicks.Create;
begin
inherited;
FTicksPerSecond := 1000;
end;

destructor TSDLTicks.Destroy;
begin
inherited;
end;

function TSDLTicks.GetElapsedSeconds : Single;
var
currentTime : Cardinal;
begin
currentTime := SDL_GetTicks;

result := ( currentTime - FElapsedLastTime ) / FTicksPerSecond;

// reset the timer
FElapsedLastTime := currentTime;
end;

function TSDLTicks.GetFPS : Single;
var
currentTime, FrameTime : UInt32;
fps : single;
begin
currentTime := SDL_GetTicks;

FrameTime := ( currentTime - FFPSLastTime );

if FrameTime = 0 then
FrameTime := 1;

fps := FTicksPerSecond / FrameTime;

// reset the timer
FFPSLastTime := currentTime;
result := fps;
end;

function TSDLTicks.Init : boolean;
begin
FStartTime := SDL_GetTicks;
FElapsedLastTime := FStartTime;
FFPSLastTime := FStartTime;
FLockFPSLastTime := FStartTime;
result := true;
end;

procedure TSDLTicks.LockFPS( targetFPS : Byte );
var
currentTime : UInt32;
targetTime : single;
begin
if ( targetFPS = 0 ) then
targetFPS := 1;

targetTime := FTicksPerSecond / targetFPS;

// delay to maintain a constant frame rate
repeat
currentTime := SDL_GetTicks;
until ( ( currentTime - FLockFPSLastTime ) > targetTime );

// reset the timer
FLockFPSLastTime := currentTime;
end;

jasonf
07-11-2006, 03:14 PM
Dom, Both GetFPS and LockFPS modify the s_LastTime variable. This means that you can't use them together. You can either view the FPS or Lock the FPS. I don't know if this is a design feature but I was hoping to use them both.. first of all as a test that LockFPS was working and to see when the FPS went really low.


In my game the process is...

Loop

Reset the counters

Do some Stuff... some rendering etc

Draw the FPS using GetFPS

Lock the FPS to a specific value

Until Quitting

At the moment, as it stands, GetFPS is resetting the counter before LockFPS has a chance to limit the FPS.

They either need to use 2 variables or there has to be an reset function which is called before anything happens.

savage
07-11-2006, 03:29 PM
That's my design flaw, as I was only thinking about using them independently. I'll add fix later tonight, or you can submit a patch ;).

jasonf
07-11-2006, 03:58 PM
Well, I believe that the GetFPS should be a non-destructive reporting function only...

Successive calls to GetFPS should return the same value (well, pretty similar anyway)

Successive calls to LockFPS should not. So it might be OK for LockFPS to modify the start time but not GetFPS.

I'll take a look at this code tonight.

savage
07-11-2006, 04:33 PM
I've edited the previous sdlticks.pas post. Testing shows that they all work independently now.

jasonf
07-11-2006, 04:39 PM
Sweet 8)

jasonf
07-11-2006, 06:40 PM
Sweet as a honey coated lollypop brought with lottery winnings.

It works a treat. the frame rate is nicely balanced at 30fps. Gonna try it on the wife's laptop, I want to see what the performance difference is..

Before my optimisations, I was getting a logic time of 9 now I get 4.. She was getting 4...

Should be smooth :)

savage
08-11-2006, 09:07 AM
This version of sdlticks.pas has not been checked into CVS.