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