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.