Author Topic: REMAKES TUTORIAL #3 - VBL and Timing  (Read 7012 times)

0 Members and 1 Guest are viewing this topic.

Offline stormbringer

  • Time moves by fast, no second chance
  • Amiga 1200
  • ****
  • Posts: 453
  • Karma: 73
    • View Profile
    • www.retro-remakes.net
REMAKES TUTORIAL #3 - VBL and Timing
« on: June 14, 2009 »
Sunday, bloody sunday! This tutorial maybe will give to some of you an urgent need for aspirine. I'll start with a stolen quote:

"Time is Not Important: Only Life is Important"

Well in our case, that's not quite true. If we want to give life to our little program, time IS important.

So before we jump into some obscure code, let's have a bit of theory. So in this 3rd tutorial we'll discuss things like:

- Timing
- delta timing
- VBL or Vertical Blanking
- VSync or Vertical Synchronization
- FPS (frames per second)
- etc.

Time is measured with a clock. Computers also have a clock, a necessary feature in order to run. So in other words time management is simple but also complex because:

- not all the computers have a clock that runs at the same speed.
- we do not only want to measure time precisely but we also want to move things on screen in sync
- we want smooth motion, not Microsoft-style animations.

Let's learn some stuff from the past.

PART 1 - Timing basics

The demo scene started more than 25 years ago on old 8bit and 16/32bit computers like the C64, Amiga, Atari, etc. Back then those computers were running with a slow CPU clock (7Mhz). Nevertheless, they had smooth animations and motion running in real-time. What does that mean?

The notion of real-time is not dependent on the speed of a clock. It's depends on our perception of the motion relative to us. Motion is obviously perceived with our eyes, so in our case, we perceive real-time motion on computers when we watch our computer monitors and by extension back in the good old days, the TV set connected to the computers. AHA!!! This is important.

It's important because we now have 2 different things: the speed of the computer's clock and the notion of real-time. For example the good old C64 was slow to compute stuff, but nevertheless it had real-time demos, games, etc. How was this possible? Well as mentioned above, real-time is just the perception of smooth motion, just like we perceive in the real world. It has nothing to do with speed or anything else. We perceive things as moving when their position, shape, color whatever changes over time. If this "change" happens at a rate greater than 1/18th of a second, then our eyes get fooled and we call this real-time motion.

Ok.... important notion here: the rate at which what is displayed changes is of great importance. On computers, old or new, motion is perceived on the monitor when the "object" has its position, shape, color, etc changed at a decent rate, obviously faster than 1/18th of a second.

Another thing to keep in mind is that our TV sets or monitors do not work with refresh rates of 1/18th of a second or 1/37th of a second. There are technical reasons for that, but we will not discuss this here, wikipedia is a good source of information. What you must keep in mind is that in Europe the standard refresh rate is 50Hz and in the US and Asia it's 60Hz. That means that the image on television is refreshed every 1/50th or 1/60th of a second. That's quite high, much higher than what we need in order to perceive smooth real-time motion.

Almost all computer monitors are now working with a minimum refresh rate of 60Hz. 50Hz was available for some time, but since the manufacturers are either American or Asian, well... 60Hz rules the world.

PAUSE: until now we did not discuss the clock speed of the computer... How is that related to our problem???

Well, the clock speed of the computer is not very relevant to our problem here (I assume we all live in the year 2009 and not 1909). It just allows us to measure time. But at the same time we want to have smooth motion on the screen. So we have to use it somehow in order to compute speeds, positions, etc....





PART 2 - Measuring time

Measuring time is done with so-called timers. On old computers, current time was stored in some registers as a counter that was incremented at a specific frequency. On modern computers, one uses so-called high-precision times by polling the values of a counter maintained by the CPU. Basically the technique is the same.

So how do we measure time and what for? Measuring time is done through system API calls. These functions return the values of the famous high-precision counter. How do we use it to measure time? Well since when you ask your computer to perform a task, it takes some time, the simple way to measure the time is:

