function [warpstruct, filterMode] = CreateDisplayWarp(window, calibfilename, showCalibOutput, varargin)
% [warpstruct, filterMode] = CreateDisplayWarp(window, calibfilename [, showCalibOutput=0]);
%
% Helper routine for Geometric display undistortion mapping, not to be
% called inside normal PTB scripts!
%
% This function reads a display calibration file 'calibfilename' and builds
% a "geometric warp function" based on the calibration information in
% 'calibfilename' for the onscreen window with handle 'window'. It returns
% a struct 'warpstruct' that defines the created warp function. You could
% pass this 'warpstruct' as a parameter to the Psychtoolbox command...
%
% PsychImaging('AddTask', viewchannel, 'GeometryCorrection', warpstruct);
%
% However, you normally do not call this routine directly from your script. Its
% called internally by the PsychImaging() command...
% 
% PsychImaging('AddTask', viewchannel, 'GeometryCorrection', calibfilename);
%
% ...in order to setup PTB's imaging pipeline for realtime geometry
% correction, based on the calibration info in the file 'calibfilename'.
%
% Example: You created a calibration file 'mycalib.mat' to undistort the
% left view display of a stereo setup. Then you could apply this
% undistortion function via the following setup code:
%
% PsychImaging('PrepareConfiguration');
% PsychImaging('AddTask', 'LeftView', 'GeometryCorrection', 'mycalib.mat');
% window = PsychImaging('OpenWindow', screenid);
%
% This would open an onscreen window just as window=Screen('OpenWindow', screenid);
% would do. It would configure the window for automatic undistortion based
% on the data in 'mycalib.mat'.
%
% Psychtoolbox provides multiple different interactive setup routines that
% allow you to create a calibration file for your setup. Currently the
% following routines are provided:
%
% DisplayUndistortionBVL.m    -- Undistortion based on 3rd order polynomial
% surface. This is the recommended calibration procedure for most cases -
% Proven in real-world use on many different display types.
%
% When used with DisplayUndistortionBVL, the GeometryCorrection takes
% up to three optional parameters:
%
% PsychImaging('AddTask', 'LeftView', 'GeometryCorrection', 'mycalib.mat' [, debug=0][, xsampling=73][,ysampling=53][, replicate=[1,1]]);
% xsampling and ysampling specify the horizontal and vertical number of subdivisions
% for the warpmesh that is used for undistortion - higher numbers give more
% close approximations but increase drawing time. replicate is a vector which
% describes how often the mesh should be replicated into horizontal and vertical
% direction. Values other than the default [1,1] only make sense for special display
% devices like ProPixx.
%
% DisplayUndistortionBezier.m -- Undistortion based on a NURBS surface (Non
% uniform rational bezier spline surface). A simple procedure.
%
% DisplayUndistortionHalfCylinder.m -- Undistortion for projection of
% images to a half-cylindrical projection surface.
%
% DisplayUndistortionSphere.m -- Undistortion for projection of
% images to a spherical or half-spherical projection surface.
%
% DisplayUndistortionCSV.m -- Import undistortion information from
% a .csv file with a warp mesh description suitable for use with NVidia's
% Warp API. This creates a compatible display warping to use of that NVidia
% technology.
%

% History:
% 19.7.2007 Written (MK).
% 17.2.2008 Added undistortion method donated by the Banks Vision Lab (MK).
% 10.3.2008 Fixed image inversion bug in BVL calibration (MK).
%  2.5.2008 Add support for bilinear texture filter shader to handle float
%           framebuffers on hw that doesn't filter float textures (MK).
% 13.4.2009 Improved support for bilinear texture filter shaders. (MK).
%           Optional 'Query' command to query last warpstruct.    (MK).
%           Support for half-cylinder projection. (MK).
% 25.8.2011 Adapt code for sphere projection undistortion to new convention
%           of Ingmar Schneider's shader code. (MK).
% 27.7.2012 Add support for DisplayUndistortionCSV() aka "NVidia Warp-API" format (MK).
% 14.3.2015 Add support for Propixx and similar devices via mesh replication to the BVL method (MK).
% 24.7.2015 Add support for scal.useUnitDisplayCoords in CSV method. (MK)
% 24.7.2015 Fix brokenness in CSV method introduced at 14.3.2015 as regression with mesh replication. (MK)

