First stimulus in Psychtoolbox trial consistently ~30 ms shorter than expected when chaining flips with vbl

Hi everyone,

I’m experiencing a consistent timing issue in my Psychtoolbox experiment. The very first stimulus in each trial (e.g., the fixation cross or the feedback after response window) is always about ~30 ms shorter than the intended duration (e.g., 0.5 s).

However, all subsequent stimuli within the same trial are perfectly precise when I chain flips using:

vbl = Screen('Flip', win, vbl + (waitframes - 0.5)*ifi);
  • The first stimulus segment in each tria or the part after response windowl (e.g., fixation) is ~20–30 ms shorter than expected.
    *The video stimuli segments that use
    vbl = Screen('Flip', win, vbl + (waitframes - 0.5)*ifi)
    are perfectly frame-accurate.
  • IFI is ~0.0167 s at 60 Hz(59.95Hz)

For example:

Expected fixation: 0.500 s
Measured fixation: ~0.470 s

Psychtoolbox 3.0.19
MATLAB R2024b
Windows

I was wondering what is the recommended way to make the first segment or the segement after break in a trial frame-accurate like subsequent chained segments?


% Preallocate a structure to store data
    results = struct();
    for trial = 1:2

        response     = -1;
        rt           = -1;
        response_key = -1; 
        distance     = trialTable_s.distance(trial);
        cat_relate   = trialTable_s.cat_relate{trial};
        
        if strcmp(cat_relate, 'same')
            correct_response_key = keySame;
        else
            correct_response_key = keyDiff;
        end

        %% -------------------- Trial Preparation -------------------------
        
        % Extract video names from design table
        v1_raw = trialTable_s.v1{trial};
        v2_raw = trialTable_s.v2{trial};
    
        % Remove file extension
        v1_id = char(erase(string(v1_raw), ".mp4"));
        v2_id = char(erase(string(v2_raw), ".mp4"));

        % Load frame file paths
        framesL = make_frame_list(stimDir, v1_id, stimFrames, 'png');
        framesR = make_frame_list(stimDir, v2_id, stimFrames, 'png');
    
        % Preallocate texture arrays (GPU memory)
        texL_all = zeros(1, stimFrames);
        texR_all = zeros(1, stimFrames);

        % Convert images into PTB textures (improves presentation timing)
        for frame = 1:stimFrames
            texL_all(frame) = Screen('MakeTexture', win, framesL{frame});
            texR_all(frame) = Screen('MakeTexture', win, framesR{frame});
        end


        %% -------------------- Fixation Period ---------------------------


        Screen('DrawDots', win, [xCenter, yCenter], 20, white, [0,0], 2);
        vbl = Screen('Flip', win);   % Fixation onset timestamp
        fixdot_t = vbl; 
    
        % Maintain fixation for remaining frames
        for frame = 2:fixFrames
            Screen('DrawDots', win, [xCenter, yCenter], 20, white, [0,0], 2);
            vbl = Screen('Flip', win, vbl + (waitframe - 0.5) * ifi);
        end
        % the time of fixation
        fprintf('Fixation: %.4f s\n', (vbl - fixdot_t));



        %% -------------------- Stimulus Presentation ---------------------
        flipTimes = zeros(1, stimFrames); 
        
        % --- Present first frame ---
        Screen('DrawTexture', win, texL_all(1), [], leftRect);
        Screen('DrawTexture', win, texR_all(1), [], rightRect);
        
        KbQueueFlush;  % Clear previous keypresses
        ispress = 0; 
        
        vbl = Screen('Flip', win, vbl + (waitframe - 0.5) * ifi);
        stimOnset = vbl;   % True physical stimulus onset time
        flipTimes(1) = stimOnset;

    % --- Stimuli frame Loop ---
        for frame = 2:stimFrames
            % 1. Check for response
            [pressed, firstPress] = KbQueueCheck;
            if pressed
                if firstPress(escapeKey), error('User quit'); 
                end
                
                % Use the variables you defined in Section 5 (keySame, keyDiff)
                if firstPress(keySame) || firstPress(keyDiff)
                    if firstPress(keySame)
                        response = 1; 
                        response_key = keySame; 
                        rt = firstPress(keySame) - stimOnset;
                    else
                        response = 2; 
                        response_key = keyDiff; 
                        rt = firstPress(keyDiff) - stimOnset;
                    end
                    stimOffset = vbl; 
                    ispress = 1;
                    break; % Exit the frame loop early

                else
                    ispress = 0;
                end
            end


            % Draw current frame
            Screen('DrawTexture', win, texL_all(frame), [], leftRect);
            Screen('DrawTexture', win, texR_all(frame), [], rightRect);
        
            % Draw fixation dot on top
            Screen('DrawDots', win, [xCenter, yCenter], 20, white, [0,0], 2);
        
            % Flip to screen at precise time
            [vbl, ~, ~, missed] = Screen('Flip', win, vbl + (waitframe - 0.5) * ifi);
            flipTimes(frame) = vbl;

            % Check if flip missed the deadline
            if missed > 0
                fprintf('Warning: Missed deadline at frame %d!\n', frame);
            end
    
            % Record last frame time
            if frame == stimFrames
                stimOffset = vbl; 
            end
        end

        %% -------------------- Timing Analysis ---------------------
        % find the flip large than 0
        actualFlips = flipTimes(flipTimes > 0);
        
        if length(actualFlips) > 1
            validIntervals = diff(actualFlips); 
            
            fprintf('\n--- Trial %d Timing Analysis ---\n', trial);
            fprintf('Real stim duration (until response): %.4f s\n', stimOffset - stimOnset + ifi);
            fprintf('Mean frame interval: %.6f s (Expected: %.6f s)\n', mean(validIntervals), ifi);
            
            % drop frame?
            dropped = sum(validIntervals > 1.5 * ifi);
            fprintf('Dropped frames count: %d\n', dropped);
        else
            fprintf('\n--- Trial %d Timing Analysis ---\n', trial);
            fprintf('Response on first frame, no intervals to analyze.\n');
        end
                


        %% -------------------- Blank / Response Window -------------------
        %if not ispress 
        if ispress == 0
                % Show blank screen (just fixation)
                Screen('DrawDots', win, [xCenter, yCenter], 20, white, [0,0], 2);
                Screen('Flip', win);
                start_time = GetSecs; 
                
                % Wait for remaining time until maxRespTime is reached
                % (Note: maxRespTime should be relative to stimOnset)
                
                while (GetSecs - start_time) < maxRespTime
                    [pressed, firstPress] = KbQueueCheck;
                    if pressed
                        if firstPress(escapeKey), error('User quit'); end
                        if firstPress(keySame)
                            response = 1; 
                            response_key = keySame; 
                            rt = firstPress(keySame) - stimOnset; 
                            ispress = 1;
                            break;
                        elseif firstPress(keyDiff)
                            response = 2; 
                            response_key = keyDiff;
                            rt = firstPress(keyDiff) - stimOnset; 
                            ispress = 1;
                            break;
                        end
                        
                    end
                    %WaitSecs(0.001); 
                end
                end_response_win = GetSecs; 
                Screen('FillRect', win, grey);
                vbl = Screen('Flip', win);
        end

        if ispress && response_key == correct_response_key
            correct = 1;
        else
            correct = 0;
        end
        % Feedback & Accuracy logic
        if ispress == 1
            if response_key == correct_response_key
                correct = 1;  % Success
                fbText = 'Correct!'; 
                fbColor = [0 1 0]; % Green
            else
                correct = 0;  % Chose the wrong button
                fbText = 'Wrong!'; 
                fbColor = [1 0 0]; % Red
            end
        else
            correct = -1;     % Failed to respond (Missed)
            fbText = 'Too Slow!'; 
            fbColor = [1 1 0]; % Yellow/Orange
        end

                % Show feedback only if in practice mode
        if isprac == 1
            % 1. Prepare the text
            Screen('TextSize', win, 40)
            Screen('TextStyle', win, 1)
            DrawFormattedText(win, fbText, 'center', 'center', fbColor);
            
            
            % 2. Flip to show the feedback (getting the onset time)
            vbl = Screen('Flip', win, vbl + (waitframe - 0.5) * ifi); 
            vbl_feedbackOnset = vbl;
            % 3. Hold for feedbackFrames (defined in Section 7)
            for frame = 2:feedbackFrames
                % Keep drawing the text so it doesn't flicker/disappear
                DrawFormattedText(win, fbText, 'center', 'center', fbColor);
                
                % Flip based on the previous VBL to maintain frame-lock
                vbl = Screen('Flip', win, vbl + (waitframe - 0.5) * ifi);
            end
            f_duration = vbl-vbl_feedbackOnset; 
            fprintf('Trial %d: Feedback duration: %.6f s\n', trial, f_duration);
            Screen('TextStyle', win, 0);
        end
