Win.flip() not producing precise stimuli presentation

I am having some issues with win.flip() for precisely presenting a stimulus. I understood win.flip() to wait until the start of the next refresh. I am using a 120Hz monitor and expected the stimulus to present every 8.33ms with the win.flip() [see example below]. However, the timing seems completely off where some iterations are pretty close to 8.33ms and others are much slower. I tried this with a simple example (below) and created an output to check it.

Does anyone have any ideas why win.flip() is not coinciding with the refresh, as well as how I can fix this?

from psychopy import visual, event

experiment_window = visual.Window(size=(800, 600), winType='pyglet', fullscr=False,
                                  screen=0, monitor='testMonitor',
                                  color="black", colorSpace='rgb')
experiment_window.mouseVisible = False

frameTolerance = 0.001

kb_resp_continue = event.waitKeys(keyList=['space'])

timeStim = []

# Setup stimulus
gabor = visual.GratingStim(experiment_window, tex='sin', mask='gauss', sf=5,
    name='gabor', autoLog=False)
fixation = visual.GratingStim(experiment_window, tex=None, mask='gauss', sf=0, size=0.02,
    name='fixation', autoLog=False)

# Let's draw a stimulus for 200 frames, drifting for frames 50:100
for frameN in range(200):   # For exactly 200 frames
    if 10 <= frameN < 150:  # Present fixation for a subset of frames
    if 50 <= frameN < 100:  # Present stim for a different subset
        gabor.phase += 0.1  # Increment by 10th of cycle
    t = experiment_window.flip()
    timeStim.append((frameN, t))

f = open('timeseries-minimal.csv', 'w')
for t in timeStim:
    line = ','.join(str(x) for x in t)
    f.write(line + '\n')

Do you need to increment the phase every frame? I’m wondering whether that operation is taking too long. What are the timings like when only incrementing if frameN % 2 == 0 ?

For a computer with a dedicated graphics card, the script you’ve written should be easily achievable within these timing constraints.

What is likely happening is that:

  • other processes on your computer are competing for CPU time. You need to be running on a computer that is as “clean” as possible, i.e. a dedicated lab computer that doesn’t have a web browser open with lots of tabs, no background tasks like DropBox, Google Drive and the like running, etc. PsychoPy would ideally be the only user-level program running. Using your personal computer is fine for developing a task, but for performance testing and actual experimental use, a dedicated, clean, minimal system is needed. Modern computers are very far from being real-time systems - the OS is continuously busy, mediating access to resources between itself and routinely hundreds of other processes. PsychoPy is just one of them, so you need to clear the playing field as much as possible.
  • your graphics card and hardware isn’t up to spec. eg Intel integrated graphics struggling to run multiple 4K displays simultaneously at 120 Hz (although this is becoming less of an issue these days).

So you should probably post your hardware specs (computer, graphics card, displays, and OS). Also strive to quantify the issue - a histogram of the achieved intervals would be good, and look for any periodicity in the dropped frames.

Have you tried turning waitBlanking off in the window object? I find when I am using a high refreshrate monitor (ASUS PG258Q) I often have awful frame timing until this is turned off.

You can turn it off when you define your window:

experiment_window = visual.Window(size=(800, 600), winType='pyglet', fullscr=False,
                                  screen=0, monitor='testMonitor',
                                  color="black", colorSpace='rgb', waitBlanking=False)