1) get current time counter value and store it in a variable called lastTime
2) perform the task
3) get the current time counter value again and now store is in a variable called newTime
4) compute a delta time value by subtracting the lastTime value to the newTime value. The resulting delta is the time spent on the specific task.

Previously we said that in order to get some smooth real-time motion, we need to perform changes in the position of the moving object at a rate higher to at least 1/18th of a second. That's not difficult to implement, but we need to make sure that the task the computer has to perform is done in less time than what is required to achieve our goal. Here is some pseudo code explaining this:

(let's assume we have two variables lastTime and newTime and a function that does the magic task)

lastTime=getCurrentTime();

while (notFinished)
{
    newTime=getCurrentTime();

    deltaTime=newTime-lastTime;

    if (deltaTime >= 1/60)
    {
        performTask();

        lastTime=newTime;
    }
}


Ok... so now we have our famous delta timer!!!

Hey wait a bit... previously we also said that we have to be somehow in sync with the refresh rate of our display.. how is that done??

Well, here is the story... You cannot really do both on modern computers. Old computers (like the C64 or Amiga) were made to work with specific displays based on the classic TV sets available at that time. In other words these computers could only display images at the maximum fixed frequency of 60Hz (for American and Asian owners) and at 50Hz (for European owners). Since these computers were built with that in mind, it was easy to know that the smoothest motion could be achieved only when changing the position, shape, color of an object at every refresh rate, i.e. 60Hz or 50Hz, in other words at a maximum of 60fps (frames per second) or 50fps. Besides this, attempting to change the motion properties of an object at a different rate (say 1/26th of a second) would result in jerky animation! What???

Yes, true, we said above that any refresh rate higher than 1/18th of a second is perceived by humans as motion.... but we did not mention one thing... the refresh rate of our eye, i.e. the ability of our optical nerves to send information to our brain.... and that believe me it's very fast. Don't believe me?? Yes you do.

In our case, 60Hz or 50Hz is very close to rates like 1/18th, or 1/24th of a second. So what is the problem?? Well the problem is that if we use the CPU to perform a specific task every 1/nth of a second, we simply do not take into account what the display does. Basically the CPU and the display controller are totally disconnected. They are not in sync. So if we try to update the screen while the screen is being refreshed, well we can't, or just half of it, or a third or whatever. We get a so-called tearing effect. We're out of sync.

Ok... Back in the good old days, the graphic controller was thightly connected to the other elements in a computer and among other things it had a nice feature called a VBL interrupt, or Vertical Blanking Interrupt. This feature was nice enough to wake up the computer everytime it was possible to change the contents of the screen. Of course this VBL interrupt was based on the refresh rate. So there was a nice hardware-based mechanism that back then you could take control of and make sure that you change the display only when possible, based on a fixed frequency.

Unfortunately this sort of interrupt is not available to the programmer anymore. You cannot take control of the display (unless you write your own driver). The only thing you can do now (and that's quite recent) is give hints that you want a specific frequency and you want to be VBL dependent. But there is no warranty for it.

Riiiiiiiiiiiiiiiiight!!!! So what do we have now? we know how to measure time with the CPU and through a delta timer we can perform a specific task at a fixed rate. We also know that we can potentially use the vertical blanking but there are some conditions to it.





PART 3 - Advanced time management

As you may have guessed, we need the best solution... but as said previously it's not so easy. What we want is an interrupt-like callback function that wakes up our program every 1/nth of a second. We also want to be in-sync with the display when this is possible. Good.

Wel also know that if we have the VBL capability working, then we can still get a delta time between 2 passes. That's also good.

In the first tutorial, we introduced a callback for the vertical synchronization. Actually this callback is our simulated VBL interrupt that is called when needed. How does it work? Here is the basic code for it:

Code: [Select]
CError intro_on_vsync(pCVoid userData, CFloat64 deltaTime)
{
    return(0);
}

The user data is a custom pointer you can pass to your callbacks. Forget about it now, it's pretty useless.

The deltaTime parameter is holding the actual delta time with 64bits precision. We don't really need that but that's the way the system gives it to us.

Before we implement something here, let's have a look at the screenProperties we initialized in the program's entry point:

Code: [Select]
screenProperties.vsyncFPS=60;

So basically, in the initialization phase we ask for 60 frames per second. Why? Well because often we base our animation increments on a reference frequency. Of course you may get the actual refresh rate from the desktop of your computer (we will see that later), but in the case of a remake for example we want a fixed, known in advance frequency.

When you start/open a screen/window with GLTK and you request that frequency, depending on what's requested and what's possible you will get a call to the vsync callback at the desired rate. How this is done will be discussed later in a more advanced tutorial, but the pricinples have been explained here. Nevertheless what we want in order to have our things moving on screen is a call informing us of the time between 2 passes. Now let's see how we handle this:

Code: [Select]
CError intro_on_vsync(pCVoid userData, CFloat64 deltaTime)
{
             /* here we test if we got the vsync turned on and if the screen refresh rate is the same as the one we have requested
                 if yes,we are in the same situation as on an old platform like the C64 and the Amiga and benefit from the VBL leading the dance
             */
if ((screenProperties.vsyncFPS == screenProperties.vsyncActual) && screenProperties.vsync)
{
                          /* process motion objects */
}
else
{
                         /* here is the "bad" case... something of what we have requested did no match the capabilities of the system. we have to adjust our time values according to the new conditions
                         */

timeValue=(CFloat32)deltaTime*screenProperties.speedFactor*((CFloat32)screenProperties.vsyncFPS);

/* process motion objects */
}


return(0);
}


OK! Now let's have a look at these 2 cases and why we have them. The test above tries to identify the ideal case where the requested FPS and vertical sync are available. Some around here may ask why we need to know this? well the truth is that if you want to remake some old-school effects you can take advantage of this and provide some better rendering (like avoiding sampling & filtering, since these features were not available on these old platforms). If these nice conditions are not met, we just fall back in the "bad" case were we need to use a "variable" delta timing since there is no sync between the display and our delta timer. So how do we do that?

As mentioned previously, the callback just gives you the delta time between 2 passes, but when you open the screen/window, some members of the screen structure get initialized with some interesting values. One of these values is the speedFactor value. For example if you requested 60Hz but the actual display gets you only 80Hz, you need to recalculate your increments to keep the same speed whatever the refresh rate is. This speedFactor thing is a correction factor for that. So the final delta time value is:

timeValue = deltaTime * screenProperties.speedFactor * screenProperties.vsyncFPS;

this new timeValue is the adjusted increment which we can use for moving our objects, changing colors, etc. It also takes into account the fact that the requested features are not present like vsync, refresh rate, etc.


Good, good, good. Nice words but let's move some stuff on the screen now. The example in this tutorial is less exciting than others, but at least it shows how to move a simple object (a quad) on the screen and make the timer properly depend on what we want. Here is the final code:

Code: [Select]
/* RETRO-REMAKES TUTORIAL #3 - VBL Timing

Written by Stormbringer (stormbringer@retro-remakes.net)


This tutorial demonstrates the use of a VBL/delta timer with the ability to handle nice cases where the vertical sync
can be controlled by the application and the requested refresh rate is available, but also the "bad" cases when one or
more of these features are not available.

To demonstrate this,we just move a coloured quad over some background rasters

*/

/*

  include the GLTK (Graphics Library Toolkit)

*/
#include "..\gltk\gltk.h"




/* routines
*/

/* background copperlist manager
*/
#include "..\routines\copper_list_bkg.h"






/* objects
*/

/* the copper list object
*/
CopperListBkgObject copperBkg_obj;






/* raster table

the format is quite simple

1st value means the line number

2nd,3rd and 4th values are the 8bit RGB values for the color (0..255)

this table simulates the way a copper list (a list of instructions given to a *magic* co-processor found in the Amiga
circuitry that controlled all the other co-processors. One of it's main uses was to change values of some graphic registers on the fly.
Programmers on Amiga use this feature to create the illusion of hundreds of colors on screen by changing them every line or so.
Exceptional gradients and visual effects were achieved through this mechanism.

here we just display some static colors in a nice gradient...
*/
CopperListBkgEntry copperBkgList[]=
{
0,0,0,0,
81,0,0,17,
86,0,0,34,
91,0,0,51,
96,0,0,68,
100,0,0,85,
104,0,0,102,
108,0,0,119,
111,0,0,136,
114,0,0,153,
117,0,0,170,
119,0,0,187,
121,0,0,204,
122,0,0,221,
123,0,0,238,
124,0,0,255,
125,0,17,255,
126,0,34,255,
127,0,51,255,
128,0,68,255,
129,0,85,255,
130,0,102,255,
131,0,119,255,
132,0,136,255,
133,0,153,255,
134,0,170,255,
135,0,187,255,
136,0,204,255,
137,0,221,255,
138,0,238,255,
139,0,255,255,

158,153,153,153,
160,136,136,136,
161,119,119,119,
162,102,102,102,


174,0,136,136,
176,0,119,136,
178,0,102,136,
180,0,85,136,
182,0,68,136,
184,0,51,136,
186,0,34,136,
188,0,17,136,
190,0,0,136,
192,0,0,119,
194,0,0,102,
196,0,0,85,
199,0,0,68,
202,0,0,51,
205,0,0,34,
209,0,0,17,
218,0,0,0,
};



/*

  our screen "object" that holds the properties of our screen/window

*/
GLTKScreenProperties screenProperties;


CFloat32 position_x=0.0f;
CFloat32 position_y=200.0f;




/*

  intro_on_mouse_left_down()

  user pressed the left mouse button

*/
CError intro_on_mouse_left_down(pCVoid userData)
{
/* quit */
gltk_screen_quit();

/* done */
return(0);
}






/*

  intro_on_vsync()

  this method works like a timer interrupt. we will use it to track time changes and update the animation of or elements, etc

*/
CError intro_on_vsync(pCVoid userData, CFloat64 deltaTime)
{
if ((screenProperties.vsyncFPS == screenProperties.vsyncActual) && screenProperties.vsync)
{
position_x += 1.0f;
}
else
{
CFloat32 timeValue;


timeValue=(CFloat32)deltaTime*screenProperties.speedFactor*((CFloat32)screenProperties.vsyncFPS);

/* process objects */
position_x += 1.0f*timeValue;
}

/* check position of the quad. if it goes off the screen, just put it back */
if (position_x >= screenProperties.width)
{
position_x=0.0f;
}

return(0);
}







/*

  intro_on_paint()

  this method is used to paint the graphic elements on screen

*/
CError intro_on_paint(pCVoid userData)
{
/* clean the screen
*/
/* black background */
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);

/* clear depth buffer with default value */
glClearDepth(1.0f);

/* disable the depth test */
glDisable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);

/* disable any clippin */
glDisable(GL_SCISSOR_TEST);


/* draw the copper list
*/
copper_list_bkg_draw(
&copperBkg_obj,
screenProperties.width,
screenProperties.height,
0,
0,
2 /* scale factor change this, depending on which resolution you want to simulate */
);

/* set 2D mode */
gltk_tools_set_2d_mode(screenProperties.width,screenProperties.height);

glDisable(GL_TEXTURE_2D);
glBegin(GL_QUADS);
glColor4f(1.0f,0.0f,0.0f,1.0f);
glVertex2i(
(CInt32)position_x,
(CInt32)position_y
);
glVertex2i(
(CInt32)position_x+100,
(CInt32)position_y
);
glVertex2i(
(CInt32)position_x+100,
(CInt32)position_y+100
);
glVertex2i(
(CInt32)position_x,
(CInt32)position_y+100
);
glEnd();

/* done */
return(0);
}