``

You timestamp the wrong flip, which makes for a 16.6 msecs error, and maybe also schedule wrong - probably a frame too short, because you seem to not understand what the waitframe variable is for. Have a look at how to use waitframe properly, in our demos or the intro pdf in the PsychDocumentation folder.

Fixation duration is not vbl - fixdot_t, but stimOnset - fixdot_t, as onset of first stimulus marks the end/offset of fixation. Also you wouldn’t do that for frame = 2:fixFrames loop, but instead simply substitute waitframe with fixFrames in the flip that shows the first stimulus frame.

This can explain ~16.6 msecs or ~33.3 msecs too short reported fixation.

Dear Mario,

Thank you very much for your detailed explanation.

I tried measuring fixation duration as:

realDuration = stimOnset - fix_onset;

However, the measured duration is still shorter than expected:

Real fixation duration = 0.463471 s

This corresponds to almost two frames missing on a 60 Hz display (~16.7 ms per frame).

Interestingly, when I use very similar fixation code in a minimal standalone script (separate file for timing validation), the timing is correct (~0.500 s as expected).

Below is the minimal script I used for timing verification:

clear; clc;

try
    %% ---------------- PTB Setup ----------------
    PsychDefaultSetup(2);
    Screen('Preference','SkipSyncTests',0);

    screenNumber = max(Screen('Screens'));
    white = WhiteIndex(screenNumber);
    black = BlackIndex(screenNumber);
    grey  = white/2;

    [win, winRect] = PsychImaging('OpenWindow', screenNumber, grey);
    [xCenter, yCenter] = RectCenter(winRect);

    ifi = Screen('GetFlipInterval', win);
    waitframe = 1; 
    fprintf('IFI = %.6f s (%.2f Hz)\n', ifi, 1/ifi);

    Priority(MaxPriority(win));
    HideCursor;

    %% ---------------- Timing ----------------
    fixDuration = 0.5;                 % 500 ms
    fixFrames   = round(fixDuration/ifi);

    fprintf('Fix frames = %d (%.6f s expected)\n', ...
        fixFrames, fixFrames*ifi);

    %% ---------------- Fixation ----------------
    % Draw first frame
    Screen('DrawDots', win, [xCenter yCenter], 20, white, [0 0], 2);
    vbl = Screen('Flip', win);
    fix_onset = vbl;

    % Present remaining frames (frame-locked)
    for frame = 1:fixFrames-1
        Screen('DrawDots', win, [xCenter yCenter], 20, white, [0 0], 2);
        Screen('Flip', win,vbl + (waitframe - 0.5) * ifi);
    end

    Screen('FillRect', win, grey);
    [iti_start, ~, ~, missed] = Screen('Flip', win, vbl + (waitframe - 0.5) * ifi);
    iti_vbl = iti_start;



    realDuration = iti_start - fix_onset;
    fprintf('Real fixation duration = %.6f s\n', realDuration);

    %% ---------------- End ----------------
    WaitSecs(0.5);
    sca;
    Priority(0);
    ShowCursor;

