function varargout = PsychDataPixx(cmd, varargin) % PsychDataPixx - High level control driver for the VPixx - DataPixx device. % % This driver provides common high-level functionality for interaction with % the VPixx Technologies DataPixx device. The driver provides high-level % functions for basic device operations and for timestamping of visual % stimulus onset. It also provides functionality needed for the % PsychImaging() command and the Screen() image processing pipeline to % properly setup the high-precision video display modes and stereo display % modes of the DataPixx. % % The driver is dependend on the low-level MEX file Datapixx() driver to be % present. It will fail if that MEX file is missing or dysfunctional. % % Normally you will have a DataPixx connected when using this driver, but % for basic code development and testing you can call % PsychDataPixx('SetDummyMode', 1); and then run without a connected % DataPixx device. % % For control of visual stimulus display on the DataPixx, read the relevant % sections of the help of PsychImaging. That command will take care of most % of the details of visual setup. % % % Commands and their syntax: % -------------------------- % % oldmode = PsychDataPixx('SetDummyMode' [, mode]); % - Switch driver into dummy mode if 'mode' is set to 1. In dummy mode, the % code mostly operates without a DataPixx device connected. This is useful % for basic debugging of code without access to the device. The simulated % device returns more or less meaningful values, good enough for initial % code development. % % A 'mode' setting of zero enabled normal mode of operation. % You must call this command before any other command for proper operation! % % % PsychDataPixx('Open'); % - Open a connection to the device, with default settings. If the device % is already open then this will do almost nothing, otherwise the % connection is opened. % % You must call this function before all other remaining commands! % % % PsychDataPixx('Close'); % - Close a connection to the device. Multiple virtual connections can be % simultaneously open. If this function call closes the last virtual % connection, then the real physical connection to the device is closed and % the driver is reset. % % If you used PsychImaging() to make use of Datapixx graphics mode, it will % automatically close that connection once the corresponding onscreen % window is closed. As a simple rule, you only need to call the 'Close' % function if you also called the 'Open' function before. % % Call this command as last command in your script! All further % PsychDataPixx() commands will be invalid after a 'Close' call! % % % status = PsychDataPixx('GetStatus' [,newstatus]); % - Retrieve a struct 'status' with the complete driver internal state. % Optionally assign new state 'newstatus' to driver. Only assign a new % state if you *really* know what you're doing! % % % oldmode = PsychDataPixx('LogOnsetTimestamps', mode); % - Return current timestamp acquisition mode as optional return argument % 'oldmode'. Set a new mode, according to argument 'mode'. 'mode' can % be one of: % % 0 = Disable all timestamp acquisition and logging. % 1 = Log a visual stimulus onset timestamp for the next Screen('Flip') or % Screen('AsyncFlipBegin') command after this function call, then stop % logging again. This is a one-shot timestamp function. % 2 = Log all Screen('Flip') and Screen('AsyncFlipBegin') visual stimulus % onset timestamps until this function is called again with a 'mode' % setting of 1 or 0. % % Timestamping is disabled by default (mode == 0), as it incurs a bit of % computational overhead to acquire and log timestamps, typically up to 2-3 % msecs of extra time per 'Flip' command. % % The driver will log all timestamps in a buffer, which can be read out via % the following timestamp related commands. % % % [boxTime, getsecsTime, flipCount, currentFlipcount] = PsychDataPixx('GetLastOnsetTimestamp'); % - Retrieve visual stimulus onset timestamps related to last logged % "Flip". This call will only return valid values after a Flip command is % finished, ie., after a successfull call to Screen('Flip') or % Screen('AsyncFlipCheckEnd') or Screen('AsyncFlipEnd'), and only if % logging of timestamps was enabled by setting the 'LogOnsetTimestamps' % mode (see above) to a non-zero value. It always returns the most recently % acquired timestamp. % % 'boxTime' is the time in seconds of stimulus onset as measured by the % DataPixx internal clock. % 'getsecsTime' is the time in seconds of stimulus onset as measured by the % Psychtoolbox GetSecs() clock. % 'flipCount' is the "serial number" of the Screen flip command to which % the returned timestamps refer. % 'currentFlipcount' is the "serial number" of the most recent Screen flip % command. % % Computation of 'getsecsTime' will become inaccurate if you don't % frequently call PsychDataPixx('GetPreciseTime') to resynchronize the % clocks! A more accurate approach to retrieve GetSecs() style timestamps % is to use the PsychDataPixx('GetTimestampLog', 1) function to retrieve % all logged timestamps at the end of a session and apply some % high-precision remapping of timestamps. % % % [getsecs, boxsecs, confidence] = PsychDataPixx('GetPreciseTime'); % - Query both the DataPixx clock and GetSecs clock and compute which GetSecs % time 'getsecs' corresponds to which Datapixx time 'boxsecs' and how % reliable this correspondence is in 'confidence'. 'confidence' is the % margin of error between both timestamps. % This function implicitely synchronizes both clocks to get more precise % values for 'getsecstime' from calls to PsychDataPixx('GetLastOnsetTimestamp'); % The function is automatically called once at device open time. % % % log = PsychDataPixx('GetTimestampLog' [, remapPrecise = 0]); % - Fetch the full log of all acquired visual stimulus onset timestamps % into the variable 'log'. 'log' is a 3-by-n matrix for the n timestamps, % one column encodes one stimulus onset, aka one logged invocation of % Screen('Flip') et al. % % log(1,i) = i'th timestamp as measured via the Datapixx internal clock. % log(2,i) = i'th timestamp as measured via the Psychtoolbox GetSecs clock. % log(3,i) = Flip serial number of i'th sample. % % If the optional parameter 'remapPrecise' is set to 1, then an expensive % calibration procedure is used to compute very precise GetSecs() % timestamps for row 2 of the returned matrix, otherwise a cheap but % inaccurate remapping is performed. The expensive procedure can take up to % 1 second, but is highly recommended for precise timestamps. See % explanation of 'BoxsecsToGetsecs' function below for more details. You'll % typically call this function only at the end of an experiment session. % % % PsychDataPixx('ClearTimestampLog'); % - Clear the log with all currently acquired timestamps. % % % tgetsecs = PsychDataPixx('FastBoxsecsToGetsecs', t); % - Map given timestamp 't' in Datapixx clock time to Psychtoolbox GetSecs % time and return it in 'tgetsecs'. This mapping is only precise if you % call PsychDataPixx('GetPreciseTime'); frequently. For a more accurate % remapping use the following PsychDataPixx('BoxsecsToGetsecs', t); % instead, which is more time consuming to execute though. % % % [tgetsecs, sd, ratio] = PsychDataPixx('BoxsecsToGetsecs', t); % - Perform remapping of a vector of Datapixx timestamps 't', ie., % timestamps expressed in Datapixx clock time, into Psychtoolbox GetSecs % timestamps 'tgetsecs'. Return measures of accuracy of remapping. % % This must be called while the device is still open! It performs the same % calibration procedure as PsychDataPixx('GetTimestampLog', 1); and can % easily take up to 1 second of time. % % The remapping is performed by fitting a mapping function to the set of % acquired timestamp samples from both the Psychtoolbox GetSecs clock and % the DataPixx clock. The best fit function is used for mapping DataPixx % timestamps in 't' to PTB timestamps in 'tgetsecs'. 'sd' is the standard % deviation of the best-fit function wrt. to fitting error. 'ratio' is an % estimate of the clock speed difference between the host clock and the % Datapixx clock. % % % PsychDataPixx('RequestPsyncedUpdate'); % - Request an update of the DataPixx register block in sync with the next % visual stimulus onset as triggered by Screen('Flip') or % Screen('AsyncFlipBegin'). This will emit the neccessary PSYNC pixel % sequence at next flip and tell the device to apply all pending settings % on reception of that PSYNC token. % % % count = PsychDataPixx('FlipCount'); % - Return "serial number" of the last executed visual stimulus update via % one of the Screen "Flip" commands. % % % PsychDataPixx('ExecuteAtFlipCount', targetFlipCount, commandString); % - Request execution of 'commandString' via eval() function in sync with % the Screen('Flip') or Screen('AsyncFlipBegin') command which will % cause the stimulus onset with "serial number" 'targetFlipCount'. You can % ask for the current "serial number" by calling current = PsychDataPixx('FlipCount'); % Providing an empty 'targetFlipCount' means to execute at next flip. % % Datapixx related commands in 'commandString' will be scheduled for % execution on the device at next visual stimulus onset by use of the psync % mechanism. % % Not all commands are allowed inside 'commandString'! Any call to Screen() % or any OpenGL command is forbidden. Datapixx commands which could block % execution to wait for the Datapixx , e.g., Datapixx('RegWrRd') are % problematic, and the calls Datapixx('RegWrPixelSync') and % Datapixx('RegWrRdPixelSync') are strictly forbidden! % % This function is mostly used by other higher-level DataPixxToolbox % functions, e.g., for I/O (sound, digital or analog), in order to % synchronize their operation and schedules to visual stimulus updates. % % % PsychDataPixx('LoadIdentityClut', win); % - Load an identity CLUT into the device at next stimulus onset, ie., next % 'Flip' command for window 'win'. This is a pure convenience function for % that common case. Normally you'd use Screen('LoadNormalizedGammaTable', win, clut, 2); % to upload a new 'clut' table at next 'Flip' command execution. % % % oldverbosity = PsychDataPixx('Verbosity' [,verbosity]); % - Retrieve and optionally set a new level of 'verbosity' for driver debug % output. verbosity can be 0 for no output, 1 for only errors, 2 for % additionally warnings, 3 for additional info, 4 for very verbose output. % % % oldtimeout = PsychDataPixx('PsyncTimeoutFrames' [, timeoutFrames]); % -- Query and/or set timeout (in video refresh cycles) for recognition of % PSYNC codes by the device. By default, the timeout is set to the % equivalent of 5 minutes, which means: If the device is instructed to wait % for a PSYNC marker token in the video stream, but doesn't receive the % expected token within 5 minutes, it will continue processing as if the % token was received. This is used to prevent device-hangs on programming % errors or other malfunctions. The default setting of 5 minutes is very % generous. You can override this default and set an arbitrary timeout % between 1 and 65535 video refresh cycles with this function. % % % % % Normally you won't call the following functions yourself, as Psychtoolbox % automatically performs neccessary setup during calls to PsychImaging() % that are related to the DataPixx device ("help PsychImaging" for % details). % % % PsychDataPixx('SetVideoMode', mode); % -- Switch DPixx device immediately into video processing mode 'mode'. % Type "Datapixx SetVideoMode?" for a list of available 'mode' settings and % explanation of their purpose and behaviour. % % % PsychDataPixx('SetVideoHorizontalSplit', mode); % -- Switch DPixx device immediately into video split mode 'mode'. % Type "Datapixx SetVideoHorizontalSplit?" for a list of available 'mode' % settings and explanation of their purpose and behaviour. % % % PsychDataPixx('SetVideoVerticalStereo', mode); % -- Switch DPixx device immediately into vertical stereo video mode 'mode'. % Type "Datapixx SetVideoVerticalStereo?" for a list of available 'mode' % settings and explanation of their purpose and behaviour. % % % PsychDataPixx('EnableVideoScanningBacklight'); % -- Enable ViewPixx panels scanning backlight. % % % PsychDataPixx('DisableVideoScanningBacklight'); % -- Disable ViewPixx panels scanning backlight. % % % PsychDataPixx('EnableVideoStereoBlueline'); % -- Enable detection and handling of blue-line stereo sync lines by the % DataPixx. % % % PsychDataPixx('DisableVideoStereoBlueline'); % -- Disable detection and handling of blue-line stereo sync lines by the % DataPixx. % % % Internal commands: NOT FOR USE BY PURE MORTALS! % % PsychDataPixx('PerformPostWindowOpenSetup'); % -- Called by PsychImaging('OpenWindow') after opening a window on the % device. Performs all low-level setup for use of DataPixx. % % % rc = PsychDataPixx('CheckGPUSanity', window, xoffset); % -- Perform online-test of GPU identity gamma tables and DVI-D display % encoders. Try to correct problems with wrong identity gamma tables and at % least detect problems with (spatio-)temporal display dithering. Returns % rc == 0 on full success, rc > 0 on failure. % % % PsychDataPixx(-1); % -- PTB callback: Request immediate emission of a PSYNC'ed RegWrRd command % to driver if a PSYNC is pending. Otherwise noop. % % % PsychDataPixx(0); % -- PTB callback at 'Flip' time: Do whatever DPixx related work is % pending for next 'Flip', e.g., DPixx clut updates, writing of PSYNC pixel % sequences, preparation of logging of timestamps etc. % % % PsychDataPixx(1, clut); % -- Callback for PTB imaging pipeline: Ultrafast upload of given 'clut' % into the device at next bufferswap. % % % PsychDataPixx(2); % -- Callback for PTB imaging pipeline after successfull finish of a % bufferswap. Do whatever needs to be done, e.g., incrementing the % swapcounter and logging of timestamps. % % % PsychDataPixx(3); % -- Callback at time of closing the DataPixx onscreen window: Perform % whatever actions are needed at shutdown, e.g., restoring identity clut on % device, restoring default video mode and settings, closing device % connection, cleanup of data structures etc. % % % Possible improvements: % % * More options for different subfunctions? More subfunctions for % high-level stuff? Response box functionality should probably go into % another high level driver which can simply utilize the timestamp % remapping routines inside this driver? % History: % 19.12.2009 mk Written. First iteration. % 20.01.2010 mk Switch psync-command to preflip operations: Now done as % part of preflip ops, potentially long before the swap % deadline, not from the prebufferswap callback anymore. % This should improve timing precision of scheduled % stimulus onset and allows to use PSYNC mechanism also for % async-flips, because now it is thread-safe. We now make % use of the 16-bit timeout value for Psync and set it to a % default 5 minutes, but possible to override by usercode. % We also setup the device to only accept psync in the % first scanline of the display and to blank that scanline % to black. % --> No remaining limitations for use with PTB and no % visual artifacts from psync mechanism :-) % % 07.09.2010 mk Implement online diagnostic and correction for proper % function of GPU identity gamma tables and encoders. % Called as 'CheckGPUSanity' from imaging pipeline setup % code in BitsPlusPlus() for high precision display mode. % % 27.11.2011 mk Improve online correction routine for LUT's. Need to % handle GPUs with more than 256 slot LUT's sensibly. E.g., % the NVidia QuadroFX-3800 has 2048 LUT slots. % % 11.09.2012 mk Add support for stereo sync line handling, and for % scanning backlight control of the ViewPixx. % % 04.04.2015 mk Only perform DataPixx lut updates if lut actually changed, % to avoid redundant call every frame. % Need GL constant for low-level OpenGL calls. Already initialized by % PsychImaging() at first time of invocation of the driver: global GL; % Global dpx device data structure with all our variables: global dpx; % Cold init at full restart: if isempty(dpx) dpx.verbosity = 1; dpx.refcount = 0; dpx.needpsync = 0; dpx.needonsettimestamp = 0; dpx.dummymode = 0; dpx.lasttOnset = nan; dpx.lastLoggedOnsetFramecount = 0; dpx.swapcount = 0; dpx.logonsettimestamps = 0; dpx.timestampLogCount = 0; dpx.onsetTimestampLog = []; dpx.timeout = []; dpx.oldlut = []; % Allocate 2 hours worth of timestamps at 200 Hz flip rate: dpx.timestampLogPreallocSize = 2 * 3600 * 200; % Syncmethod 1 is empirically the most accurate for clock sync: dpx.syncmode = 1; % Use a maximum duration for syncClocks call of 0.5 seconds. dpx.maxDuration = 0.5; % No optimum for abort: Try the best you can! dpx.optMinwinThreshold = 0.0; % Maximum allowable minwin is 1.3 msecs, the expected maximum for an ok % but not great USB write transaction: dpx.maxMinwinThreshold = 0.0013; % Logging array for host <-> device clock sync samples: dpx.syncSamples = []; % Clockratio is assumed to be 1, until we decide to implement % clockRatio calibration routines: dpx.clkRatio = 1.0; % ID unknown until 'Open' call: dpx.ID = ''; % Datapixx already open? if doDatapixx('IsReady') % Yup: Some external client holds an open connection. Artificially % increase our refcount to prevent us from accidentally closing % that connection down when we close our connections: dpx.refcount = 1; end % Init flip-schedule to empty, ie., one free slot. It will % automatically grow if more than 1 slot is ever needed: dpx.targetswapcounts = -1; dpx.scheduledcommands = cell(1,1); % No windowhandle: dpx.window = []; end if cmd == 0 % Fast callback from within Screen - This one is issued each time % before scheduling a doublebuffer-swap on the Datapixx display device. % Done as part of preflip operations on the main thread. There can be % an arbitrarily long unknown delay between invocation of this callback % and the actual bufferswap, but there will be no graphics related % activity at all for the display between return from this callback and actual % bufferswap -- the backbuffer is in its final state, ready for display % at next bufferswap. % % The callback has to perform any work scheduled for the next stimulus % onset that is related to the Datapixx: if isempty(GL) fprintf('PsychDataPixx: FATAL ERROR! OpenGL struct GL not initialized in Screen callback!! Driver bug?!?\n'); return; end % Find all work-to-be-done items for target swapcount of next flip: worklets = find(dpx.targetswapcounts == dpx.swapcount + 1); if ~isempty(worklets) % Iterate over all work items on the todo-list: for workletidx = worklets % Execute! eval(dpx.scheduledcommands{workletidx}); % Done. Free slot in schedule: dpx.scheduledcommands{workletidx} = []; dpx.targetswapcounts(workletidx) = -1; end % Need PSYNC'ed submission of these commands to DataPixx: dpx.needpsync = 1; end % We always unconditionally draw scanline 1 in all-black, so PSYNC may % work reliably. Leave out 1st leftmost pixel on VPixx request, so to avoid % interference with the EEG single-pixel marker in the top-left corner: glRasterPos2i(1, 1); glDrawPixels(size(dpx.blackline, 2), 1, GL.RGB, GL.UNSIGNED_BYTE, dpx.blackline); % Any work to do in sync with next stimulus onset? if dpx.needpsync || dpx.needonsettimestamp % Yes! Previous calls to PsychDataPixx have emitted Datapixx() % register status update commands. These need to be applied on % reception of the next psync token as part of stimulus onset. if dpx.needonsettimestamp % Emit latch command for timestamping: doDatapixx('SetMarker'); end % Define psync code: dpx.psync = [ 255 0 0 ; 0 255 0 ; 0 0 255 ; 255 255 0 ; 255 0 255 ; 0 255 255 ; 255 255 255 ; 0 0 0 ]'; % Make code unique for this frame, so no two consecutive psync % updates ever have the same psync token: % bitand(bitshift(x, -0), 255) dpx.psync(2, 8) = mod(dpx.swapcount + 1, 256); % Blit it to top-left corner of framebuffer: glRasterPos2i(10, 1); glDrawPixels(size(dpx.psync, 2), 1, GL.RGB, GL.UNSIGNED_BYTE, uint8(dpx.psync)); % Following is asyncflip-safe, as opposed to the old implementation % which would only work for syncflips: % Emit psync - synced register writecommand: % Set device timeout for PSYNC detection to dpx.timeout video % refresh cycles: doDatapixx('RegWrPixelSync', dpx.psync, dpx.timeout); % Set "psync work to do" flag to "psync pending" dpx.needpsync = 2; end % Return control to Screen(): return; end if cmd == 1 % Fast callback from within Screen's imaging pipeline for setting a new % hardware clut in device at next doublebufferswap: newlut = varargin{1}; % Only do update if LUT actually changed since last call: if ~isequal(newlut, dpx.oldlut) % Keep track: dpx.oldlut = newlut; % Upload clut to device: doDatapixx('SetVideoClut', newlut); % Request PSYNC'ed application at next stimulus onset: dpx.needpsync = 1; if dpx.verbosity > 5 fprintf('CLUT UPDATE!\n\n'); disp(varargin{1}); fprintf('------------\n\n'); end end % Return control to Screen(): return; end if cmd == 2 % Fast callback from within Screen's imaging pipeline some arbitrary % time after successfull completion of a bufferswap, but certainly % before any other graphics related activity for the onscreen window % will happen. This is called at end of execution of Screen('Flip') or % Screen('Asyncflipend') or successfull completion of % Screen('Asyncflipcheckend'): % Reset "psync work to do" flag to "none": dpx.needpsync = 0; % Increment swap-completed count: % Could do this to query for true flip count: dpx.swapcount = varargin{1} % But this is better for now, as the above doesn't start with value 1 % at our first invocation, but is already at around 7 due to prior % flips of setup code - Avoid the hassle, keep it simple: dpx.swapcount = dpx.swapcount + 1; % Were there any onset timestamps requested for the finished swap? if dpx.needonsettimestamp % Yes. Get'em from device: doDatapixx('RegWrRd'); dpx.lasttOnset = doDatapixx('GetMarker'); dpx.lastLoggedOnsetFramecount = dpx.swapcount; % Was this a singleshot timestamp request (==1) or a continuous one % (==2) if dpx.needonsettimestamp == 1 % Single-Shot: Reset to "don't need timestamp": dpx.needonsettimestamp = 0; end % Timestamp logging requested? if dpx.logonsettimestamps dpx.timestampLogCount = dpx.timestampLogCount + 1; dpx.onsetTimestampLog(:, dpx.timestampLogCount) = [dpx.lasttOnset ; box2GetSecsTime(dpx.lasttOnset); dpx.swapcount]; end end % Return control to Screen(): return; end if cmd == 3 % Callback from within Screen at window close time as part of % Screen('Close', window), Screen('CloseAll') or auto-close on error or % Screen shutdown. Called with the OpenGL subsystem already offline: % Just call the 'ResetOnWindowClose' routine for now: cmd = 'ResetOnWindowClose'; end if strcmpi(cmd, 'ResetOnWindowClose') % Shutdown and cleanup operations when the onscreen window is closed % that displays on the Datapixx: if dpx.verbosity > 3 fprintf('PsychDataPixx: Closing device connection for this onscreen window...\n'); end % Immediately load a standard identity CLUT into the device: linear_lut = repmat(linspace(0, 1, 256)', 1, 3); doDatapixx('SetVideoClut', linear_lut); dpx.oldlut = []; % Reset videomode to pass-through: doDatapixx('SetVideoMode', 0); if doDatapixx('IsDatapixx') % Set stereomode to "auto" and horizontal split to "auto": doDatapixx('SetVideoHorizontalSplit', 2); doDatapixx('SetVideoVerticalStereo', 2); end % Disable Pixelsyncline: doDatapixx('SetVideoPixelSyncLine', 0, 0, 0); % Apply all changes immediately: doDatapixx('RegWrRd'); % Delete windowhandle: dpx.window = []; % Perform standard close op if needed: cmd = 'Close'; end if strcmpi(cmd, 'Verbosity') % Return old verbosity: varargout{1} = dpx.verbosity; if nargin >= 2 && ~isempty(varargin{1}) % Assign new verbosity: dpx.verbosity = varargin{1}; %#ok end return; end if strcmpi(cmd, 'PsyncTimeoutFrames') % Return old timeout in frames: varargout{1} = dpx.timeout; if nargin >= 2 && ~isempty(varargin{1}) % Assign new verbosity: dpx.timeout = varargin{1}; %#ok end return; end if strcmpi(cmd, 'LogOnsetTimestamps') % Switch state of automatic timestamp logging: % Return old setting: varargout{1} = dpx.logonsettimestamps; if nargin < 2 || isempty(varargin{1}) error('PsychDataPixx: LogOnsetTimestamps: You must provide the "enable" flag to subfunction!'); end % Assign new mode: dpx.logonsettimestamps = varargin{1}; %#ok % Set new prealloc size for timestamp log? if nargin >= 3 && ~isempty(varargin{2}) % Yes: Assign it. dpx.timestampLogPreallocSize = varargin{2}; % Clear current log, so it gets realloced below: dpx.onsetTimestampLog = []; dpx.timestampLogCount = 0; end % Disable logging requested? if dpx.logonsettimestamps == 0 % Yes. Reset the need timestamp flag to '0' aka off: dpx.needonsettimestamp = 0; end % Singleshot logging requested, ie., only for next Screen('Flip')? if dpx.logonsettimestamps == 1 % Yes. Set the need timestamp flag to '1' aka singleshot: dpx.needonsettimestamp = 1; end % Continouous logging requested, ie., for each Screen('Flip')? if dpx.logonsettimestamps == 2 % Yes. Set the need timestamp flag to '2' aka always: dpx.needonsettimestamp = 2; end % Allocate timestamp log if not already alloated: if dpx.logonsettimestamps && isempty(dpx.onsetTimestampLog) % Always allocate dpx.timestampLogPreallocSize number of elements: dpx.onsetTimestampLog = zeros(3, dpx.timestampLogPreallocSize); end return; end if strcmpi(cmd, 'GetPreciseTime') % Perform a clocksync between host and device, return triplet of % hosttime, corresponding DataPixx device clock time and a confidence % interval for the accuracy of clock sync. Return all three: res = syncClocks; varargout{1} = res(1); varargout{2} = res(2); varargout{3} = res(3); return; end if strcmpi(cmd, 'GetStatus') % Return copy of internal driver status structure: varargout{1} = dpx; % Optionally assign driver status from external input argument: if nargin > 1 && ~isempty(varargin{1}) dpx = varargin{1}; end return; end if strcmpi(cmd, 'ClearTimestampLog') % Clear/Delete the current timestamp log of flip onset timestamps: dpx.onsetTimestampLog = []; dpx.timestampLogCount = 0; return; end if strcmpi(cmd, 'GetTimestampLog') if nargin >= 2 && ~isempty(varargin{1}) if varargin{1} > 0 % Precision timestamps in GetSecs() time requested: % We perform a - expensive - high precision remapping of the % raw timestamps in row 1 of the current log and store the % resulting "best fit" GetSecs timestamps in row 2: [timing, sd, clockratio] = box2GetSecsTimePostHoc(dpx.onsetTimestampLog(1, 1:dpx.timestampLogCount)); dpx.onsetTimestampLog(2, 1:dpx.timestampLogCount) = timing; varargout{2} = sd; varargout{3} = clockratio; end end % Get a copy of the current timestamp log of flip onset timestamps: varargout{1} = dpx.onsetTimestampLog(:, 1:dpx.timestampLogCount); return; end if strcmpi(cmd, 'FastBoxsecsToGetsecs') if nargin < 2 || isempty(varargin{1}) error('PsychDataPixx: FastBoxsecsToGetsecs: Vector with timestamps to remap missing!'); end % Return result of fast remapping, based on last syncClocks run: varargout{1} = box2GetSecsTime(varargin{1}); return; end if strcmpi(cmd, 'BoxsecsToGetsecs') if nargin < 2 || isempty(varargin{1}) error('PsychDataPixx: BoxsecsToGetsecs: Vector with timestamps to remap missing!'); end [timing, sd, clockratio] = box2GetSecsTimePostHoc(varargin{1}); varargout{1} = timing; varargout{2} = sd; varargout{3} = clockratio; return; end if strcmpi(cmd, 'GetLastOnsetTimestamp') % Get a copy of the onset flip timestamp of the most recent flip: varargout{1} = dpx.lasttOnset; % Also provide time remapped to GetSecs time, based on last % syncClocks() run: varargout{2} = box2GetSecsTime(dpx.lasttOnset); % Also return the specific swapcount to which the timestamp refers: varargout{3} = dpx.lastLoggedOnsetFramecount; % Also return the current swapcount of last flip: varargout{4} = dpx.swapcount; return; end if strcmpi(cmd, 'RequestPsyncedUpdate') % External calling code needs a psync - synced register update at next % Screen('Flip')'ed stimulus onset: if dpx.needpsync > 1 % Ouch! Request happens after PSYNC command emission. This means % chaos! We error out: fprintf('PsychDataPixx: ERROR: RequestPsyncedUpdate called inbetween a call to\n'); fprintf('PsychDataPixx: ERROR: Screen("Drawingfinished/Flip/AsyncFlipBegin") and\n'); fprintf('PsychDataPixx: ERROR: Screen("AsyncFlipEnd/AsyncFlipCheckEnd").\n'); fprintf('PsychDataPixx: ERROR: This is not allowed! You must call outside that block\n'); fprintf('PsychDataPixx: ERROR: of code!\n\n'); error('PsychDataPixx: RequestPsyncedUpdate: Function called too late!'); end % Set flag: dpx.needpsync = 1; return; end if strcmpi(cmd, 'IsBusy') varargout{1} = dpx.needpsync > 1; return; end if strcmpi(cmd, 'FlipCount') varargout{1} = dpx.swapcount; return; end if strcmpi(cmd, 'WindowHandle') varargout{1} = dpx.window; return; end if strcmpi(cmd, 'ExecuteAtFlipCount') if nargin < 2 error('PsychDataPixx: ExecuteAtFlipCount: TargetFlipcount argument missing!'); end % Assign swapcount of next flip if no target swapcount given, or % referring to the past: if isempty(varargin{1}) || (varargin{1} <= dpx.swapcount) tcount = dpx.swapcount + 1; else tcount = varargin{1}; end if nargin < 3 || isempty(varargin{2}) error('PsychDataPixx: ExecuteAtFlipCount: CommandString argument missing!'); end % Try to find free slot in schedule, otherwise define a new one, ie., % grow schedule to accomodate additional need: freeslot = min(find(dpx.targetswapcounts == -1)); %#ok if isempty(freeslot) freeslot = length(dpx.targetswapcounts) + 1; end % Add 'tcount' as target swapcount for execution of given command: dpx.targetswapcounts(freeslot) = tcount; % Add given command at corresponding slot of schedule: dpx.scheduledcommands{freeslot} = varargin{2}; return; end if strcmpi(cmd, 'LoadIdentityClut') % Load an identity CLUT into DataPixx at next Screen('Flip'). This is % just a little convenience wrapper around 'LoadNormalizedGammaTable': if nargin < 2 || isempty(varargin{1}) error('PsychDataPixx: LoadIdentityClut: Window handle for onscreen window missing!'); end linear_lut = repmat(linspace(0, 1, 256)', 1, 3); Screen('LoadNormalizedGammaTable', varargin{1}, linear_lut, 2); return; end if strcmpi(cmd, 'Close') % Close of device connection requested. % Drop refcount: dpx.refcount = dpx.refcount - 1; % No clients left? if dpx.refcount <= 0 % Close device for real: doDatapixx('Close'); % Reset whole driver: dpx = []; end return; end if strcmpi(cmd, 'Open') % Datapixx already open? isReady = doDatapixx('IsReady'); if ~isReady % Nope: Open connection to it. isReady = doDatapixx('Open'); end % Check if connection is open and ready: if isReady % Increase our refcount to it: dpx.refcount = dpx.refcount + 1; % Assign default name to device: dpx.ID = 'DEFAULT'; % Perform initial syncClocks run: syncClocks; else warning('PsychDataPixx: Device connection not ready after init! TROUBLE AHEAD!!'); end return; end if strcmpi(cmd, 'SetDummyMode') % Set opmode, dummy without device, or the real thing. Apply immediately: varargout{1} = dpx.dummymode; if ~isempty(varargin) && ~isempty(varargin{1}) dpx.dummymode = varargin{1}; end return; end if strcmpi(cmd, 'SetVideoMode') % Set new video processing mode, apply immediately: doDatapixx('SetVideoMode', varargin{1}); doDatapixx('RegWrRd'); return; end if strcmpi(cmd, 'SetVideoHorizontalSplit') % Set new horizontal split mode, apply immediately: if doDatapixx('IsDatapixx') doDatapixx('SetVideoHorizontalSplit', varargin{1}); doDatapixx('RegWrRd'); end return; end if strcmpi(cmd, 'SetVideoVerticalStereo') % Set new vertical sync-doubling mode, apply immediately: if doDatapixx('IsDatapixx') doDatapixx('SetVideoVerticalStereo', varargin{1}); doDatapixx('RegWrRd'); end return; end % Stereo sync line control: if strcmpi(cmd, 'EnableVideoStereoBlueline') doDatapixx('EnableVideoStereoBlueline'); doDatapixx('RegWrRd'); return; end if strcmpi(cmd, 'DisableVideoStereoBlueline') doDatapixx('DisableVideoStereoBlueline'); doDatapixx('RegWrRd'); return; end % Scanning backlight control: if strcmpi(cmd, 'EnableVideoScanningBacklight') if doDatapixx('IsViewpixx') doDatapixx('EnableVideoScanningBacklight'); doDatapixx('RegWrRd'); end return; end if strcmpi(cmd, 'DisableVideoScanningBacklight') if doDatapixx('IsViewpixx') doDatapixx('DisableVideoScanningBacklight'); doDatapixx('RegWrRd'); end return; end if strcmpi(cmd, 'PerformPostWindowOpenSetup') % This is called from within PsychImaging()'s PostConfiguration % subroutine. It performs all remaining setup work to be done after the % onscreen window is open and the imaging pipeline mostly setup: % Retrieve window handle to onscreen window: win = varargin{1}; if ~isempty(dpx.window) error('PsychDataPixx: Tried to open 2nd onscreen window on device while onscreen window already open. There can be only one onscreen window open on the DataPixx display device!'); end % Store windowhandle of our window for future use: dpx.window = win; % Load the graphics hardwares gamma table with an identity mapping, % so it doesn't interfere with PSYNC code recognition: LoadIdentityClut(win); % Setup reasonable default PSYNC timeout: if isempty(dpx.timeout) % Query framerate and calculate a timeout for the device of 5 minutes % by default: dpx.timeout = Screen('Framerate', win); if dpx.timeout == 0 % OS returns unknown framerate: This means 60 Hz... dpx.timeout = 60 * 60 * 5; else % dpx.timeout fps * 60 secs/minute * 5 minutes: dpx.timeout = dpx.timeout * 60 * 5; end end % Retrieve window status: winfo = Screen('GetWindowInfo', win); % Retrieve Datapixx status: doDatapixx('RegWrRd'); dpixstatus = doDatapixx('GetVideoStatus'); % Compute an "all-black" zero pixel line for use in the imagingpipeline % callback to fill the first row of the display physical framebuffer % with all-zeros in order to facilitate robust operation of the PSYNC % mechanism: dpx.blackline = uint8(zeros(3, dpixstatus.horizontalResolution)); % Tell Datapixx to only accept PSYNC codes in the first scanline (line 0) % of the display, and to unconditionally blank that scanline to % black, so we avoid distraction visual cues from the PSYNC pixels: doDatapixx('SetVideoPixelSyncLine', 0, 1, 1); doDatapixx('RegWrRd'); % Attach a callback into the datapixx driver at the end of the % left finalizer blit chain to trigger pending preflip operations for % DataPixx device: Screen('Hookfunction', win, 'AppendMFunction', 'LeftFinalizerBlitChain', 'Final preflip callback into PsychDataPixx driver.', 'PsychDataPixx(0);'); Screen('HookFunction', win, 'Enable', 'LeftFinalizerBlitChain'); % Attach a postflip callback for things like timestamp and status % collection from the device after flip completion: % Screen('Hookfunction', win, 'AppendMFunction', 'ScreenFlipImpliedOperations', 'Postflip callback into PsychDataPixx driver.', 'PsychDataPixx(2, IMAGINGPIPE_FLIPCOUNT);'); Screen('Hookfunction', win, 'AppendMFunction', 'ScreenFlipImpliedOperations', 'Postflip callback into PsychDataPixx driver.', 'PsychDataPixx(2);'); Screen('HookFunction', win, 'Enable', 'ScreenFlipImpliedOperations'); % Attach a window close callback for Device teardown at window close time: Screen('Hookfunction', win, 'AppendMFunction', 'CloseOnscreenWindowPostGLShutdown', 'Shutdown window callback into PsychDataPixx driver.', 'PsychDataPixx(''ResetOnWindowClose'');'); Screen('HookFunction', win, 'Enable', 'CloseOnscreenWindowPostGLShutdown'); % Stereomode 2 for sync-doubling active? if winfo.StereoMode == 2 % Yes: Need to adapt the y-offset of bottom (right-eye) blitted % framebuffer to correct for the extra VBLANK interval which the % device inserts in sync doubling mode. % Find relevant slot and parameters in imaging pipeline: [slot, shaderid, blittercfg, voidptr, glsl, lutid] = Screen('HookFunction', win, 'Query', 'StereoCompositingBlit', 'StereoCompositingShaderCompressedTop'); % Shader found? if slot == -1 warning('Could not find shader plugin for stereo display inside imaging pipeline for window? Prepare for trouble!!'); end if glsl == 0 Screen('CloseAll'); error('Stereo shader is not operational for unknown reason, maybe a Psychtoolbox bug? Sorry...'); end % Build new config string for blitting of left-eye buffer in % sync-doubling mode: blittercfg = sprintf('Builtin:IdentityBlit:Offset:%i:%i:Scaling:%f:%f:Bilinear', 0, 0, 1.0, 0.5); % Insert modified thingy at former position of the old shader: Screen('HookFunction', win, 'Remove', 'StereoCompositingBlit', slot); posstring = sprintf('InsertAt%iShader', slot); Screen('Hookfunction', win, posstring, 'StereoCompositingBlit', shaderid, glsl, blittercfg, lutid); % Find relevant slot and parameters in imaging pipeline: [slot, shaderid, blittercfg, voidptr, glsl, lutid] = Screen('HookFunction', win, 'Query', 'StereoCompositingBlit', 'StereoCompositingShaderCompressedBottom'); % Shader found? if slot == -1 warning('Could not find shader plugin for stereo display inside imaging pipeline for window? Prepare for trouble!!'); end if glsl == 0 Screen('CloseAll'); error('Stereo shader is not operational for unknown reason, maybe a Psychtoolbox bug? Sorry...'); end % Corrective vertical offset for right-eye bottom view: Center it % in the middle of the VTOTAL total scanline count, after half of % all scanlines. VTOTAL includes the VBL interval of both % half-frames: voffset = ceil(dpixstatus.verticalTotal / 2) + 0; % Build new config string for blitting of right-eye buffer in % sync-doubling mode: blittercfg = sprintf('Builtin:IdentityBlit:Offset:%i:%i:Scaling:%f:%f:Bilinear', 0, voffset, 1.0, 0.5); % Insert modified thingy at former position of the old shader: Screen('HookFunction', win, 'Remove', 'StereoCompositingBlit', slot); posstring = sprintf('InsertAt%iShader', slot); Screen('Hookfunction', win, posstring, 'StereoCompositingBlit', shaderid, glsl, blittercfg, lutid); end return; end if strcmpi(cmd, 'CheckGPUSanity') % Window handle for onscreen window. The imaging pipeline output % formatter isn't active at time of this function call: win = varargin{1}; % Corrective xoffset for the framebuffer: xoffset = varargin{2}; % Execute test and optimization (tweaking) procedure which uses % onscreen window 'win' for sending test stimuli to the DataPixx, % ViewPixx or ProPixx device and use the builtin measurement functions % of that device to drive the tweaking procedure. Return success status, % 0 = Success, 1 = Failure. varargout{1} = PsychGPUTestAndTweakGammaTables(win, xoffset, 0, 0); return; end % Undispatched command? Probably an error: error('PsychDataPixx: Unknown command %s! Read the help for valid commands.', cmd); return; end % Helper function: Call into driver, or perform emulation of real calls in % dummy mode: function rc = doDatapixx(varargin) global dpx; if dpx.verbosity > 6 fprintf('Datapixx-Call: '); disp(varargin); end if dpx.dummymode if (nargin > 0) && strcmpi(varargin{1}, 'GetVideoStatus') % Dummy mode: Need to return something for this call: Make up % two fake but reasonable values for the returned statusstruct: dstat.verticalTotal = 1250; dstat.verticalResolution = 1200; dstat.horizontalResolution = 1680; rc = dstat; else % Dummy mode: Need to return something for some calls, usually a % positive number is the right response, and some want time, so % GetSecs always does the right thing ;-) rc = GetSecs; end else % Call into MEX-File driver: if nargout rc = Datapixx(varargin{:}); else rc = []; Datapixx(varargin{:}); end % Check and clear error status: err = Datapixx('GetError'); if err ~= 0 % Print error unless from 'GetVideoStatus' which is known to us and benign: if ~strcmp(varargin{1}, 'GetVideoStatus') fprintf('PsychDataPixx: Warning: DataPixx command %s returned error code %i\n', varargin{1}, err); end Datapixx('ClearError'); end end end % Clock sync routine: Synchronizes host clock (aka GetSecs time) to box % internal clock via a sampling and calibration procedure: function syncresult = syncClocks global dpx; % Default return argument to "invalid": syncresult = [nan nan nan]; % Query level of verbosity to use: verbosity = dpx.verbosity; % Perform multiple measurement trials per syncClocks run, take the best % one for final result. We use the "best" one because we have a good % criterion to find the best one. % We preallocate the sampling arrays for 250 samples at most. The % arrays will grow if 250 samples are not sufficient, at a small % performance penalty: ntrials = 250; % Any PSYNC synced write pending? if dpx.needpsync % Ohoh! Can't syncClocks then!! warning('PsychDataPixx: syncClocks: Device is waiting for a Screen(''Flip'') to finish! Can''t sync!'); return; end % Dummy mode active? if dpx.dummymode % Yep, create plausible fake result and return: td = GetSecs; syncresult = [td td 0.0001]; dpx.syncSamples(end+1, :) = syncresult; return; end % If we reach this point then we are not in emulation mode and can % access the real Datapixx MEX-File directly. This is important because % call overhead for the emulation may impair the sync accuracy we could % get. % Switch to realtime priority if not already there: oldPriority=Priority; if oldPriority < MaxPriority('GetSecs') Priority(MaxPriority('GetSecs')); end t = zeros(3,ntrials); minwin = inf; tdeadline = GetSecs + dpx.maxDuration; ic = 0; % Perform measurement trials until success criterion satisfied: % Either a sample with a maximum error 'minwin' less than desired % threshold, or maximum allowable calibration time reached: while (minwin > dpx.optMinwinThreshold) && (GetSecs < tdeadline) % Prepare clock query: Datapixx('SetMarker'); % Wait some random fraction of a millisecond. This will desync us % from the USB duty cycle and increase the chance of getting a very % small time window between scheduling, execution and acknowledge % of the send operation: WaitSecs(rand / 1000); % Take pre-Write timestamp in tpre - Sync command not emitted % before that time: tpre = GetSecs; % Emit immediate register writecommand: Datapixx('RegWr'); % Store completion time in post-write timestamp tpost: tpost = GetSecs; % We know that write command emission has happened at some time % after tpre and before tpost. This by design of the USB % standard, host controllers and operating system USB stack. This % is the only thing we can take for granted wrt. timing, so the % "time window" between those two timestamps is our window of % uncertainty about the real host time when sync started. However, % on a well working system without massive system overload one can % be reasonably confident that the real emission of the sync % command happened no more than 1 msec before tpost. That is a % soft constraint however - useful for computing the final estimate % for hosttime, but nothing to be taken 100% for granted. % Retrieve latched 'SetMarker' timestamp from box, by first reading % back the register block, then getting the latched marker value: Datapixx('RegWrRd'); tbox = Datapixx('GetMarker'); % Compute confidence interval for this sample: % For each measurement, the time window tpost - tpre defines a % worst case confidence interval for the "real" host system time % when the sync command was emitted. confidencewindow = tpost - tpre; % If the confidencewindow is greater than the maximum acceptable % window, then we reject this sample, else we accept it: if confidencewindow <= dpx.maxMinwinThreshold % Within acceptable range. Accept this sample and check if it % is the best wrt. to window size so far: if confidencewindow < minwin % Best confidencewindow so far. Update minwin, as this is one % of the abortion critera: minwin = confidencewindow; end % Increase sample index to permanently accept this sample for % final set of competitors: ic = ic + 1; % Assign values: t(1,ic) = tpre; t(2,ic) = tpost; t(3,ic) = tbox; else % Inacceptably large error confidencewindow. Reject this sample: continue; end % Next sample pass: end % Done with sampling: We have up to 'ic' valid samples, unless minwin % is still == inf. % Restore priority to state pre syncClocks: if Priority ~= oldPriority Priority(oldPriority); end % At least one sample with acceptable precision acquired? if (minwin > dpx.maxMinwinThreshold) || (ic < 1) % No, not even a single one! if verbosity > 1 fprintf('PsychDataPixx: Warning: On Box "%s", Clock sync failed due to confidence interval of best sample %f secs > allowable maximum %f secs.\n', dpx.ID, minwin, dpx.maxMinwinThreshold); fprintf('PsychDataPixx: Warning: Likely your system is massively overloaded or misconfigured!\n'); fprintf('PsychDataPixx: Warning: See the help for PsychDataPixx(''SyncConstraints'') on how to relax the constraints if you wish to do so.\n'); end % That's it: return; end % Ok, we have 'ic' > 0 samples with acceptable precision, according to % user specified constraints. Prune result array to valid samples 1 to ic: t = t(:, 1:ic); % We have three different methods for final sample selection. Each has % its favorable cases, but none of them is better by a large margin. % All can't guarantee accurate results on highly overloaded systems, % but only provide the best under given circumstances. % % Under low-load conditions, the new style methods may be slightly more % accurate under some conditions, but the difference is usually in the % sub-millisecond range, so either choice is ok. The options are mostly % here for benchmarking and stress testing of the driver... % % The default method (unless overriden by usercode or % userconfiguration) is method 1 -- New style with postwrite timestamps. % This because this method empirically seems to give best results on % low or medium load on our test set of machines and operating systems. switch dpx.syncmode case 2 % Old style method 2 - Middle of minwin confidence window: % Select sample with smallest confidence interval [tpre; tpost]: [mintdiff, idx] = min(t(2,:) - t(1,:)); % Host time corresponds to midpoint of the confidence interval, % assuming a uniform distribution of likelyhood of the true write % time in the interval [tpre ; tpost]: hosttime = (t(1,idx) + t(2,idx)) / 2; case 1 % New style method 1 - Postwrite timestamps: % Choose the most accurate sample from the set of candidates. This is % the sample with the smallest difference between the postwrite % timestamp and the associated box timestamp, ie., with the smallest % offset between postwrite host clock time and box clock time at % receive of sync command. The reasoning behind this goes like this: % % 1) The time offset between host clock and box clock is a constant - % at least within a significant multi-second time interval between % successive syncClocks calls (due to only small clock drift), but % certainly within a syncClocks run of a few hundred milliseconds % (error due to clock drift in this interval is negligible). % % 2) Computed box clock time t(3,:) is "close to perfect", as this % timestamp is taken by box microprocessor and firmware with a very % small and basically constant delay after sync token receive, ie., % write completion. (Maximum theoretical error is smaller than 0.1 msecs). % % 3) The correct and optimal clock offset between host and box would be % tdiff = tsend - t(3,:) iff tsend would be host time at true write % completion. % % 4) The measured host time at write completion t(2,:) is always later % (and therefore numerically greater) than the true host time tsend at % write completion due to an unknown, random, greater than zero delay % tdelta, i.e., t(2,:) = tsend + tdelta, tdelta > 0. tdelta is the sum % of: % % a) The unknown delay of up to 1 msec between USB write-URB completion % by the USB host controller (which would be the real completion time % tsend) and detection of completion due to USB IOC (Interrupt-On- % Completion) due to invocation of the host controllers hardware % interrupt handler and host controller schedule scan and URB % retirement inside the interrupt handler. % % b) Random (and theoretically unbounded) scheduling delay / execution % delay between status update of the serial port data structures by the % interrupt handler and detection of write completion + timestamping by % the IOPort driver in polling mode, or scheduling delay between % wakeup-operation caused by the interrupt handler and start of % execution of the timestamping in the IOPort driver in blocking mode. % % The syncClocks error is therefore directly proportional to the size % of tdelta. Therefore: % % tdiff(:) = t(2,:) - t(3,:) by definition of clock offset host vs. box. % t(2,:) = tsend(:) + tdelta(:) by unknown scheduling/execution noise tdelta. % % It follows that by above definitions: % % tdiff(:) = tsend(:) - t(3,:) + tdelta(:); % % --> As we defined tsend(:) to be the unknown, but perfect and % noise-free, true send timestamp, and t(3,:) to be the perfect receive % timestamp by the box, it follows that by selecting the sample 'idx' % with the minimal tdiff(idx) from the set tdiff(:), we will select the % sample with the unknown, but minimal tdelta(idx). As tdelta accounts % for all the remaining calibration error, minimizing tdelta will % maximize the accuracy of the clock sync. % % ==> Select sample with minimum t(2,:) - t(3,:) as final best result: [mintdiff, idx] = min(t(2,:) - t(3,:)); % mintdiff is our best estimate of clock offset host vs. box, and % t(:,idx) is the associated best sample. Unfortunately there isn't any % way to compute the exact residual calibration error tdelta(idx). The % only thing we know is that the error is bounded by the length of the % associated 'minwin' confidence interval of this sample, so we will % return 'minwin' as an upper bound on the calibration error. As % 'minwin' was used as a threshold in the sample loop for outlier % rejection, we can be certain that our estimate carries no greater % error than 'dpx.maxMinwinThreshold'. % Extract all relevant values for the final sample: % Host time corresponds to tpost write timestamp, which should be as % close as possible to real host send timestamp: hosttime = t(2,idx); case 0 % New style method 0 - Prewrite timestamps: % Choose the most accurate sample from the set of candidates. % This is the sample with the biggest difference between the % prewrite timestamp and the associated box timestamp, ie., % with the biggest offset between prewrite host clock time and % box clock time at receive of sync command. The reasoning % behind this goes like this: % % 1) The time offset between host clock and box clock is a constant - % at least within a significant multi-second time interval between % successive syncClocks calls (due to only small clock drift), but % certainly within a syncClocks run of a few hundred milliseconds % (error due to clock drift in this interval is negligible). % % 2) Computed box clock time t(3,:) is "close to perfect", as this % timestamp is taken by box microprocessor and firmware with a very % small and basically constant delay after sync token receive, ie., % write completion. (Maximum theoretical error is smaller than 0.1 msecs). % % 3) The correct and optimal clock offset between host and box would be % tdiff = tsend - t(3,:) where tsend would be unknown host time % at true write completion. % % 4) The measured host time before write submission t(1,:) is % always earlier % (and therefore numerically smaller) than the true host time tsend at % write completion due to an unknown, random, greater than zero delay % tdelta, i.e., t(1,:) = tsend - tdelta, tdelta > 0. tdelta is the sum % of: % % a) The unknown delay of up to 1 msec between USB write-URB completion % by the USB host controller (which would be the real completion time % tsend) and detection of completion due to USB IOC (Interrupt-On- % Completion) due to invocation of the host controllers hardware % interrupt handler and host controller schedule scan and URB % retirement inside the interrupt handler. % % b) Random (and theoretically unbounded) scheduling delay / execution % delay between status update of the serial port data structures by the % interrupt handler and detection of write completion + timestamping by % the IOPort driver in polling mode, or scheduling delay between % wakeup-operation caused by the interrupt handler and start of % execution of the timestamping in the IOPort driver in blocking mode. % % The syncClocks error is therefore directly proportional to the size % of tdelta. Therefore: % % tdiff(:) = t(1,:) - t(3,:) by definition of clock offset host vs. box. % t(1,:) = tsend(:) - tdelta(:) by unknown execution noise tdelta. % % It follows that by above definitions: % % tdiff(:) = tsend(:) - t(3,:) - tdelta(:); % % --> As we defined tsend(:) to be the unknown, but perfect and % noise-free, true send timestamp, and t(3,:) to be the perfect receive % timestamp by the box, it follows that by selecting the sample 'idx' % with the maximal tdiff(idx) from the set tdiff(:), we will select the % sample with the unknown, but minimal tdelta(idx). As tdelta accounts % for all the remaining calibration error, minimizing tdelta will % maximize the accuracy of the clock sync. % % ==> Select sample with maximum t(1,:) - t(3,:) as final best result: [maxtdiff, idx] = max(t(1,:) - t(3,:)); % maxtdiff is our best estimate of clock offset host vs. box, and % t(:,idx) is the associated best sample. Unfortunately there isn't any % way to compute the exact residual calibration error tdelta(idx). The % only thing we know is that the error is bounded by the length of the % associated 'minwin' confidence interval of this sample, so we will % return 'minwin' as an upper bound on the calibration error. As % 'minwin' was used as a threshold in the sample loop for outlier % rejection, we can be certain that our estimate carries no greater % error than 'dpx.maxMinwinThreshold'. % Extract all relevant values for the final sample: % Host time corresponds to tpre write timestamp, which should be as % close as possible to real host send timestamp: hosttime = t(1,idx); otherwise error('PsychDataPixx: syncClocks: Unknown timestamping method provided. This is a driverbug!!'); end % Box timers time taken "as is": boxtime = t(3,idx); % Recalculate upper bound on worst case error 'minwin' from this best % samples tpost - tpre: minwin = t(2,idx) - t(1,idx); if verbosity > 3 fprintf('PsychDataPixx: ClockSync(%i) on box "%s": Got %i valid samples, maxconfidence interval = %f msecs, winner interval %f msecs.\n', dpx.syncmode, dpx.ID, ic, 1000 * dpx.maxMinwinThreshold, 1000 * minwin); if dpx.syncmode == 1 fprintf('PsychDataPixx: Confidence windows in interval [%f - %f] msecs. Range of clock offset variation: %f msecs.\n', 1000 * min(t(2,:)-t(1,:)), 1000 * max(t(2,:)-t(1,:)), 1000 * psychrange(t(2,:) - t(3,:))); else fprintf('PsychDataPixx: Confidence windows in interval [%f - %f] msecs. Range of clock offset variation: %f msecs.\n', 1000 * min(t(2,:)-t(1,:)), 1000 * max(t(2,:)-t(1,:)), 1000 * psychrange(t(1,:) - t(3,:))); end end % Assign (host,box,confidence) sample to sync struct: syncresult = [hosttime, boxtime, minwin]; % Also store the sample in the syncSamples history: dpx.syncSamples(end+1, :) = syncresult; % SyncClocks run finished: end % Map box timestamps to host clock (aka GetSecs) timestamps, based on clock % sync results from syncClocks, and clock drift correction from clock ratio % calibration: function timing = box2GetSecsTime(timing) global dpx; thostbase = dpx.syncSamples(end, 1); tboxbase = dpx.syncSamples(end, 2); % Compute delta in box seconds from time when calibration was % done, wrt. box timer, then multiply with clockRatio % conversion factor to translate that delta into a delta wrt. % host clock, so timing will be the delta in host clock seconds % wrt. calibration time: timing = (timing - tboxbase) * dpx.clkRatio; % Add basetime on host - this should give us correctly mapped % time in the GetSecs timebase of the computer: timing = timing + thostbase; end % Map box timestamps to host clock (aka GetSecs) timestamps, based on clock % sync results from all syncClocks samples from a session: function [timing, sd, clockratio] = box2GetSecsTimePostHoc(timing) global dpx; % Check if the latest syncClocks sample is older than 30 seconds. If % so, then we acquire a new final sample. We also resample if the last % sample is of too low accuracy, or if there are less than 2 samples in % total, as the fitting procedure needs at least 2 samples to work: while (size(dpx.syncSamples, 1) < 2) || ... ((GetSecs - dpx.syncSamples(end, 1)) > 30) || ... (dpx.syncSamples(end, 3) > dpx.maxMinwinThreshold) % Perform a syncClocks to get a fresh sample to finalize the sampleset: syncClocks; end % Extract samples for line fit: tbox = dpx.syncSamples(:, 2); thost = dpx.syncSamples(:, 1); % MK: Change of strategy: We always use polyfit() instead of sometimes % regress() or robustfit() for the following reasons: % % 1. Strategy switching may introduce artifacts / non-predictable % behaviour into the results. % % 2. A simple polyfit() is better understood than the more advanced % robustfit() with its tons of tunable parameters and strategies, for % which i don't know which are appropriate for our data. % % 3. robustfit() and regress() require the Matlab statistics toolbox % and therefore won't work on a plain standard Matlab installation, % whereas polyfit() is part of default Matlab and Octave. % % The old code is left here for documentation purpose: % % % More than 2 samples available? % if 0 && length(tbox) > 2 % % Octave and older versions of Matlab don't have 'robustfit', % % so we fall back to 'regress' if this function is lacking: % if exist('robustfit') %#ok % [coef st]=robustfit(tbox,thost); % fit a line % sd=st.robust_s; % stddev. in seconds. % else % coef =regress(thost, [ones(size(thost,1), 1), tbox ]); % fit a line % sd=0; % stddev. undefined with regress(). % end % clockratio = coef(2); % clockbias = coef(1); % else % Use polyfit, i.e., a least squares fit of a polynomial of either % degree 1 (purely linear), or degree 2 (quadratic). We choose a purely % linear fit if only 2 samples are available - the best we can do. If % more than 2 samples are available, we use a quadratic fit to account % for small non-linearities in the clock hardware over long intervals: [coef, st, mu] = polyfit(tbox, thost, min(length(tbox)-1, 2)); clockratio = coef(1); clockbias = coef(2); %#ok sd.norm = st.normr; % end % Ok, got mapping equation getsecst = timing * clockratio + clockbias; % Apply it to our input timestamps: % % timing = timing * clockratio + clockbias; % % % Actually, we use polyval() to evaluate the equation. For linear case, % this is the same as above equation, but it conveniently also handles % the quadratic case and provides some margins on mapping error: if length(tbox) > 3 [timing, sd.delta] = polyval(coef, timing, st, mu); else % sd.delta undefined for less than 4 elements in timing: sd.delta = repmat(Inf, size(timing)); timing = polyval(coef, timing, st, mu); end if dpx.verbosity > 3 fprintf('PsychDataPixx: In post-hoc box->host mapping: Coefficients of mapping polynom (in decreasing order) are: '); disp(coef); fprintf('\n\n'); end % Ready. end