% Global GL handle for access to OpenGL constants needed in setup:
global GL;

% Cache last generated warpstruct, so code can easily query it:
persistent oldwarpstruct;

if isempty(GL)
    sca;
    error('PTB internal error: GL struct not initialized?!?');
end

% Special case of simple query of last created 'warpstruct'?
if nargin == 1
    if ~ischar(window)
        error('Single provided argument is not a command string!');
    end
    
    if ~strcmpi(window, 'Query')
        error('Single provided argument is not the command string ''Query''!');
    end
    
    % "Query" command recognized. Return last created warpstruct:
    warpstruct = oldwarpstruct;
    return;
end

if nargin < 2
    sca;
    error('PTB internal error: Must provide all parameters!');
end

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

% Is calibfilename a struct with calibration settings, or a filename of a
% calibration file?
if isstruct(calibfilename)
    % A struct: Assign it directly.
    calib = calibfilename;
else
    % Supposedly the filename of a calibration file:
    if ~ischar(calibfilename)
        error('In setup of geometry undistortion: Parameter "calibfilename" is not a filename string!');
    end
    
    % Load calibration file:
    if ~exist(calibfilename, 'file')
        sca;
        error('In setup of geometry undistortion: No such calibration file %s!', calibfilename);
    end

    calib = load(calibfilename);
end

% Preinit warpstruct:
warpstruct.glsl = [];
warpstruct.gld = [];

% Assume no need for texture filter shader:
needFilterShader = 0;
filterMode = ':Bilinear';

% Do we need a GLSL texture filter shader? We'd need one if the given
% gfx-hardware is not capable of filtering the input image buffer:
winfo = Screen('GetWindowInfo', window);
effectivebpc = 8;
if winfo.BitsPerColorComponent >= 16
    % Window is a floating point window with at least 16bpc.
    effectivebpc = 16;
    
    if winfo.BitsPerColorComponent >= 32
        % All buffers are 32 bpc for certain:
        effectivebpc = 32;
    end
    
    if (winfo.BitsPerColorComponent == 16)
        % First buffer is 16 bpc, following ones could be 32 bpc:
        if bitand(winfo.ImagingMode, kPsychUse32BPCFloatAsap)
            % All following buffers are 32bpc float. In the tradition of
            % "better safe than sorry", we assume that the warp op will use
            % one of the 32 bpc float buffers as input.
            effectivebpc = 32;            
        end
    end    
end

% Highres input buffer?
if effectivebpc > 8
    % Yes. Our input is a float texture. Check if the hardware can filter
    % textures of effectivebpc bpc in hardware:
    if effectivebpc > winfo.GLSupportsFilteringUpToBpc
        % Hardware not capable of handling such deep textures. We need to
        % create and attach our own bilinear texture filter shader:
        needFilterShader = 1;
        filterMode = '';
    end
end

% Actual setup code for display warp struct.
% ==========================================