catch
    sca;
    Priority(0);
    ShowCursor;
    psychrethrow(psychlasterror);
end

In this minimal script above, fixation duration is accurate.

However, inside my full experiment (where textures are prepared and drawn immediately after fixation), the duration becomes ~0.463 s.

I also see the warning: INFO:

PTB’s Screen(‘Flip’, 10) command seems to have missed the requested stimulus presentation deadline
INFO: a total of 2 times out of a total of 322 flips during this session.

INFO: This number is fairly accurate (and indicative of real timing problems in your own code or your system)
INFO: if you provided requested stimulus onset times with the ‘when’ argument of Screen(‘Flip’, window [, when]);

Here is the code after I updated:

    % Preallocate a structure to store data
    results = struct();
    for trial = 1:2

        response     = -1;
        rt           = -1;
        response_key = -1; 
        distance     = trialTable_s.distance(trial);
        cat_relate   = trialTable_s.cat_relate{trial};
        
        if strcmp(cat_relate, 'same')
            correct_response_key = keySame;
        else
            correct_response_key = keyDiff;
        end

        %% -------------------- Trial Preparation -------------------------
        
        % Extract video names from design table
        v1_raw = trialTable_s.v1{trial};
        v2_raw = trialTable_s.v2{trial};
    
        % Remove file extension
        v1_id = char(erase(string(v1_raw), ".mp4"));
        v2_id = char(erase(string(v2_raw), ".mp4"));

        % Load frame file paths
        framesL = make_frame_list(stimDir, v1_id, stimFrames, 'png');
        framesR = make_frame_list(stimDir, v2_id, stimFrames, 'png');
    
        % Preallocate texture arrays (GPU memory)
        texL_all = zeros(1, stimFrames);
        texR_all = zeros(1, stimFrames);

        % Convert images into PTB textures (improves presentation timing)
        for frame = 1:stimFrames
            texL_all(frame) = Screen('MakeTexture', win, framesL{frame});
            texR_all(frame) = Screen('MakeTexture', win, framesR{frame});
        end


        %% -------------------- Fixation Period ---------------------------
          
        Screen('DrawDots', win, [xCenter yCenter], 20, white, [0 0], 2);
        vbl = Screen('Flip', win);
        fix_onset = vbl;
    
        % Present remaining frames (frame-locked)
        for frame = 1:fixFrames-1
            Screen('DrawDots', win, [xCenter yCenter], 20, white, [0 0], 2);
            vbl = Screen('Flip', win,vbl + (waitframe - 0.5) * ifi);
        end



        %% -------------------- Stimulus Presentation ---------------------
        flipTimes = zeros(1, stimFrames); 
        
        % --- Present first frame ---
        Screen('DrawTexture', win, texL_all(1), [], leftRect);
        Screen('DrawTexture', win, texR_all(1), [], rightRect);
        
        KbQueueFlush;  % Clear previous keypresses
        ispress = 0; 
        
        vbl = Screen('Flip', win, vbl + (waitframe - 0.5) * ifi);
        stimOnset = vbl;   % True physical stimulus onset time
        flipTimes(1) = stimOnset;

        realDuration = stimOnset - fix_onset;
        fprintf('Real fixation duration = %.6f s\n', realDuration);

