function varargout = PsychHDR(cmd, varargin)
% PsychHDR - Support and control stimulus display to HDR "High dynamic range" displays.
%
% This driver provides both, the setup code for the HDR main setup command
% PsychImaging('AddTask', 'General', 'EnableHDR'), to get HDR display enabled
% for a specific Psychtoolbox onscreen window on a specific HDR display, and
% utility subfunctions for user-scripts to call after initial setup, to modify
% HDR display behaviour.
%
% HDR currently only works on graphics card + operating system combinations which
% support both the OpenGL and Vulkan rendering api's and also efficient OpenGL-Vulkan
% interoperation. Additionally, the Vulkan driver, graphics card, and your display
% device must support at least the HDR-10 standard for high dynamic range display.
%
% As of April 2021, these graphics cards would be suitable:
%
% - Modern AMD (RX 500 "Polaris" and later recommended) and NVidia (GeForce 1000
%   "Pascal" and later recommended) graphics cards under a Microsoft Windows-10
%   operating system, which is up to date for the year 2021.
%
% - Modern AMD graphics cards (like above) under modern GNU/Linux (Ubuntu 18.04.4-LTS
%   at a minimum (untested!), or better Ubuntu 20.04-LTS and later recommended), with
%   the AMD open-source Vulkan driver "amdvlk". Install driver release 2020-Q3.5 from
%   September 2020, which was tested, or any later versions. The following webpage has
%   amdvlk download and installation instructions:
%
%   https://github.com/GPUOpen-Drivers/AMDVLK/releases
%
% - Some Apple Mac computers, e.g., the MacBookPro 2017 15 inch Retina with AMD
%   graphics, under macOS 10.15.4 Catalina or later, do now have experimental and
%   limited HDR support. This has been tested with macOS 10.15.7 Catalina final,
%   on the MBP 2017 with AMD Radeon Pro 560 in a limited way on an external HDR-10
%   monitor, connected via USB-C to DisplayPort adapter. Precision of content
%   reproduction during leight testing was worse than on Linux and Windows. The
%   presentation timing was awful and unreliable, and performance was bad. Flexibility
%   and functionality was very limited in comparison to Windows-10, and even more so
%   compared to Linux. Querying HDR display properties from the HDR display is not
%   supported due to macOS limitations, and high performance HDR movie playback is
%   completely missing due to severe deficiencies of Apple's OpenGL implementation.
%   This uses the Apple Metal EDR "Extended dynamic range" support in macOS. Note
%   that Vulkan and HDR support on macOS is considered alpha quality at best, and
%   we do not provide any support for this feature. As always, if you care about
%   the quality of your results, use preferrably Linux, or Windows-10 instead.
%
%   You need at least MoltenVK version 1.1.4 and LunarG Vulkan SDK 1.2.182.0 from
%   5th July 2021 or later. MoltenVK v1.1.5 or later is recommended at this time.
%
%   Download link for the MoltenVK open-source "Vulkan on Metal" driver:
%
%   https://sdk.lunarg.com/sdk/download/latest/mac/vulkan-sdk.dmg
%   Overview on: https://vulkan.lunarg.com/sdk/home
%
%
% HDR functionality is demonstrated in multiple demos:
%
% SimpleHDRDemo.m as a simple starter for basic image display and rendering.
% HDRViewer.m as a more fancy static image viewer.
% HDRTest.m for testing HDR reproduction with a colorimeter supported by Psychtoolbox.
% MinimalisticOpenGLDemo.m with the optional 'hdr' parameter set to 1 for most basic OpenGL rendering in HDR.
% PlayMoviesDemo.m with the optional 'hdr' parameter set to 1 for playback of HDR movies.
%
% Useful helper functions beyond PsychImaging('AddTask', 'General', 'EnableHDR');
% for basic HDR setup and configuration, and PsychHDR() for tweaking, are
% the HDRRead() command for reading some HDR image file formats, e.g.,
% Radiance .hdr or OpenEXR .exr, ComputeHDRStaticMetadataType1ContentLightLevels()
% for computing HDR static metadata type one for an image or stack of
% images, and ConvertRGBSourceToRGBTargetColorSpace() for converting images
% from a source color space / gamut to a destination color space / gamut.
%
%
% Most often you won't call this function directly, but Psychtoolbox will call
% it appropriately from the PsychImaging() function. Read relevant sections
% related to 'EnableHDR' in 'help PsychImaging' first, before venturing into the
% subfunctions offered by this function.
%
% User accessible commands and their meaning:
% -------------------------------------------
%
% oldVerbosity = PsychHDR('Verbosity' [, verbosity]);
% - Returns and optionally sets level of 'verbosity' for driver debug output.
%   'verbosity' = New level of verbosity: 0 = Silent, 1 = Errors only, 2 = Warnings,
%   3 = Info, 4 = Debug, 5 -- ... = Higher debug levels.
%
%
% isSupported = PsychHDR('Supported');
% - Returns if HDR visual stimulus display is in principle supported on this setup.
%   1 = Supported, 0 = No driver, hardware or operating system support.
%
%
% hdrProperties = PsychHDR('GetHDRProperties', window);
% - Returns hdrProperties, a struct with information about the HDR display properties
%   of the onscreen window 'window'. Most importantly, it returns information about the
%   native color gamut of the HDR display device and its brightness range. The following
%   fields in hdrProperties exist at least:
%
%   'Valid'   Are the HDR display properties valid? 0 = No, as no data could be
%             queried from the display, 1 = Yes, data has been queried from display and is
%             supposed to represent actual display HDR capabilities and properties.
%
%   'HDRMode' 0 = None (SDR), 1 = Basic HDR-10 enabled with BT-2020 color space, 10
%             bpc color precision, and ST-2084 PQ Perceptual Quantizer EOTF.
%
%   'LocalDimmingControl' 0 = No, 1 = Local dimming control supported.
%
%   'MinLuminance' Minimum supported luminance in nits.
%
%   'MaxLuminance' Maximum supported peak / burst luminance in nits. This is often
%                  only achievable for a small area of the display surface over extended
%                  periods of time, e.g., only for 10% of the pixel area. The full display
%                  may only be able to sustain that luminance for a few seconds.
%
%   'MaxFrameAverageLightLevel' Maximum sustainable supported luminance in nits.
%
%   'MaxContentLightLevel' Maximum desired content light level in nits.
%
%   'ColorGamut' A 2-by-4 matrix encoding the CIE-1931 2D chromaticity coordinates of the
%                red, green, and blue color primaries in columns 1, 2 and 3, and
%                the white-point in column 4.
%
%
% oldlocalDimmmingEnable = PsychHDR('HDRLocalDimming', window [, localDimmmingEnable]);
% - Return and/or set HDR local backlight dimming enable setting for display
%   associated with window 'window'.
%
%   This function returns the currently set HDR local backlight dimming setting for
%   dynamic contrast control on the HDR display monitor associated with window
%   'window'.
%
%   Return argument 'oldlocalDimmmingEnable' is the current setting.
%   The optional 'localDimmingEnable' is the new setting to apply. This will only
%   work if the display and display driver supports the VK_AMD_display_native_hdr
%   Vulkan extension. As of July 2020, only "AMD FreeSync2 HDR compliant" HDR
%   monitors under Windows-10 with an AMD graphics card in fullscreen mode support
%   this.
%
%   The hdrProperties = PsychHDR('GetHDRProperties', window); function allows to query
%   if the current setup supports this function. Please note that this function will
%   always report the selected 'localDimmingEnable' setting made by your code on a
%   nominally supported setup. There is no way for our driver to detect if the mode
%   change on the display was accepted, as the operating system provides no feedback
%   about this. At least one model of "compatible" monitor is already known to
%   ignore this setting, unknown if this is an AMD driver bug or monitor firmware
%   bug. Tread carefully! Manual control of this setting on the monitor itself may
%   be the safer choice.
%
%
% a) oldHdrMetadata = PsychHDR('HDRMetadata', window, metadataType [, maxFrameAverageLightLevel][, maxContentLightLevel][, minLuminance][, maxLuminance][, colorGamut]);
% b) oldHdrMetadata = PsychHDR('HDRMetadata', window [, newHdrMetadata]);
% - Return and/or set HDR metadata for presentation window 'window'.
%
%   This function returns the currently defined HDR metadata that is sent
%   to the HDR display associated with the window 'window'. It optionally
%   allows to define new HDR metadata to send to the display, starting with
%   the next presented visual stimulus image, ie. the successfull completion
%   of the next Screen('Flip').
%
%   The mandatory parameter 'metadataType' specifies the format in which
%   HDR metadata should be returned or set.
%
%   Return argument 'oldHdrMetadata' is a struct with information about the
%   current metadata. Optionally you can define new metadata to be sent to
%   the display in one of the two formats a) or b) shown above: Either a)
%   as separate parameters, or b) as a 'newHdrMetadata' struct. If you use
%   the separate parameters format a) and specify any new settings, but
%   omit some of the optional parameter values or leave them [] empty, then
%   those values will remain at their current / old values. If you use the
%   struct format b), then you must pass in a non-empty 'newHdrMetadata'
%   struct which contains the same fields as the struct returned in
%   'oldHdrMetadata', with all fields for the given 'MetadataType' properly
%   defined, otherwise an error will occur. Format b) is useful as a
%   convenience for querying 'oldHdrMetadata', then modifying some of its
%   values, and then passing this modified variant back in as
%   'newHdrMetadata'. For HDR movie playback, Screen('OpenMovie') also
%   optionally returns a suitable hdrStaticMetaData struct in the right
%   format for passing it as 'newHdrMetadata'.
%
%   The following fields in the struct and as new settings are defined:
%
%   'MetadataType' Type of metadata to send or query. Currently only a
%   value of 0 is supported, which defines "Static HDR metadata type 1", as
%   used and specified by the HDR standards CTA-861-3 / CTA-861-G (content
%   light levels) and SMPTE 2086 (mastering display color properties, ie.
%   color volume).
%
%   The content light level properties 'MaxFrameAverageLightLevel' and
%   'MaxContentLightLevel' default to 0 at startup, which signals to the
%   display device that they are unknown, a reasonable assumption for
%   dynamically rendered content with prior unknown maximum values over a
%   whole session.
%
%   'MaxFrameAverageLightLevel' Maximum frame average light level of the visual
%   content in nits, range 0 - 65535 nits.
%
%   'MaxContentLightLevel' Maximum light level of the visual content in
%   nits, range 0 - 65535 nits.
%
%   The following mastering display properties (~ color volume) default to
%   the properties of the connected HDR display monitor for presentation, if
%   they could be queried from the connected monitor. It is advisable to
%   override them with the real properties of the mastering display, e.g.,
%   for properly mastered movie content or image files where this data may
%   be available.
%
%   'MinLuminance' Minimum supported luminance of the mastering display in
%   nits, range 0 - 6.5535 nits.
%
%   'MaxLuminance' Maximum supported luminance of the mastering display in
%   nits, range 0 - 65535 nits.
%
%   'ColorGamut' A 2-by-4 matrix encoding the CIE-1931 2D chromaticity
%   coordinates of the red, green, and blue color primaries in columns 1,
%   2, and 3, and the location of the white-point in column 4. This defines
%   the color space and gamut in which the visual content was produced.
%