% Type of setup depends on type of calibration:
switch(calib.warptype)
    case {'HalfCylinderProjection', 'SphereProjection'}
        % Build combo of displaylist and GLSL shader for projection of flat
        % screen image onto a half-cylinder or sphere:

        % Query effective onscreen window size:
        [winWidth, winHeight] = Screen('WindowSize', window);
                
        % Build the unwarp mesh display list within the OpenGL context of
        % Psychtoolbox:
        Screen('BeginOpenGL', window, 1);
                
        % Build a display list that corresponds to the current calibration:
        gld = glGenLists(1);
        glNewList(gld, GL.COMPILE);
        
        % "Draw" the warp-mesh once, so it gets recorded in the display list:
        if isempty(calib.rotationAngle)
            calib.rotationAngle = 0;
        end
        
        if isempty(calib.inSize)
            calib.inSize = [winWidth, winHeight];
        end
        
        if isempty(calib.inOffset)
            calib.inOffset = [0, 0];
        end
        
        if isempty(calib.outOffset)
            calib.outOffset = [0, 0];
        end
        
        if isempty(calib.outSize)
            calib.outSize = [winWidth, winHeight];
        end

        if isempty(calib.Wflat)
            calib.Wflat = 44;
        end

        if isempty(calib.R)
            calib.R = 32;
        end

        % No color gain correction:
        glColor4f(1,1,1,1);
        
        glTranslatef(calib.outOffset(1), calib.outOffset(2), 0);
        
        % Apply some rotation correction for misaligned displays:
        glTranslatef(calib.outSize(1)/2, calib.outSize(2)/2, 0);
        glRotatef(calib.rotationAngle, 0.0, 0.0, 1.0);
        glTranslatef(-calib.outSize(1)/2, -calib.outSize(2)/2, 0);
        
        % Draw a single default quad:
        glBegin(GL.QUADS)
        glTexCoord2f(0,calib.outSize(2));
        glVertex2f(0,0);

        glTexCoord2f(calib.outSize(1),calib.outSize(2));
        glVertex2f(calib.outSize(1),0);

        glTexCoord2f(calib.outSize(1),0);
        glVertex2f(calib.outSize(1),calib.outSize(2));

        glTexCoord2f(0,0);
        glVertex2f(0,calib.outSize(2));
        glEnd;

        
        % List ready - and already updated in the imaging pipeline:
        glEndList;

        Screen('EndOpenGL', window);

        % Assign display list to output warpstruct:
        warpstruct.gld = gld;

        if strcmpi(calib.warptype, 'SphereProjection')
            % Use spherical projection shader:
            warpstruct.glsl = LoadGLSLProgramFromFiles('SphereProjectionShader');
        else
            % Use cylindrical projection shader:
            warpstruct.glsl = LoadGLSLProgramFromFiles('CylinderProjectionShader');
        end
        glUseProgram(warpstruct.glsl);
        
        glUniform1i(glGetUniformLocation(warpstruct.glsl, 'doFilter'), needFilterShader);
        glUniform1i(glGetUniformLocation(warpstruct.glsl, 'Image'), 0);
        glUniform2f(glGetUniformLocation(warpstruct.glsl, 'inSize'), calib.inSize(1), calib.inSize(2));
        glUniform2f(glGetUniformLocation(warpstruct.glsl, 'inOffset'), calib.inOffset(1), calib.inOffset(2));
        glUniform2f(glGetUniformLocation(warpstruct.glsl, 'outSize'), calib.outSize(1), calib.outSize(2));

        if strcmpi(calib.warptype, 'SphereProjection')
            % Additional parameters for sphere projection:
            glUniform1f(glGetUniformLocation(warpstruct.glsl, 'Wflat'), calib.Wflat);
            glUniform1f(glGetUniformLocation(warpstruct.glsl, 'R'), calib.R);
        end        
        
        glUseProgram(0);
        
        % Ready.
        
    case {'BezierDisplayList'}
        % Build warp display list for Bezier surface based
        % calibration/remapping:
        
        Screen('BeginOpenGL', window, 1);
        
        gld = glGenLists(1);
        
        % Build a display list that corresponds to the current calibration:
        glNewList(gld, GL.COMPILE);

        glColor4f(1,1,1,1);

        subdivision = calib.subdivision;
        
        % Setup a 2D parametric grid with 'subdivision' subdivisions:
        glMapGrid2d(subdivision, 0, 1, subdivision, 0, 1);

        % Enable Bezier evaluators:
        glEnable(GL.MAP2_VERTEX_3);
        glEnable(GL.MAP2_TEXTURE_COORD_2);

        % Setup initial mapping table for texture coordinates (source image control
        % points):
        frompts = calib.frompts;
        % Establish mapping for texture coordinates:
        glMap2d(GL.MAP2_TEXTURE_COORD_2, 0, 1, 2, size(frompts,2), 0, 1, 2*size(frompts,2), size(frompts,3), frompts);

        % Establish mapping for vertex coordinates:
        topts = calib.topts;
        % Setup mapping based on current control point matrix for destination
        % points:
        glMap2d(GL.MAP2_VERTEX_3, 0, 1, 3, size(topts,2), 0, 1, 3*size(topts,2), size(topts,3), topts);
        
        % Compute the mesh based on current mappings:
        glEvalMesh2(GL.FILL, 0, subdivision, 0, subdivision);
        
        % Disable mesh evaluators:
        glDisable(GL.MAP2_VERTEX_3);
        glDisable(GL.MAP2_TEXTURE_COORD_2);

        % List ready - and already updated in the imaging pipeline:
        glEndList;

        % Assign display list to output warpstruct:
        warpstruct.gld = gld;

        % Ready.
        Screen('EndOpenGL', window);
        
    case {'BVLDisplayList'}
        % Build warp display list for calibration/remapping method
        % donated by the Banks Vision Lab:
        
        % Query effective onscreen window size:
        [winWidth, winHeight] = Screen('WindowSize', window);
        
        % Additional optional parameters provided?
        xLoomSize = [];
        yLoomSize = [];

        if nargin >= 5
            % At least two additional parameters. Really?
            if ~isempty(varargin{1})
                xLoomSize = varargin{1};
            end
            
            if ~isempty(varargin{2})
                yLoomSize = varargin{2};
            end
        end

        if (length(varargin) >= 3) && ~isempty(varargin{3})
            % Subdivision/Replication [xdiv, ydiv] given. We shall
            % generate a calibration display list which only has
            % 1/xdiv the window widht, 1/ydiv the window height,
            % but is replicated xdiv x ydiv times. E.g., a [2,2]
            % subdivision will split the window into 4 quadrants and
            % apply the undistortion independently to each quadrants.
            % This is, e.g., useful for special display modes of special
            % display devices like the ProPrixx.
            subdiv = varargin{3};
            if ~length(subdiv) == 2 || ~isnumeric(subdiv)
                error('CreateDisplayWarp: Provided optional subdiv parameter is not a [xrep, yrep] vector as required.');
            end

            xrep = subdiv(1);
            yrep = subdiv(2);

            if xrep ~=round(xrep) || yrep ~= round(yrep)
                error('CreateDisplayWarp: Provided subdiv parameter [xrep, yrep] does not contain integral values as required.');
            end

            % Adapt width and height of target area for calibration:
            winWidth = winWidth / xrep;
            winHeight = winHeight / yrep;
        else
            xrep = 1;
            yrep = 1;
        end

        % Compute vertex- and texcoord-arrays that define the mesh
        % of quadrilaterals which should be rendered (with the stimulus
        % texture applied) to create the undistortion warp:
        [xyzcalibpos, xytexcoords] = BVLComputeWarpMesh(winWidth, winHeight, calib.scal, showCalibOutput, xLoomSize, yLoomSize, xrep, yrep);

        % Build the unwarp mesh display list within the OpenGL context of
        % Psychtoolbox:
        Screen('BeginOpenGL', window, 1);
                
        % Build a display list that corresponds to the current calibration:
        gld = glGenLists(1);
        glNewList(gld, GL.COMPILE);
        
        % "Draw" the warp-mesh once, so it gets recorded in the display list:
        glColor4f(1,1,1,1);
        glEnableClientState(GL.VERTEX_ARRAY);
        glVertexPointer(2, GL.DOUBLE, 0, xyzcalibpos);
        glEnableClientState(GL.TEXTURE_COORD_ARRAY);
        glTexCoordPointer(2, GL.DOUBLE, 0, xytexcoords);

        glDrawArrays(GL.QUADS, 0, length(xyzcalibpos)/2);

        glDisableClientState(GL.TEXTURE_COORD_ARRAY);
        glDisableClientState(GL.VERTEX_ARRAY);

        % List ready - and already updated in the imaging pipeline:
        glEndList;

        Screen('EndOpenGL', window);

        % Assign display list to output warpstruct:
        warpstruct.gld = gld;

        % Ready.
        
    case {'CSVDisplayList'}
        % Build warp display list for a calibration/remapping method that
        % is compatible with the method used, e.g., by NVidia's Warp API:
        
        % Query effective onscreen window size:
        [winWidth, winHeight] = Screen('WindowSize', window);
        
        % Compute vertex- and texcoord-arrays that define the mesh
        % of quadrilaterals which should be rendered (with the stimulus
        % texture applied) to create the undistortion warp:
        [xyzcalibpos, xytexcoords] = CSVComputeWarpMesh(winWidth, winHeight, calib.scal, showCalibOutput);

        % Build the unwarp mesh display list within the OpenGL context of
        % Psychtoolbox:
        Screen('BeginOpenGL', window, 1);
        
        % Build a display list that corresponds to the current calibration:
        gld = glGenLists(1);
        glNewList(gld, GL.COMPILE);
        
        % "Draw" the warp-mesh once, so it gets recorded in the display list:
        glColor4f(1,1,1,1);
        glEnableClientState(GL.VERTEX_ARRAY);
        glVertexPointer(2, GL.DOUBLE, 0, xyzcalibpos);
        glEnableClientState(GL.TEXTURE_COORD_ARRAY);
        glTexCoordPointer(2, GL.DOUBLE, 0, xytexcoords);

        glDrawArrays(GL.QUADS, 0, length(xyzcalibpos)/2);

        glDisableClientState(GL.TEXTURE_COORD_ARRAY);
        glDisableClientState(GL.VERTEX_ARRAY);

        % List ready - and already updated in the imaging pipeline:
        glEndList;

        Screen('EndOpenGL', window);

        % Assign display list to output warpstruct:
        warpstruct.gld = gld;

        % Ready.
        
    otherwise
        sca;
        error('Unknown calibration method id: %s!', calib.warptype);