/*

  entry point

*/
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
/* hide cursor */
gltk_screen_show_cursor(FALSE);

/* init screen structure */
memset(&screenProperties,0,C_SIZE_OF(screenProperties));

/* name of the  */
screenProperties.name="My new cracktro";

/* define window dimensions
*/
screenProperties.width=640;
screenProperties.height=480;

/* request vsync

NOTE: in order for this option to work fine, you need to set your graphics driver Vertical Sync option on "Application Controlled"
*/
screenProperties.vsync=FALSE;

/* request refresh rate

NOTE: this will only work 100% in full screen mode, if the graphics card is capable of it
*/
screenProperties.vsyncFPS=60;

/* flag for window mode/full screen mode
*/
screenProperties.fullScreen=TRUE;

/* our method for painting the screen
*/
screenProperties.paintMethod=intro_on_paint;

/* our method for managing the time (where we change animation, etc)
*/
screenProperties.vsyncMethod=intro_on_vsync;

/* our method to intercept the left mouse button
*/
screenProperties.leftMouseDown=intro_on_mouse_left_down;


/* init our copper list here
*/
/* number of entries in our list */
copperBkg_obj.count=C_SIZE_OF(copperBkgList)/C_SIZE_OF(copperBkgList[0]);
/* address of the list */
copperBkg_obj.list=copperBkgList;