% History:
% 10-Jul-2020   mk  Written.

global GL;
persistent verbosity;
persistent targetUUIDs;
persistent oldHDRMeta;
persistent oldHDRProperties;

if nargin < 1 || isempty(cmd)
  help PsychHDR;
  return;
end

if isempty(verbosity)
    verbosity = PsychVulkan('Verbosity');
end

% oldVerbosity = PsychHDR('Verbosity' [, verbosity]);
if strcmpi(cmd, 'Verbosity')
    varargout{1} = verbosity;

    if (~isempty(varargin)) && ~isempty(varargin{1})
        verbosity = varargin{1};
        PsychVulkan('Verbosity', verbosity);
    end

    return;
end

% isSupported = PsychHDR('Supported');
if strcmpi(cmd, 'Supported')
    try
        % Enumerate all Vulkan gpu's, try to find if at least one
        % supports HDR. Return 1 == Supported if so, 0 == Unsupported otherwise.
        varargout{1} = 0;
        if PsychVulkan('Supported')
            devices = PsychVulkan('GetDevices');
            for d = devices
                if d.SupportsHDR
                    varargout{1} = 1;
                    break;
                end
            end
        end
    catch
        lasterror('reset'); %#ok<*LERR>
        varargout{1} = 0;
    end

    return;
