function AdditiveBlendingForLinearSuperpositionTutorial(outputdevice, overlay, colorclut, doGainCorrection, useVulkan)
% AdditiveBlendingForLinearSuperpositionTutorial([outputdevice='None'][, overlay=1][, colorclut=0][, doGainCorrection=0][, useVulkan=0]);
%
% Illustrates use of floating point textures in combination with
% source-weighted additive alpha blending to create linear superpositions
% of image patches, in this case of two superimposed gratings. Due to the
% use of additive alpha-blending, you'll see that the two gratings will
% superimpose onto each other in a mathematically correct way -- pixel-wise
% addition/subtraction of luminance values. The demo uses a 32 bit floating
% point framebuffer on the latest hardware. This allows for an effective 23
% bits of precision in all math done and in the final stimuli - more than
% any display device in existence could resolve. On previous generation
% hardware (older than NVidia Geforce 8800 or ATI Radeon HD2000), alpha
% blending isn't supported in 32 bpc float precision. Therefore the demo
% will select 16 bpc floating point precision, where alpha blending works.
% This way the effective precision is 11 bits, a bit less than what special
% display devices can resolve. However, final gamma correction is done in
% full 23 bits precision, so you can make use of the extra bits for proper
% display linearization.
%
% The demo also demonstrates Psychtoolbox support for high precision
% display output devices, ie., display extenders, graphics cards and
% display modes that allow for luminance display with more than the
% standard 8 bits precision. By default the standard 8 bit framebuffer of
% standard graphics cards is demonstrated.
%
% However, by providing the first optional parameter 'outputdevice', you
% can select amongst all devices supported by PTB:
%
% 'NoneNoGamma' - Same as 'None': Use standard 8 bit framebuffer, but
% disable the gamma correction provided by PTB's imaging pipeline. This is
% usually not what you want, but it allows to test how much faster the
% display runs without gamma correction.
%
% 'PseudoGray' - PseudoGray display, also known as "Bit stealing". This
% technique allows to create the perception of up to 1786 different
% luminance levels on standard 8 bit graphics hardware by use of some
% clever color rendering trick. See "help CreatePseudoGrayLUT" for
% references and details.
%
% 'Native10Bit' - Enables the native 10 bpc framebuffer support on all supported
% GPUs. All gpus from AMD since ~2006, Intel since around ~2010 and NVidia since
% around ~2008 support this on Linux. This also works similar modern with NVidia
% Quadro and AMD Fire professional gpu's under MS-Windows. Also works on
% Apple Silicon Macs.
%
% 'Native11Bit' - Enables the native ~11 bpc framebuffer support on some ATI
% Radeon X1xxx / HDxxx GPU's with DCE-8 to DCE-12 display engine when used under
% Linux. These GPU's do support ~11 bits per color channel when this special mode
% is used (11 bits red, 11 bits green, 10 bits blue).
%
% 'Native16Bit' - Enables the native up to 16 bpc framebuffer support on AMD
% GPU's when used under Linux with Vulkan display backend. While this
% activates a 16 bpc framebuffer, the precision of the video output signal
% depends on the specific gpu, connection and display. As of 2023, the "Sea
% Islands" AMD gpu family and later can output at most 12 bpc precision to
% suitable displays over HDMI or DisplayPort. This works for gpu's up to
% and including the Vega gpu family. However, as of May 2025 it is unclear
% though if this support still works on current generation AMD "Navi" gpu
% family "RDNA" gpu's, or if some bugs in AMD's current Vulkan driver
% releases prevent this, and if that problem can be worked around.
%
% 'Native16BitFloat' - Enable native 16 bit floating point (~11 bit linear)
% framebuffer support on suitable operating systems and graphics cards.
%
% 'VideoSwitcher' - Enable the Xiangrui Li et al. VideoSwitcher, a special
% type of video attenuator (see "help PsychVideoSwitcher") in standard
% "simple" mode.
%
% 'VideoSwitcherCalibrated' - Enable the Xiangrui Li et al. VideoSwitcher,
% but use the more complex (and more accurate?) mode with calibrated lookup
% tables (see "help PsychVideoSwitcher").
%
% 'Attenuator' - Enable support for standard Pelli & Zhang style video
% attenuators by use of lookup tables.
%
% Then we have support for the different modes of operation of the
% Cambridge Research Systems Bits++ box:
%
% 'Mono++' - Use 14 bit mono output mode, either with color index overlay
% (if the optional 2nd 'overlay' flag is set to 1, which is the default),
% or without color index overlay.
%
% 'Color++' - User 14 bits per color component mode.
%
% Then we have support for the different modes of operation of the
% VPixx Technologies DPixx (DataPixx) box:
%
% 'M16' - Use 16 bit mono output mode, either with color index overlay
% (if the optional 2nd 'overlay' flag is set to 1, which is the default),
% or without color index overlay.
%
% 'C48' - User 16 bits per color component mode.
%
%
% 'DualPipeHDR' - Use experimental output to dual-pipeline HDR display
% device.
%
%
% The third optional parameter 'colorclut' if provided, will use color
% lookup table based color correction / mapping, ie., mapping of luminance
% values to RGB values in the framebuffer - or intensity values if
% applicable. Any non-zero number will select a different CLUT. Look into
% the code for description. This just to demonstrate that you can use CLUT
% based color/intensity correction instead of simple power-law gamma
% correction if you want.
%
%
% The fourth optional parameter 'doGainCorrection' if provided and set to
% 1, will demonstrate use of display per-pixel gain correction, aka
% vignetting correction. It will modulate the brightness of each pixel with
% a gain factor, the gains increasing linearly from the left border to the
% right border of the display. See "help VignettingCorrectionDemo" for more
% details of this feature.
%
%
% The fifth optional parameter 'useVulkan' if provided and set to 1, will try
% to use a Vulkan based display backend, instead of the standard OpenGL based
% display backend. See "help PsychVulkan" for system requirements and caveats.
% You need to set useVulkan to 1 on Linux to get Native16Bit or Native16BitFloat
% mode on AMD graphics under Linux.
%
% The demo shows two superimposed sine wave gratings in the center of the
% screen. You can shift the 2nd grating up and down in subpixel steps by
% use of the cursor up-/down keys. You can change the contrast of the 2nd
% grating by use of the cursor left-/right keys. You can move the 2nd
% grating with the mouse, and rotate it clockwise or counterclockwise by
% pressing the mouse buttons. The keys 'i' and 'd' allow to change the
% "encoding gamma" factor used for the gamma correction algorithm. The ESC
% ape key ends the demo. Actually the demo performs some benchmark run for
% a few seconds after you've pressed ESC key, just to measure the speed of
% your graphics card in stimulus conversion.
%
% In 'VideoSwitcher' mode, it also draws some vertically moving greenish
% sync line just to show how to generate trigger signals on the
% VideoSwitcher device.
%
% 
% Needs hardware with support for imaging pipeline (GLSL shaders and
% floating point framebuffers). Should work well on ATI Radeon X1000 and
% later, Geforce 6000 and later and even better on DirectX10 hardware like
% Radeon HD series and NVidia Geforce 8 / 9 series and later.