end

% Need a filtershader and don't have one assigned yet?
if (needFilterShader > 0) && isempty(warpstruct.glsl)
    % Yes. Load, create and assign our default bilinear texture filter
    % shader:
    warpstruct.glsl = LoadGLSLProgramFromFiles('BilinearTextureFilterShader');
    glUseProgram(warpstruct.glsl);
    glUniform1i(glGetUniformLocation(warpstruct.glsl, 'Image'), 0);
    glUseProgram(0);
end

% Cache created warptstruct for later queries:
oldwarpstruct = warpstruct;

% Done. Return the warpstruct:
return;

% --- Helper routines for setup of the calibration method 'BVLDisplayList' ---

function [xyzcalibpos, xytexcoords] = BVLComputeWarpMesh(windowWidth, windowHeight, scal, showCalibOutput, xLoomSize, yLoomSize, xrep, yrep)
% [xyzcalibpos, xytexcoords] = BVLComputeWarpMesh(windowWidth, windowHeight, scal, showCalibOutput, xLoomSize, yLoomSize, xrep, yrep)
%
% Internal helper routine: Called by CreateDisplayWarp.m, which is in turn
% called by PsychImaging.m. Implements the geometric display calibration
% and undistortion procedure developed by the Banks Vision Lab at UC
% Berkeley.
%
% Use the calibration information stored in 'scal', together with the
% current 'windowWidth' and 'windowHeight' of the onscreen window to
% calibrate and compute vectors of vertex coordinates and texture
% coordinates for a mesh that performs the proper "display undistortion".
%
% History:
% 02/17/08  Derived from BFloadtimecalib.m with minimal modifications. (MK)
%