end

% hdrImagingModeFlags = PsychHDR('GetClientImagingParameters', hdrArguments);
if strcmpi(cmd, 'GetClientImagingParameters')
    % Parse caller provided EnableHDR task parameters into an easily usable
    % hdrArgs settings struct, while validating user input:
    hdrArgs = parseHDRArguments(varargin{1}); %#ok<NASGU>

    % We need the final output formatting imaging pipeline chain to attach our
    % HDR post-processing shader, at a minimum for OETF mapping, e.g., PQ. Also
    % mark the onscreen window as HDR window, so texture precision for 'MakeTexture'
    % et al. can be adapted, and the movie playback engine can decode/remap video
    % frames from movies accordingly:
    hdrImagingModeFlags = mor(kPsychNeedOutputConversion, kPsychNeedHDRWindow);

    varargout{1} = hdrImagingModeFlags;
    return;
end

% [useVulkan, vulkanHDRMode, vulkanColorPrecision, vulkanColorSpace, vulkanColorFormat] = PsychHDR('GetVulkanHDRParameters', win, hdrArguments);
if strcmpi(cmd, 'GetVulkanHDRParameters')
    % Onscreen window handle:
    win = varargin{1}; %#ok<NASGU>

    % Parse caller provided EnableHDR task parameters into an easily usable
    % hdrArgs settings struct, while validating user input:
    hdrArgs = parseHDRArguments(varargin{2});

    % useVulkan = 1, as we currently only do HDR via Vulkan/WSI:
    varargout{1} = 1;

    % vulkanHDRMode:
    varargout{2} = hdrArgs.hdrMode;

    % vulkanColorPrecision = 0 -- Derive from vulkanHDRMode by default:
    varargout{3} = 0;

    % vulkanColorSpace:
    varargout{4} = hdrArgs.colorSpace;

    % vulkanColorFormat = 0 -- Derive from vulkanHDRMode by default:
    varargout{5} = 0;

    return;
end