% History:
% 16.04.2007 Written (MK).
% 13.08.2008 Cleaned up, commented, more help text etc... (MK).
% 18.03.2013 Cleaned up, changed contrast of gratings to sensible 0.25 (MK).
% 26.06.2014 Also show off Native11Bits framebuffers on AMD (MK).
% 17.09.2014 Also show off Native16Bits framebuffers on AMD (MK).
% 10.07.2019 Also show off Native16BitFloat framebuffers (MK).
% 18.02.2026 Disable Retina scaling for 16 bpc formats under Wayland (MK).

KbName('UnifyKeyNames');
UpArrow = KbName('UpArrow');
DownArrow = KbName('DownArrow');
LeftArrow = KbName('LeftArrow');
RightArrow = KbName('RightArrow');
esc = KbName('ESCAPE');
space = KbName('space');
GammaIncrease = KbName('i');
GammaDecrease = KbName('d');

if nargin < 1 || isempty(outputdevice)
    outputdevice = 'None';
end

if nargin < 2 || isempty(overlay)
    overlay = 1;
end

if nargin < 3 || isempty(colorclut)
    colorclut = 0;
end

if nargin < 4 || isempty(doGainCorrection)
    doGainCorrection = 0;
end

if nargin < 5 || isempty(useVulkan)
    useVulkan = 0;
end