% Check resolution against the calibration file resolutions:
if ((windowWidth * xrep) ~= RectWidth(scal.rect)) || ((windowHeight * yrep) ~= RectHeight(scal.rect))
    fprintf('\n\nCALIBRATION WARNING!\n');
    fprintf('Onscreen window resolution (%d, %d) does not match ', ...
        windowWidth, windowHeight);
    fprintf('the resolution used in the calibration file (%d, %d)!\n', ...
        RectWidth(scal.rect), RectHeight(scal.rect));
    fprintf('Using the window resolution to draw the stimuli, scaling down proportionally.\n');
    fprintf('\n\n');
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Calibration
% Generate the calibration vertices here so we only do this once.
% We calculate the uncalibrated vertex coordinates, in screenspace
% (pixel coordinates), then pass them into our fitting routine to
% interpolate the calibrated coordinates.

% PreCompute vertex values
if isempty(xLoomSize)
    xLoomSize   = 73; %length(XVALUES)   %Can reduce loom resolution to speed up calib
end

if isempty(yLoomSize)
    yLoomSize   = 53; %length(YVALUES)
end

% Compute sampling positions for the calibration data:
xStep       = RectWidth(scal.rect) / (xLoomSize-1);
yStep       = RectHeight(scal.rect) / (yLoomSize-1);

