/*-------------------------------------------------------------------------
 * drawElements Quality Program OpenGL ES Utilities
 * ------------------------------------------------
 *
 * Copyright 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

'use strict';
goog.provide('functional.gles3.es3fShaderDerivateTests');
goog.require('framework.delibs.debase.deMath');
goog.require('framework.delibs.debase.deRandom');
goog.require('framework.delibs.debase.deString');
goog.require('framework.opengl.gluDrawUtil');
goog.require('framework.opengl.gluPixelTransfer');
goog.require('framework.opengl.gluShaderProgram');
goog.require('framework.opengl.gluShaderUtil');
goog.require('framework.opengl.gluTexture');
goog.require('framework.common.tcuInterval');
goog.require('framework.common.tcuFloat');
goog.require('framework.common.tcuLogImage');
goog.require('framework.common.tcuMatrix');
goog.require('framework.common.tcuPixelFormat');
goog.require('framework.common.tcuRGBA');
goog.require('framework.common.tcuStringTemplate');
goog.require('framework.common.tcuSurface');
goog.require('framework.common.tcuTexture');
goog.require('framework.common.tcuTextureUtil');
goog.require('framework.common.tcuTestCase');
goog.require('modules.shared.glsShaderRenderCase');

goog.scope(function() {
    var es3fShaderDerivateTests = functional.gles3.es3fShaderDerivateTests;
    var deMath = framework.delibs.debase.deMath;
    var deRandom = framework.delibs.debase.deRandom;
    var deString = framework.delibs.debase.deString;
    var gluDrawUtil = framework.opengl.gluDrawUtil;
    var gluPixelTransfer = framework.opengl.gluPixelTransfer;
    var gluShaderProgram = framework.opengl.gluShaderProgram;
    var gluShaderUtil = framework.opengl.gluShaderUtil;
    var gluTexture = framework.opengl.gluTexture;
    var tcuInterval = framework.common.tcuInterval;
    var tcuFloat = framework.common.tcuFloat;
    var tcuLogImage = framework.common.tcuLogImage;
    var tcuMatrix = framework.common.tcuMatrix;
    var tcuPixelFormat = framework.common.tcuPixelFormat;
    var tcuRGBA = framework.common.tcuRGBA;
    var tcuStringTemplate = framework.common.tcuStringTemplate;
    var tcuSurface = framework.common.tcuSurface;
    var tcuTexture = framework.common.tcuTexture;
    var tcuTextureUtil = framework.common.tcuTextureUtil;
    var tcuTestCase = framework.common.tcuTestCase;
    var glsShaderRenderCase = modules.shared.glsShaderRenderCase;

    /** @const {number} */ es3fShaderDerivateTests.VIEWPORT_WIDTH = 167;
    /** @const {number} */ es3fShaderDerivateTests.VIEWPORT_HEIGHT = 103;
    /** @const {number} */ es3fShaderDerivateTests.FBO_WIDTH = 99;
    /** @const {number} */ es3fShaderDerivateTests.FBO_HEIGHT = 133;
    /** @const {number} */ es3fShaderDerivateTests.MAX_FAILED_MESSAGES = 10;
    /** @const {number} */ es3fShaderDerivateTests.INTERPOLATION_LOST_BITS = 3; // number mantissa of bits allowed to be lost in varying interpolation
    /**
     * @enum {number}
     */
    es3fShaderDerivateTests.DerivateFunc = {
        DFDX: 0,
        DFDY: 1,
        FWIDTH: 2
    };

    /**
     * @enum {number}
     */
    es3fShaderDerivateTests.SurfaceType = {
        DEFAULT_FRAMEBUFFER: 0,
        UNORM_FBO: 1,
        FLOAT_FBO: 2 // \note Uses RGBA32UI fbo actually, since FP rendertargets are not in core spec.
    };

    /**
     * @enum {number}
     */
    es3fShaderDerivateTests.VerificationLogging = {
        LOG_ALL: 0,
        LOG_NOTHING: 1
    };

    /**
     * @param {es3fShaderDerivateTests.DerivateFunc} func
     * @return {string}
     */
    es3fShaderDerivateTests.getDerivateFuncName = function(func) {
        switch (func) {
            case es3fShaderDerivateTests.DerivateFunc.DFDX: return 'dFdx';
            case es3fShaderDerivateTests.DerivateFunc.DFDY: return 'dFdy';
            case es3fShaderDerivateTests.DerivateFunc.FWIDTH: return 'fwidth';
            default: throw new Error('Derivate Func not supported.');
        }
    };

    /**
     * @param {es3fShaderDerivateTests.DerivateFunc} func
     * @return {string}
     */
    es3fShaderDerivateTests.getDerivateFuncCaseName = function(func) {
        switch (func) {
            case es3fShaderDerivateTests.DerivateFunc.DFDX: return 'dfdx';
            case es3fShaderDerivateTests.DerivateFunc.DFDY: return 'dfdy';
            case es3fShaderDerivateTests.DerivateFunc.FWIDTH: return 'fwidth';
            default: throw new Error('Derivate Func not supported.');
        }
    };

    /**
     * @param {?gluShaderUtil.DataType} type
     * @return {Array<boolean>}
     */
    es3fShaderDerivateTests.getDerivateMask = function(type) {
        switch (type) {
            case gluShaderUtil.DataType.FLOAT: return [true, false, false, false];
            case gluShaderUtil.DataType.FLOAT_VEC2: return [true, true, false, false];
            case gluShaderUtil.DataType.FLOAT_VEC3: return [true, true, true, false];
            case gluShaderUtil.DataType.FLOAT_VEC4: return [true, true, true, true];
            default: throw new Error('Data Type not supported.');
        }
    };

    /**
     * @param {tcuTexture.ConstPixelBufferAccess} surface
     * @param {Array<number>} derivScale
     * @param {Array<number>} derivBias
     * @param {number} x
     * @param {number} y
     * @return {Array<number>}
     */
    es3fShaderDerivateTests.readDerivate = function(surface, derivScale, derivBias, x, y) {
        return deMath.divide(deMath.subtract(surface.getPixel(x, y), derivBias), derivScale);
    };

    /**
     * @param {Array<number>} v
     * @return {Array<number>}
     */
    es3fShaderDerivateTests.getCompExpBits = function(v) {
        return [tcuFloat.newFloat32(v[0]).exponentBits(),
            tcuFloat.newFloat32(v[1]).exponentBits(),
            tcuFloat.newFloat32(v[2]).exponentBits(),
            tcuFloat.newFloat32(v[3]).exponentBits()];
    };

    /**
     * @param {number} value
     * @param {number} numAccurateBits
     * @return {number}
     */
    es3fShaderDerivateTests.computeFloatingPointError = function(value, numAccurateBits) {
        /** @type {number} */ var numGarbageBits = 23 - numAccurateBits;
        /** @type {number} */ var mask = (1 << numGarbageBits) - 1 ;
        /** @type {number} */ var exp = tcuFloat.newFloat32(value).exponent();

        return (new tcuFloat.deFloat()).construct(1, exp, (1 << 23) | mask).getValue() - (new tcuFloat.deFloat()).construct(1, exp, 1 << 23).getValue();
    };

    /**
       * @param {?gluShaderUtil.precision} precision
     * @return {number}
     */
    es3fShaderDerivateTests.getNumMantissaBits = function(precision) {
        switch (precision) {
            case gluShaderUtil.precision.PRECISION_HIGHP: return 23;
            case gluShaderUtil.precision.PRECISION_MEDIUMP: return 10;
            case gluShaderUtil.precision.PRECISION_LOWP: return 6;
            default:
                throw new Error('Precision not supported: ' + precision);
        }
    };

    /**
     * @param {?gluShaderUtil.precision} precision
     * @return {number}
     */
    es3fShaderDerivateTests.getMinExponent = function(precision) {
        switch (precision) {
            case gluShaderUtil.precision.PRECISION_HIGHP: return -126;
            case gluShaderUtil.precision.PRECISION_MEDIUMP: return -14;
            case gluShaderUtil.precision.PRECISION_LOWP: return -8;
            default:
                throw new Error('Precision not supported: ' + precision);
        }
    };

    /**
     * @param {number} exp
     * @param {number} numMantissaBits
     * @return {number}
     */
    es3fShaderDerivateTests.getSingleULPForExponent = function(exp, numMantissaBits) {
        if (numMantissaBits > 0) {
            assertMsgOptions(numMantissaBits <= 23, 'numMantissaBits must be less or equal than 23.', false, true);

            /** @type {number} */ var ulpBitNdx = 23 - numMantissaBits;

            return (new tcuFloat.deFloat()).construct(1, exp, (1 << 23) | (1 << ulpBitNdx)).getValue() - (new tcuFloat.deFloat()).construct(1, exp, 1 << 23).getValue();
        } else {
            assertMsgOptions(numMantissaBits === 0, 'numMantissaBits must equal to 0.', false, true);
            return (new tcuFloat.deFloat()).construct(1, exp, (1 << 23)).getValue()
        }
    };

    /**
     * @param {number} value
     * @param {number} numMantissaBits
     * @return {number}
     */
    es3fShaderDerivateTests.getSingleULPForValue = function(value, numMantissaBits) {
        /** @type {number} */ var exp = (new tcuFloat.deFloat().deFloatNumber(value)).exponent();
        return es3fShaderDerivateTests.getSingleULPForExponent(exp, numMantissaBits);
    };

    /**
     * @param {number} value
     * @param {number} minExponent
     * @param {number} numAccurateBits
     * @return {number}
     */
    es3fShaderDerivateTests.convertFloorFlushToZero = function(value, minExponent, numAccurateBits) {
        if (value === 0.0) {
            return 0.0;
        } else {
            /** @type {tcuFloat.deFloat} */ var inputFloat = new tcuFloat.deFloat().deFloatNumber(value);
            /** @type {number} */ var numTruncatedBits = 23 - numAccurateBits;
            /** @type {number} */ var truncMask = (1 << numTruncatedBits) - 1;

            if (value > 0.0) {
                if (value > 0.0 && (new tcuFloat.deFloat().deFloatNumber(value)).exponent() < minExponent) {
                    // flush to zero if possible
                    return 0.0;
                } else {
                    // just mask away non-representable bits
                    return (new tcuFloat.deFloat()).construct(1, inputFloat.exponent(), inputFloat.mantissa() & ~truncMask).getValue();
                }
            } else {
                if (inputFloat.mantissa() & truncMask) {
                    // decrement one ulp if truncated bits are non-zero (i.e. if value is not representable)
                    return (new tcuFloat.deFloat()).construct(-1, inputFloat.exponent(), inputFloat.mantissa() & ~truncMask).getValue() - es3fShaderDerivateTests.getSingleULPForExponent(inputFloat.exponent(), numAccurateBits);
                } else {
                    // value is representable, no need to do anything
                    return value;
                }
            }
        }
    };

    /**
     * @param {number} value
     * @param {number} minExponent
     * @param {number} numAccurateBits
     * @return {number}
     */
    es3fShaderDerivateTests.convertCeilFlushToZero = function(value, minExponent, numAccurateBits) {
        return -es3fShaderDerivateTests.convertFloorFlushToZero(-value, minExponent, numAccurateBits);
    };

    /**
     * @param {number} value
     * @param {number} numUlps
     * @param {number} numMantissaBits
     * @return {number}
     */
    es3fShaderDerivateTests.addErrorUlp = function(value, numUlps, numMantissaBits) {
        return value + numUlps * es3fShaderDerivateTests.getSingleULPForValue(value, numMantissaBits);
    };

    /**
     * @param {?gluShaderUtil.precision} precision
     * @param {Array<number>} valueMin
     * @param {Array<number>} valueMax
     * @param {Array<number>} expectedDerivate
     * @return {Array<number>}
     */
    es3fShaderDerivateTests.getDerivateThreshold = function(precision, valueMin, valueMax, expectedDerivate) {
        /** @type {number} */ var baseBits = es3fShaderDerivateTests.getNumMantissaBits(precision);
        /** @type {Array<number>} */ var derivExp = es3fShaderDerivateTests.getCompExpBits(expectedDerivate);
        /** @type {Array<number>} */ var maxValueExp = deMath.max(es3fShaderDerivateTests.getCompExpBits(valueMin), es3fShaderDerivateTests.getCompExpBits(valueMax));
        /** @type {Array<number>} */ var numBitsLost = deMath.subtract(maxValueExp, deMath.min(maxValueExp, derivExp));
        /** @type {Array<number>} */
        var numAccurateBits = deMath.max(
            deMath.addScalar(
                deMath.subtract(
                    [baseBits, baseBits, baseBits, baseBits],
                    numBitsLost),
                -es3fShaderDerivateTests.INTERPOLATION_LOST_BITS),
            [0, 0, 0, 0]);

        return [es3fShaderDerivateTests.computeFloatingPointError(expectedDerivate[0], numAccurateBits[0]),
                es3fShaderDerivateTests.computeFloatingPointError(expectedDerivate[1], numAccurateBits[1]),
                es3fShaderDerivateTests.computeFloatingPointError(expectedDerivate[2], numAccurateBits[2]),
                es3fShaderDerivateTests.computeFloatingPointError(expectedDerivate[3], numAccurateBits[3])];
    };

    /**
     * @param {tcuTexture.ConstPixelBufferAccess} result
     * @param {tcuTexture.PixelBufferAccess} errorMask
     * @param {?gluShaderUtil.DataType} dataType
     * @param {Array<number>} reference
     * @param {Array<number>} threshold
     * @param {Array<number>} scale
     * @param {Array<number>} bias
     * @param {es3fShaderDerivateTests.VerificationLogging=} logPolicy
     * @return {boolean}
     */
    es3fShaderDerivateTests.verifyConstantDerivate = function(result, errorMask, dataType, reference, threshold, scale, bias, logPolicy) {
        logPolicy = logPolicy === undefined ? es3fShaderDerivateTests.VerificationLogging.LOG_ALL : logPolicy;
        /** @type {Array<boolean>} */ var mask = deMath.logicalNotBool(es3fShaderDerivateTests.getDerivateMask(dataType));
        /** @type {number} */ var numFailedPixels = 0;

        if (logPolicy === es3fShaderDerivateTests.VerificationLogging.LOG_ALL)
            bufferedLogToConsole('Expecting ' + reference + ' with threshold ' + threshold);

        for (var y = 0; y < result.getHeight(); y++) {
            for (var x = 0; x < result.getWidth(); x++) {
                /** @type {Array<number>} */ var resDerivate = es3fShaderDerivateTests.readDerivate(result, scale, bias, x, y);
                /** @type {boolean} */
                var isOk = deMath.boolAll(
                    deMath.logicalOrBool(
                        deMath.lessThanEqual(
                            deMath.abs(deMath.subtract(reference, resDerivate)),
                            threshold),
                        mask));

                if (!isOk) {
                    if (numFailedPixels < es3fShaderDerivateTests.MAX_FAILED_MESSAGES && logPolicy === es3fShaderDerivateTests.VerificationLogging.LOG_ALL)
                        bufferedLogToConsole('FAIL: got ' + resDerivate + ', diff = ' + deMath.abs(deMath.subtract(reference, resDerivate)) + ', at x = ' + x + ', y = ' + y);
                    numFailedPixels += 1;
                    errorMask.setPixel(tcuRGBA.RGBA.red.toVec(), x, y);
                }
            }
        }

        if (numFailedPixels >= es3fShaderDerivateTests.MAX_FAILED_MESSAGES && logPolicy === es3fShaderDerivateTests.VerificationLogging.LOG_ALL)
            bufferedLogToConsole('...');

        if (numFailedPixels > 0 && logPolicy === es3fShaderDerivateTests.VerificationLogging.LOG_ALL)
            bufferedLogToConsole('FAIL: found ' + numFailedPixels + ' failed pixels');

        return numFailedPixels === 0;
    };

    /**
     *      .-----.
     *      | s_x |
     *  M x | s_y |
     *      | 1.0 |
     *      '-----'
     * @struct
     * @constructor
     */
    es3fShaderDerivateTests.Linear2DFunctionEvaluator = function() {
        /** @type {tcuMatrix.Matrix} */ this.matrix = new tcuMatrix.Matrix(4, 3);
    };

    es3fShaderDerivateTests.Linear2DFunctionEvaluator.prototype.evaluateAt = function(screenX, screenY) {
        /** @type {Array<number>} */ var position = [screenX, screenY, 1.0];
        return tcuMatrix.multiplyMatVec(this.matrix, position);
    };

    /**
     * @param {tcuTexture.ConstPixelBufferAccess} result
     * @param {tcuTexture.PixelBufferAccess} errorMask
     * @param {?gluShaderUtil.DataType} dataType
     * @param {?gluShaderUtil.precision} precision
     * @param {Array<number>} derivScale
     * @param {Array<number>} derivBias
     * @param {Array<number>} surfaceThreshold
     * @param {es3fShaderDerivateTests.DerivateFunc} derivateFunc
     * @param {es3fShaderDerivateTests.Linear2DFunctionEvaluator} func
     * @return {boolean}
     */
    es3fShaderDerivateTests.reverifyConstantDerivateWithFlushRelaxations = function(result, errorMask, dataType, precision, derivScale, derivBias, surfaceThreshold, derivateFunc, func) {
        assertMsgOptions(result.getWidth() === errorMask.getWidth(), 'Dimensions of result and errorMask inconsistent.', false, true);
        assertMsgOptions(result.getHeight() === errorMask.getHeight(), 'Dimensions of result and errorMask inconsistent.', false, true);
        assertMsgOptions(derivateFunc === es3fShaderDerivateTests.DerivateFunc.DFDX || derivateFunc === es3fShaderDerivateTests.DerivateFunc.DFDY, 'Derivate Function should be DFDX or DFDY.', false, true);

        /** @type {Array<number>} */ var red = [255, 0, 0, 255];
        /** @type {Array<number>} */ var green = [0, 255, 0, 255];
        /** @type {number} */ var divisionErrorUlps = 2.5;

        /** @type {number} */ var numComponents = gluShaderUtil.getDataTypeScalarTypeAsDataType(dataType);
        /** @type {number} */ var numBits = es3fShaderDerivateTests.getNumMantissaBits(precision);
        /** @type {number} */ var minExponent = es3fShaderDerivateTests.getMinExponent(precision);

        /** @type {number} */ var numVaryingSampleBits = numBits - es3fShaderDerivateTests.INTERPOLATION_LOST_BITS;
        /** @type {number} */ var numFailedPixels = 0;

        errorMask.clear(green);

        // search for failed pixels
        for (var y = 0; y < result.getHeight(); ++y)
        for (var x = 0; x < result.getWidth(); ++x) {
            //                 flushToZero?(f2z?(functionValueCurrent) - f2z?(functionValueBefore))
            // flushToZero? ( ------------------------------------------------------------------------ +- 2.5 ULP )
            //                                                  dx

            /** @type {Array<number>} */ var resultDerivative = es3fShaderDerivateTests.readDerivate(result, derivScale, derivBias, x, y);

            // sample at the front of the back pixel and the back of the front pixel to cover the whole area of
            // legal sample positions. In general case this is NOT OK, but we know that the target funtion is
            // (mostly*) linear which allows us to take the sample points at arbitrary points. This gets us the
            // maximum difference possible in exponents which are used in error bound calculations.
            // * non-linearity may happen around zero or with very high function values due to subnorms not
            //   behaving well.
            /** @type {Array<number>} */ var functionValueForward = (derivateFunc === es3fShaderDerivateTests.DerivateFunc.DFDX) ?
                                                        (func.evaluateAt(x + 2.0, y + 0.5)) :
                                                        (func.evaluateAt(x + 0.5, y + 2.0));
            /** @type {Array<number>} */ var functionValueBackward = (derivateFunc === es3fShaderDerivateTests.DerivateFunc.DFDX) ?
                                                        (func.evaluateAt(x - 1.0, y + 0.5)) :
                                                        (func.evaluateAt(x + 0.5, y - 1.0));

            /** @type {boolean} */ var anyComponentFailed = false;

            // check components separately
            for (var c = 0; c < numComponents; ++c) {
                // interpolation value range
                /** @type {tcuInterval.Interval} */ var forwardComponent = tcuInterval.withIntervals(
                    new tcuInterval.Interval(es3fShaderDerivateTests.convertFloorFlushToZero(functionValueForward[c], minExponent, numVaryingSampleBits)),
                    new tcuInterval.Interval(es3fShaderDerivateTests.convertCeilFlushToZero(functionValueForward[c], minExponent, numVaryingSampleBits))
                );

                /** @type {tcuInterval.Interval} */ var backwardComponent = tcuInterval.withIntervals(
                    new tcuInterval.Interval(es3fShaderDerivateTests.convertFloorFlushToZero(functionValueBackward[c], minExponent, numVaryingSampleBits)),
                    new tcuInterval.Interval(es3fShaderDerivateTests.convertCeilFlushToZero(functionValueBackward[c], minExponent, numVaryingSampleBits)));

                /** @type {number} */
                var maxValueExp = Math.max(
                        (new tcuFloat.deFloat().deFloatNumber(forwardComponent.lo())).exponent(),
                        (new tcuFloat.deFloat().deFloatNumber(forwardComponent.hi())).exponent(),
                        (new tcuFloat.deFloat().deFloatNumber(backwardComponent.lo())).exponent(),
                        (new tcuFloat.deFloat().deFloatNumber(backwardComponent.hi())).exponent());

                // subtraction in nominator will likely cause a cancellation of the most
                // significant bits. Apply error bounds.
                /** @type {tcuInterval.Interval} */ var nominator = tcuInterval.Interval.operatorSub(forwardComponent, backwardComponent);
                /** @type {number} */ var nominatorLoExp = (new tcuFloat.deFloat().deFloatNumber(nominator.lo())).exponent();
                /** @type {number} */ var nominatorHiExp = (new tcuFloat.deFloat().deFloatNumber(nominator.hi())).exponent();
                /** @type {number} */ var nominatorLoBitsLost = maxValueExp - nominatorLoExp;
                /** @type {number} */ var nominatorHiBitsLost = maxValueExp - nominatorHiExp;
                /** @type {number} */ var nominatorLoBits = Math.max(0, numBits - nominatorLoBitsLost);
                /** @type {number} */ var nominatorHiBits = Math.max(0, numBits - nominatorHiBitsLost);

                /** @type {tcuInterval.Interval} */ var nominatorRange = tcuInterval.withIntervals(
                    new tcuInterval.Interval(es3fShaderDerivateTests.convertFloorFlushToZero(nominator.lo(), minExponent, nominatorLoBits)),
                    new tcuInterval.Interval(es3fShaderDerivateTests.convertCeilFlushToZero(nominator.hi(), minExponent, nominatorHiBits)));
                //
                /** @type {tcuInterval.Interval} */ var divisionRange = tcuInterval.Interval.operatorDiv(nominatorRange, new tcuInterval.Interval(3.0)); // legal sample area is anywhere within this and neighboring pixels (i.e. size = 3)
                /** @type {tcuInterval.Interval} */ var divisionResultRange = tcuInterval.withIntervals(
                    new tcuInterval.Interval(es3fShaderDerivateTests.convertFloorFlushToZero(es3fShaderDerivateTests.addErrorUlp(divisionRange.lo(), -divisionErrorUlps, numBits), minExponent, numBits)),
                    new tcuInterval.Interval(es3fShaderDerivateTests.convertCeilFlushToZero(es3fShaderDerivateTests.addErrorUlp(divisionRange.hi(), divisionErrorUlps, numBits), minExponent, numBits)));
                /** @type {tcuInterval.Interval} */ var finalResultRange = tcuInterval.withIntervals(
                    new tcuInterval.Interval(divisionResultRange.lo() - surfaceThreshold[c]),
                    new tcuInterval.Interval(divisionResultRange.hi() + surfaceThreshold[c]));

                if (resultDerivative[c] >= finalResultRange.lo() && resultDerivative[c] <= finalResultRange.hi()) {
                    // value ok
                } else {
                    if (numFailedPixels < es3fShaderDerivateTests.MAX_FAILED_MESSAGES)
                        bufferedLogToConsole('Error in pixel at ' + x + ', ' + y + ' with component ' + c + ' (channel ' + ('rgba'[c]) + ')\n' +
                            '\tGot pixel value ' + result.getPixelInt(x, y) + '\n' +
                            '\t\tdFd' + ((derivateFunc === es3fShaderDerivateTests.DerivateFunc.DFDX) ? 'x' : 'y') + ' ~= ' + resultDerivative[c] + '\n' +
                            '\t\tdifference to a valid range: ' +
                            ((resultDerivative[c] < finalResultRange.lo()) ? '-' : '+') +
                            ((resultDerivative[c] < finalResultRange.lo()) ? (finalResultRange.lo() - resultDerivative[c]) : (resultDerivative[c] - finalResultRange.hi())) +
                            '\n' +
                            '\tDerivative value range:\n' +
                            '\t\tMin: ' + finalResultRange.lo() + '\n' +
                            '\t\tMax: ' + finalResultRange.hi() + '\n');

                    ++numFailedPixels;
                    anyComponentFailed = true;
                }
            }

            if (anyComponentFailed)
                errorMask.setPixel(red, x, y);
        }

        if (numFailedPixels >= es3fShaderDerivateTests.MAX_FAILED_MESSAGES)
            bufferedLogToConsole('...');

        if (numFailedPixels > 0)
            bufferedLogToConsole('FAIL: found ' + numFailedPixels + ' failed pixels');

        return numFailedPixels === 0;
    };

    /**
     * @constructor
     * @extends {tcuTestCase.DeqpTest}
     * @param {string} name
     * @param {string} description
     */
    es3fShaderDerivateTests.TriangleDerivateCase = function(name, description) {
        tcuTestCase.DeqpTest.call(this, name, description);
        /** @type {?gluShaderUtil.DataType} */ this.m_dataType = null;
        /** @type {?gluShaderUtil.precision} */ this.m_precision = null;

        /** @type {?gluShaderUtil.DataType} */ this.m_coordDataType = null;
        /** @type {?gluShaderUtil.precision} */ this.m_coordPrecision = null;

        /** @type {string} */ this.m_fragmentSrc;

        /** @type {Array<number>} */ this.m_coordMin = [];
        /** @type {Array<number>} */ this.m_coordMax = [];
        /** @type {Array<number>} */ this.m_derivScale = [];
        /** @type {Array<number>} */ this.m_derivBias = [];

        /** @type {es3fShaderDerivateTests.SurfaceType} */ this.m_surfaceType = es3fShaderDerivateTests.SurfaceType.DEFAULT_FRAMEBUFFER;
        /** @type {number} */ this.m_numSamples = 0;
        /** @type {number} */ this.m_hint = gl.DONT_CARE;

        assertMsgOptions(this.m_surfaceType !== es3fShaderDerivateTests.SurfaceType.DEFAULT_FRAMEBUFFER || this.m_numSamples === 0, 'Did not expect surfaceType = DEFAULT_FRAMEBUFFER or numSamples = 0', false, true);
    };

    es3fShaderDerivateTests.TriangleDerivateCase.prototype = Object.create(tcuTestCase.DeqpTest.prototype);
    es3fShaderDerivateTests.TriangleDerivateCase.prototype.constructor = es3fShaderDerivateTests.TriangleDerivateCase;

    es3fShaderDerivateTests.TriangleDerivateCase.prototype.deinit = function() {};

    /** @param {WebGLProgram} program */
    es3fShaderDerivateTests.TriangleDerivateCase.prototype.setupRenderState = function(program) {};

    /**
     * @param {?gluShaderUtil.DataType} coordType
     * @param {?gluShaderUtil.precision} precision
     * @return {string}
     */
    es3fShaderDerivateTests.genVertexSource = function(coordType, precision) {
        assertMsgOptions(gluShaderUtil.isDataTypeFloatOrVec(coordType), 'Coord Type not supported', false, true);

        /** @type {string} */ var vertexTmpl = '' +
            '#version 300 es\n' +
            'in highp vec4 a_position;\n' +
            'in ${PRECISION} ${DATATYPE} a_coord;\n' +
            'out ${PRECISION} ${DATATYPE} v_coord;\n' +
            'void main (void)\n' +
            '{\n' +
            ' gl_Position = a_position;\n' +
            ' v_coord = a_coord;\n' +
            '}\n';

        /** @type {Object} */ var vertexParams = {};

        vertexParams['PRECISION'] = gluShaderUtil.getPrecisionName(precision);
        vertexParams['DATATYPE'] = gluShaderUtil.getDataTypeName(coordType);

        return tcuStringTemplate.specialize(vertexTmpl, vertexParams);
    };

    /**
     * @return {Array<number>}
     */
    es3fShaderDerivateTests.TriangleDerivateCase.prototype.getViewportSize = function() {
        if (this.m_surfaceType === es3fShaderDerivateTests.SurfaceType.DEFAULT_FRAMEBUFFER) {
            /** @type {number} */ var width = Math.min(gl.drawingBufferWidth, es3fShaderDerivateTests.VIEWPORT_WIDTH);
            /** @type {number} */ var height = Math.min(gl.drawingBufferHeight, es3fShaderDerivateTests.VIEWPORT_HEIGHT);
            return [width, height];
        } else
            return [es3fShaderDerivateTests.FBO_WIDTH, es3fShaderDerivateTests.FBO_HEIGHT];
    };

    /**
     * @return {tcuTestCase.IterateResult}
     */
    es3fShaderDerivateTests.TriangleDerivateCase.prototype.iterate = function() {
        /** @type {gluShaderProgram.ShaderProgram} */ var program = new gluShaderProgram.ShaderProgram(gl, gluShaderProgram.makeVtxFragSources(es3fShaderDerivateTests.genVertexSource(this.m_coordDataType, this.m_coordPrecision), this.m_fragmentSrc));
        /** @type {deRandom.Random} */ var rnd = new deRandom.Random(deString.deStringHash(this.name) ^ 0xbbc24);
        /** @type {boolean} */ var useFbo = this.m_surfaceType != es3fShaderDerivateTests.SurfaceType.DEFAULT_FRAMEBUFFER;
        /** @type {number} */ var fboFormat = this.m_surfaceType === es3fShaderDerivateTests.SurfaceType.FLOAT_FBO ? gl.RGBA32UI : gl.RGBA8;
        /** @type {Array<number>} */ var viewportSize = this.getViewportSize();
        /** @type {number} */ var viewportX = useFbo ? 0 : rnd.getInt(0, gl.drawingBufferWidth - viewportSize[0]);
        /** @type {number} */ var viewportY = useFbo ? 0 : rnd.getInt(0, gl.drawingBufferHeight - viewportSize[1]);
        /** @type {?WebGLFramebuffer} */ var fbo = null;
        /** @type {?WebGLRenderbuffer} */ var rbo = null;
        /** @type {tcuTexture.TextureLevel} */ var result = null;

        bufferedLogToConsole(program.getProgramInfo().infoLog);

        if (!program.isOk())
            assertMsgOptions(false, 'Compile failed', false, true);

        if (useFbo) {
            bufferedLogToConsole('Rendering to FBO, format = ' + wtu.glEnumToString(gl, fboFormat) + ', samples = ' + this.m_numSamples);

            fbo = gl.createFramebuffer();
            rbo = gl.createRenderbuffer();

            gl.bindRenderbuffer(gl.RENDERBUFFER, rbo);
            gl.renderbufferStorageMultisample(gl.RENDERBUFFER, this.m_numSamples, fboFormat, viewportSize[0], viewportSize[1]);
            gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
            gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, rbo);
        } else {
            /** @type {tcuPixelFormat.PixelFormat} */ var pixelFormat = tcuPixelFormat.PixelFormatFromContext(gl);

            bufferedLogToConsole('Rendering to default framebuffer\n' +
                '\tColor depth: R=' + pixelFormat.redBits + ', G=' + pixelFormat.greenBits + ', B=' + pixelFormat.blueBits + ', A=' + pixelFormat.alphaBits);
        }

        bufferedLogToConsole('in: ' + this.m_coordMin + ' ' + this.m_coordMax + '\n' +
            'v_coord.x = in.x * x\n' +
            'v_coord.y = in.y * y\n' +
            'v_coord.z = in.z * (x+y)/2\n' +
            'v_coord.w = in.w * (1 - (x+y)/2)\n' +
            '\n' +
            'u_scale: ' + this.m_derivScale + ', u_bias: ' + this.m_derivBias + ' (displayed values have scale/bias removed)' +
            'Viewport: ' + viewportSize[0] + 'x' + viewportSize[1] +
            'gl.FRAGMENT_SHADER_DERIVATE_HINT: ' + wtu.glEnumToString(gl, this.m_hint));
        // Draw
        /** @type {Array<number>} */ var positions = [
            -1.0, -1.0, 0.0, 1.0,
            -1.0, 1.0, 0.0, 1.0,
            1.0, -1.0, 0.0, 1.0,
            1.0, 1.0, 0.0, 1.0
        ];

        /** @type {Array<number>} */ var coords =[
            this.m_coordMin[0], this.m_coordMin[1], this.m_coordMin[2], this.m_coordMax[3],
            this.m_coordMin[0], this.m_coordMax[1], (this.m_coordMin[2] + this.m_coordMax[2]) * 0.5, (this.m_coordMin[3]+this.m_coordMax[3]) * 0.5,
            this.m_coordMax[0], this.m_coordMin[1], (this.m_coordMin[2] + this.m_coordMax[2]) * 0.5, (this.m_coordMin[3]+this.m_coordMax[3]) * 0.5,
            this.m_coordMax[0], this.m_coordMax[1], this.m_coordMax[2], this.m_coordMin[3]
        ];

        /** @type {Array<gluDrawUtil.VertexArrayBinding>} */ var vertexArrays = [
            gluDrawUtil.newFloatVertexArrayBinding('a_position', 4, 4, 0, positions),
            gluDrawUtil.newFloatVertexArrayBinding('a_coord', 4, 4, 0, coords)
        ];

        /** @type {Array<number>} */ var indices = [0, 2, 1, 2, 3, 1];

        gl.clearColor(0.125, 0.25, 0.5, 1.0);
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
        gl.disable(gl.DITHER);

        gl.useProgram(program.getProgram());

        /** @type {WebGLUniformLocation} */ var scaleLoc = gl.getUniformLocation(program.getProgram(), 'u_scale');
        /** @type {WebGLUniformLocation} */ var biasLoc = gl.getUniformLocation(program.getProgram(), 'u_bias');

        switch (this.m_dataType) {
            case gluShaderUtil.DataType.FLOAT:
                gl.uniform1f(scaleLoc, this.m_derivScale[0]);
                gl.uniform1f(biasLoc, this.m_derivBias[0]);
                break;

            case gluShaderUtil.DataType.FLOAT_VEC2:
                gl.uniform2fv(scaleLoc, this.m_derivScale.slice(0,2));
                gl.uniform2fv(biasLoc, this.m_derivBias.slice(0,2));
                break;

            case gluShaderUtil.DataType.FLOAT_VEC3:
                gl.uniform3fv(scaleLoc, this.m_derivScale.slice(0,3));
                gl.uniform3fv(biasLoc, this.m_derivBias.slice(0,3));
                break;

            case gluShaderUtil.DataType.FLOAT_VEC4:
                gl.uniform4fv(scaleLoc, this.m_derivScale);
                gl.uniform4fv(biasLoc, this.m_derivBias);
                break;

            default:
                throw new Error('Data Type not supported: ' + this.m_dataType);
        }

        glsShaderRenderCase.setupDefaultUniforms(program.getProgram());
        this.setupRenderState(program.getProgram());

        gl.hint(gl.FRAGMENT_SHADER_DERIVATIVE_HINT, this.m_hint);

        gl.viewport(viewportX, viewportY, viewportSize[0], viewportSize[1]);
        gluDrawUtil.draw(gl, program.getProgram(), vertexArrays, gluDrawUtil.triangles(indices));

        // Read back results

        /** @type {boolean} */ var isMSAA = useFbo && this.m_numSamples > 0;
        /** @type {?WebGLFramebuffer} */ var resFbo = null;
        /** @type {?WebGLRenderbuffer} */ var resRbo = null;

        // Resolve if necessary
        if (isMSAA) {
            resFbo = gl.createFramebuffer();
            resRbo = gl.createRenderbuffer();

            gl.bindRenderbuffer(gl.RENDERBUFFER, resRbo);
            gl.renderbufferStorageMultisample(gl.RENDERBUFFER, 0, fboFormat, viewportSize[0], viewportSize[1]);
            gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, resFbo);
            gl.framebufferRenderbuffer(gl.DRAW_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, resRbo);

            gl.blitFramebuffer(0, 0, viewportSize[0], viewportSize[1], 0, 0, viewportSize[0], viewportSize[1], gl.COLOR_BUFFER_BIT, gl.NEAREST);

            gl.bindFramebuffer(gl.READ_FRAMEBUFFER, resFbo);
        }
        switch (this.m_surfaceType) {
            case es3fShaderDerivateTests.SurfaceType.DEFAULT_FRAMEBUFFER:
            case es3fShaderDerivateTests.SurfaceType.UNORM_FBO:
                var dataFormat = new tcuTexture.TextureFormat(tcuTexture.ChannelOrder.RGBA, tcuTexture.ChannelType.UNORM_INT8);
                result = new tcuTexture.TextureLevel(dataFormat, viewportSize[0], viewportSize[1]);
                gluPixelTransfer.readPixels(gl, viewportX, viewportY, dataFormat, result);
                break;

            case es3fShaderDerivateTests.SurfaceType.FLOAT_FBO:
                var dataFormat = new tcuTexture.TextureFormat(tcuTexture.ChannelOrder.RGBA, tcuTexture.ChannelType.FLOAT);
                var transferFormat = new tcuTexture.TextureFormat(tcuTexture.ChannelOrder.RGBA, tcuTexture.ChannelType.UNSIGNED_INT32);
                result = new tcuTexture.TextureLevel(dataFormat, viewportSize[0], viewportSize[1]);
                gluPixelTransfer.readPixels(gl, viewportX, viewportY, transferFormat, result);
                break;

            default:
                throw new Error('Surface Type not supported: ' + this.m_surfaceType);
        }

        // Verify
        /** @type {tcuSurface.Surface} */
        var errorMask = new tcuSurface.Surface(result.getWidth(), result.getHeight());

        errorMask.getAccess().clear(tcuRGBA.RGBA.green.toVec());

        /** @type {boolean} */ var isOk = this.verify(result.getAccess(), errorMask.getAccess());

        if (!isOk) {
            tcuLogImage.logImage('Rendered', 'Rendered image', result.getAccess());
            tcuLogImage.logImage('ErrorMask', 'Error mask', errorMask.getAccess());
            testFailedOptions('Fail', false);
        } else
            testPassedOptions('Pass', true);

        // Cleaning up buffers
        gl.deleteFramebuffer(fbo);
        gl.deleteRenderbuffer(rbo);
        gl.deleteFramebuffer(resFbo);
        gl.deleteRenderbuffer(resRbo);

        return tcuTestCase.IterateResult.STOP;
    };

    /**
     * @return {Array<number>}
     */
    es3fShaderDerivateTests.TriangleDerivateCase.prototype.getSurfaceThreshold = function() {
        switch (this.m_surfaceType) {
            case es3fShaderDerivateTests.SurfaceType.DEFAULT_FRAMEBUFFER:
                /** @type {tcuPixelFormat.PixelFormat} */ var pixelFormat = tcuPixelFormat.PixelFormatFromContext(gl);
                /** @type {Array<number>} */ var channelBits = [pixelFormat.redBits, pixelFormat.greenBits, pixelFormat.blueBits, pixelFormat.alphaBits];
                /** @type {Array<number>} */ var intThreshold = deMath.arrayShiftLeft([1, 1, 1, 1], deMath.subtract([8, 8, 8, 8], channelBits));
                /** @type {Array<number>} */ var normThreshold = deMath.scale(intThreshold, 1.0/255.0);

                return normThreshold;

            case es3fShaderDerivateTests.SurfaceType.UNORM_FBO: return deMath.scale([1, 1, 1, 1], 1.0/255.0);
            case es3fShaderDerivateTests.SurfaceType.FLOAT_FBO: return [0.0, 0.0, 0.0, 0.0];
            default:
                assertMsgOptions(false, 'Surface Type not supported. Falling back to default retun value [0.0, 0.0, 0.0, 0.0]', false, false);
                return [0.0, 0.0, 0.0, 0.0];
        }
    };

    /**
     * @constructor
     * @extends {es3fShaderDerivateTests.TriangleDerivateCase}
     * @param {string} name
     * @param {string} description
     * @param {es3fShaderDerivateTests.DerivateFunc} func
     * @param {gluShaderUtil.DataType} type
     */
    es3fShaderDerivateTests.ConstantDerivateCase = function(name, description, func, type) {
        es3fShaderDerivateTests.TriangleDerivateCase.call(this, name, description);
        /** @type {es3fShaderDerivateTests.DerivateFunc} */ this.m_func = func;
        this.m_dataType = type;
        this.m_precision = gluShaderUtil.precision.PRECISION_HIGHP;
        this.m_coordDataType = this.m_dataType;
        this.m_coordPrecision = this.m_precision;
    };

    es3fShaderDerivateTests.ConstantDerivateCase.prototype = Object.create(es3fShaderDerivateTests.TriangleDerivateCase.prototype);
    es3fShaderDerivateTests.ConstantDerivateCase.prototype.constructor = es3fShaderDerivateTests.ConstantDerivateCase;

    es3fShaderDerivateTests.ConstantDerivateCase.prototype.init = function() {
        /** @type {string} */ var fragmentTmpl = '' +
            '#version 300 es\n' +
            'layout(location = 0) out mediump vec4 o_color;\n' +
            'uniform ${PRECISION} ${DATATYPE} u_scale;\n' +
            'uniform ${PRECISION} ${DATATYPE} u_bias;\n' +
            'void main (void)\n' +
            '{\n' +
            ' ${PRECISION} ${DATATYPE} res = ${FUNC}(${VALUE}) * u_scale + u_bias;\n' +
            ' o_color = ${CAST_TO_OUTPUT};\n' +
            '}\n';

        /** @type {Object} */ var fragmentParams = {};
        fragmentParams['PRECISION'] = gluShaderUtil.getPrecisionName(this.m_precision);
        fragmentParams['DATATYPE'] = gluShaderUtil.getDataTypeName(this.m_dataType);
        fragmentParams['FUNC'] = es3fShaderDerivateTests.getDerivateFuncName(this.m_func);
        fragmentParams['VALUE'] = this.m_dataType === gluShaderUtil.DataType.FLOAT_VEC4 ? 'vec4(1.0, 7.2, -1e5, 0.0)' :
            this.m_dataType === gluShaderUtil.DataType.FLOAT_VEC3 ? 'vec3(1e2, 8.0, 0.01)' :
            this.m_dataType === gluShaderUtil.DataType.FLOAT_VEC2 ? 'vec2(-0.0, 2.7)' :
            '7.7';
        fragmentParams['CAST_TO_OUTPUT'] = this.m_dataType === gluShaderUtil.DataType.FLOAT_VEC4 ? 'res' :
            this.m_dataType === gluShaderUtil.DataType.FLOAT_VEC3 ? 'vec4(res, 1.0)' :
            this.m_dataType === gluShaderUtil.DataType.FLOAT_VEC2 ? 'vec4(res, 0.0, 1.0)' :
            'vec4(res, 0.0, 0.0, 1.0)';

        this.m_fragmentSrc = tcuStringTemplate.specialize(fragmentTmpl, fragmentParams);

        this.m_derivScale = [1e3, 1e3, 1e3, 1e3];
        this.m_derivBias = [0.5, 0.5, 0.5, 0.5];
    };

    /**
     * @param {tcuTexture.ConstPixelBufferAccess} result
     * @param {tcuTexture.PixelBufferAccess} errorMask
     * @return {boolean}
     */
    es3fShaderDerivateTests.ConstantDerivateCase.prototype.verify = function(result, errorMask) {
        /** @type {Array<number>} */ var reference = [0.0, 0.0, 0.0, 0.0]; // Derivate of constant argument should always be 0
        /** @type {Array<number>} */ var threshold = deMath.divide(this.getSurfaceThreshold(), deMath.abs(this.m_derivScale));
        return es3fShaderDerivateTests.verifyConstantDerivate(result, errorMask, this.m_dataType,
            reference, threshold, this.m_derivScale, this.m_derivBias);
    };

    /**
     * @constructor
     * @extends {es3fShaderDerivateTests.TriangleDerivateCase}
     * @param {string} name
     * @param {string} description
     * @param {es3fShaderDerivateTests.DerivateFunc} func
     * @param {gluShaderUtil.DataType} type
     * @param {gluShaderUtil.precision} precision
     * @param {number} hint
     * @param {es3fShaderDerivateTests.SurfaceType} surfaceType
     * @param {number} numSamples
     * @param {string} fragmentSrcTmpl
     */
    es3fShaderDerivateTests.LinearDerivateCase = function(name, description, func, type, precision, hint, surfaceType, numSamples, fragmentSrcTmpl) {
        es3fShaderDerivateTests.TriangleDerivateCase.call(this, name, description);
        /** @type {es3fShaderDerivateTests.DerivateFunc} */ this.m_func = func;
        /** @type {string} */ this.m_fragmentTmpl = fragmentSrcTmpl;
        this.m_dataType = type;
        this.m_precision = precision;
        this.m_coordDataType = this.m_dataType;
        this.m_coordPrecision = this.m_precision;
        this.m_hint = hint;
        this.m_surfaceType = surfaceType;
        this.m_numSamples = numSamples;
    };

    es3fShaderDerivateTests.LinearDerivateCase.prototype = Object.create(es3fShaderDerivateTests.TriangleDerivateCase.prototype);
    es3fShaderDerivateTests.LinearDerivateCase.prototype.constructor = es3fShaderDerivateTests.LinearDerivateCase;

    es3fShaderDerivateTests.LinearDerivateCase.prototype.init = function() {
        /** @type {Array<number>} */ var viewportSize = this.getViewportSize();
        /** @type {number} */ var w = viewportSize[0];
        /** @type {number} */ var h = viewportSize[1];
        /** @type {boolean} */ var packToInt = this.m_surfaceType === es3fShaderDerivateTests.SurfaceType.FLOAT_FBO;

        /** @type {Object} */ var fragmentParams = {};
        fragmentParams['OUTPUT_TYPE'] = gluShaderUtil.getDataTypeName(packToInt ? gluShaderUtil.DataType.UINT_VEC4 : gluShaderUtil.DataType.FLOAT_VEC4);
        fragmentParams['OUTPUT_PREC'] = gluShaderUtil.getPrecisionName(packToInt ? gluShaderUtil.precision.PRECISION_HIGHP : this.m_precision);
        fragmentParams['PRECISION'] = gluShaderUtil.getPrecisionName(this.m_precision);
        fragmentParams['DATATYPE'] = gluShaderUtil.getDataTypeName(this.m_dataType);
        fragmentParams['FUNC'] = es3fShaderDerivateTests.getDerivateFuncName(this.m_func);

        if (packToInt) {
            fragmentParams['CAST_TO_OUTPUT'] = this.m_dataType === gluShaderUtil.DataType.FLOAT_VEC4 ? 'floatBitsToUint(res)' :
                this.m_dataType === gluShaderUtil.DataType.FLOAT_VEC3 ? 'floatBitsToUint(vec4(res, 1.0))' :
                this.m_dataType === gluShaderUtil.DataType.FLOAT_VEC2 ? 'floatBitsToUint(vec4(res, 0.0, 1.0))' :
                'floatBitsToUint(vec4(res, 0.0, 0.0, 1.0))';
        } else {
            fragmentParams['CAST_TO_OUTPUT'] = this.m_dataType === gluShaderUtil.DataType.FLOAT_VEC4 ? 'res' :
                this.m_dataType === gluShaderUtil.DataType.FLOAT_VEC3 ? 'vec4(res, 1.0)' :
                this.m_dataType === gluShaderUtil.DataType.FLOAT_VEC2 ? 'vec4(res, 0.0, 1.0)' :
                'vec4(res, 0.0, 0.0, 1.0)';
        }

        this.m_fragmentSrc = tcuStringTemplate.specialize(this.m_fragmentTmpl, fragmentParams);

        switch (this.m_precision) {
            case gluShaderUtil.precision.PRECISION_HIGHP:
                this.m_coordMin = [-97., 0.2, 71., 74.];
                this.m_coordMax = [-13.2, -77., 44., 76.];
                break;

            case gluShaderUtil.precision.PRECISION_MEDIUMP:
                this.m_coordMin = [-37.0, 47., -7., 0.0];
                this.m_coordMax = [-1.0, 12., 7., 19.];
                break;

            case gluShaderUtil.precision.PRECISION_LOWP:
                this.m_coordMin = [0.0, -1.0, 0.0, 1.0];
                this.m_coordMax = [1.0, 1.0, -1.0, -1.0];
                break;

            default:
                throw new Error('Precision not supported: ' + this.m_precision);
        }

        if (this.m_surfaceType === es3fShaderDerivateTests.SurfaceType.FLOAT_FBO) {
            // No scale or bias used for accuracy.
            this.m_derivScale = [1.0, 1.0, 1.0, 1.0];
            this.m_derivBias = [0.0, 0.0, 0.0, 0.0];
        } else {
            // Compute scale - bias that normalizes to 0..1 range.
            /** @type {Array<number>} */ var dx = deMath.divide(deMath.subtract(this.m_coordMax, this.m_coordMin), [w, w, w * 0.5, -w * 0.5]);
            /** @type {Array<number>} */ var dy = deMath.divide(deMath.subtract(this.m_coordMax, this.m_coordMin), [h, h, h * 0.5, -h * 0.5]);

            switch (this.m_func) {
                case es3fShaderDerivateTests.DerivateFunc.DFDX:
                    this.m_derivScale = deMath.divide([0.5, 0.5, 0.5, 0.5], dx);
                    break;

                case es3fShaderDerivateTests.DerivateFunc.DFDY:
                    this.m_derivScale = deMath.divide([0.5, 0.5, 0.5, 0.5], dy);
                    break;

                case es3fShaderDerivateTests.DerivateFunc.FWIDTH:
                    this.m_derivScale = deMath.divide([0.5, 0.5, 0.5, 0.5], deMath.add(deMath.abs(dx), deMath.abs(dy)));
                    break;

                default:
                    throw new Error('Derivate Function not supported: ' + this.m_func);
            }

            this.m_derivBias = [0.0, 0.0, 0.0, 0.0];
        }
    };

    /**
     * @param {tcuTexture.ConstPixelBufferAccess} result
     * @param {tcuTexture.PixelBufferAccess} errorMask
     * @return {boolean}
     */
    es3fShaderDerivateTests.LinearDerivateCase.prototype.verify = function(result, errorMask) {
        /** @type {Array<number>} */ var xScale = [1.0, 0.0, 0.5, -0.5];
        /** @type {Array<number>} */ var yScale = [0.0, 1.0, 0.5, -0.5];
        /** @type {Array<number>} */ var surfaceThreshold = deMath.divide(this.getSurfaceThreshold(), deMath.abs(this.m_derivScale));

        /** @type {number} */ var w;
        /** @type {number} */ var h;
        /** @type {Array<number>} */ var reference;
        /** @type {Array<number>} */ var threshold;

        if (this.m_func === es3fShaderDerivateTests.DerivateFunc.DFDX || this.m_func === es3fShaderDerivateTests.DerivateFunc.DFDY) {
            /** @type {boolean} */ var isX = this.m_func === es3fShaderDerivateTests.DerivateFunc.DFDX;
            /** @type {number} */ var div = isX ? result.getWidth() : result.getHeight();
            /** @type {Array<number>} */ var scale = isX ? xScale : yScale;
            reference = deMath.multiply(deMath.scale(deMath.subtract(this.m_coordMax, this.m_coordMin), 1/div), scale);
            /** @type {Array<number>} */ var opThreshold = es3fShaderDerivateTests.getDerivateThreshold(this.m_precision, deMath.multiply(this.m_coordMin, scale), deMath.multiply(this.m_coordMax, scale), reference);
            threshold = deMath.max(surfaceThreshold, opThreshold);
            bufferedLogToConsole('Verifying result image.\n' +
                '\tValid derivative is ' + reference + ' with threshold ' + threshold);

            // short circuit if result is strictly within the normal value error bounds.
            // This improves performance significantly.
            if (es3fShaderDerivateTests.verifyConstantDerivate(result, errorMask,
                this.m_dataType, reference, threshold, this.m_derivScale,
                this.m_derivBias, es3fShaderDerivateTests.VerificationLogging.LOG_NOTHING)) {
                bufferedLogToConsole('No incorrect derivatives found, result valid.');
                return true;
            }

            // some pixels exceed error bounds calculated for normal values. Verify that these
            // potentially invalid pixels are in fact valid due to (for example) subnorm flushing.

            bufferedLogToConsole('Initial verification failed, verifying image by calculating accurate error bounds for each result pixel.\n' +
                '\tVerifying each result derivative is within its range of legal result values.');

            /** @type {Array<number>} */ var viewportSize = this.getViewportSize();
            /** @type {Array<number>} */ var valueRamp = deMath.subtract(this.m_coordMax, this.m_coordMin);
            /** @type {es3fShaderDerivateTests.Linear2DFunctionEvaluator} */ var function_ = new es3fShaderDerivateTests.Linear2DFunctionEvaluator();
            w = viewportSize[0];
            h = viewportSize[1];

            function_.matrix.setRow(0, [valueRamp[0] / w, 0.0, this.m_coordMin[0]]);
            function_.matrix.setRow(1, [0.0, valueRamp[1] / h, this.m_coordMin[1]]);
            function_.matrix.setRow(2, deMath.scale([valueRamp[2] / w, valueRamp[2] / h, this.m_coordMin[2] + this.m_coordMin[2]], 1 / 2.0));
            function_.matrix.setRow(3, deMath.scale([-valueRamp[3] / w, -valueRamp[3] / h, this.m_coordMax[3] + this.m_coordMax[3]], 1 / 2.0));

            return es3fShaderDerivateTests.reverifyConstantDerivateWithFlushRelaxations(
                result, errorMask, this.m_dataType, this.m_precision, this.m_derivScale,
                this.m_derivBias, surfaceThreshold, this.m_func, function_);
        } else {
            assertMsgOptions(this.m_func === es3fShaderDerivateTests.DerivateFunc.FWIDTH, 'Expected DerivateFunc.FWIDTH', false, true);
            w = result.getWidth();
            h = result.getHeight();

            /** @type {Array<number>} */ var dx = deMath.multiply(deMath.scale(deMath.subtract(this.m_coordMax, this.m_coordMin), 1 / w), xScale);
            /** @type {Array<number>} */ var dy = deMath.multiply(deMath.scale(deMath.subtract(this.m_coordMax, this.m_coordMin), 1 / h), yScale);
            reference = deMath.add(deMath.abs(dx), deMath.abs(dy));
            /** @type {Array<number>} */ var dxThreshold = es3fShaderDerivateTests.getDerivateThreshold(this.m_precision, deMath.multiply(this.m_coordMin, xScale), deMath.multiply(this.m_coordMax, xScale), dx);
            /** @type {Array<number>} */ var dyThreshold = es3fShaderDerivateTests.getDerivateThreshold(this.m_precision, deMath.multiply(this.m_coordMin, yScale), deMath.multiply(this.m_coordMax, yScale), dy);
            threshold = deMath.max(surfaceThreshold, deMath.max(dxThreshold, dyThreshold));

            return es3fShaderDerivateTests.verifyConstantDerivate(result, errorMask, this.m_dataType,
                                          reference, threshold, this.m_derivScale, this.m_derivBias);
        }
    };

    /**
     * @constructor
     * @extends {es3fShaderDerivateTests.TriangleDerivateCase}
     * @param {string} name
     * @param {string} description
     * @param {es3fShaderDerivateTests.DerivateFunc} func
     * @param {gluShaderUtil.DataType} type
     * @param {gluShaderUtil.precision} precision
     * @param {number} hint
     * @param {es3fShaderDerivateTests.SurfaceType} surfaceType
     * @param {number} numSamples
     */
    es3fShaderDerivateTests.TextureDerivateCase = function(name, description, func, type, precision, hint, surfaceType, numSamples) {
        es3fShaderDerivateTests.TriangleDerivateCase.call(this, name, description);
        /** @type {es3fShaderDerivateTests.DerivateFunc} */ this.m_func = func;
        /** @type {gluTexture.Texture2D} */ this.m_texture = null;
        /** @type {Array<number>} */ this.m_texValueMin = [];
        /** @type {Array<number>} */ this.m_texValueMax = [];
        this.m_dataType = type;
        this.m_precision = precision;
        this.m_coordDataType = gluShaderUtil.DataType.FLOAT_VEC2;
        this.m_coordPrecision = gluShaderUtil.precision.PRECISION_HIGHP;
        this.m_hint = hint;
        this.m_surfaceType = surfaceType;
        this.m_numSamples = numSamples;
    };

    es3fShaderDerivateTests.TextureDerivateCase.prototype = Object.create(es3fShaderDerivateTests.TriangleDerivateCase.prototype);
    es3fShaderDerivateTests.TextureDerivateCase.prototype.constructor = es3fShaderDerivateTests.TextureDerivateCase;

    es3fShaderDerivateTests.TextureDerivateCase.prototype.deinit = function() {
            this.m_texture = null;
    };

    es3fShaderDerivateTests.TextureDerivateCase.prototype.init = function() {
        // Generate shader
        /** @type {string} */ var fragmentTmpl = '' +
            '#version 300 es\n' +
            'in highp vec2 v_coord;\n' +
            'layout(location = 0) out ${OUTPUT_PREC} ${OUTPUT_TYPE} o_color;\n' +
            'uniform ${PRECISION} sampler2D u_sampler;\n' +
            'uniform ${PRECISION} ${DATATYPE} u_scale;\n' +
            'uniform ${PRECISION} ${DATATYPE} u_bias;\n' +
            'void main (void)\n' +
            '{\n' +
            ' ${PRECISION} vec4 tex = texture(u_sampler, v_coord);\n' +
            ' ${PRECISION} ${DATATYPE} res = ${FUNC}(tex${SWIZZLE}) * u_scale + u_bias;\n' +
            ' o_color = ${CAST_TO_OUTPUT};\n' +
            '}\n';

        /** @type {boolean} */ var packToInt = this.m_surfaceType === es3fShaderDerivateTests.SurfaceType.FLOAT_FBO;
        /** @type {Object} */ var fragmentParams = {};
        /** @type {Array<number>} */ var viewportSize;
        fragmentParams['OUTPUT_TYPE'] = gluShaderUtil.getDataTypeName(packToInt ? gluShaderUtil.DataType.UINT_VEC4 : gluShaderUtil.DataType.FLOAT_VEC4);
        fragmentParams['OUTPUT_PREC'] = gluShaderUtil.getPrecisionName(packToInt ? gluShaderUtil.precision.PRECISION_HIGHP : this.m_precision);
        fragmentParams['PRECISION'] = gluShaderUtil.getPrecisionName(this.m_precision);
        fragmentParams['DATATYPE'] = gluShaderUtil.getDataTypeName(this.m_dataType);
        fragmentParams['FUNC'] = es3fShaderDerivateTests.getDerivateFuncName(this.m_func);
        fragmentParams['SWIZZLE'] = this.m_dataType === gluShaderUtil.DataType.FLOAT_VEC4 ? '' :
            this.m_dataType === gluShaderUtil.DataType.FLOAT_VEC3 ? '.xyz' :
            this.m_dataType === gluShaderUtil.DataType.FLOAT_VEC2 ? '.xy' :
            '.x';

        if (packToInt) {
            fragmentParams['CAST_TO_OUTPUT'] = this.m_dataType === gluShaderUtil.DataType.FLOAT_VEC4 ? 'floatBitsToUint(res)' :
                this.m_dataType === gluShaderUtil.DataType.FLOAT_VEC3 ? 'floatBitsToUint(vec4(res, 1.0))' :
                this.m_dataType === gluShaderUtil.DataType.FLOAT_VEC2 ? 'floatBitsToUint(vec4(res, 0.0, 1.0))' :
                'floatBitsToUint(vec4(res, 0.0, 0.0, 1.0))';
        } else {
            fragmentParams['CAST_TO_OUTPUT'] = this.m_dataType === gluShaderUtil.DataType.FLOAT_VEC4 ? 'res' :
                this.m_dataType === gluShaderUtil.DataType.FLOAT_VEC3 ? 'vec4(res, 1.0)' :
                this.m_dataType === gluShaderUtil.DataType.FLOAT_VEC2 ? 'vec4(res, 0.0, 1.0)' :
                'vec4(res, 0.0, 0.0, 1.0)';
        }

        this.m_fragmentSrc = tcuStringTemplate.specialize(fragmentTmpl, fragmentParams);

        // Texture size matches viewport and nearest sampling is used. Thus texture sampling
        // is equal to just interpolating the texture value range.

        // Determine value range for texture.

        switch (this.m_precision) {
            case gluShaderUtil.precision.PRECISION_HIGHP:
                this.m_texValueMin = [-97., 0.2, 71., 74.];
                this.m_texValueMax = [-13.2, -77., 44., 76.];
                break;

            case gluShaderUtil.precision.PRECISION_MEDIUMP:
                this.m_texValueMin = [-37.0, 47., -7., 0.0];
                this.m_texValueMax = [-1.0, 12., 7., 19.];
                break;

            case gluShaderUtil.precision.PRECISION_LOWP:
                this.m_texValueMin = [0.0, -1.0, 0.0, 1.0];
                this.m_texValueMax = [1.0, 1.0, -1.0, -1.0];
                break;

            default:
                throw new Error(false, 'Precision not supported:' + this.m_precision);
        }

        // Lowp and mediump cases use RGBA16F format, while highp uses RGBA32F.
        viewportSize = this.getViewportSize();
        assertMsgOptions(!this.m_texture, 'Texture not null', false, true);
        this.m_texture = gluTexture.texture2DFromInternalFormat(gl, this.m_precision === gluShaderUtil.precision.PRECISION_HIGHP ? gl.RGBA32F : gl.RGBA16F, viewportSize[0], viewportSize[1]);
        this.m_texture.getRefTexture().allocLevel(0);

        // Texture coordinates
        this.m_coordMin = [0.0, 0.0, 0.0, 0.0];
        this.m_coordMax = [1.0, 1.0, 1.0, 1.0];

        // Fill with gradients.
        /** @type {tcuTexture.PixelBufferAccess} */ var level0 = this.m_texture.getRefTexture().getLevel(0);
        for (var y = 0; y < level0.getHeight(); y++) {
            for (var x = 0; x < level0.getWidth(); x++) {
                /** @type {number} */ var xf = (x + 0.5) / level0.getWidth();
                /** @type {number} */ var yf = (y + 0.5) / level0.getHeight();
                /** @type {Array<number>} */ var s = [xf, yf, (xf + yf) / 2.0, 1.0 - (xf + yf) / 2.0];

                level0.setPixel(deMath.add(this.m_texValueMin, deMath.multiply(deMath.subtract(this.m_texValueMax, this.m_texValueMin), s)), x, y);
            }
        }

        this.m_texture.upload();

        if (this.m_surfaceType === es3fShaderDerivateTests.SurfaceType.FLOAT_FBO) {
            // No scale or bias used for accuracy.
            this.m_derivScale = [1.0, 1.0, 1.0, 1.0];
            this.m_derivBias = [0.0, 0.0, 0.0, 0.0];
        } else {
            // Compute scale - bias that normalizes to 0..1 range.
            viewportSize = this.getViewportSize();
            /** @type {number} */ var w = viewportSize[0];
            /** @type {number} */ var h = viewportSize[1];
            /** @type {Array<number>} */ var dx = deMath.divide(deMath.subtract(this.m_texValueMax, this.m_texValueMin), [w, w, w * 0.5, -w * 0.5]);
            /** @type {Array<number>} */ var dy = deMath.divide(deMath.subtract(this.m_texValueMax, this.m_texValueMin), [h, h, h * 0.5, -h * 0.5]);

            switch (this.m_func) {
                case es3fShaderDerivateTests.DerivateFunc.DFDX:
                    this.m_derivScale = deMath.divide([0.5, 0.5, 0.5, 0.5], dx);
                    break;

                case es3fShaderDerivateTests.DerivateFunc.DFDY:
                    this.m_derivScale = deMath.divide([0.5, 0.5, 0.5, 0.5], dy);
                    break;

                case es3fShaderDerivateTests.DerivateFunc.FWIDTH:
                    this.m_derivScale = deMath.divide([0.5, 0.5, 0.5, 0.5], deMath.add(deMath.abs(dx), deMath.abs(dy)));
                    break;

                default:
                    throw new Error('Derivate Function not supported: ' + this.m_func);
            }

            this.m_derivBias = [0.0, 0.0, 0.0, 0.0];
        }
    };

    /**
     * @param {WebGLProgram} program
     */
    es3fShaderDerivateTests.TextureDerivateCase.prototype.setupRenderState = function(program) {
        /** @type {number} */ var texUnit = 1;

        gl.activeTexture(gl.TEXTURE0 + texUnit);
        gl.bindTexture(gl.TEXTURE_2D, this.m_texture.getGLTexture());
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

        gl.uniform1i(gl.getUniformLocation(program, 'u_sampler'), texUnit);
    };

    /**
     * @param {tcuTexture.PixelBufferAccess} result
     * @param {tcuTexture.PixelBufferAccess} errorMask
     * @return {boolean}
     */
    es3fShaderDerivateTests.TextureDerivateCase.prototype.verify = function(result, errorMask) {
        // \note Edges are ignored in comparison
        if (result.getWidth() < 2 || result.getHeight() < 2)
            throw new Error('Too small viewport');

        /** @type {tcuTexture.PixelBufferAccess} */ var compareArea = tcuTextureUtil.getSubregion(result, 1, 1, 0, result.getWidth() - 2, result.getHeight() - 2, 1);
        /** @type {tcuTexture.PixelBufferAccess} */ var maskArea = tcuTextureUtil.getSubregion(errorMask, 1, 1, 0, errorMask.getWidth() - 2, errorMask.getHeight() - 2, 1);
        /** @type {Array<number>} */ var xScale = [1.0, 0.0, 0.5, -0.5];
        /** @type {Array<number>} */ var yScale = [0.0, 1.0, 0.5, -0.5];
        /** @type {number} */ var w = result.getWidth();
        /** @type {number} */ var h = result.getHeight();

        /** @type {Array<number>} */ var surfaceThreshold = deMath.divide(this.getSurfaceThreshold(), deMath.abs(this.m_derivScale));
        /** @type {Array<number>} */ var reference;
        /** @type {Array<number>} */ var threshold;
        if (this.m_func == es3fShaderDerivateTests.DerivateFunc.DFDX || this.m_func == es3fShaderDerivateTests.DerivateFunc.DFDY) {
            /** @type {boolean} */ var isX = this.m_func == es3fShaderDerivateTests.DerivateFunc.DFDX;
            /** @type {number} */ var div = isX ? w : h;
            /** @type {Array<number>} */ var scale = isX ? xScale : yScale;
            reference = deMath.multiply(deMath.scale(deMath.subtract(this.m_texValueMax, this.m_texValueMin), 1 / div), scale);
            /** @type {Array<number>} */ var opThreshold = es3fShaderDerivateTests.getDerivateThreshold(this.m_precision, deMath.multiply(this.m_texValueMin, scale), deMath.multiply(this.m_texValueMax, scale), reference);
            threshold = deMath.max(surfaceThreshold, opThreshold);

            bufferedLogToConsole('Verifying result image.\n'+
                '\tValid derivative is ' + reference + ' with threshold ' + threshold);

            // short circuit if result is strictly within the normal value error bounds.
            // This improves performance significantly.
            if (es3fShaderDerivateTests.verifyConstantDerivate(compareArea, maskArea, this.m_dataType,
                reference, threshold, this.m_derivScale, this.m_derivBias,
                es3fShaderDerivateTests.VerificationLogging.LOG_NOTHING)) {
                    bufferedLogToConsole('No incorrect derivatives found, result valid.');
                    return true;
            }
            // some pixels exceed error bounds calculated for normal values. Verify that these
            // potentially invalid pixels are in fact valid due to (for example) subnorm flushing.

            bufferedLogToConsole('Initial verification failed, verifying image by calculating accurate error bounds for each result pixel.\n' +
                '\tVerifying each result derivative is within its range of legal result values.');

            /** @type {Array<number>} */ var valueRamp = deMath.subtract(this.m_texValueMax, this.m_texValueMin);
            /** @type {es3fShaderDerivateTests.Linear2DFunctionEvaluator} */ var function_ = new es3fShaderDerivateTests.Linear2DFunctionEvaluator();

            function_.matrix.setRow(0, [valueRamp[0] / w, 0.0, this.m_texValueMin[0]]);
            function_.matrix.setRow(1, [0.0, valueRamp[1] / h, this.m_texValueMin[1]]);
            function_.matrix.setRow(2, deMath.scale([valueRamp[2] / w, valueRamp[2] / h, this.m_texValueMin[2] + this.m_texValueMin[2]], 1 / 2.0));
            function_.matrix.setRow(3, deMath.scale([-valueRamp[3] / w, -valueRamp[3] / h, this.m_texValueMax[3] + this.m_texValueMax[3]], 1 / 2.0));

            return es3fShaderDerivateTests.reverifyConstantDerivateWithFlushRelaxations(compareArea, maskArea, this.m_dataType, this.m_precision,
                this.m_derivScale, this.m_derivBias, surfaceThreshold, this.m_func, function_);
        } else {
            assertMsgOptions(this.m_func == es3fShaderDerivateTests.DerivateFunc.FWIDTH, 'Expected Derivate Function FWIDTH', false, true);
            /** @type {Array<number>} */ var dx = deMath.multiply(deMath.scale(deMath.subtract(this.m_texValueMax, this.m_texValueMin), 1 / w), xScale);
            /** @type {Array<number>} */ var dy = deMath.multiply(deMath.scale(deMath.subtract(this.m_texValueMax, this.m_texValueMin), 1 / h), yScale);
            reference = deMath.add(deMath.abs(dx), deMath.abs(dy));
            /** @type {Array<number>} */ var dxThreshold = es3fShaderDerivateTests.getDerivateThreshold(this.m_precision, deMath.multiply(this.m_texValueMin, xScale), deMath.multiply(this.m_texValueMax, xScale), dx);
            /** @type {Array<number>} */ var dyThreshold = es3fShaderDerivateTests.getDerivateThreshold(this.m_precision, deMath.multiply(this.m_texValueMin, yScale), deMath.multiply(this.m_texValueMax, yScale), dy);
            threshold = deMath.max(surfaceThreshold, deMath.max(dxThreshold, dyThreshold));

            return es3fShaderDerivateTests.verifyConstantDerivate(compareArea, maskArea, this.m_dataType,
                reference, threshold, this.m_derivScale, this.m_derivBias);
        };
    };

    /**
     * @constructor
     * @extends {tcuTestCase.DeqpTest}
     */
    es3fShaderDerivateTests.ShaderDerivateTests = function() {
        tcuTestCase.DeqpTest.call(this, 'derivate', 'Derivate Function Tests');
    };

    es3fShaderDerivateTests.ShaderDerivateTests.prototype = Object.create(tcuTestCase.DeqpTest.prototype);
    es3fShaderDerivateTests.ShaderDerivateTests.prototype.constructor = es3fShaderDerivateTests.ShaderDerivateTests

    /**
     * @struct
     * @constructor
     * @param {string} name
     * @param {es3fShaderDerivateTests.DerivateFunc} func
     * @param {gluShaderUtil.DataType} dataType_
     * @param {gluShaderUtil.precision} precision_
     */
    es3fShaderDerivateTests.FunctionSpec = function(name, func, dataType_, precision_) {
        this.name = name;
        this.function_ = func;
        this.dataType = dataType_;
        this.precision = precision_;
    };

    es3fShaderDerivateTests.ShaderDerivateTests.prototype.init = function() {
        var testGroup = tcuTestCase.runner.testCases;
        /**
         * @struct
         * @constructor
         * @param {string} name
         * @param {string} description
         * @param {string} source
         */
        var LinearDerivateCase = function(name, description, source) {
            /** @type {string} */ this.name = name;
            /** @type {string} */ this.description = description;
            /** @type {string} */ this.source = source;
        };

        /** @type {Array<LinearDerivateCase>} */
        var s_linearDerivateCases = [
            new LinearDerivateCase(
                'linear',
                'Basic derivate of linearly interpolated argument',
                '#version 300 es\n' +
                'in ${PRECISION} ${DATATYPE} v_coord;\n' +
                'layout(location = 0) out ${OUTPUT_PREC} ${OUTPUT_TYPE} o_color;\n' +
                'uniform ${PRECISION} ${DATATYPE} u_scale;\n' +
                'uniform ${PRECISION} ${DATATYPE} u_bias;\n' +
                'void main (void)\n' +
                '{\n' +
                ' ${PRECISION} ${DATATYPE} res = ${FUNC}(v_coord) * u_scale + u_bias;\n' +
                ' o_color = ${CAST_TO_OUTPUT};\n' +
                '}\n'),
            new LinearDerivateCase(
                'in_function',
                'Derivate of linear function argument',
                '#version 300 es\n' +
                'in ${PRECISION} ${DATATYPE} v_coord;\n' +
                'layout(location = 0) out ${OUTPUT_PREC} ${OUTPUT_TYPE} o_color;\n' +
                'uniform ${PRECISION} ${DATATYPE} u_scale;\n' +
                'uniform ${PRECISION} ${DATATYPE} u_bias;\n' +
                '\n' +
                '${PRECISION} ${DATATYPE} computeRes (${PRECISION} ${DATATYPE} value)\n' +
                '{\n' +
                ' return ${FUNC}(v_coord) * u_scale + u_bias;\n' +
                '}\n' +
                '\n' +
                'void main (void)\n' +
                '{\n' +
                ' ${PRECISION} ${DATATYPE} res = computeRes(v_coord);\n' +
                ' o_color = ${CAST_TO_OUTPUT};\n' +
                '}\n'),
            new LinearDerivateCase(
                'static_if',
                'Derivate of linearly interpolated value in static if',
                '#version 300 es\n' +
                'in ${PRECISION} ${DATATYPE} v_coord;\n' +
                'layout(location = 0) out ${OUTPUT_PREC} ${OUTPUT_TYPE} o_color;\n' +
                'uniform ${PRECISION} ${DATATYPE} u_scale;\n' +
                'uniform ${PRECISION} ${DATATYPE} u_bias;\n' +
                'void main (void)\n' +
                '{\n' +
                ' ${PRECISION} ${DATATYPE} res;\n' +
                ' if (false)\n' +
                ' res = ${FUNC}(-v_coord) * u_scale + u_bias;\n' +
                ' else\n' +
                ' res = ${FUNC}(v_coord) * u_scale + u_bias;\n' +
                ' o_color = ${CAST_TO_OUTPUT};\n' +
                '}\n'),
            new LinearDerivateCase(
                'static_loop',
                'Derivate of linearly interpolated value in static loop',
                '#version 300 es\n' +
                'in ${PRECISION} ${DATATYPE} v_coord;\n' +
                'layout(location = 0) out ${OUTPUT_PREC} ${OUTPUT_TYPE} o_color;\n' +
                'uniform ${PRECISION} ${DATATYPE} u_scale;\n' +
                'uniform ${PRECISION} ${DATATYPE} u_bias;\n' +
                'void main (void)\n' +
                '{\n' +
                ' ${PRECISION} ${DATATYPE} res = ${DATATYPE}(0.0);\n' +
                ' for (int i = 0; i < 2; i++)\n' +
                ' res += ${FUNC}(v_coord * float(i));\n' +
                ' res = res * u_scale + u_bias;\n' +
                ' o_color = ${CAST_TO_OUTPUT};\n' +
                '}\n'),
            new LinearDerivateCase(
                'static_switch',
                'Derivate of linearly interpolated value in static switch',
                '#version 300 es\n' +
                'in ${PRECISION} ${DATATYPE} v_coord;\n' +
                'layout(location = 0) out ${OUTPUT_PREC} ${OUTPUT_TYPE} o_color;\n' +
                'uniform ${PRECISION} ${DATATYPE} u_scale;\n' +
                'uniform ${PRECISION} ${DATATYPE} u_bias;\n' +
                'void main (void)\n' +
                '{\n' +
                ' ${PRECISION} ${DATATYPE} res;\n' +
                ' switch (1)\n' +
                ' {\n' +
                ' case 0: res = ${FUNC}(-v_coord) * u_scale + u_bias; break;\n' +
                ' case 1: res = ${FUNC}(v_coord) * u_scale + u_bias; break;\n' +
                ' }\n' +
                ' o_color = ${CAST_TO_OUTPUT};\n' +
                '}\n'),
            new LinearDerivateCase(
                'uniform_if',
                'Derivate of linearly interpolated value in uniform if',
                '#version 300 es\n' +
                'in ${PRECISION} ${DATATYPE} v_coord;\n' +
                'layout(location = 0) out ${OUTPUT_PREC} ${OUTPUT_TYPE} o_color;\n' +
                'uniform ${PRECISION} ${DATATYPE} u_scale;\n' +
                'uniform ${PRECISION} ${DATATYPE} u_bias;\n' +
                'uniform bool ub_true;\n' +
                'void main (void)\n' +
                '{\n' +
                ' ${PRECISION} ${DATATYPE} res;\n' +
                ' if (ub_true)\n' +
                ' res = ${FUNC}(v_coord) * u_scale + u_bias;\n' +
                ' else\n' +
                ' res = ${FUNC}(-v_coord) * u_scale + u_bias;\n' +
                ' o_color = ${CAST_TO_OUTPUT};\n' +
                '}\n'),
            new LinearDerivateCase(
                'uniform_loop',
                'Derivate of linearly interpolated value in uniform loop',
                '#version 300 es\n' +
                'in ${PRECISION} ${DATATYPE} v_coord;\n' +
                'layout(location = 0) out ${OUTPUT_PREC} ${OUTPUT_TYPE} o_color;\n' +
                'uniform ${PRECISION} ${DATATYPE} u_scale;\n' +
                'uniform ${PRECISION} ${DATATYPE} u_bias;\n' +
                'uniform int ui_two;\n' +
                'void main (void)\n' +
                '{\n' +
                ' ${PRECISION} ${DATATYPE} res = ${DATATYPE}(0.0);\n' +
                ' for (int i = 0; i < ui_two; i++)\n' +
                ' res += ${FUNC}(v_coord * float(i));\n' +
                ' res = res * u_scale + u_bias;\n' +
                ' o_color = ${CAST_TO_OUTPUT};\n' +
                '}\n'),
            new LinearDerivateCase(
                'uniform_switch',
                'Derivate of linearly interpolated value in uniform switch',
                '#version 300 es\n' +
                'in ${PRECISION} ${DATATYPE} v_coord;\n' +
                'layout(location = 0) out ${OUTPUT_PREC} ${OUTPUT_TYPE} o_color;\n' +
                'uniform ${PRECISION} ${DATATYPE} u_scale;\n' +
                'uniform ${PRECISION} ${DATATYPE} u_bias;\n' +
                'uniform int ui_one;\n' +
                'void main (void)\n' +
                '{\n' +
                ' ${PRECISION} ${DATATYPE} res;\n' +
                ' switch (ui_one)\n' +
                ' {\n' +
                ' case 0: res = ${FUNC}(-v_coord) * u_scale + u_bias; break;\n' +
                ' case 1: res = ${FUNC}(v_coord) * u_scale + u_bias; break;\n' +
                ' }\n' +
                ' o_color = ${CAST_TO_OUTPUT};\n' +
                '}\n')
        ];

        /**
         * @struct
         * @constructor
         * @param {string} name
         * @param {es3fShaderDerivateTests.SurfaceType} surfaceType
         * @param {number} numSamples
         */
        var FboConfig = function(name, surfaceType, numSamples) {
            /** @type {string} */ this.name = name;
            /** @type {es3fShaderDerivateTests.SurfaceType} */ this.surfaceType = surfaceType;
            /** @type {number} */ this.numSamples = numSamples;
        };

        /** @type {Array<FboConfig>} */ var s_fboConfigs = [
            new FboConfig('fbo', es3fShaderDerivateTests.SurfaceType.DEFAULT_FRAMEBUFFER, 0),
            new FboConfig('fbo_msaa2', es3fShaderDerivateTests.SurfaceType.UNORM_FBO, 2),
            new FboConfig('fbo_msaa4', es3fShaderDerivateTests.SurfaceType.UNORM_FBO, 4),
            new FboConfig('fbo_float', es3fShaderDerivateTests.SurfaceType.FLOAT_FBO, 0)
        ];

        /**
         * @struct
         * @constructor
         * @param {string} name
         * @param {number} hint
         */
        var Hint = function(name, hint) {
            /** @type {string} */ this.name = name;
            /** @type {number} */ this.hint = hint;
        };

        /** @type {Array<Hint>} */ var s_hints = [
            new Hint('fastest', gl.FASTEST),
            new Hint('nicest', gl.NICEST)
        ];

        /**
         * @struct
         * @constructor
         * @param {string} name
         * @param {es3fShaderDerivateTests.SurfaceType} surfaceType
         * @param {number} numSamples
         */
        var HintFboConfig = function(name, surfaceType, numSamples) {
            /** @type {string} */ this.name = name;
            /** @type {es3fShaderDerivateTests.SurfaceType} */ this.surfaceType = surfaceType;
            /** @type {number} */ this.numSamples = numSamples;
        };

        /** @type {Array<HintFboConfig>} */ var s_hintFboConfigs = [
            new HintFboConfig('default', es3fShaderDerivateTests.SurfaceType.DEFAULT_FRAMEBUFFER, 0),
            new HintFboConfig('fbo_msaa4', es3fShaderDerivateTests.SurfaceType.UNORM_FBO, 4),
            new HintFboConfig('fbo_float', es3fShaderDerivateTests.SurfaceType.FLOAT_FBO, 0)
        ];

        /**
         * @struct
         * @constructor
         * @param {string} name
         * @param {es3fShaderDerivateTests.SurfaceType} surfaceType
         * @param {number} numSamples
         * @param {number} hint
         */
        var TextureConfig = function(name, surfaceType, numSamples, hint) {
            /** @type {string} */ this.name = name;
            /** @type {es3fShaderDerivateTests.SurfaceType} */ this.surfaceType = surfaceType;
            /** @type {number} */ this.numSamples = numSamples;
            /** @type {number} */ this.hint = hint;
        };

        /** @type {Array<TextureConfig>} */ var s_textureConfigs = [
            new TextureConfig('basic', es3fShaderDerivateTests.SurfaceType.DEFAULT_FRAMEBUFFER, 0, gl.DONT_CARE),
            new TextureConfig('msaa4', es3fShaderDerivateTests.SurfaceType.UNORM_FBO, 4, gl.DONT_CARE),
            new TextureConfig('float_fastest', es3fShaderDerivateTests.SurfaceType.FLOAT_FBO, 0, gl.FASTEST),
            new TextureConfig('float_nicest', es3fShaderDerivateTests.SurfaceType.FLOAT_FBO, 0, gl.NICEST)
        ];

        /** @type {gluShaderUtil.DataType} */ var dataType;
        /** @type {string} */ var source;
        /** @type {gluShaderUtil.precision} */ var precision;
        /** @type {es3fShaderDerivateTests.SurfaceType} */ var surfaceType;
        /** @type {number} */ var numSamples;
        /** @type {number} */ var hint;
        /** @type {string} */ var caseName;
        /** @type {tcuTestCase.DeqpTest} */ var fboGroup;

        // .dfdx, .dfdy, .fwidth
        for (var funcNdx in es3fShaderDerivateTests.DerivateFunc) {
            /** @type {es3fShaderDerivateTests.DerivateFunc} */ var function_ = es3fShaderDerivateTests.DerivateFunc[funcNdx];
            /** @type {tcuTestCase.DeqpTest} */ var functionGroup = tcuTestCase.newTest(es3fShaderDerivateTests.getDerivateFuncCaseName(function_), es3fShaderDerivateTests.getDerivateFuncName(function_));
            testGroup.addChild(functionGroup);

            // .constant - no precision variants, checks that derivate of constant arguments is 0
            /** @type {tcuTestCase.DeqpTest} */ var constantGroup = tcuTestCase.newTest('constant', 'Derivate of constant argument');
            functionGroup.addChild(constantGroup);

            for (var vecSize = 1; vecSize <= 4; vecSize++) {
                dataType = vecSize > 1 ? gluShaderUtil.getDataTypeFloatVec(vecSize) : gluShaderUtil.DataType.FLOAT;
                constantGroup.addChild(new es3fShaderDerivateTests.ConstantDerivateCase(gluShaderUtil.getDataTypeName(dataType), '', function_, dataType));
            }

            // Cases based on LinearDerivateCase
            for (var caseNdx = 0; caseNdx < s_linearDerivateCases.length; caseNdx++) {
                /** @type {tcuTestCase.DeqpTest} */ var linearCaseGroup = tcuTestCase.newTest(s_linearDerivateCases[caseNdx].name, s_linearDerivateCases[caseNdx].description);
                source = s_linearDerivateCases[caseNdx].source;
                functionGroup.addChild(linearCaseGroup);

                for (var vecSize = 1; vecSize <= 4; vecSize++)
                for (var precNdx in gluShaderUtil.precision) {
                    dataType = vecSize > 1 ? gluShaderUtil.getDataTypeFloatVec(vecSize) : gluShaderUtil.DataType.FLOAT;
                    precision = gluShaderUtil.precision[precNdx];
                    surfaceType = es3fShaderDerivateTests.SurfaceType.DEFAULT_FRAMEBUFFER;
                    numSamples = 0;
                    hint = gl.DONT_CARE;

                    if (caseNdx !== 0 && precision === gluShaderUtil.precision.PRECISION_LOWP)
                        continue; // Skip as lowp doesn't actually produce any bits when rendered to default FB.

                    caseName = gluShaderUtil.getDataTypeName(dataType) + '_' + gluShaderUtil.getPrecisionName(precision);

                    linearCaseGroup.addChild(new es3fShaderDerivateTests.LinearDerivateCase(caseName, '', function_, dataType, precision, hint, surfaceType, numSamples, source));
                }
            }

            // Fbo cases
            for (var caseNdx = 0; caseNdx < s_fboConfigs.length; caseNdx++) {
                fboGroup = tcuTestCase.newTest(s_fboConfigs[caseNdx].name, 'Derivate usage when rendering into FBO');
                source = s_linearDerivateCases[0].source; // use source from .linear group
                surfaceType = s_fboConfigs[caseNdx].surfaceType;
                numSamples = s_fboConfigs[caseNdx].numSamples;
                functionGroup.addChild(fboGroup);

                for (var vecSize = 1; vecSize <= 4; vecSize++)
                for (var precNdx in gluShaderUtil.precision) {
                    dataType = vecSize > 1 ? gluShaderUtil.getDataTypeFloatVec(vecSize) : gluShaderUtil.DataType.FLOAT;
                    precision = gluShaderUtil.precision[precNdx];
                    hint = gl.DONT_CARE;

                    if (surfaceType !== es3fShaderDerivateTests.SurfaceType.FLOAT_FBO && precision === gluShaderUtil.precision.PRECISION_LOWP)
                        continue; // Skip as lowp doesn't actually produce any bits when rendered to U8 RT.

                    caseName = gluShaderUtil.getDataTypeName(dataType) + '_' + gluShaderUtil.getPrecisionName(precision);

                    fboGroup.addChild(new es3fShaderDerivateTests.LinearDerivateCase(caseName, '', function_, dataType, precision, hint, surfaceType, numSamples, source));
                }
            }

            // .fastest, .nicest
            for (var hintCaseNdx = 0; hintCaseNdx < s_hints.length; hintCaseNdx++) {
                /** @type {tcuTestCase.DeqpTest} */ var hintGroup = tcuTestCase.newTest(s_hints[hintCaseNdx].name, 'Shader derivate hints');
                source = s_linearDerivateCases[0].source; // use source from .linear group
                hint = s_hints[hintCaseNdx].hint;
                functionGroup.addChild(hintGroup);

                for (var fboCaseNdx = 0; fboCaseNdx < s_hintFboConfigs.length; fboCaseNdx++) {
                    fboGroup = tcuTestCase.newTest(s_hintFboConfigs[fboCaseNdx].name, '');
                    surfaceType = s_hintFboConfigs[fboCaseNdx].surfaceType;
                    numSamples = s_hintFboConfigs[fboCaseNdx].numSamples;
                    hintGroup.addChild(fboGroup);

                    for (var vecSize = 1; vecSize <= 4; vecSize++)
                    for (var precNdx in gluShaderUtil.precision) {
                        dataType = vecSize > 1 ? gluShaderUtil.getDataTypeFloatVec(vecSize) : gluShaderUtil.DataType.FLOAT;
                        precision = gluShaderUtil.precision[precNdx];

                        if (surfaceType !== es3fShaderDerivateTests.SurfaceType.FLOAT_FBO && precision === gluShaderUtil.precision.PRECISION_LOWP)
                            continue; // Skip as lowp doesn't actually produce any bits when rendered to U8 RT.

                        caseName = gluShaderUtil.getDataTypeName(dataType) + '_' + gluShaderUtil.getPrecisionName(precision);

                        fboGroup.addChild(new es3fShaderDerivateTests.LinearDerivateCase(caseName, '', function_, dataType, precision, hint, surfaceType, numSamples, source));
                    }
                }
            }

            // .texture
            /** @type {tcuTestCase.DeqpTest} */ var textureGroup = tcuTestCase.newTest('texture', 'Derivate of texture lookup result');
            functionGroup.addChild(textureGroup);

            for (var texCaseNdx = 0; texCaseNdx < s_textureConfigs.length; texCaseNdx++) {
                /** @type {tcuTestCase.DeqpTest} */ var caseGroup = tcuTestCase.newTest(s_textureConfigs[texCaseNdx].name, '');
                surfaceType = s_textureConfigs[texCaseNdx].surfaceType;
                numSamples = s_textureConfigs[texCaseNdx].numSamples;
                hint = s_textureConfigs[texCaseNdx].hint;
                textureGroup.addChild(caseGroup);

                for (var vecSize = 1; vecSize <= 4; vecSize++)
                for (var precNdx in gluShaderUtil.precision) {
                    dataType = vecSize > 1 ? gluShaderUtil.getDataTypeFloatVec(vecSize) : gluShaderUtil.DataType.FLOAT;
                    precision = gluShaderUtil.precision[precNdx];

                    if (surfaceType !== es3fShaderDerivateTests.SurfaceType.FLOAT_FBO && precision === gluShaderUtil.precision.PRECISION_LOWP)
                        continue; // Skip as lowp doesn't actually produce any bits when rendered to U8 RT.

                    caseName = gluShaderUtil.getDataTypeName(dataType) + '_' + gluShaderUtil.getPrecisionName(precision);

                    caseGroup.addChild(new es3fShaderDerivateTests.TextureDerivateCase(caseName, '', function_, dataType, precision, hint, surfaceType, numSamples));
                }
            }
        }
    };

    /**
     * Run test
     * @param {WebGL2RenderingContext} context
     */
    es3fShaderDerivateTests.run = function(context) {
        gl = context;
        //Set up Test Root parameters
        var state = tcuTestCase.runner;
        state.setRoot(new es3fShaderDerivateTests.ShaderDerivateTests());

        //Set up name and description of this test series.
        setCurrentTestName(state.testCases.fullName());
        description(state.testCases.getDescription());

        try {
            //Run test cases
            tcuTestCase.runTestCases();
        }
        catch (err) {
            testFailedOptions('Failed to es3fShaderDerivateTests.run tests', false);
            tcuTestCase.runner.terminate();
        }
    };

});
