Synchronizing with video refresh

by Richard Russell, April 2007

(Note that the code in this article requires DirectX 7 or later).

To achieve perfectly smooth animated graphics it is necessary to synchronize the output from your program with the video refresh rate of your graphics card and monitor. For example, if your display is running at 75 Hz your program should update its output (preferably) 75 times per second, synchronized with the start of each video frame. If it cannot run that quickly, it should update its output at a sub-multiple of the video refresh rate (e.g. 37.5 or 25 times per second).

The Windows Graphical Device Interface (GDI) provides no means of synchronizing with the video refresh rate, but DirectX (in particular its Direct Draw subsystem) does. It is not necessary to use DirectX to output the graphics themselves; you can use it solely for the purpose of synchronization.

To achieve this you should first incorporate the following code in the initialisation section of your program:

        SYS "LoadLibrary", "DDRAW.DLL" TO ddraw%
        IF ddraw%=0 THEN ERROR 100, "Direct Draw not available"
        SYS "GetProcAddress", ddraw%, "DirectDrawCreateEx" TO `DirectDrawCreateEx`
 
        DIM IID_IDirectDraw7{a%, b%, c%, d%}
        IID_IDirectDraw7.a% = &15E65EC0
        IID_IDirectDraw7.b% = &11D23B9C
        IID_IDirectDraw7.c% = &60002FB9
        IID_IDirectDraw7.d% = &5BEA9797
 
        DIM IDirectDraw{QueryInterface%, AddRef%, Release%, Compact%, CreateClipper%, \
        \               CreatePalette%, CreateSurface%, DuplicateSurface%, \
        \               EnumDisplayModes%, EnumSurfaces%, FlipToGDISurface%, \
        \               GetCaps%, GetDisplayMode%, GetFourCCCodes%, GetGDISurface%, \
        \               GetMonitorFrequency%, GetScanLine%, GetVerticalBlankStatus%, \
        \               Initialize%, RestoreDisplayMode%, SetCooperativeLevel%, \
        \               SetDisplayMode%, WaitForVerticalBlank%}
 
        SYS `DirectDrawCreateEx`, 0, ^IDirectDraw%, IID_IDirectDraw7{}, 0
        !(^IDirectDraw{}+4) = !IDirectDraw%

Here an error results if Direct Draw isn't available. In practice you may prefer not to issue an error but to fall back to using timers or the WAIT statement to set the frame rate. The animation will not be as smooth but at least your program will still run.

The simplest way of achieving synchronization is to wait for the beginning of each video frame before updating your graphics output. To do that you can use code similar to the following (do not use this method - see below):

        _DDWAITVB_BLOCKBEGIN = 1
        REPEAT
          SYS IDirectDraw.WaitForVerticalBlank%, IDirectDraw%, _DDWAITVB_BLOCKBEGIN, 0
          PROCpaint
        UNTIL FALSE

Here it is assumed that your output will be updated in PROCpaint.

Unfortunately there are a couple of shortcomings with this simple method. Firstly, the WaitForVerticalBlank function does not always work reliably. Secondly, it uses 100% CPU time while waiting for the next frame, which is undesirable (the importance of this will depend on how much time is spent 'waiting' and how much doing something useful).

An alternative and more satisfactory approach is to poll for the beginning of the video frame as follows:

        SYS "timeBeginPeriod", 1
        lastline% = 0
        REPEAT
          SYS IDirectDraw.GetScanLine%, IDirectDraw%, ^thisline%
          IF thisline% < lastline% PROCpaint
          lastline% = thisline%
          SYS "Sleep", 1
        UNTIL FALSE

Again it is assumed that your output will be updated in PROCpaint. The call to timeBeginPeriod is to ensure that the Sleep function waits for the shortest possible (non-zero) period.

This method may not result in as accurate synchronization as the previous one, but it avoids wasting CPU time and allows you to carry out low-priority 'background' tasks whilst waiting for the start of the next frame.

When you have finished with the Direct Draw subsystem, execute the following code (for example on exit, by including it in a cleanup routine called from ON ERROR and ON CLOSE statements):

        SYS IDirectDraw.Release%, IDirectDraw%