% Compute scale factor in case the output window size doesn't
% match the size used for generating the scal calibration data:
scaleX = windowWidth / RectWidth(scal.rect);
scaleY = windowHeight / RectHeight(scal.rect);

numVerts    = xLoomSize * yLoomSize;
% vertexCoords = Nx2 array, N rows of [x y] pairs.
vertexCoords    = zeros(numVerts, 2);
vertexCoordsFit = zeros(numVerts, 2);

% Calculate the 'uncalibrated' vertex coordinates
for y=1:yLoomSize
    for x=1:xLoomSize
        index = ((y-1) * xLoomSize) + x;
        xCoord = (x-1) * xStep;
        yCoord = (y-1) * yStep;
        vertexCoords(index, :) = [xCoord yCoord];
        %fprintf('(%f, %f)\n', vertexCoords(index, 1), vertexCoords(index, 2));
    end
end

% Some debug plots, if requested:
if showCalibOutput
    figure(9)
    hold off
    plot(scal.XCALIBDOTS, scal.YCALIBDOTS, 'b.')
    hold on
    plot(scal.SELECTXCALIBDOTS, scal.SELECTYCALIBDOTS, 'r.')
    size(scal.XCALIBDOTS)
    size(scal.YCALIBDOTS)
    size(scal.SELECTXCALIBDOTS)
    size(vertexCoords(:,1))
    size(vertexCoords(:,2))
end

% Fit mesh vertices to locations of calibrated points - Use Matlabs
% griddata fitting and interpolation routine:
if ~IsOctave
    % Matlab: Use 'v4' method - the interpolation method used by Matlab V4
    % for interpolation:
    vertexCoordsFit(:,1)= griddata(scal.XCALIBDOTS, scal.YCALIBDOTS, scal.SELECTXCALIBDOTS, vertexCoords(:,1), vertexCoords(:,2), 'v4'); %#ok<*GRIDD>
    vertexCoordsFit(:,2)= griddata(scal.XCALIBDOTS, scal.YCALIBDOTS, scal.SELECTYCALIBDOTS, vertexCoords(:,1), vertexCoords(:,2), 'v4');
else
    % Octave: griddata() is also supported by GNU/Octave, but the 'v4' method is
    % only supported by Matlab, not by Octave. 'Cubic' isn't supported
    % either, so the best we can do is to use the default 'linear' method.
    % This is problematic as it creates different results when running on
    % Octave vs. Matlab:
    % Ok - Actually it doesn't work at all on Octave, because the relevant
    % implementation of Octave's griddata() seems to be quite buggy :-(
    % TODO FIXME: Is this still true? Octave 3.4 supports 'cubic' and maybe
    % 'linear' has been fixed already?
    vertexCoordsFit(:,1)= griddata(scal.XCALIBDOTS, scal.YCALIBDOTS, scal.SELECTXCALIBDOTS, vertexCoords(:,1), vertexCoords(:,2));
    vertexCoordsFit(:,2)= griddata(scal.XCALIBDOTS, scal.YCALIBDOTS, scal.SELECTYCALIBDOTS, vertexCoords(:,1), vertexCoords(:,2));
end

% Rescale input/output positions to fit output window:
vertexCoords(:,1) = vertexCoords(:,1) * scaleX;
vertexCoordsFit(:,1) = vertexCoordsFit(:,1) * scaleX;

vertexCoords(:,2) = vertexCoords(:,2) * scaleY;
vertexCoordsFit(:,2) = vertexCoordsFit(:,2) * scaleY;