try
    % This script calls Psychtoolbox commands available only in OpenGL-based 
    % versions of the Psychtoolbox. The Psychtoolbox command AssertPsychOpenGL will issue
    % an error message if someone tries to execute this script on a computer without
    % an OpenGL Psychtoolbox
    AssertOpenGL;

    % Get the list of screens and choose the one with the highest screen number.
    % Screen 0 is, by definition, the display with the menu bar. Often when 
    % two monitors are connected the one without the menu bar is used as 
    % the stimulus display.  Chosing the display with the highest dislay number is 
    % a best guess about where you want the stimulus displayed.  
    screenNumber = max(Screen('Screens'));

    % Open a double-buffered fullscreen window with a gray (intensity =
    % 0.5) background and support for 16- or 32 bpc floating point framebuffers.
    PsychImaging('PrepareConfiguration');

    % Shall Vulkan be used as display backend?
    if useVulkan
        PsychImaging('AddTask', 'General', 'UseVulkanDisplay');
    end

    lrect = [];
    rrect = [];

    % This will try to get 32 bpc float precision if the hardware supports
    % simultaneous use of 32 bpc float and alpha-blending. Otherwise it
    % will use a 16 bpc floating point framebuffer for drawing and
    % alpha-blending, but a 32 bpc buffer for gamma correction and final
    % display. The effective stimulus precision is reduced from 23 bits to
    % about 11 bits when a 16 bpc float buffer must be used instead of a 32
    % bpc float buffer:
    PsychImaging('AddTask', 'General', 'FloatingPoint32BitIfPossible');

    if IsWayland && strncmp(outputdevice, 'Native16Bit', 11)
        % Request full Retina resolution by default for native 16 bpc modes, as they
        % will not work under Retina scaling with any but the latest display hardware.
        % Trying to use them in scaled mode would cause Wayland compositors to fall
        % back to desktop composition at reduced color precision, e.g., only 10 bpc.
        %
        % As of early 2026, only GNOME Mutter v50+ can do native display of 16 bpc
        % fullscreen windows under Wayland direct scanout mode on suitable hardware,
        % and often only without Retina/HiDPI scaling. E.g., AMD gpus older than a
        % few years can't do it with hardware scaling, but full native resolution works.
        % So lets play it safe for now.
        PsychImaging('AddTask', 'General', 'UseRetinaResolution');
    end

    switch outputdevice
        case {'Mono++'}
            if overlay
                % Use Mono++ mode with color index overlay support:
                PsychImaging('AddTask', 'General', 'EnableBits++Mono++OutputWithOverlay');
            else
                % Use Mono++ mode without color overlay:
                PsychImaging('AddTask', 'General', 'EnableBits++Mono++Output');
            end

        case {'Color++'}
            % Use Color++ mode: We select averaging between even/odd
            % pixels, aka mode 2:
            PsychImaging('AddTask', 'General', 'EnableBits++Color++Output', 2);
            overlay = 0;

        case {'M16'}
            if overlay
                % Use M16 mode of Datapixx with color index overlay support:
                PsychImaging('AddTask', 'General', 'EnableDataPixxM16OutputWithOverlay');
            else
                % Use M16 mode of Datapixx without color overlay:
                PsychImaging('AddTask', 'General', 'EnableDataPixxM16Output');
            end

        case {'C48'}
            % Use C48 mode of Datapixx: We select averaging between even/odd
            % pixels, aka mode 2:
            PsychImaging('AddTask', 'General', 'EnableDataPixxC48Output', 2);
            overlay = 0;

        case {'Attenuator'}
            % Use the standard Pelli & Zhang style attenuator driver. This
            % uses a simple 3 row (for the three color channels Red, Green,
            % Blue) by n slots lookup table to map wanted intensity values
            % to RGB triplets for driving the attenuator. Any number of
            % slots up to 2^16 is supported, for a max precision of 16 bits
            % luminance. As we don't have a calibrated table in this demo,
            % we simply load a 2048 slot table (11 bit precision) with
            % random values:
            PsychImaging('AddTask', 'General', 'EnableGenericHighPrecisionLuminanceOutput', uint8(rand(3, 2048) * 255));
            overlay = 0;

        case {'VideoSwitcher'}
            % Select simple opmode of VideoSwitcher, where only the btrr
            % blue-to-red ratio from the global configuration file is used
            % for calibrated output:
            PsychImaging('AddTask', 'General', 'EnableVideoSwitcherSimpleLuminanceOutput', [], 1);

            % Switch the device to high precision luminance mode:
            PsychVideoSwitcher('SwitchMode', screenNumber, 1);
            overlay = 0;

        case {'VideoSwitcherCalibrated'}
            % Again the videoswitcher, but in lookup-table calibrated mode,
            % where additionally to the BTRR, a lookup table is loaded:
            PsychImaging('AddTask', 'General', 'EnableVideoSwitcherCalibratedLuminanceOutput', [], [], 1);

            % Switch the device to high precision luminance mode:
            PsychVideoSwitcher('SwitchMode', screenNumber, 1);
            overlay = 0;

        case {'PseudoGray'}
            % Enable bitstealing aka PseudoGray shader:
            PsychImaging('AddTask', 'General', 'EnablePseudoGrayOutput');
            overlay = 0;

        case {'Native10Bit'}
            % Enable ATI GPU's 10 bit framebuffer under certain conditions
            % (see help for this file):
            PsychImaging('AddTask', 'General', 'EnableNative10BitFramebuffer');
            overlay = 0;

        case {'Native11Bit'}
            % Enable ATI GPU's ~11 bit framebuffer under certain conditions
            % (see help for this file):
            PsychImaging('AddTask', 'General', 'EnableNative11BitFramebuffer');
            overlay = 0;

        case {'Native16Bit'}
            % Enable AMD GPU's up to 16 bit framebuffer under certain conditions
            % (see help for this file):
            PsychImaging('AddTask', 'General', 'EnableNative16BitFramebuffer');
            overlay = 0;

        case {'Native16BitFloat'}
            % Enable native 16 bit floating point framebuffer under certain conditions
            % currently only known to work on NVidia GeForce 1000 and later on Windows-10:
            PsychImaging('AddTask', 'General', 'EnableNative16BitFloatingPointFramebuffer');
            overlay = 0;

        case {'None'}
            % No high precision output, just the plain 8 bit framebuffer,
            % but with software gamma correction:
            PsychImaging('AddTask', 'General', 'NormalizedHighresColorRange');
            overlay = 0;

        case {'NoneNoGamma'}
            % No high precision output, just the plain 8 bit framebuffer,
            % even without gamma correction:
            PsychImaging('AddTask', 'General', 'NormalizedHighresColorRange');
            overlay = 0;

        case {'DualPipeHDR'}
            % Enable experimental dual display, dual pipeline HDR output:

            % Handle single-screen vs. dual-screen output:
            if length(Screen('Screens')) == 1
                lrect = [0 0 600 600];
                rrect = [601 0 1201 600];
            end

            % Request actual output mode:
            PsychImaging('AddTask', 'General', 'EnableDualPipeHDROutput', min(Screen('Screens')), rrect);
            overlay = 0;

        otherwise
            error('Unknown "outputdevice" provided.');
    end

    if doGainCorrection
        % Request per pixel 2D gain correction for display:
        PsychImaging('AddTask', 'AllViews', 'DisplayColorCorrection', 'GainMatrix');
    end

    % Do not use gamma correction in calibrated video switcher mode or no
    % gamma mode -- wouldn't make sense in either of these:
    if ~strcmp(outputdevice, 'NoneNoGamma')
        if ~colorclut
            % Choose method of color correction: 'SimpleGamma' is simple gamma
            % correction of monochrome stims via power-law, ie., Lout = Lin ^ gamma.
            PsychImaging('AddTask', 'FinalFormatting', 'DisplayColorCorrection', 'SimpleGamma');
            doTheGamma =1;
        else
            % Use CLUT based color correction:
            PsychImaging('AddTask', 'FinalFormatting', 'DisplayColorCorrection', 'LookupTable');
            doTheGamma =0;
        end
    else
        doTheGamma = 0;
    end

    %PsychImaging('AddTask', 'General', 'InterleavedLineStereo', 0);

    % Finally open a window according to the specs given with above
    % PsychImaging calls, clear it to a background color of 0.5 aka 50%
    % luminance:
    [w, wRect]=PsychImaging('OpenWindow', screenNumber, 0.5, lrect);

    % Use of overlay in Bits++ box Mono++ mode or DPixx box M16 mode wanted?
    if overlay
        % Get overlay window handle: Drawing into this window will affect
        % the overlay:
        wo = PsychImaging('GetOverlayWindow', w);
        Screen('Preference','TextAntiAliasing', 1);
        Screen('Preference','TextAlphaBlending', 1);
    else
        wo = 0;
    end

    % Calibrated conversion driver for VideoSwitcher in use?
    if strcmp(outputdevice, 'VideoSwitcherCalibrated')
        % Tell the driver what luminance the background has. This allows
        % for some quite significant speedups in stimulus conversion:
        PsychVideoSwitcher('SetBackgroundLuminanceHint', w, 0.5);
    end

    % Assign a CLUT for color correction?
    if colorclut
        % Yes. 
        switch colorclut
            case 1
                % Create a standard colormap() 3 channel RGB CLUT for a nice effect:
                clut = colormap;
            case 2
                % A 1024 slots CLUT with inverted green channel:
                clut = ((0:1/1023:1)' * ones(1, 3));
                clut(:, 2) = 1 - clut(:, 2);
            case 3
                % A simple inverted one channel (luminance) map with only two slots
                % for linearly interpolated values inbetween:
                clut = 1 - (0:1)';
            case 4
                % Extreme amplification CLUT:
                clut = 3800 * (0:1)';
            otherwise
                clut = colormap;
        end

        % Assign the CLUT:
        PsychColorCorrection('SetLookupTable', w, clut);
    end

    if doTheGamma
        % We set initial encoding gamma to correct for a display with a
        % decoding gamma of 2.0 -- A good tradeoff, given most displays are
        % somewhere between 1.8 and 2.2:
        gamma = 1 / 2.0;
        PsychColorCorrection('SetEncodingGamma', w, gamma);
    else
        gamma = 1;
    end

    % From here on, all color values should be specified in the range 0.0
    % to 1.0 for displayable luminance values. Values outside that range
    % are allowed as intermediate results, but the final stimulus image
    % should be in range 0-1, otherwise result will be undefined.
    [width, height]=Screen('WindowSize', w);

    if doGainCorrection
        % Test support for display gain correction:
        gainmatrix = meshgrid(1:width, 1:height) / width * 1.5;
        PsychColorCorrection('SetGainMatrix', w, gainmatrix);
    end

    % Enable alpha blending. We switch it into additive mode which takes
    % source alpha into account:
    Screen('BlendFunction', w, GL_SRC_ALPHA, GL_ONE);

    inc=0.25;

    % Compute one frame of a static grating: It has a total size of third
    % the screen size:
    s=min(width, height) / 6;
    [x,y]=meshgrid(-s:s-1, -s:s-1);
    angle=30*pi/180; % 30 deg orientation.
    f=0.01*2*pi; % cycles/pixel
    a=cos(angle)*f;
    b=sin(angle)*f;

    % Build grating texture:
    m=sin(a*x+b*y);
    tex=Screen('MakeTexture', w, m,[],[], 2);

    % Show the gray background:
    Screen('Flip', w);

    i=0;
    rotate = 0;
    yd =0;
    show2nd = 1;

    if overlay
        Screen('TextSize', wo, 18);
    else
        Screen('TextSize', w, 18);
    end

    % Center mouse on stimulus display, then make mouse cursor invisible:
    [cx, cy] = RectCenter(wRect);
    SetMouse(cx, cy, w);
    HideCursor(screenNumber);
    framecount = 0;

    tstart = GetSecs;

    % Animation loop:
    while 1
        Screen('DrawTexture', w, tex, [], [], [], [], 0.25);
        i=i+rotate;
        [x,y,buttons]=GetMouse(w);
        [x,y] = RemapMouse(w, 'AllViews', x, y);
        if any(buttons)
            if buttons(1)
                i=i+0.1;
            end
            if buttons(2)
                i=i-0.1;
            end
        end

        if show2nd
           dstRect=CenterRectOnPoint(Screen('Rect', tex), x, y+yd);
           Screen('DrawTexture', w, tex, [], dstRect, i, [], inc);
        end

        [d1, d2, keycode]=KbCheck; %#ok<*ASGLU>
        if d1
            if keycode(UpArrow)
                yd=yd-0.1;
            end

            if keycode(DownArrow)
                yd=yd+0.1;
            end

            if keycode(LeftArrow) && inc >= 0.001
                inc=inc-0.001;
            end

            if keycode(RightArrow) && inc <= 0.999
                inc=inc+0.001;
            end

            % Change of encoding gamma?
            if keycode(GammaIncrease) && doTheGamma
                gamma = min(gamma+0.001, 1.0);
                PsychColorCorrection('SetEncodingGamma', w, gamma);
            end

            if keycode(GammaDecrease) && doTheGamma
                gamma = max(gamma-0.001, 0.0);
                PsychColorCorrection('SetEncodingGamma', w, gamma);
            end

            if keycode(space)
                show2nd = 1-show2nd;
                KbReleaseWait;
                if show2nd
                    HideCursor(screenNumber);
                else
                    ShowCursor(screenNumber);
                end
            end

            if keycode(esc)
                break;
            end
        end

        txt0= 'At startup:\ngrating = sin(f*cos(angle)*x + f*sin(angle)*y);         % Compute luminance grating matrix in Matlab.\n';
        txt1= 'tex = Screen(''MakeTexture'', win, grating, [], [], 2); % Convert it into a 32bpc floating point texture.\n';
        txt2= 'Screen(''BlendFunction'', win, GL_SRC_ALPHA, GL_ONE);   % Enable additive alpha-blending.\n\nIn Display loop:\n\n';
        txt3= 'Screen(''DrawTexture'', win, tex, [], [], [], [], 0.25); % Draw static grating at center of screen.\n';
        txt4 = sprintf('Screen(''DrawTexture'', win, tex, [], [%i %i %i %i], %f, [], %f);\n', dstRect(1), dstRect(2), dstRect(3), dstRect(4), i, inc);
        txt5 = sprintf('\nEncoding Gamma is %f --> Correction for a %f gamma display.', gamma, 1/gamma);

        if overlay
            % Need to manually clear the overlay window:
            Screen('FillRect', wo, 0);
            % Need to use text color values in 0-255 range, instead of
            % normalized 0-1 range:
            DrawFormattedText(wo, [txt0 txt1 txt2 txt3 txt4 txt5], 0, 20, 255);
        else
            DrawFormattedText(w, [txt0 txt1 txt2 txt3 txt4 txt5], 0, 20, 1.0);
        end

        framecount = framecount + 1;

        % For the fun of it: Set a specific scanline to send a trigger
        % signal for the VideoSwitcher. This does nothing if the driver for
        % VideoSwitcher is not selected. We send out a trigger for 1 redraw
        % cycle every 30 redraw cycles. The triggerline is placed at
        % scanline 10 (for no special reason):
        if mod(framecount, 30) == 0
            PsychVideoSwitcher('SetTrigger', w, 10, 1);
        end

        % Show stimulus at next display retrace:
        Screen('Flip', w);
    end

    % Done.
    avgfps = framecount / (GetSecs - tstart);
    fprintf('Average redraw rate in demo was %f Hz.\n', avgfps);

    % Again, just to test conversion speed: A fast benchmark with sync of
    % buffer swaps to retrace disabled -- Go as fast as you can!
    nmaxbench = 300;
    tstart = Screen('Flip', w);
    for i=1:nmaxbench
        Screen('Flip', w, 0, 2, 2);
    end
    tend = Screen('Flip', w);
    fprintf('Average update rate in pipeline was %f Hz.\n', nmaxbench / (tend - tstart));

    Screen('Preference','TextAntiAliasing', 1);
    Screen('Preference','TextAlphaBlending', 0);
    % We're done: Close all windows and textures:
    sca;

catch %#ok<*CTCH>
    %this "catch" section executes in case of an error in the "try" section
    %above.  Importantly, it closes the onscreen window if its open.
    ShowCursor(screenNumber);
    sca;
    Screen('Preference','TextAntiAliasing', 1);
    Screen('Preference','TextAlphaBlending', 0);
    psychrethrow(psychlasterror);
end %try..catch..

if ~isempty(strfind(outputdevice, 'VideoSwitcher')) %#ok<STREMP>
    % If VideoSwitcher was active, switch it back to standard RGB desktop
    % display mode:
    PsychVideoSwitcher('SwitchMode', screenNumber, 0);
end

% Restore gfx gammatables if needed:
RestoreCluts;

return;