% [hdrShader, hdrShaderIdString] = PsychHDR('PerformPostWindowOpenSetup', win, hdrArguments, icmShader, icmShaderIdString);
if strcmpi(cmd, 'PerformPostWindowOpenSetup')
    % Setup operations after Screen's PTB onscreen window is opened, and OpenGL and
    % the imaging pipeline are brought up, as well as the corresponding Vulkan/WSI
    % window. Needs to hook up the imaging pipeline to our HDR post-processing shaders.

    % Must have global GL constants:
    if isempty(GL)
        varargout{1} = 0;
        warning('PTB internal error in PsychHDR: GL struct not initialized?!?');
        return;
    end

    % Psychtoolbox Screen onscreen window handle:
    win = varargin{1};

    % Parse caller provided EnableHDR task parameters into an easily usable
    % hdrArgs settings struct, while validating user input:
    hdrArgs = parseHDRArguments(varargin{2});

    % hdrMode: 0 = SDR, 1 = HDR-10:
    hdrMode = hdrArgs.hdrMode;

    % Special static HDR stereo hack on Linux/X11 enabled?
    if hdrArgs.dummy ~= 2
        % No, standard case: Retrieve actual parameters of Vulkan window:
        hdrWindowProps = PsychVulkan('GetHDRProperties', win);
    else
        % Yes: We don't have an actual Vulkan window available at this point,
        % only the regular Screen() onscreen window 'win', spanning all displays
        % of a HDR setup. Therefore we can not query hdrWindowProps and need to
        % make them up in a way compatible with this special purpose hack.
        % Assign something that triggers the 'otherwise' case below:
        hdrWindowProps.ColorSpace = 1;
    end

    % hdrWindowProps provides us with the actual output colorspace, framebuffer
    % precision, and if it is unorm or float.

    % TODO: Potentially adapt type of shader to use or add colorspace conversion
    % if source and target colorspace are different...

    % Get ICM color correction shader and shader name to use / attach to our HDR shader program:
    icmshader = varargin{3};
    icmstring = varargin{4};

    % HDR emulation dummy mode requested?
    if hdrMode == 0 && hdrArgs.dummy
        % For purpose of setup code in this subfunction, treat it as hdrMode 1:
        hdrMode = 1;
    end

    switch (hdrMode)
        case 1
            % HDR-10: At least 10 bpc color depth, BT-2020/Rec-2020 color space,
            % SMPTE ST-2084 PQ "Perceptual Quantizer" OETF encoding by us.

            % Select scalefactor from user framebuffer to shader input:
            % Input scaling from input unit to 0 - 1 range, where 1 = 10000 nits:
            scalefactor = hdrArgs.inputScalefactor;

            % Set scaling factors for mapping SDR or HDR image and movie content into our HDR linear color space,
            % using the correct unit of luminance. Used by movie playback and Screen('MakeTexture') among others:
            Screen('HookFunction', win, 'SetHDRScalingFactors', [], hdrArgs.contentSDRToHDRFactor, 1 / scalefactor);

            % Set color gamut of HDR color space:
            Screen('HookFunction', win, 'WindowColorGamut', [], hdrArgs.colorGamut);

            % Load PQ shader:
            oetfshader = LoadGLSLProgramFromFiles('HDR10-PQ_Shader', [], icmshader);

            % Define mapping from BT-2020 colorspace to output colorspace:
            switch (hdrWindowProps.ColorSpace)
                case 1000104002 % VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT
                    % scRGB colorspace with linear encoding 0-10000 nits ==> 0-125:
                    eotfName = 'scRGB-Linear';
                    doPQEncode = 0;

                    % macOS as usual needs special treatment...
                    if IsOSX
                        % As of MoltenVK 1.1.3, HDRMetaData setup via vkSetHDRMetadataEXT()
                        % hard-codes a Metal surface CAEDRMetadata opticalOutputScale
                        % factor of 1.0 nits, which means that provided pixel
                        % color values are supposed to be interpreted as
                        % unit of nits. Therefore we must scale by 10000
                        % for a mapping from [0; 1] input to [0; 10000 nits]:
                        scalefactor = scalefactor * 10000;
                    else
                        % Linear encoding is simply multiplication by 125:
                        scalefactor = scalefactor * 125;
                    end

                    % Need CSC from BT-2020 to scRGB:
                    doCSC = 1;

                    % scRGB color gamut with D65 white point and identical color gamut to sRGB and BT-709:
                    gamutRec709 = [[0.64 ; 0.33], [0.30 ; 0.60], [0.15 ; 0.06], [0.31271 ; 0.32902]];
                    MCSC = ConvertRGBSourceToRGBTargetColorSpace(hdrArgs.colorGamut, gamutRec709);

                case 1000213000 % VK_COLOR_SPACE_DISPLAY_NATIVE_AMD
                    if hdrWindowProps.ColorFormat == 97 % == VK_FORMAT_R16G16B16A16_SFLOAT
                        % FS2 scRGB mode: Implemented, but not tested/validated, possibly incomplete!
                        %
                        % scRGB colorspace with linear encoding 0-10000
                        % nits ==> 0-125, more specifically from display
                        % minimum luminance / 80 to display maximum
                        % luminance / 80, but for a max 10k nits display
                        % that would translate to 0 - 125:
                        eotfName = 'FS2-scRGB-Linear';
                        doPQEncode = 0;

                        % Linear encoding is simply multiplication by 125:
                        scalefactor = scalefactor * 125;

                        % Need CSC from BT-2020 to scRGB:
                        doCSC = 1;

                        % scRGB color gamut with D65 white point and identical color gamut to sRGB and BT-709:
                        gamutRec709 = [[0.64 ; 0.33], [0.30 ; 0.60], [0.15 ; 0.06], [0.31271 ; 0.32902]];
                        MCSC = ConvertRGBSourceToRGBTargetColorSpace(hdrArgs.colorGamut, gamutRec709);
                    else
                        % FS2 Gamma 2.2 mode: TODO!
                        error('PsychHDR: FreeSync2 Gamma 2.2 HDR mode is not yet implemented!');
                    end

                    warning('PsychHDR: AMD FreeSync2 HDR modes are incomplete, not officially supported, not validated, and possibly faulty!');

                otherwise % VK_COLOR_SPACE_HDR10_ST2084_EXT
                    % Standard HDR-10 BT-2020 colorspace with PQ encoding:
                    eotfName = 'BT-2020-PQ';
                    doPQEncode = 1;

                    % From BT-2020 to BT-2020, therefore CSC not needed:
                    doCSC = 0;
                    MCSC = [[1 0 0]; [0 1 0]; [0 0 1]];
            end

            if verbosity >= 3
                if hdrArgs.dummy ~= 1
                    % The real thing:
                    fprintf('PsychHDR-INFO: HDR-10 mode activated. BT-2020 input window color space, pixel color unit is %s. Output uses %s EOTF.\n', hdrArgs.inputUnit, eotfName);
                else
                    % Only minimal emulation:
                    fprintf('PsychHDR-INFO: HDR-10 mode EMULATION ON SDR DISPLAY activated. BT-2020 color space, PQ EOTF. Unit is %s.\n', hdrArgs.inputUnit);
                    fprintf('PsychHDR-INFO: This is only a most bare-bones emulation. VISUAL STIMULI WILL DISPLAY WRONG!\n');
                end
            end

            % Set it up - Assign texture image unit 0 and input values
            % scalefactor:
            glUseProgram(oetfshader);
            glUniform1i(glGetUniformLocation(oetfshader, 'Image'), 0);
            glUniform1f(glGetUniformLocation(oetfshader, 'Prescale'), scalefactor);
            glUniform1i(glGetUniformLocation(oetfshader, 'doPQEncode'), doPQEncode);
            glUniform1i(glGetUniformLocation(oetfshader, 'doCSC'), doCSC);
            glUniformMatrix3fv(glGetUniformLocation(oetfshader, 'MCSC'), 1, 0, MCSC);
            glUseProgram(0);

            % Assign shader name: Add name of color correction shader:
            oetfshaderstring = sprintf('HDR-OETF-%s-Formatter: %s', eotfName, icmstring);

            % Shader is ready for OETF mapping.
        otherwise
            error('Unknown hdrMode %i - Implementation bug?!?', hdrMode);
    end

    % Return formatting shader to caller:
    varargout{1} = oetfshader;
    varargout{2} = oetfshaderstring;

    return;