% Some debug plots, if requested:
if showCalibOutput
    figure(10)
    hold off
    plot(vertexCoords(:,1), vertexCoords(:,2), 'b.')
    hold on
    plot(vertexCoordsFit(:,1), vertexCoordsFit(:,2), 'r.')

    figure(11)
    hold off
    plot(scal.SELECTXCALIBDOTS, scal.SELECTYCALIBDOTS, 'o')
    hold on
    plot(vertexCoordsFit(:,1), vertexCoordsFit(:,2), 'r.')
end

% Fit the coordinates to the calibrated space (values are in pixels)
%    [vertexCoordsFit(:,1) vertexCoordsFit(:,2)] = BFbvlFitCoords(vertexCoords(:,1), ...
%        vertexCoords(:,2), xFitCoef_R, yFitCoef_R);

% Compute final vertex- and texcoords. Need to swap y-positions upside-down
% as our internal vertex/texcoord assignment is upside down wrt. original
% Banks lab calibration:
vertexCoords(:,2) = windowHeight - vertexCoords(:,2);

xyzcalibpos = [];
xytexcoords = [];
for x = 0:xrep-1
    for y = 0:yrep-1
        xoffset = x * windowWidth;
        yoffset = y * windowHeight;
        [newxyzcalibpos, newxytexcoords] = BVLGeneratetextcoord(yLoomSize, xLoomSize, vertexCoords, vertexCoordsFit, showCalibOutput, xoffset, yoffset);

        xyzcalibpos = [xyzcalibpos, newxyzcalibpos];
        xytexcoords = [xytexcoords, newxytexcoords];
    end
end

% Done. Return results:
return;


function [xyzcalibpos, xytexcoords]=BVLGeneratetextcoord(yLoomSize, xLoomSize, vertexCoords, vertexCoordsFit, showCalibOutput, xoffset, yoffset)
% Internal helper routine: Called by BVLComputeWarpMesh.m.
% Implements the geometric display calibration and undistortion procedure
% developed by the Banks Vision Lab at UC Berkeley.
%
% History:
% 02/17/08  Derived from BFGeneratetextcoord.m with minimal modifications. (MK)
%

numVerts = (xLoomSize-1) * (yLoomSize-1) * 4;

xyzcalibpos = zeros(1, numVerts*2);
xytexcoords = zeros(1, numVerts*2);

xtemp = zeros(1, numVerts);
ytemp = zeros(1, numVerts);

xverts= zeros(1, numVerts);
yverts= zeros(1, numVerts);

vectaddress=0;

if showCalibOutput
    figure(100);
    axis ij;
    hold on;
    plot(vertexCoords(:,1), vertexCoords(:,2), 'r.');
    plot(vertexCoordsFit(:,1), vertexCoordsFit(:,2), 'b.');
end

for y=1:(yLoomSize-1)
    for x=1:(xLoomSize-1)
        index = ((y-1) * xLoomSize) + x;

        vectaddress=vectaddress+1;

        xtemp(vectaddress)  = vertexCoords(index, 1);  %LowerLeftXTex
        ytemp(vectaddress)  = vertexCoords(index, 2);  %LowerLeftYTex

        xverts(vectaddress) = vertexCoordsFit(index, 1);  % Lower left fit coord  x
        yverts(vectaddress) = vertexCoordsFit(index, 2);  % lower left fit coord  y

        vectaddress=vectaddress+1;

        xtemp(vectaddress)  = vertexCoords(index+1, 1);  %LowerrightXTex
        ytemp(vectaddress)  = vertexCoords(index+1, 2); %LowerrightYTex

        xverts(vectaddress) = vertexCoordsFit(index+1, 1);   %lower right fit coord  x
        yverts(vectaddress) = vertexCoordsFit(index+1, 2);   %lower right fit coord  y

        vectaddress=vectaddress+1;

        xtemp(vectaddress)  = vertexCoords(index+xLoomSize+1, 1);  %UpperRightXTex
        ytemp(vectaddress)  = vertexCoords(index+xLoomSize+1, 2);  %UpperRightYTex

        xverts(vectaddress) = vertexCoordsFit(index+xLoomSize+1, 1);  %Upper right fit coord  x
        yverts(vectaddress) = vertexCoordsFit(index+xLoomSize+1, 2);  %upper right fit coord  y

        vectaddress=vectaddress+1;

        xtemp(vectaddress)  = vertexCoords(index+xLoomSize, 1);  %UpperLeft X Tex
        ytemp(vectaddress)  = vertexCoords(index+xLoomSize, 2);  %Upperleft Y tex

        xverts(vectaddress) = vertexCoordsFit(index+xLoomSize, 1);   %upper left fit coord   x
        yverts(vectaddress) = vertexCoordsFit(index+xLoomSize, 2);   %upper left fit coord   y
    end