Also when i try to use for loop, the timing of fixation dot from minimal script is also not precise, 2 frames shorter than expectation:

IFI = 0.016677 s (59.96 Hz)
Fix frames = 30 (0.500296 s expected)
Real fixation duration = 0.500246 s
Real fixation duration = 0.465624 s
Real fixation duration = 0.465941 s
%% ============================================================
%  Minimal 500 ms Fixation Test
%  Author: Yuanrui Zheng
%  Description:
%  Presents a fixation dot for exactly 500 ms (frame-locked)
% =============================================================

clear; clc;

try
    %% ---------------- PTB Setup ----------------
    PsychDefaultSetup(2);
    Screen('Preference','SkipSyncTests',0);

    screenNumber = max(Screen('Screens'));
    white = WhiteIndex(screenNumber);
    black = BlackIndex(screenNumber);
    grey  = white/2;

    [win, winRect] = PsychImaging('OpenWindow', screenNumber, grey);
    [xCenter, yCenter] = RectCenter(winRect);

    ifi = Screen('GetFlipInterval', win);
    waitframe = 1; 
    fprintf('IFI = %.6f s (%.2f Hz)\n', ifi, 1/ifi);

    Priority(MaxPriority(win));
    HideCursor;

    %% ---------------- Timing ----------------
    fixDuration = 0.5;                 % 500 ms
    fixFrames   = round(fixDuration/ifi);
    blankFrames = round(0.8/ifi);

    fprintf('Fix frames = %d (%.6f s expected)\n', ...
        fixFrames, fixFrames*ifi);
    for i=1:3
    %% ---------------- Fixation ----------------
        % Draw first frame
        Screen('DrawDots', win, [xCenter yCenter], 20, white, [0 0], 2);
        vbl = Screen('Flip', win);
        fix_onset = vbl;
    
        % Present remaining frames (frame-locked)
        for frame = 1:fixFrames-1
            Screen('DrawDots', win, [xCenter yCenter], 20, white, [0 0], 2);
            vbl = Screen('Flip', win,vbl + (waitframe - 0.5) * ifi);
        end
    
        Screen('FillRect', win, grey);
        [iti_start, ~, ~, missed] = Screen('Flip', win, vbl + (waitframe - 0.5) * ifi);
        vbl = iti_start;

        for frame = 1:blankFrames-1
            Screen('FillRect', win, grey);
            vbl = Screen('Flip', win, vbl + (waitframe - 0.5) * ifi);
        end
    
        realDuration = iti_start - fix_onset;
        fprintf('Real fixation duration = %.6f s\n', realDuration);
    
        %% ---------------- End ----------------
        WaitSecs(0.5);
    end

    sca;
    Priority(0);
    ShowCursor;