/* open the screen/window using the give properties
*/
if (gltk_screen_open(&screenProperties))
{
/* run the main loop, intercept system events, etc
*/
gltk_screen_run_loop(&screenProperties);


/* close screen window
*/
gltk_screen_close(&screenProperties);
}


/* exit and return code to system */
return(0);
}

Now let's have a look at some options before you start to yell at me and my endless tutorial.

First of all some of you will have the example running fine and some other running too fast. Why?? Well if it's running too fast, the it means your Vertical Sync is set on "Always Off" in the driver of your graphics card. Ideally it should be set on "Application Controlled", but since some of you believe they will get more frames per second in their favourite game if the set it on off... At least after this tutorial you know that there can't be things like true 300fps on a 60Hz display...

So let's assume it's off. Then check this little flag when you initialize the screen:

Code: [Select]
/* request vsync

NOTE: in order for this option to work fine, you need to set your graphics driver Vertical Sync option on "Application Controlled"
*/
screenProperties.vsync=FALSE; // set on TRUE if you want to be VSync dependent, FALSE if you do not care

Also try different refresh rates by changing the values here:

Code: [Select]
/* request refresh rate

NOTE: this will only work 100% in full screen mode, if the graphics card is capable of it
*/
screenProperties.vsyncFPS=60;

