1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292
|
function res = CV1Test(waitframes, useRTbox)
% res = CV1Test([waitframes=90][, useRTbox=0]) - A timing test script for HMDs by use of a photometer.
%
% Needs the RTBox, and a photo-diode or such, e.g., a ColorCal-II,
% connected to the TTL trigger input of a RTBox or CRS Bits#.
%
% While measured timestamps/timing on OculusVR-1 via PsychOculusVR1 is catastrophic,
% and bad on all proprietary OpenXR runtimes on Windows (OculusVR, SteamVR) and Linux
% (SteamVR), as well as with standard Monado, we get close to perfect timestamps with
% our "metrics enhanced" Monado on Linux + Mesa Vulkan drivers with timestamping support,
% as tested with both Oculus Rift CV-1 and HTC Vive Pro Eye on AMD Raven Ridge apu with
% radv + timing extension and Monado metrics mode. Errors are sub-millisecond wrt. to
% testing with a ColorCal2 and also with a Videoswitcher in simulated HMD mode.
%
if nargin < 2 || isempty(useRTbox)
useRTbox = 0;
end
if nargin < 1 || isempty(waitframes)
waitframes = 90;
end
% Setup unified keymapping and unit color range:
PsychDefaultSetup(2);
% Select screen with highest id as Oculus output display:
screenid = max(Screen('Screens'));
% Open our fullscreen onscreen window with black background clear color:
PsychImaging('PrepareConfiguration');
% Setup the HMD to act as a regular "monoscopic" display monitor
% by displaying the same image to both eyes. We need reliable timing and
% timestamping support for this test script:
hmd = PsychVRHMD('AutoSetupHMD', 'Monoscopic', 'TimingPrecisionIsCritical TimingSupport TimestampingSupport');
if isempty(hmd)
error('No supported XR device found. Game over!');
end
win = PsychImaging('OpenWindow', screenid, [0 0 0]);
ifi = Screen('GetFlipInterval', win)
hmdinfo = PsychVRHMD('GetInfo', hmd)
% Render one view for each eye in stereoscopic mode, in an animation loop:
res.getsecs = [];
res.blackDelayMsecs = [];
res.vbl = [];
res.failFlag = [];
res.tBase = [];
res.measuredTime = [];
if useRTbox
rtbox = PsychRTBox('Open'); %, 'COM5');
% Query and print all box settings inside the returned struct 'boxinfo':
res.boxinfo = PsychRTBox('BoxInfo', rtbox);
disp(res.boxinfo);
% Enable photo-diode and TTL trigger input of box, and only those:
PsychRTBox('Disable', rtbox, 'all');
%PsychRTBox('Enable', rtbox, 'pulse');
WaitSecs(1);
% Clear receive buffers to start clean:
PsychRTBox('Stop', rtbox);
PsychRTBox('Clear', rtbox);
PsychRTBox('Start', rtbox);
if ~IsWin
% Hack: Enable async background reads to speedup box operations:
IOPort ('ConfigureSerialport', res.boxinfo.handle, 'BlockingBackgroundRead=1 StartBackgroundRead=1');
end
end
if 0 % ~useRTbox
isBad = 0;
KbReleaseWait;
de = waitframes * ifi
for pass=0:1
tBase = Screen('Flip', win);
tactual = tBase;
t1 = GetSecs;
for i=1:10
Screen('FillRect', win, 0);
DrawFormattedText(win, num2str(i), 'center', 'center', 1);
tic;
if pass == 0
dt = i * de;
tWhen = tBase + dt;
else
tWhen = tactual + de;
end
tactual = Screen('Flip', win, tWhen);
fprintf('After flip delay %f secs : Frame %i reported %f vs. requested %f. Delta %f msecs: ', toc, i, tactual, tWhen, 1000 * (tactual - tWhen));
if abs(tactual - tWhen) > 1.2 * ifi
fprintf('BAD!');
isBad = isBad + 1;
end
fprintf('\n');
if KbCheck
break;
end
end
t2 = GetSecs;
fps = i / (t2 - t1)
WaitSecs(1);
end
%KbStrokeWait;
sca;
if isBad > 0
fprintf('\nBAD timing in %i trials.\n', isBad);
else
fprintf('\nALL GOOD.\n');
end
return;
end
Screen('FillRect', win, 0);
tBase = Screen('Flip', win);
while ~KbCheck
if useRTbox
PsychRTBox('Clear', rtbox);
%PsychRTBox('EngageLightTrigger', rtbox);
PsychRTBox('EngagePulseTrigger', rtbox);
end
Screen('FillRect', win, 1);
% Draw VideoSwitcher horizontal trigger line:
Screen('DrawLine', win, [255 255 255], 0, 1, 1000, 1, 5);
res.tBase(end+1) = tBase;
res.vbl(end+1) = Screen('Flip', win, tBase + waitframes * ifi);
Screen('FillRect', win, 0);
tBase = Screen('Flip', win);
res.blackDelayMsecs(end+1) = 1000 * (tBase - res.vbl(end));
res.getsecs(end+1) = GetSecs;
% Measure real onset time:
if useRTbox
% Fetch sample immediately to preserve correspondence:
[time, event, mytstamp] = PsychRTBox('GetSecs', rtbox);
if isempty(mytstamp)
% Failed within expected time window. This probably due to
% tearing artifacts or GPU malfunction. Mark it as "tearing"
% and retry for 1 full more video refresh:
res.failFlag(end+1) = 1;
[time, event, mytstamp] = PsychRTBox('GetSecs', rtbox);
if isempty(mytstamp)
% Ok, this is fucked up. No way to recover :-(
res.failFlag(end) = 2;
res.measuredTime(end+1) = nan;
else
% Got something:
res.measuredTime(end+1) = min(mytstamp);
end
else
% Success!
res.failFlag(end+1) = 0;
%foo = mytstamp
%bar = time
%baz = event
res.measuredTime(end+1) = min(mytstamp);
end
% Only online-print for large deltas between frames, to not
% throttle stuff on that:
if ~isempty(time) && waitframes > 30
fprintf('DT Flip %f msecs. Box uncorrected %f msecs. Range %f msecs.\n', 1000 * (res.vbl(end) - res.tBase(end)), 1000 * (min(time) - res.tBase(end)), 1000 * range(time));
end
else
fprintf('DT Flip %f msecs.\n', 1000 * (res.vbl(end) - res.tBase(end)));
end
end
% Backup save for safety:
%save('VRTimingResults.mat', 'res', '-V6');
%KbStrokeWait;
sca;
close all;
figure;
if useRTbox
PsychRTBox('Stop', rtbox);
PsychRTBox('Clear', rtbox);
if ~IsWin
% Hack: Disable async background reads:
fprintf('Stopping background read op on box...\n');
IOPort ('ConfigureSerialport', res.boxinfo.handle, 'StopBackgroundRead');
IOPort ('ConfigureSerialport', res.boxinfo.handle, 'BlockingBackgroundRead=0');
fprintf('...done, now remapping timestamps.\n');
end
scanoutToPhotonOffset = 0;
if strcmpi(hmdinfo.type, 'OpenXR') && strcmpi(hmdinfo.subtype(1:6), 'Monado')
% Monado v21 has a hard-coded offset from hw present timestamp to reported
% onset timestamp of 4 msecs, so correct for that to get some
% "reference" value for simulated HMD mode on a standard display
% monitor vs. photodiode/ColorCal measurement:
scanoutToPhotonOffset = 0.004;
% Monado with a simulated HMD?
if strcmpi(hmdinfo.modelName, 'Monado: Simulated HMD')
% This is assumed to be Mario Kleiner's simulated test setup
% with Monado->GPU->HDMI/DP->Samsung C27HG70 monitor->ColorCal2.
% This monitor at native modes HDMI:1920x1080@120Hz or
% DP:2560x1440@144Hz has a reported input lag of 5 msecs from
% signal reception to pixel switching start. Correct for that
% offset to make data better readable (Note the counter-
% intuitive but correct negative sign!):
scanoutToPhotonOffset = scanoutToPhotonOffset - 0.005;
end
% Monado with a Oculus Rift CV-1?
if strcmpi(hmdinfo.modelName, 'Monado: Rift (CV1) (OpenHMD)')
% Rift CV-1 has a OLED with essentially "rolling shutter".
% Estimated to about ~8 msecs in a 11.111 msecs / 90 Hz refresh
% cycle. (Note the counter-intuitive but correct negative sign!):
scanoutToPhotonOffset = scanoutToPhotonOffset - 0.008;
end
% Monado with a HTC Vive Pro (Eye)?
if ~isempty(strfind(hmdinfo.modelName, 'Monado: HTC Vive Pro'))
% HTC Vive Pro (Eye) has a OLED with essentially "rolling shutter".
% Estimated to about ~8 msecs in a 11.111 msecs / 90 Hz refresh
% cycle. (Note the counter-intuitive but correct negative sign!):
scanoutToPhotonOffset = scanoutToPhotonOffset - 0.004;
end
end
if strcmpi(hmdinfo.type, 'OpenXR') && ~isempty(strfind(hmdinfo.subtype, 'SteamVR'))
% SteamVR/OpenXR with Monado Linux plugin? If so assume this is a
% Oculus Rift CV-1 driven via Monado, although it could be some
% other Monado supported HMD as well...
if strcmpi(hmdinfo.modelName, 'SteamVR/OpenXR : monado')
% Rift CV-1 has a OLED with essentially "rolling shutter".
% Estimated to about ~8 msecs in a 11.111 msecs / 90 Hz refresh
% cycle. (Note the counter-intuitive but correct negative sign!):
scanoutToPhotonOffset = scanoutToPhotonOffset - 0.008;
end
% SteamVR/OpenXR on MS-Windows with HTC Vive Pro Eye?
if strcmpi(hmdinfo.modelName, 'Vive OpenXR: Vive SRanipal')
% Vive Pro Eye has a 90 Hz OLED with essentially "rolling shutter".
% The measurement is 2 msecs earlier than flip mid-display ts
% with the specific photometer setup of kleinerm, so lets
% compensate for that to simplify data analysis:
scanoutToPhotonOffset = scanoutToPhotonOffset + 0.002;
end
end
res.tBase = res.tBase - scanoutToPhotonOffset;
res.vbl = res.vbl - scanoutToPhotonOffset;
% Primary save for safety:
%save('OculusTimingResults.mat', 'res', '-V6');
% Remap box timestamps to GetSecs timestamps:
res.measuredTime = PsychRTBox('BoxsecsToGetsecs', rtbox, res.measuredTime);
fprintf('...done, saving backup copy of data, then closing box.\n');
plot(1:length(res.vbl), 1000 * (res.vbl - res.tBase), 1:length(res.measuredTime), 1000 * (res.measuredTime - res.tBase));
title('Absolute measured (red) and flip (blue) relative to tbase [msecs]:')
% Close connection to box:
PsychRTBox('CloseAll');
dT = res.vbl - res.measuredTime;
dT = dT(~isnan(dT)) * 1000;
figure;
plot(1:length(dT), dT);
title('Difference flip - measured [msecs]:');
figure;
hist(dT, 100);
title('Difference histogram flip - measured [msecs]:');
ifi = ifi * 1000;
fprintf('Mean difference Flip - Measured: %f msecs [stddev %f msecs] range %f msecs [frames %f], frames %f\n', mean(dT), std(dT), range(dT), range(dT) / ifi, mean(dT) / ifi);
res.dT = dT;
else
plot(1:length(res.vbl), 1000 * (res.vbl - res.tBase));
title('Corrected data [msecs]:');
end
|