end

% PsychHDR('HDRMetadata', window, hdrmetadatastruct) ?
if strcmpi(cmd, 'HDRMetadata') && (length(varargin) == 2) && isstruct(varargin{2})
    % This is a set call for HDRMetadata, with a struct providing the
    % parameters. Unpack the struct and convert into a conventional call to
    % PsychVulkanCore('HDRMetadata'):
    window = varargin{1};
    meta = varargin{2};

    % Check if this window is associated with a Vulkan/WSI / Vulkan window:
    winfo = Screen('GetWindowInfo', window, 7);
    if ~bitand(winfo.ImagingMode, kPsychNeedFinalizedFBOSinks)
        % Nope: Must be part of the static HDR stereo hack.

        % Return retrieved old HDR metadata settings of 1st HDR monitor:
        if length(oldHDRMeta) >= window
            varargout{1} = oldHDRMeta{window};
        else
            varargout{1} = [];
        end

        % These are the only settings we support for the static HDR Linux/X11 hack:
        flags = 0;
        gpuIndex = 0;
        vulkanHDRMode = 1;

        % Perform HDR update hack:
        PsychHDR('ExecuteStaticHDRHack', window, 2, vulkanHDRMode, gpuIndex, flags, meta);

        % Assign new settings as future old cached settings:
        oldHDRMeta{window} = meta;

        return;
    end

    [ varargout{1:nargout} ] = PsychVulkan(cmd, window, meta.MetadataType, meta.MaxFrameAverageLightLevel, meta.MaxContentLightLevel, meta.MinLuminance, meta.MaxLuminance, meta.ColorGamut);

    return;
end