NOTE: only the refresh rates supported by your graphics card and monitor will work ... and only for some selected resolutions.

Happy demo making now!
« Last Edit: March 06, 2011 by Shockwave »
We once had a passion
It all seemed so right
So young and so eager
No end in sight
But now we are prisoners
In our own hearts
Nothing seems real
It's all torn apart

Offline Shockwave

  • good/evil
  • Founder Member
  • DBF Aficionado
  • ********
  • Posts: 17378
  • Karma: 497
  • evil/good
    • View Profile
    • My Homepage
Re: TUTORIAL #3 - VBL and Timing
« Reply #1 on: June 14, 2009 »
Really nice explanation, really smooth example.

Thats one of the best tutorials on Delta Timing I have read. K+
Shockwave ^ Codigos
Challenge Trophies Won:

Offline stormbringer

  • Time moves by fast, no second chance
  • Amiga 1200
  • ****
  • Posts: 453
  • Karma: 73
    • View Profile
    • www.retro-remakes.net
Re: TUTORIAL #3 - VBL and Timing
« Reply #2 on: June 15, 2009 »
thanks mate!

for the ones that want to get a little bit more out of GLTK, here is a modified version of the paint method. This version checks the out-of-sync flag:

Code: [Select]
/*

  intro_on_paint()

  this method is used to paint the graphic elements on screen

*/
CError intro_on_paint(pCVoid userData)
{
/* clean the screen
*/
/* black background */
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);


/* clear depth buffer with default value */
glClearDepth(1.0f);

