function PerceptualVBLSyncTest(screen, stereomode, fullscreen, doublebuffer, maxduration, vblSync, testdualheadsync, useVulkan) % PerceptualVBLSyncTest([screen=max][, stereomode=0][, fullscreen=1][, doublebuffer=1][, maxduration=10][, vblSync=1][, testdualheadsync=0][, useVulkan=0]) % % Perceptual synchronization test for synchronization of Screen('Flip') and % Screen('WaitBlanking') to the vertical retrace. % % Arguments: % 'screen' Either a single screen handle, or none (in which case the % display with the maximum id will be used), or a vector of two handles in % stereomode 10, e.g., [0 1] if you want to output to screens 0 and 1. You % can also pass a vector of two screens when stereomode is not set to 10. % In this case two separate (non-stereo) onscreen windows will be opened on % both displays and they will get flipped in multiflip mode 2. That means % that the first display (first element of 'screen') is synced to VBL, but % the 2nd one is synced to bufferswaps of the first one. This is a % straightforward test to check if two displays of a stereosetup run with a % synchronized retrace cycle (good!) or if they are phase-shifted or % drifting against each other (not good!). % % 'stereomode' Which stereomode to use? Defaults to zero, ie. no stereo. % % 'fullscreen' Fullscreen presentation? Defaults to 1 ie. yes. In % non-fullscreen mode, no proper synchronization of bufferswaps can be % expected. % % 'doublebuffer' Single- or double-buffering (1). Defaults to 1. In single % buffer mode there is no sync to retrace, so this is a good way to % simulate the tearing artifacts that would happen on sync failure, just to % get an impression. % % 'maxduration' Maximum runtime of test: Runs until keypress or maxduration % seconds have elapsed (Default is 10 seconds). % % 'vblSync' If 1, synchronize bufferswaps to vertical retrace of monitor, % otherwise (setting 0) swap immediately without sync, ie., usually with tearing. % % 'testdualheadsync' If non-zero, and 'vblSync' is zero, manually wait until the video % scanout position reaches half the height of the display, then swap. If this % is done on a multi-display setup and the video scanout cycles of all the % participating displays are properly synchronized, you should see a "static" % crack line roughly at half the height of the display, maybe a bit lower. If % you see a wandering crack line, at least on some displays, or you see vertical % offsets of the position of the crack line between displays, then the displays % are not properly synchronized, ie., not suitable for artifact free binocular % stimulation. Caveat: This logic has been developed and tested specifically % for testing on Linux with a single X-Screen spanning multiple displays. It may % or may not be suitable to assess other operating systems or display configurations. % % 'useVulkan' If 1, try to use a Vulkan display backend instead of the % OpenGL display backend. See 'help PsychVulkan'. % % After starting this test, you should see a flickering greyish background % that flickers in a homogenous way - without cracks or weird moving patterns % in the flickering area. If you see an imhogenous flicker, this means that % synchronization of stimulus onset to the vertical retrace doesn't work due % to some serious bug or limitation of your graphics hardware or its driver. % If you don't know what this means, you can test this script with parameter % doublebuffer == 0 to artificially create a synchronization failure. % % On many systems you should also see some emerging pattern of yellow horizontal lines. % These lines should be tightly concentrated/clustered in the topmost area of % the screen. Lots of yellow lines in the middle or bottom area or even % randomly distributed lines indicate some bug in the driver of your graphics % hardware. This is a common problem of all ATI graphics adapters on MacOS-X % versions earlier than OS-X 10.4.3 when running a dual-display setup... % % A second reason for distributed yellow lines could be bad timing on your % machine, e.g., due to background activity by virus scanners or the Spotlight % indexing service on OS-X. Turn these off for conducting your studies! % % History: % 01/28/06 mk Written. Replaces the built-in flickertest of Screen('OpenWindow') % on multi-display setups. That test wasn't well received :( % Check for presence of OpenGL PTB. AssertOpenGL; if nargin < 4 doublebuffer = []; end if isempty(doublebuffer) % Use double-buffered windows by default: Single-buffered ones can't sync to % retrace and are discouraged anyway. Setting doublebuffer=0 is an easy way % to reproduce the visual pattern created by a complete sync-failure though. doublebuffer=1; end doublebuffer=doublebuffer+1; if nargin < 2 stereomode = []; end if isempty(stereomode) % Use non-stereo display by default. stereomode=0; end if nargin < 3 fullscreen = []; end if isempty(fullscreen) fullscreen=1; end if nargin < 1 screen = []; end if isempty(screen) if stereomode == 10 screen(1) = max(Screen('Screens')) - 1; screen(2) = max(Screen('Screens')); if screen(1)<0 error('Stereomode 10 only works on setups with two attached displays!'); end else screen=max(Screen('Screens')); end end if nargin < 5 maxduration = []; end if isempty(maxduration) maxduration = 10; end if nargin < 6 vblSync = []; end if isempty(vblSync) vblSync = 1; end if nargin < 7 testdualheadsync = []; end if isempty(testdualheadsync) testdualheadsync = 0; end if nargin < 8 || isempty(useVulkan) useVulkan = 0; end thickness = (1-vblSync) * 4 + 1; try if fullscreen rect1=[]; rect2=[]; else rect1=InsetRect(Screen('GlobalRect', screen(1)), 1, 0); if length(screen)>1 rect2=InsetRect(Screen('GlobalRect', screen(2)), 1, 0); end end if stereomode~=10 % Standard case: PsychImaging('PrepareConfiguration'); PsychImaging('AddTask', 'General', 'UseRetinaResolution'); if useVulkan PsychImaging('AddTask', 'General', 'UseVulkanDisplay'); end [win , winRect]=PsychImaging('OpenWindow', screen(1), 0, rect1, [], doublebuffer, stereomode); if length(screen)>1 PsychImaging('PrepareConfiguration'); PsychImaging('AddTask', 'General', 'UseRetinaResolution'); if useVulkan PsychImaging('AddTask', 'General', 'UseVulkanDisplay'); end win2 = PsychImaging('OpenWindow', screen(2), 0, rect2, [], doublebuffer, stereomode); end else % Special case for dual-window stereo: % Setup master window: [win , winRect]=Screen('OpenWindow', screen(1), 0, rect1, [], doublebuffer, stereomode); % Setup slave window: Screen('OpenWindow', screen(2), 0, rect2, [], doublebuffer, stereomode); end flickerRect = InsetRect(winRect, 100, 0); color = 0; deadline = GetSecs + maxduration; beampos=0; ifi = Screen('GetFlipInterval', win); winfo = Screen('GetWindowInfo', win); % Normally we do not do anything after flip, as a performance optimization: dontclear = 2; if ~isempty(strfind(winfo.GPUCoreId, 'VC4')) % Unless we are on the RaspberryPi with its Broadcom VideoCore gpu, % where we do a full clear to background color after each Flip. Why? % Because it turns out this gives much better performance! E.g., we can % almost do 120 fps stable on a RPi 400 / VideoCore-6 at 1920x1080, % whereas without clear we can only do about 60 fps. Not sure why this % is, but it could be either a VideoCore 6+ thing do to the use of separate % 3D render engine and display engine, and the need to do some buffer % passing, with a possible detiling blit, or it could be related to the VideoCore % gpu being a tiled renderer. In either case, it might be more efficient to % completely invalidate / discard backbuffers, and the driver may get its % hint from a full framebuffer clear to trigger this optimization. % TODO: Other tiled renderers or split gpu + display engine hardware might % benefit from this as well, especially on low powered mobile SoC devices? dontclear = 0; end VBLTimestamp = Screen('Flip', win, 0, 2); while ~KbCheck && (GetSecs < deadline) % Draw alternating black/white rectangle to left eye view or mono view: Screen('FillRect', win, color, flickerRect); % If beamposition is available, visualize it via yellow horizontal line: if (beampos>=0), Screen('DrawLine', win, [255 255 0], 0, beampos, winRect(3), beampos, thickness); end if stereomode > 0 % Same for right-eye view... Screen('SelectStereoDrawBuffer', win, 1); Screen('FillRect', win, color, flickerRect); if (beampos>=0), Screen('DrawLine', win, [255 255 0], 0, beampos, winRect(3), beampos, thickness); end % Back to right eye for next loop iteration: Screen('SelectStereoDrawBuffer', win, 0); end if stereomode == 0 && length(screen)>1 Screen('FillRect', win2, color, flickerRect); Screen('DrawingFinished', win2, 0, 2); Screen('DrawingFinished', win, 0, 2); multiflip = 0; else multiflip = 0; end % Alternate drawing color from white -> black, or black -> white color=255 - color; if doublebuffer>1 if vblSync % Flip buffer on next vertical retrace, query rasterbeam position on flip, if available: [VBLTimestamp, StimulusOnsetTime, FlipTimestamp, Missed, beampos] = Screen('Flip', win, VBLTimestamp + ifi/2, dontclear, [], multiflip); %#ok if exist('win2', 'var') [VBLTimestamp, StimulusOnsetTime, FlipTimestamp, Missed, beampos] = Screen('Flip', win2, VBLTimestamp + ifi/2, dontclear, [], multiflip); %#ok end else % BeampositionQueries based wait until target "tear-flip" position: if testdualheadsync == 1 && winfo.VBLEndline ~= -1 Screen('DrawingFinished', win, 0, 1); beampos = -1000; while abs(beampos - winRect(4)/2) > 5 beampos = Screen('GetWindowInfo', win, 1); end end % VBLTimestamp based wait until target "tear-flip" position: if testdualheadsync == 1 && winfo.VBLEndline == -1 Screen('DrawingFinished', win, 0, 1); winfo = Screen('GetWindowInfo', win); WaitSecs('UntilTime', winfo.LastVBLTime + ifi / 2); end % Flip immediately without sync to vertical retrace, do clear % backbuffer after flip for visualization purpose: VBLTimestamp = Screen('Flip', win, VBLTimestamp + ifi/2, 0, 2, multiflip); % Above flip won't return a 'beampos' in non-VSYNC'ed mode, % so we query it manually: beampos = Screen('GetWindowInfo', win, 1); % Throttle a little bit for visualization purpose: WaitSecs('YieldSecs', 0.005); end else % Just wait a bit in non-buffered case: pause(0.001); end end Screen('CloseAll'); return; catch Screen('CloseAll'); end