% PsychHDR('ExecuteStaticHDRHack', win, enable, vulkanHDRMode, gpuIndex, flags);
if strcmpi(cmd, 'ExecuteStaticHDRHack')
    win = varargin{1};
    enable = varargin{2};
    vulkanHDRMode = varargin{3};
    gpuIndex = varargin{4};
    flags = bitor(varargin{5}, 1); % Must have no OpenGL-Vulkan interop.
    hdrMetadata = varargin{6}; % Struct with HDR metadata to use.

    % Get screenId of the X-Screen on which our onscreen window displays:
    screenId = Screen('WindowScreenNumber', win);

    if enable
        % Get the UUID of the Vulkan device that is compatible with our associated
        % OpenGL renderer/gpu. Compatible means: Can by used for OpenGL-Vulkan interop:
        winfo = Screen('GetWindowInfo', win);
        if ~isempty(winfo.GLDeviceUUID)
            targetUUID = winfo.GLDeviceUUID;
        else
            % None provided, because the OpenGL implementation does not support
            % OpenGL-Vulkan interop. Assign empty id for most basic testing:
            targetUUID = zeros(1, 16, 'uint8');

            % Lacking any better way to choose the gpu, assume gpuIndex 1 is the
            % right choice: TODO: Could go for GL_VENDOR or such for simple cases...
            gpuIndex = 1;
        end

        % Store this targetUUID in an internal persistent per-window variable:
        targetUUIDs{win} = targetUUID;

        % We want an identity hardware gamma lut in HDR, but at maximum lut precision,
        % so output does not get truncated to 8 bpc. Therefore we can't use
        % LoadIdentityClut() which is aimed at 8 bpc identity pixel passthrough.
        % This is only done on first-time enable hack:
        if enable == 1
            % Backup old lut for this screen:
            BackupCluts(screenId);

            % Upload a perfectly linear lut for the given gpu:
            [~, ~, reallutsize] = Screen('ReadNormalizedGammaTable', win);

            % AMD gpu under Linux?
            if IsLinux && strcmp(winfo.DisplayCoreId, 'AMD')
                % Use special identity gamma table (like in LoadIdentityClut()) that
                % is known and verified to get recognized by amdgpu-kms DC and trigger
                % gamma table hardware bypass mode in hardware:
                identityLUT = (linspace(0, 1, 256)' * ones(1, 3));
            else
                % Other gpu + driver + os combo: Standard identity lut:
                identityLUT = repmat(linspace(0, 1, reallutsize)', 1, 3);
            end

            Screen('LoadNormalizedGammaTable', win, identityLUT, 0);

            if verbosity >= 3
                fprintf('PsychHDR-INFO: Loaded identity gamma table into X-Screen %i for HDR.\n', screenId);
            end
        end
    else
        % Disable: Can not do a 'GetWindowInfo' query for targetUUID, because we
        % are executing from within the Screen('Close', win) path, where calls to
        % that function are unsafe (crash!). Retrieve the targetUUID that we cached
        % during the previous enable call:
        targetUUID = targetUUIDs{win};
        targetUUIDs{win} = [];
    end

    % Get video output properties of primary display output monitor on the X-Screen:
    output = Screen('ConfigureDisplay', 'Scanout', screenId, 0);

    % Rect needs to define the video resolution of the selected video mode, ie.,
    % wanted width x height in pixels. It is static across all participating
    % monitors, as either there is only one such monitor, or in a stereo dual-
    % display setup or similar, we need identical video modes on all participating
    % displays for synchronized video refresh cycles:
    rect = [0, 0, output.width, output.height];

    % Target video refreshHz is also identical for all participating displays:
    refreshHz = output.hz;

    % Build list of RandR outputHandle's for all outputs in a single/dual-display
    % or multi-display stimulation setup for stereo or surround style stimulation:
    outputHandle = uint64([]);
    for i = 0:(Screen('ConfigureDisplay', 'NumberOutputs', screenId) - 1)
        output = Screen('ConfigureDisplay', 'Scanout', screenId, i);
        outputHandle(end+1) = uint64(output.outputHandle);
    end

    if enable
        if enable == 1
            cmdString = sprintf('PsychHDR(''ExecuteStaticHDRHack'', %i, 0, %i, %i, %i, [])', win, vulkanHDRMode, gpuIndex, flags);
            Screen('Hookfunction', win, 'PrependMFunction', 'CloseOnscreenWindowPostGLShutdown', 'Vulkan HDR hack cleanup', cmdString);
            Screen('Hookfunction', win, 'Enable', 'CloseOnscreenWindowPostGLShutdown');
            cmdString = 'octave-cli --no-gui --no-history --silent --eval ''PsychHDR("DoExecuteStaticHDRHack", 1)''';
        else
            cmdString = 'octave-cli --no-gui --no-history --silent --eval ''PsychHDR("DoExecuteStaticHDRHack", 2)''';
        end
    else
        cmdString = 'octave-cli --no-gui --no-history --silent --eval ''PsychHDR("DoExecuteStaticHDRHack", 0)''';
    end

    if verbosity > 2
        switch(enable)
        case 0,
            fprintf('PsychHDR-INFO: Executing Vulkan one-time HDR disable hack in helper process - Engage!\n');
        case 1,
            fprintf('PsychHDR-INFO: Executing Vulkan one-time HDR enable hack in helper process - Today is a good day to die! Engage!\n');
        case 2,
            fprintf('PsychHDR-INFO: Executing Vulkan one-time HDR static metadata setup hack in helper process - Today is a good day to die! Engage!\n');
        end
    end

    for outputId = outputHandle
        % Store all relevant variables with parameters into a file, so our helper
        % process can read them from there. This format should work for both Octave
        % and Matlab, also across Matlab and Octave:
        save([PsychtoolboxConfigDir 'PsychHDRIPC.mat'], '-mat', '-V6');

        % Execute helper process:
        cmdString = [cmdString ' 2>&1'];
        [rc, msg] = system(cmdString);
        if ~ismember(rc, [0, 137])
            % Trouble!
            fprintf('PsychHDR-ERROR: Switch for outputId %i - Failed! Error output from helper process [rc=%i]:\n\n', outputId, rc);
            disp(msg);
            error('PsychHDR-ERROR: Vulkan en-/disable sequence for static HDR display hack failed! See error above.');
        end

        if verbosity > 4
            fprintf('PsychHDR-DEBUG: Switch for outputId %i - Killer trick success! Debug output from helper process:\n\n', outputId);
            disp(msg);
            fprintf('PsychHDR-DEBUG: Done. Success!\n');
        elseif verbosity > 3
            if enable
                fprintf('PsychHDR-INFO: For outputId %i - Vulkan one-time HDR setup killer trick - Success!\n', outputId);
            else
                fprintf('PsychHDR-INFO: For outputId %i - Vulkan HDR teardown hack - Success!\n', outputId);
            end
        end

        % Retrieve HDR properties and metadata settings provided by helper process,
        % but only for first output, as the working assumption for multi-display is
        % that all HDR monitors are identical models with identical properties and
        % settings:
        if enable && (outputId == outputHandle(1))
            load([PsychtoolboxConfigDir 'PsychHDRIPC.mat'], 'oldHdrMetadata', 'hdrProperties');
            oldHDRMeta{win} = oldHdrMetadata;
            oldHDRProperties{win} = hdrProperties;
        end
    end

    % Give X-Server some time to settle, as some of this is a bit racy:
    WaitSecs(0.25);

    return;
end

if strcmpi(cmd, 'DoExecuteStaticHDRHack')
    % Load all relevant variables with parameters into a file, so our helper
    % process can read them from there. This format should work for both Octave
    % and Matlab, also across Matlab and Octave:
    load([PsychtoolboxConfigDir 'PsychHDRIPC.mat']);

    % 'enable' encodes type of request: 1/2 = Enable, 0 = Disable HDR.

    PsychVulkanCore('Verbosity', verbosity);

    % Open a Vulkan fullscreen window on target output 'outputId', with suitable
    % hdrMode enabled, on the proper gpu gpuIndex / targetUUID and X-Screen screenId.
    %
    % This should trigger direct display mode and sending of HDR metadata to switch
    % HDR monitors into HDR-10 mode:
    vwin = PsychVulkanCore('OpenWindow', gpuIndex, targetUUID, 1, screenId, rect, outputId, vulkanHDRMode, 1, refreshHz, 0, 0, flags);

    % Disable of HDR mode requested?
    if ~enable
        % Yes. Simply close the HDR window after opening it in HDR mode. This will
        % not only close the window, but also send the HDR disable command to the
        % Linux kernel, given that we just enabled HDR during the 'OpenWindow':
        PsychVulkanCore('CloseWindow', vwin);

        % We are done and can return control to the calling process, which will
        % then exit cleanly:
        return;
    end

    % Enable of HDR mode or change of HDR static metadata requested:

    % Custom static HDR metadata provided by caller?
    if ~isempty(hdrMetadata)
        % Yes: Set custom caller provided static HDR metadata for this session:
        oldHdrMetadata = PsychVulkanCore('HDRMetadata', vwin, hdrMetadata.MetadataType, hdrMetadata.MaxFrameAverageLightLevel, hdrMetadata.MaxContentLightLevel, hdrMetadata.MinLuminance, hdrMetadata.MaxLuminance, hdrMetadata.ColorGamut);

        % Trigger a single present to latch the new HDR metadata to the HDR monitor:
        PsychVulkanCore('Present', vwin, 0, 1);
    else
        oldHdrMetadata = PsychVulkanCore('HDRMetadata', vwin);
    end

    % Return old HDR metadata and properties:
    hdrProperties = PsychVulkanCore('GetHDRProperties', vwin);
    save([PsychtoolboxConfigDir 'PsychHDRIPC.mat'], '-mat', '-V6');

    % End of enable sequence, now we kill ourselves: Today is a good day to die!
    % This will kill the Vulkan driver and hosting Octave/Matlab process without
    % closing the Vulkan window. As a result, the Linux kernel will clean up after
    % our dead process, releasing all resources and also releasing the HDR monitor/
    % RandR output back to the X-Server and thereby to the regular Screen() fullscreen
    % onscreen window that we want to use for purely OpenGL driven HDR stimulus
    % presentation without any further involvement of Vulkan.
    % We do the kill, because if our driver/process gets killed, the one thing
    % the Linux kernel does not do (as of Linux 5.8 at least) is disable HDR
    % metadata transmission to the HDR monitor. So the HDR monitor continues to
    % receive our static HDR metadata and stays in HDR-10 mode with the statically
    % assigned HDR properties, ready to receive OpenGL rendered and displayed HDR
    % PQ encoded visual stimuli for display:
    kill(getpid, 9);

    % Never reached:
    return;
end

% Dispatch subfunctions with "HDR" in their name to PsychVulkan(), which may
% hand them off to PsychVulkanCore():
if ~isempty(strfind(lower(cmd), 'hdr')) %#ok<*STREMP>
    if ~isempty(varargin)
        % Check if 1st arg is a window and if it is associated with a Vulkan/WSI / Vulkan window:
        if ~isempty(varargin{1}) && isscalar(varargin{1}) && isreal(varargin{1}) && (Screen('WindowKind', varargin{1}) == 1)
            winfo = Screen('GetWindowInfo', varargin{1}, 7);
            if ~bitand(winfo.ImagingMode, kPsychNeedFinalizedFBOSinks)
                % Nope: Windows must be part of the static HDR stereo hack. Can we handle it?
                if strcmpi(cmd, 'HDRMetadata')
                    % Return cached HDR metadata from last enable/update hack:
                    meta = oldHDRMeta{varargin{1}};
                    varargout{1} = meta;

                    % Optionally assign new HDR metadata by parsing args into struct,
                    % then using that setup code:
                    if length(varargin) >= 2
                        meta.MetadataType = varargin{2};

                        if length(varargin) >= 3
                            meta.MaxFrameAverageLightLevel = varargin{3};
                        end

                        if length(varargin) >= 4
                            meta.MaxContentLightLevel = varargin{4};
                        end

                        if length(varargin) >= 5
                            meta.MinLuminance = varargin{5};
                        end

                        if length(varargin) >= 6
                            meta.MaxLuminance = varargin{6};
                        end

                        if length(varargin) >= 7
                            meta.ColorGamut = varargin{7};
                        end

                        PsychHDR('HDRMetadata', varargin{1}, meta);
                    end

                    return;
                end

                if strcmpi(cmd, 'GetHDRProperties')
                    % Returned cached HDR properties from last enable hack:
                    varargout{1} = oldHDRProperties{varargin{1}};
                    return;
                end

                if strcmpi(cmd, 'HDRLocalDimming')
                    % Not supported on Linux atm., so always report disabled:
                    varargout{1} = 0;

                    % Reject any setup requests:
                    if (length(varargin) > 1) && ~isempty(varargin{2})
                        error('PsychHDR(''HDRLocalDimming'') control is not supported on Linux in OpenGL HDR hack mode!');
                    end

                    return;
                end

                % This call is unsupported:
                warning(sprintf('PsychHDR(''%s'') call unsupported in static HDR OpenGL Linux hack mode. Ignored!', cmd));

                varargout{1} = [];
                return;
            end
        end

        [ varargout{1:nargout} ] = PsychVulkan(cmd, varargin{1:end});
    else
        PsychVulkan(cmd);
    end

    return;
end

sca;
error('Invalid command ''%s'' specified. Read ''help PsychHDR'' for list of valid commands.', cmd);
end

% Parse arguments provided by user-script in PsychImaging('AddTask','General','EnableHDR', ...)
% and turn them into a settings struct for internal use:
function hdrArgs = parseHDRArguments(hdrArguments)
    % hdrArguments{1} = Tasktype 'General'
    % hdrArguments{2} = Task 'EnableHDR'
    % hdrArguments{3} = unit of color input values.
    % hdrArguments{4} = hdrMode='Auto' aka 'HDR-10'.
    % hdrArguments{5} = extraRequirements='' by default.

    % Validate inputs:
    if ~strcmpi(hdrArguments{1}, 'General')
        error('PsychHDR-ERROR: Invalid task type ''%s'' provided for HDR operation.', hdrArguments{1});
    end

    if ~strcmpi(hdrArguments{2}, 'EnableHDR')
        error('PsychHDR-ERROR: Invalid task ''%s'' provided for HDR operation.', hdrArguments{2});
    end

    if isempty(hdrArguments{3})
        hdrArguments{3} = 'Nits';
    end

    switch lower(hdrArguments{3})
        case 'nits'
            hdrArgs.inputScalefactor = 1 / 10000;
            hdrArgs.contentSDRToHDRFactor = 80;
            hdrArgs.inputUnit = 'Nits';

        case '80nits'
            hdrArgs.inputScalefactor = 80 / 10000;
            hdrArgs.contentSDRToHDRFactor = 1;
            hdrArgs.inputUnit = '80Nits';

        otherwise
            error('PsychHDR-ERROR: Invalid input color value unit argument ''%s'' provided for HDR operation.', hdrArguments{3});
    end

    hdrArgs.unit = hdrArgs.inputUnit;

    if isempty(hdrArguments{4})
        hdrArguments{4} = 'Auto';
    end

    switch lower(hdrArguments{4})
        case 'auto'
            % Auto maps to only currently supported mode 'HDR-10' aka 1:
            hdrArgs.hdrMode = 1;

        case 'hdr10'
            % HDR-10: 10 bpc precision, BT-2020 color space, PQ EOTF:
            hdrArgs.hdrMode = 1;

        otherwise
            error('PsychHDR-ERROR: Invalid hdrMode ''%s'' provided for HDR operation.', hdrArguments{4});
    end

    % Map hdrMode to default color gamut for that mode:
    switch (hdrArgs.hdrMode)
        case 1
            % HDR-10, BT-2100 container, ie. BT-2020 aka ITU Rec.2020 color space with D65 white point:
            hdrArgs.colorGamut = [[0.708 ; 0.292], [0.170 ; 0.797], [0.131 ; 0.046], [0.31271 ; 0.32902]];
            hdrArgs.colorSpace = 0; % Auto-Select Vulkan colorspace based on hdrMode.

            %hdrArgs.colorSpace = 1000213000; % This would request VK_COLOR_SPACE_DISPLAY_NATIVE_AMD for Freesync2 HDR. Disabled atm., because this is neither complete nor validated at all!

        otherwise
            error('PsychHDR-ERROR: Default color gamut for hdrMode %i unknown for HDR operation! PsychHDR() implementation bug?!?', hdrArgs.hdrMode);
    end

    % No dummy HDR mode by default:
    hdrArgs.dummy = 0;

    % Handle extraRequirements:
    if ~isempty(hdrArguments{5})
        if ~isempty(strfind(lower(hdrArguments{5}), 'statichdrhack'))
            % Static HDR stereo hack on Linux/X11 requested, for driving the HDR display via purely OpenGL:
            hdrArgs.dummy = 2;
        end

        if ~isempty(strfind(lower(hdrArguments{5}), 'dummy'))
            % Dummy mode requested, for minimal emulation on a SDR display:
            hdrArgs.hdrMode = 0;
            if hdrArgs.dummy == 0
                hdrArgs.dummy = 1;
            end
        end
    end
end
