%{
Helper class for using the DLP4710 (written by Mark Wexler, public domain)
Keeps track of colors, bits, clears, flips in both 180 and 1440 Hz modes

Class constructor parameters:
window: window handle (returned by PsychImaging)
bg: background color
mode: 60, 180 or 1440 (default: 60)
autoclear: whether to clear window automatically; if false then call clear() before drawing (default: true)
autodlpmode: whether to automatically set projector mode (default: true)

If autodlpmode is true, then you must:
- install the dlpmode.exe program and any required DLLs
- set the dlpmode_path property of the class (below) to the absolute or relative path of dlpmode.exe
- set the dlp_mode_180_pars and dlp_mode_1440_pars class properties to the parameters of the 180 and 1440 Hz modes,
  respectively (see documentation for dlpmode)

Typical use (1440 Hz). See comments for non-standard things you need to do:

sca;
close all;
clearvars;

mode = 1440;
size = 100;
amplitude = 0.2;
frequency = 2.0;
bg = [0 0 0];

PsychDefaultSetup(2);
InitializeMatlabOpenGL();                       % must do this
screenNumber = max(Screen('Screens'));
[window, windowRect] = PsychImaging('OpenWindow', screenNumber, bg);
[screenXpixels, screenYpixels] = Screen('WindowSize', window);
ifi = 1.0/mode;									% actual frame duration in seconds
[xCenter, yCenter] = RectCenter(windowRect);
Priority(MaxPriority(window));

W = ManagedWindow(window, bg, mode);            % initialize class; because autodlpmode is true, will set the
												% projector mode using the (externally installed) dlpmode.exe program
												% don't forget to set the dlpmode_path, dlp_mode_180_pars and dlp_mode_1440_pars
												% class properties in the ManagedWindow.m file to appropriate values

baseRect = [0 0 size size];
time = 0;
while ~KbCheck
    x = xCenter + amplitude*screenXpixels*sin(2*pi*frequency*time);
    y = yCenter + amplitude*screenXpixels*cos(2*pi*frequency*time);
    rect = CenterRectOnPointd(baseRect, x, y);
    Screen('FillArc', window, W.color, rect, 0, 360);	% always use W.color for drawing
														% (changes automatically from frame to frame)
    W.flip();									% do this instead of something like Screen('Flip', window, 0)
    time = time + ifi;
end

W.cleanup();                                    % will put the DLP4710 back into normal mode
sca;
%}

classdef ManagedWindow < handle
    properties
        window
        bg
        mode
        autoclear
        subframes
        frame
        color
        last_flip_t
        last_flip_t_def
        frame_dur_sum
        frame_dur_n
        extra_dur_sum
        extra_dur_n
        dlpmode_path = 'dlpmode.exe'			%%%%% SET TO ACTUAL PATH OF dlpmode.exe (ABSOLUTE OR RELATIVE)
        dlp_mode_180_pars = 'G 350 5000 206'	%%%%% SET TO 180 HZ PARAMETERS
        dlp_mode_1440_pars = 'G 193 451 50'		%%%%% SET TO 1440 HZ PARAMETERS
    end
    methods
        function obj = ManagedWindow(window, bg, mode, autoclear, autodlpmode)
            if nargin < 5
                autodlpmode = true;
            end
            if nargin < 4
                autoclear = true;
            end
            if nargin < 3
                mode = 60;
            end
            obj.window = window;
            obj.bg = bg;
            obj.mode = mode;
            obj.subframes = obj.mode/60;
            obj.autoclear = autoclear;
            
            if autodlpmode
                if mode ~= 60
                    obj.set_dlp_mode(mode)
                end
            end
            
            if obj.mode == 1440
                [~, opengl] = Screen('GetOpenGLDrawMode');
                if ~opengl
                    Screen('BeginOpenGL', obj.window);
                end
                Screen('BlendFunction', obj.window, GL_ONE, GL_ONE);
                if ~opengl
                    Screen('EndOpenGL', obj.window);
                end
            end
            
            obj.frame = 0;
            obj.last_flip_t_def = false;
            obj.frame_dur_sum = 0;
            obj.frame_dur_n = 0;
            obj.extra_dur_sum = 0;
            obj.extra_dur_n = 0;
            if autoclear
                obj.clear();
            end
        end
        
        function cleanup(obj)
            obj.set_dlp_mode(60)
        end

        % the clear function clears the window, if necessary,
        % and sets the appropriate color mask or drawing color, if necessary
        function clear(obj)
            if obj.mode == 60
                obj.really_clear();
            elseif obj.mode == 180
                f = mod(obj.frame, 3);
                if f == 0
                    obj.really_clear();
                    obj.set_color_mask([0 0 1]);
                elseif f == 1
                    obj.set_color_mask([0 1 0]);
                elseif f == 2
                    obj.set_color_mask([1 0 0]);
                end
            elseif obj.mode == 1440
                f = mod(obj.frame, 24);
                if f == 0
                    obj.really_clear();
                end
                component = (2 - floor(f/8)) + 1;
                bit = mod(f, 8);
                ivalue = 2^bit;
                value = ivalue/255.0;   % because OpenGL computes integer colors by multiplying by 255 and then ROUNDING
                obj.color = [0 0 0];
                obj.color(component) = value;
            end
        end

        function flip(obj)
            really_flip = false;
            if obj.mode == 60
                really_flip = true;
            else
                f = mod(obj.frame, obj.subframes);
                if f == obj.subframes - 1
                    really_flip = true;
                end
            end
            if really_flip
                Screen('Flip', obj.window, 0, 1);
                flip_t = GetSecs();
                if obj.last_flip_t_def
                    frame_dur = flip_t - obj.last_flip_t;
                    obj.frame_dur_sum = obj.frame_dur_sum + frame_dur;
                    obj.frame_dur_n = obj.frame_dur_n + 1;
                end
                obj.last_flip_t = flip_t;
                obj.last_flip_t_def = true;
            end
            obj.frame = obj.frame + 1;
            if obj.autoclear
                t0 = GetSecs();
                obj.clear();
                t1 = GetSecs();
                obj.extra_dur_sum = obj.extra_dur_sum + t1 - t0;
                obj.extra_dur_n = obj.extra_dur_n + 1;
            end
        end
        
        % how many times per second your video card refreshes
        % should be 60 if everything's ok, regardless of projector mode
        function rate = video_frame_rate(obj)
            if obj.frame_dur_n > 0
                rate = obj.frame_dur_n/obj.frame_dur_sum;
            else
                rate = NaN;
            end
        end
        
        % how many times per second the projector refreshes
        % should be 1440 for 1440 mode, for example
        function rate = frame_rate(obj)
            rate = obj.subframes*obj.video_frame_rate();
        end
        
        function set_dlp_mode(obj, mode)
            if mode == 60
                cmd = join([obj.dlpmode_path ' 60']);
            elseif mode == 180
                cmd = join([obj.dlpmode_path ' 180 ' obj.dlp_mode_180_pars]);
            elseif mode == 1440
                cmd = join([obj.dlpmode_path ' 1440 ' obj.dlp_mode_1440_pars]);
            else
                error('unknown mode');
            end
            system(cmd);
        end
    end
    methods (Access = private)
        function really_clear(obj)
            obj.set_color_mask([1 1 1]);
            Screen('FillRect', obj.window, obj.bg);
        end
        function set_color_mask(obj, mask)
            if length(mask) < 4
                mask = [mask 1];
            end
            [src_old, dst_old, ~] = Screen('BlendFunction', obj.window);
            Screen('BlendFunction', obj.window, src_old, dst_old, mask);
        end
    end
end