end

xyzcalibpos(1:2:end) = xverts + xoffset;
xyzcalibpos(2:2:end) = yverts + yoffset;
xytexcoords(1:2:end) = xtemp + xoffset;
xytexcoords(2:2:end) = ytemp + yoffset;

return;

% --- End of Helper routines for setup of the calibration method 'BVLDisplayList' ---

% --- Helper routines for setup of the calibration method 'CSVDisplayList' ---

function [xyzcalibpos, xytexcoords] = CSVComputeWarpMesh(windowWidth, windowHeight, scal, showCalibOutput)
% [xyzcalibpos, xytexcoords] = CSVComputeWarpMesh(windowWidth, windowHeight, scal, showCalibOutput)
%
% Use the calibration information stored in 'scal', together with the
% current 'windowWidth' and 'windowHeight' of the onscreen window to
% generate vectors of vertex coordinates and texture coordinates for a mesh
% that performs the proper "display undistortion".
%
% History:
% 07/26/12  Derived from BVLComputeWarpMesh.m with major simplifications. (MK)
%

% Generate the calibration vertices here so we only do this once.
xLoomSize = size(scal.vcoords, 2);
yLoomSize = size(scal.vcoords, 1);
numVerts = xLoomSize * yLoomSize;

% vertexCoords = Nx2 array, N rows of [x y] pairs. Row-Major format encoding.
textureCoords = zeros(numVerts, 2);
vertexCoords  = zeros(numVerts, 2);

% Parse the matrices passed in scal and rearrange them to the format of the
% vertexCoords and textureCoords vectors: Scanning is row-major order.
if ~isfield(scal, 'useUnitDisplayCoords') || scal.useUnitDisplayCoords
    % We also scale all positions with window width and height, as the scal
    % matrices contain normalized coordinates in 0.0 - 1.0 range for display
    % width/height of a "unit display". vcoords can exceed that range or be
    % negative - they are assigned to positions outside the framebuffer.
    scaleWidth = windowWidth;
    scaleHeight = windowHeight;
else
    % scal contains absolute pixel locations, not 0-1 normalized range:
    scaleWidth = 1;
    scaleHeight = 1;
end

for y=1:yLoomSize
    for x=1:xLoomSize
        index = ((y-1) * xLoomSize) + x;
        vertexCoords(index, :)  = [scal.vcoords(y, x, 1) * scaleWidth, scal.vcoords(y, x, 2) * scaleHeight];
        textureCoords(index, :) = [scal.tcoords(y, x, 1) * scaleWidth, scal.tcoords(y, x, 2) * scaleHeight];
    end
end

% Compute final vertex- and texcoords. Need to swap y-positions upside-down
% as our internal vertex/texcoord assignment is upside down wrt. original
% calibration:
textureCoords(:,2) = windowHeight - textureCoords(:,2);

% Some debug plots, if requested:
if showCalibOutput
    figure;
    hold on;
    axis ij;
    plot(textureCoords(:,1), textureCoords(:,2), 'b.');
    plot(vertexCoords(:,1), vertexCoords(:,2), 'r.');
    hold off;
end

% textureCoords are regularly spaced texture 2D coordinates.
% vertexCoords are irregularly placed vertex 2D coordinates.
[xyzcalibpos, xytexcoords] = BVLGeneratetextcoord(yLoomSize, xLoomSize, textureCoords, vertexCoords, showCalibOutput, 0 , 0);

% Done. Return results:
return;

% --- End of helper routines for setup of the calibration method 'CSVDisplayList' ---