catch
    sca;
    Priority(0);
    ShowCursor;
    psychrethrow(psychlasterror);
end

Reiterating again: Don’t use those " % Present remaining frames (frame-locked)" for loops, use the waitframe parameter for 1st stimulus onset properly instead, setting it to fixFrames, as described in the intro pdf in the PsychDocumentation folder. E.g.
[iti_start, ~, ~, missed] = Screen('Flip', win, vbl + (fixFrames - 0.5) * ifi);

Similar for substituting waitframe with blankFrames. This flips a new stimulus image, blank image, whatever, the proper amount of refresh cycles after onset of the previous image. It is explained in the intro pdf “PTBTutorial-ECVP2013.pdf” that is pointed out as recommended reading after each installation of PTB.

If you see a reported duration that isn’t a multiple of 16.6 msecs on a 60 Hz display, but something like 0.465624, that could mean that timestamping is broken on your setup, e.g., because your Windows machine uses a Intel graphics card - which we strongly advise against, because of its well known brokenness. Or one of the various other problems caused by the MS-Windows operating system, e.g., on multi-display or HiDPI setups. All explained on our website and numerous forum posts and help texts, and in PTB’s output. The best solution is usually switching to Linux, or fixing your Windows setup, e.g., by use of a different graphics card.

My advice ends here. As you are currently not a paying customer, but user of the old and unsupported free Psychtoolbox 3.0.19, this was already a substantial courtesy. Maybe others can help.