/* disable the depth test */
glDisable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);

/* disable any clippin */
glDisable(GL_SCISSOR_TEST);


/* draw the copper list
*/
copper_list_bkg_draw(
&copperBkg_obj,
screenProperties.width,
screenProperties.height,
0,
0,
2 /* scale factor change this, depending on which resolution you want to simulate */
);

/* set 2D mode */
gltk_tools_set_2d_mode(screenProperties.width,screenProperties.height);

glDisable(GL_TEXTURE_2D);
glBegin(GL_QUADS);

/* test if we are out of sync */
if (screenProperties.outOfVSync)
{
/* we are out of sync */
glColor4f(1.0f,0.0f,0.0f,1.0f);
}
else
{
glColor4f(0.0f,1.0f,0.0f,1.0f);
}

glVertex2i(
(CInt32)position_x,
(CInt32)position_y
);
glVertex2i(
(CInt32)position_x+100,
(CInt32)position_y
);
glVertex2i(
(CInt32)position_x+100,
(CInt32)position_y+100
);
glVertex2i(
(CInt32)position_x,
(CInt32)position_y+100
);
glEnd();

/* done */
return(0);
}

In this small example, we simply check if the out of sync flag is on, if yes, we set the color of our quad to red, if we are in sync, we set it to green. The test is performed like this:

Code: [Select]
/* test if we are out of sync */
if (screenProperties.outOfVSync)
{
/* we are out of sync */
glColor4f(1.0f,0.0f,0.0f,1.0f);
}
else
{
glColor4f(0.0f,1.0f,0.0f,1.0f);
}


NOTE: this out of sync feature is a hack. There is no standardized API for this. So the way I implemented it in GLTK, is by measuring time between frames and keep an average over a bunch of frames. Since in this tutorial we measure time with the CPU and the timing between frames is supposed to be almost always the same, then, the average time between frames must be constant. This little hack simply uses a threshold and checks the average time against that threshold. If the average is over the threshold, then the out of sync flag is set.

Basically in my remakes I just use this feature in the menus at the beginning to display a warning for the user. However I still let the user select if the VSync is on or off, on purpose.

Smart guys could take advantage of this feature and automatically adjust their internal timers. Since this flag is updated during the screen refresh, it works fine even during the runnig time of the loop. So potentially, even if the user changes the VBL settings on the fly, you should be able to catch the changes.
We once had a passion
It all seemed so right
So young and so eager
No end in sight
But now we are prisoners
In our own hearts
Nothing seems real
It's all torn apart

Offline bj

  • ZX 81
  • *
  • Posts: 20
  • Karma: 10
    • View Profile
Re: TUTORIAL #3 - VBL and Timing
« Reply #3 on: September 23, 2009 »
didn't work to start with because you haven't included the gltk folder etc in the zip file download. Worked fine after I'd copied those files across from the TUTORIAL2 folder.

Offline Positron

  • C= 64
  • **
  • Posts: 93
  • Karma: 7
    • View Profile
Re: TUTORIAL #3 - VBL and Timing
« Reply #4 on: February 18, 2010 »
Really nice tutorial. I can second Shockwave, one of the best Delta Timing tutorials.

Offline rdc

  • Pentium
  • *****
  • Posts: 1495
  • Karma: 140
  • Yes, it is me.
    • View Profile
    • Clark Productions
Re: TUTORIAL #3 - VBL and Timing
« Reply #5 on: February 18, 2010 »
Excellent work.

Offline Clyde

  • A Little Fuzzy Wuzzy
  • DBF Aficionado
  • ******
  • Posts: 7271
  • Karma: 71
    • View Profile
Re: REMAKES TUTORIAL #3 - VBL and Timing
« Reply #6 on: May 06, 2011 »
I'd love to know how to employ a similar method with FreeBASIC and CPP - not OGL. An area i've not really dealt with, and will be very usefull. Thanks in advance.
Still Putting The IT Into Gravy
If Only I Knew Then What I Know Now.

Challenge Trophies Won: