/*-------------------------------------------------------------------------
 * 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.es3fFragmentOutputTests');
goog.require('framework.common.tcuImageCompare');
goog.require('framework.common.tcuTestCase');
goog.require('framework.common.tcuTexture');
goog.require('framework.common.tcuTextureUtil');
goog.require('framework.delibs.debase.deMath');
goog.require('framework.delibs.debase.deRandom');
goog.require('framework.opengl.gluShaderProgram');
goog.require('framework.opengl.gluShaderUtil');
goog.require('framework.opengl.gluTextureUtil');
goog.require('functional.gles3.es3fFboTestUtil');

goog.scope(function() {

var es3fFragmentOutputTests = functional.gles3.es3fFragmentOutputTests;
var gluShaderProgram = framework.opengl.gluShaderProgram;
var es3fFboTestUtil = functional.gles3.es3fFboTestUtil;
var gluShaderUtil = framework.opengl.gluShaderUtil;
var deRandom = framework.delibs.debase.deRandom;
var tcuTestCase = framework.common.tcuTestCase;
var gluTextureUtil = framework.opengl.gluTextureUtil;
var tcuTexture = framework.common.tcuTexture;
var tcuTextureUtil = framework.common.tcuTextureUtil;
var deMath = framework.delibs.debase.deMath;
var tcuImageCompare = framework.common.tcuImageCompare;

    /** @type {WebGL2RenderingContext} */ var gl;

    var DE_ASSERT = function(x) {
        if (!x)
            throw new Error('Assert failed');
    };

    /**
     * es3fFragmentOutputTests.BufferSpec. Constructs the es3fFragmentOutputTests.BufferSpec object
     * @constructor
     * @param {WebGLRenderingContextBase.GLenum} format_
     * @param {number} width_
     * @param {number} height_
     * @param {number} samples_
     */
    es3fFragmentOutputTests.BufferSpec = function(format_, width_, height_, samples_) {
        this.format = format_;
        this.width = width_;
        this.height = height_;
        this.samples = samples_;
    };

    /**
     * es3fFragmentOutputTests.FragmentOutput. Constructs the es3fFragmentOutputTests.FragmentOutput object
     * @constructor
     * @param {gluShaderUtil.DataType} type_
     * @param {gluShaderUtil.precision} precision_
     * @param {number} location_
     * @param {number=} arrayLength_
     */
    es3fFragmentOutputTests.FragmentOutput = function(type_, precision_, location_, arrayLength_) {
        this.type = type_;
        this.precision = precision_;
        this.location = location_;
        this.arrayLength = arrayLength_ || 0;
    };

    /**
     * es3fFragmentOutputTests.FragmentOutputCase. Constructs the es3fFragmentOutputTests.FragmentOutputCase object
     * @constructor
     * @extends {tcuTestCase.DeqpTest}
     * @param {string} name
     * @param {string} description
     * @param {Array<es3fFragmentOutputTests.BufferSpec>} fboSpec
     * @param {Array<es3fFragmentOutputTests.FragmentOutput>} outputs
     * @return {Object} The currently modified object
     */
    es3fFragmentOutputTests.FragmentOutputCase = function(name, description, fboSpec, outputs) {
        tcuTestCase.DeqpTest.call(this, name, description);
        /** @type {Array<es3fFragmentOutputTests.BufferSpec>} */ this.m_fboSpec = fboSpec;
        /** @type {Array<es3fFragmentOutputTests.FragmentOutput>} */ this.m_outputs = outputs;
        /** @type {gluShaderProgram.ShaderProgram} */ this.m_program = null;
        /** @type {WebGLFramebuffer} */ this.m_framebuffer = null;

        /** @type {WebGLRenderbuffer} */ this.m_renderbuffer = null;
    };

    es3fFragmentOutputTests.FragmentOutputCase.prototype = Object.create(tcuTestCase.DeqpTest.prototype);
    es3fFragmentOutputTests.FragmentOutputCase.prototype.constructor = es3fFragmentOutputTests.FragmentOutputCase;

    /**
     * es3fFragmentOutputTests.createProgram. Returns a ShaderProgram object
     * @param {Array<es3fFragmentOutputTests.FragmentOutput>} outputs
     * @return {gluShaderProgram.ShaderProgram} program
     */
    es3fFragmentOutputTests.createProgram = function(outputs) {

        var vtx = '';
        var frag = '';

        vtx = '#version 300 es\n' + 'in highp vec4 a_position;\n';
        frag = '#version 300 es\n';

    /** @type {es3fFragmentOutputTests.FragmentOutput} */ var output = null;
    /** @type {boolean} */ var isArray = false;
     // Input-output declarations.
        for (var outNdx = 0; outNdx < outputs.length; outNdx++) {
            output = outputs[outNdx];
            isArray = output.arrayLength > 0;
            /** @type {string} */ var typeName = gluShaderUtil.getDataTypeName(output.type);
            /** @type {string} */ var precName = gluShaderUtil.getPrecisionName(output.precision);
            /** @type {boolean} */ var isFloat = gluShaderUtil.isDataTypeFloatOrVec(output.type);
            /** @type {string} */ var interp = isFloat ? 'smooth' : 'flat';

            if (isArray) {
                for (var elemNdx = 0; elemNdx < output.arrayLength; elemNdx++) {
                    vtx += 'in ' + precName + ' ' + typeName + ' in' + outNdx + '_' + elemNdx + ';\n' +
                    interp + ' out ' + precName + ' ' + typeName + ' var' + outNdx + '_' + elemNdx + ';\n';
                    frag += interp + ' in ' + precName + ' ' + typeName + ' var' + outNdx + '_' + elemNdx + ';\n';
                }
                frag += 'layout(location = ' + output.location + ') out ' + precName + ' ' + typeName + ' out' + outNdx + '[' + output.arrayLength + '];\n';
            } else {
                vtx += 'in ' + precName + ' ' + typeName + ' in' + outNdx + ';\n' +
                interp + ' out ' + precName + ' ' + typeName + ' var' + outNdx + ';\n';
                frag += interp + ' in ' + precName + ' ' + typeName + ' var' + outNdx + ';\n' +
                'layout(location = ' + output.location + ') out ' + precName + ' ' + typeName + ' out' + outNdx + ';\n';
            }
        }

        vtx += '\nvoid main()\n{\n';
        frag += '\nvoid main()\n{\n';

        vtx += ' gl_Position = a_position;\n';

        // Copy body
        for (var outNdx = 0; outNdx < outputs.length; outNdx++) {
            output = outputs[outNdx];
            isArray = output.arrayLength > 0;

            if (isArray) {
                for (var elemNdx = 0; elemNdx < output.arrayLength; elemNdx++) {
                    vtx += '\tvar' + outNdx + '_' + elemNdx + ' = in' + outNdx + '_' + elemNdx + ';\n';
                    frag += '\tout' + outNdx + '[' + elemNdx + '] = var' + outNdx + '_' + elemNdx + ';\n';
                }
            } else {
                vtx += '\tvar' + outNdx + ' = in' + outNdx + ';\n';
                frag += '\tout' + outNdx + ' = var' + outNdx + ';\n';
            }
        }

        vtx += '}\n';
        frag += '}\n';

        /** @type {gluShaderProgram.ShaderProgram} */
        var program = new gluShaderProgram.ShaderProgram(gl, gluShaderProgram.makeVtxFragSources(vtx, frag));
        return program;
    };

    es3fFragmentOutputTests.FragmentOutputCase.prototype.init = function() {
        // Check that all attachments are supported
        for (var iter = 0; iter < this.m_fboSpec.length; ++iter) {
            if (!gluTextureUtil.isSizedFormatColorRenderable(this.m_fboSpec[iter].format))
                throw new Error('Unsupported attachment format');
        }

        DE_ASSERT(!this.m_program);
        this.m_program = es3fFragmentOutputTests.createProgram(this.m_outputs);

       // log << *m_program;
        if (!this.m_program.isOk())
            throw new Error('Compile failed. Program no created');

        /*
        // Print render target info to log.
        log << TestLog::Section("Framebuffer", "Framebuffer configuration");

        for (int ndx = 0; ndx < (int)m_fboSpec.size(); ndx++)
            log << TestLog::Message << "COLOR_ATTACHMENT" << ndx << ": "
                                    << glu::getPixelFormatStr(m_fboSpec[ndx].format) << ", "
                                    << m_fboSpec[ndx].width << "x" << m_fboSpec[ndx].height << ", "
                                    << m_fboSpec[ndx].samples << " samples"
                << TestLog::EndMessage;

        log << TestLog::EndSection;*/

        // Create framebuffer.
        this.m_framebuffer = gl.createFramebuffer();
        gl.bindFramebuffer(gl.FRAMEBUFFER, this.m_framebuffer);

        for (var bufNdx = 0; bufNdx < /* m_renderbuffers.size() */ this.m_fboSpec.length; bufNdx++) {
            this.m_renderbuffer = gl.createRenderbuffer();
            /** @type {es3fFragmentOutputTests.BufferSpec} */ var bufSpec = this.m_fboSpec[bufNdx];
            /** @type {number} */ var attachment = gl.COLOR_ATTACHMENT0 + bufNdx;

            gl.bindRenderbuffer(gl.RENDERBUFFER, this.m_renderbuffer);

            gl.renderbufferStorageMultisample(gl.RENDERBUFFER, bufSpec.samples, bufSpec.format, bufSpec.width, bufSpec.height);
            gl.framebufferRenderbuffer(gl.FRAMEBUFFER, attachment, gl.RENDERBUFFER, this.m_renderbuffer);
        }
        /** @type {number} */ var fboStatus = gl.checkFramebufferStatus(gl.FRAMEBUFFER);

        if (fboStatus == gl.FRAMEBUFFER_UNSUPPORTED)
            throw new Error('Framebuffer not supported');
        else if (fboStatus != gl.FRAMEBUFFER_COMPLETE)
            throw new Error('Incomplete framebuffer');
            // throw tcu::TestError((string("Incomplete framebuffer: ") + glu::getFramebufferStatusStr(fboStatus), "", __FILE__, __LINE__);

        // gl.bindRenderbuffer(gl.RENDERBUFFER, null); // TODO: maybe needed?
        gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    };

    es3fFragmentOutputTests.FragmentOutputCase.prototype.deinit = function() {
        // TODO: implement?
    };

    /**
     * es3fFragmentOutputTests.getMinSize.
     * @param {Array<es3fFragmentOutputTests.BufferSpec>} fboSpec
     * @return {Array<number>} minSize
     */
    es3fFragmentOutputTests.getMinSize = function(fboSpec) {
        /** @type {Array<number>} */ var minSize = [0x7fffffff, 0x7fffffff];
        for (var i = 0; i < fboSpec.length; i++) {
            minSize[0] = Math.min(minSize[0], fboSpec[i].width);
            minSize[1] = Math.min(minSize[1], fboSpec[i].height);
        }
        return minSize;
    };

    /**
     * es3fFragmentOutputTests.getNumInputVectors. Returns the length of the array of all the outputs (es3fFragmentOutputTests.FragmentOutput object)
     * @param {Array<es3fFragmentOutputTests.FragmentOutput>} outputs
     * @return {number} numVecs
     */
    es3fFragmentOutputTests.getNumInputVectors = function(outputs) {
        /** @type {number} */ var numVecs = 0;
        for (var i = 0; i < outputs.length; i++)
            numVecs += (outputs[i].arrayLength > 0 ? outputs[i].arrayLength : 1);
        return numVecs;
    };

    /**
     * es3fFragmentOutputTests.getFloatRange
     * @param {gluShaderUtil.precision} precision
     * @return {Array<number>} Vec2
     */
    es3fFragmentOutputTests.getFloatRange = function(precision) {
        /** @type {Array<Array<number>>} */
        var ranges = // Vec2
        [
            [-2.0, 2.0],
            [-16000.0, 16000.0],
            [-1e35, 1e35]
        ];
        // DE_STATIC_ASSERT(DE_LENGTH_OF_ARRAY(ranges) == glu::PRECISION_LAST);
        // DE_ASSERT(de::inBounds<int>(precision, 0, DE_LENGTH_OF_ARRAY(ranges)));
        return ranges[precision];
    };

    /**
     * es3fFragmentOutputTests.getIntRange
     * @param {gluShaderUtil.precision} precision
     * @return {Array<number>} IVec2
     */
    es3fFragmentOutputTests.getIntRange = function(precision) {
        /** @type {Array<Array<number>>} */
        var ranges = // IVec2
        [
            [-(1 << 7), (1 << 7) - 1],
            [-(1 << 15), (1 << 15) - 1],
            [-0x80000000, 0x7fffffff]
        ];
        // DE_STATIC_ASSERT(DE_LENGTH_OF_ARRAY(ranges) == glu::PRECISION_LAST);
        // DE_ASSERT(de::inBounds<int>(precision, 0, DE_LENGTH_OF_ARRAY(ranges)));
        return ranges[precision];
    };

    /**
     * es3fFragmentOutputTests.getUintRange
     * @param {gluShaderUtil.precision} precision
     * @return {Array<number>} UVec2
     */
    es3fFragmentOutputTests.getUintRange = function(precision) {
        /** @type {Array<Array<number>>} */
        var ranges = // UVec2
        [
            [0, (1 << 8) - 1],
            [0, (1 << 16) - 1],
            [0, 0xffffffff]
        ];
        // DE_STATIC_ASSERT(DE_LENGTH_OF_ARRAY(ranges) == glu::PRECISION_LAST);
        // DE_ASSERT(de::inBounds<int>(precision, 0, DE_LENGTH_OF_ARRAY(ranges)));
        return ranges[precision];

    };

    /**
     * es3fFragmentOutputTests.readVec4
     * @param {Array<number>} ptr
     * @param {number} index
     * @param {number} numComponents
     * @return {Array<number>} Vec4
     */
    es3fFragmentOutputTests.readVec4 = function(ptr, index, numComponents) {
        DE_ASSERT(numComponents >= 1);
        return [
                ptr[index + 0],
                numComponents >= 2 ? ptr[index + 1] : 0.0,
                numComponents >= 3 ? ptr[index + 2] : 0.0,
                numComponents >= 4 ? ptr[index + 3] : 0.0
                ];
    };

    /**
     * es3fFragmentOutputTests.readIVec4
     * @param {Array<number>} ptr
     * @param {number} numComponents
     * @return {Array<number>} IVec4
     */
    es3fFragmentOutputTests.readIVec4 = function(ptr, index, numComponents) {
        DE_ASSERT(numComponents >= 1);
        return [
                ptr[index + 0],
                numComponents >= 2 ? ptr[index + 1] : 0,
                numComponents >= 3 ? ptr[index + 2] : 0,
                numComponents >= 4 ? ptr[index + 3] : 0
                ];
    };

    /**
     * es3fFragmentOutputTests.renderFloatReference
     * @param {tcuTexture.PixelBufferAccess} dst
     * @param {number} gridWidth
     * @param {number} gridHeight
     * @param {number} numComponents
     * @param {Array<number>} vertices
     */
    es3fFragmentOutputTests.renderFloatReference = function(dst, gridWidth, gridHeight, numComponents, vertices) {

        /** @type {boolean} */ var isSRGB = dst.getFormat().order == tcuTexture.ChannelOrder.sRGB || dst.getFormat().order == tcuTexture.ChannelOrder.sRGBA;
        /** @type {number} */ var cellW = dst.getWidth() / (gridWidth - 1);
        /** @type {number} */ var cellH = dst.getHeight() / (gridHeight - 1);

        for (var y = 0; y < dst.getHeight(); y++) {
            for (var x = 0; x < dst.getWidth(); x++) {
                /** @type {number} */ var cellX = deMath.clamp(Math.floor(x / cellW), 0, gridWidth - 2);
                /** @type {number} */ var cellY = deMath.clamp(Math.floor(y / cellH), 0, gridHeight - 2);
                /** @type {number} */ var xf = (x - cellX * cellW + 0.5) / cellW;
                /** @type {number} */ var yf = (y - cellY * cellH + 0.5) / cellH;

                /** @type {Array<number>} */ var v00 = es3fFragmentOutputTests.readVec4(vertices, ((cellY + 0) * gridWidth + cellX + 0) * numComponents, numComponents); // Vec4
                /** @type {Array<number>} */ var v01 = es3fFragmentOutputTests.readVec4(vertices, ((cellY + 1) * gridWidth + cellX + 0) * numComponents, numComponents); // Vec4
                /** @type {Array<number>} */ var v10 = es3fFragmentOutputTests.readVec4(vertices, ((cellY + 0) * gridWidth + cellX + 1) * numComponents, numComponents); // Vec4
                /** @type {Array<number>} */ var v11 = es3fFragmentOutputTests.readVec4(vertices, ((cellY + 1) * gridWidth + cellX + 1) * numComponents, numComponents); // Vec4

                /** @type {boolean} */ var tri = xf + yf >= 1.0;
                /** @type {Array<number>} */ var v0 = tri ? v11 : v00; // Vec4&
                /** @type {Array<number>} */ var v1 = tri ? v01 : v10; // Vec4&
                /** @type {Array<number>} */ var v2 = tri ? v10 : v01; // Vec4&
                /** @type {number} */ var s = tri ? 1.0 - xf : xf;
                /** @type {number} */ var t = tri ? 1.0 - yf : yf;
                /** @type {Array<number>} */ var color = deMath.add(v0, deMath.add(deMath.multiply((deMath.subtract(v1, v0)), [s, s, s, s]), deMath.multiply((deMath.subtract(v2, v0)), [t, t, t, t]))); // Vec4

                dst.setPixel(isSRGB ? tcuTextureUtil.linearToSRGB(color) : color, x, y);
            }
        }
    };

    /**
     * es3fFragmentOutputTests.renderIntReference
     * @param {tcuTexture.PixelBufferAccess} dst
     * @param {number} gridWidth
     * @param {number} gridHeight
     * @param {number} numComponents
     * @param {Array<number>} vertices
     */
    es3fFragmentOutputTests.renderIntReference = function(dst, gridWidth, gridHeight, numComponents, vertices) {

        /** @type {number} */ var cellW = dst.getWidth() / (gridWidth - 1);
        /** @type {number} */ var cellH = dst.getHeight() / (gridHeight - 1);

        for (var y = 0; y < dst.getHeight(); y++) {
            for (var x = 0; x < dst.getWidth(); x++) {
                /** @type {number} */ var cellX = deMath.clamp(Math.floor(x / cellW), 0, gridWidth - 2);
                /** @type {number} */ var cellY = deMath.clamp(Math.floor(y / cellH), 0, gridHeight - 2);
                /** @type {Array<number>} */ var c = es3fFragmentOutputTests.readIVec4(vertices, (cellY * gridWidth + cellX + 1) * numComponents, numComponents); // IVec4

                dst.setPixelInt(c, x, y);
            }
        }
    };

    /**
     * es3fFragmentOutputTests.s_swizzles
     * @return {Array<Array<number>>}
     */
    es3fFragmentOutputTests.s_swizzles = function() {
        var mat_swizzles = [
            [0, 1, 2, 3],
            [1, 2, 3, 0],
            [2, 3, 0, 1],
            [3, 0, 1, 2],
            [3, 2, 1, 0],
            [2, 1, 0, 3],
            [1, 0, 3, 2],
            [0, 3, 2, 1]
        ];

        return mat_swizzles;
    };

    /**
     * es3fFragmentOutputTests.swizzleVec. Returns an Array from a position contained in the Array es3fFragmentOutputTests.s_swizzles []
     * @param {Array<number>} vec
     * @param {number} swzNdx
     * @return {Array<number>} Swizzled array
     */
    es3fFragmentOutputTests.swizzleVec = function(vec, swzNdx) {
    /** @type {Array<number>} */ var swz = es3fFragmentOutputTests.s_swizzles()[swzNdx % es3fFragmentOutputTests.s_swizzles().length];

        return deMath.swizzle(vec, swz);
    };

    /**
     * es3fFragmentOutputTests.AttachmentData struct class
     * @constructor
     * @return {Object}
     */
    es3fFragmentOutputTests.AttachmentData = function() {
        return {
        /** @type {tcuTexture.TextureFormat} */ format: null, //!< Actual format of attachment.
        /** @type {tcuTexture.TextureFormat} */ referenceFormat: null, //!< Used for reference rendering.
        /** @type {tcuTexture.TextureFormat} */ readFormat: null,
        /** @type {number} */ numWrittenChannels: 0,
        /** @type {gluShaderUtil.precision} */ outPrecision: gluShaderUtil.precision.PRECISION_LOWP,
        /** @type {ArrayBuffer} */ renderedData: null,
        /** @type {ArrayBuffer} */ referenceData: null
        };
    };

    es3fFragmentOutputTests.FragmentOutputCase.prototype.iterate = function() {
        // Compute grid size & index list.
        /** @type {number} */ var minCellSize = 8;
        /** @type {Array<number>} */ var minBufSize = es3fFragmentOutputTests.getMinSize(this.m_fboSpec); // IVec2
        /** @type {number} */ var gridWidth = deMath.clamp(Math.floor(minBufSize[0] / minCellSize), 1, 255) + 1;
        /** @type {number} */ var gridHeight = deMath.clamp(Math.floor(minBufSize[1] / minCellSize), 1, 255) + 1;
        /** @type {number} */ var numVertices = gridWidth * gridHeight;
        /** @type {number} */ var numQuads = (gridWidth - 1) * (gridHeight - 1);
        /** @type {number} */ var numIndices = numQuads * 6;

        /** @type {number} */ var numInputVecs = es3fFragmentOutputTests.getNumInputVectors(this.m_outputs);
        /** @type {Array<Array<number>>} */ var inputs = []; // originally vector<vector<deUint32>

        for (var inputNdx = 0; inputNdx < numInputVecs; inputNdx++)
            inputs[inputNdx] = []; // inputs.length = numInputVecs;

        /** @type {Array<number>} */ var positions = []; // originally vector<float>
        /** @type {Array<number>} */ var indices = []; // originally vector<deUint16>

        /** @type {number} */ var readAlignment = 4;
        /** @type {number} */ var viewportW = minBufSize[0];
        /** @type {number} */ var viewportH = minBufSize[1];
        /** @type {number} */ var numAttachments = this.m_fboSpec.length;

        /** @type {Array<number>} */ var drawBuffers = []; // originally vector<deUint32>
        /** @type {Array<es3fFragmentOutputTests.AttachmentData>} */ var attachments = [];
        /** @type {number} */ var attachmentW;
        /** @type {number} */ var attachmentH;

        // Initialize attachment data.
        for (var ndx = 0; ndx < numAttachments; ndx++) {
            /** @type {tcuTexture.TextureFormat} */ var texFmt = gluTextureUtil.mapGLInternalFormat(this.m_fboSpec[ndx].format);
            /** @type {tcuTexture.TextureChannelClass} */ var chnClass = tcuTexture.getTextureChannelClass(texFmt.type);
            /** @type {boolean} */ var isFixedPoint = (chnClass == tcuTexture.TextureChannelClass.SIGNED_FIXED_POINT ||
                                                              chnClass == tcuTexture.TextureChannelClass.UNSIGNED_FIXED_POINT);

            // \note Fixed-point formats use float reference to enable more accurate result verification.
            /** @type {tcuTexture.TextureFormat} */ var refFmt = isFixedPoint ? new tcuTexture.TextureFormat(texFmt.order, tcuTexture.ChannelType.FLOAT) : texFmt;
            /** @type {tcuTexture.TextureFormat} */ var readFmt = es3fFboTestUtil.getFramebufferReadFormat(texFmt);
            attachmentW = this.m_fboSpec[ndx].width;
            attachmentH = this.m_fboSpec[ndx].height;

            drawBuffers[ndx] = gl.COLOR_ATTACHMENT0 + ndx;
            attachments[ndx] = new es3fFragmentOutputTests.AttachmentData();
            attachments[ndx].format = texFmt;
            attachments[ndx].readFormat = readFmt;
            attachments[ndx].referenceFormat = refFmt;
            attachments[ndx].renderedData = new ArrayBuffer(readFmt.getPixelSize() * attachmentW * attachmentH);
            attachments[ndx].referenceData = new ArrayBuffer(refFmt.getPixelSize() * attachmentW * attachmentH);
        }

        // Initialize indices.
        for (var quadNdx = 0; quadNdx < numQuads; quadNdx++) {
            /** @type {number} */ var quadY = Math.floor(quadNdx / (gridWidth - 1));
            /** @type {number} */ var quadX = quadNdx - quadY * (gridWidth - 1);

            indices[quadNdx * 6 + 0] = quadX + quadY * gridWidth;
            indices[quadNdx * 6 + 1] = quadX + (quadY + 1) * gridWidth;
            indices[quadNdx * 6 + 2] = quadX + quadY * gridWidth + 1;
            indices[quadNdx * 6 + 3] = indices[quadNdx * 6 + 1];
            indices[quadNdx * 6 + 4] = quadX + (quadY + 1) * gridWidth + 1;
            indices[quadNdx * 6 + 5] = indices[quadNdx * 6 + 2];
        }

        /** @type {number} */ var xf = 0;
        /** @type {number} */ var yf = 0;
        for (var y = 0; y < gridHeight; y++) {
            for (var x = 0; x < gridWidth; x++) {
                xf = x / (gridWidth - 1);
                yf = y / (gridHeight - 1);

                positions[(y * gridWidth + x) * 4 + 0] = 2.0 * xf - 1.0;
                positions[(y * gridWidth + x) * 4 + 1] = 2.0 * yf - 1.0;
                positions[(y * gridWidth + x) * 4 + 2] = 0.0;
                positions[(y * gridWidth + x) * 4 + 3] = 1.0;
            }
        }
        /** @type {es3fFragmentOutputTests.FragmentOutput} */ var output;
        /** @type {boolean} */ var isArray;
        /** @type {boolean} */ var isFloat;
        /** @type {boolean} */ var isInt;
        /** @type {boolean} */ var isUint;
        /** @type {number} */ var numVecs;
        /** @type {number} */ var numScalars;

        var curInVec = 0;
        for (var outputNdx = 0; outputNdx < this.m_outputs.length; outputNdx++) {
            output = this.m_outputs[outputNdx];
            isFloat = gluShaderUtil.isDataTypeFloatOrVec(output.type);
            isInt = gluShaderUtil.isDataTypeIntOrIVec(output.type);
            isUint = gluShaderUtil.isDataTypeUintOrUVec(output.type);
            numVecs = output.arrayLength > 0 ? output.arrayLength : 1;
            numScalars = gluShaderUtil.getDataTypeScalarSize(output.type);

            for (var vecNdx = 0; vecNdx < numVecs; vecNdx++) {
                inputs[curInVec].length = numVertices * numScalars;

                // Record how many outputs are written in attachment.
                DE_ASSERT(output.location + vecNdx < attachments.length);
                attachments[output.location + vecNdx].numWrittenChannels = numScalars;
                attachments[output.location + vecNdx].outPrecision = output.precision;

                /** @type {Array<number>} */ var range = null;
                /** @type {Array<number>} */ var minVal = null;
                /** @type {Array<number>} */ var maxVal = null;
                /** @type {Array<number>} */ var fmtBits = null;
                /** @type {Array<number>} */ var fmtMaxVal = [];
                /** @type {Array<number>} */ var rangeDiv = null;
                /** @type {Array<number>} */ var step = [];
                /** @type {number} */ var ix = 0;
                /** @type {number} */ var iy = 0;
                /** @type {Array<number>} */ var c = null;
                /** @type {number} */ var pos = 0;
               if (isFloat) {
                    range = es3fFragmentOutputTests.getFloatRange(output.precision); // Vec2
                    minVal = [range[0], range[0], range[0], range[0]]; // Vec4
                    maxVal = [range[1], range[1], range[1], range[1]]; // Vec4

                    if (deMath.deInBounds32(output.location + vecNdx, 0, attachments.length)) {
                    // \note Floating-point precision conversion is not well-defined. For that reason we must
                    // limit value range to intersection of both data type and render target value ranges.
                    /** @type {tcuTextureUtil.TextureFormatInfo} */ var fmtInfo = tcuTextureUtil.getTextureFormatInfo(attachments[output.location + vecNdx].format);
                        minVal = deMath.max(minVal, fmtInfo.valueMin);
                        maxVal = deMath.min(maxVal, fmtInfo.valueMax);
                    }

                    console.log('out ' + curInVec + ' value range: ' + minVal + ' -> ' + maxVal);

                    for (var y = 0; y < gridHeight; y++) {
                        for (var x = 0; x < gridWidth; x++) {
                            xf = x / (gridWidth - 1);
                            yf = y / (gridHeight - 1);
                            /** @type {number} */ var f0 = (xf + yf) * 0.5;
                            /** @type {number} */ var f1 = 0.5 + (xf - yf) * 0.5;

                            /** @type {Array<number>} */ var f = es3fFragmentOutputTests.swizzleVec([f0, f1, 1.0 - f0, 1.0 - f1], curInVec); // Vec4
                            c = deMath.add(minVal, deMath.multiply(deMath.subtract(maxVal, minVal), f)); // Vec4

                            pos = (y * gridWidth + x) * numScalars;

                            for (var ndx = 0; ndx < numScalars; ndx++)
                                inputs[curInVec][pos + ndx] = c[ndx];
                        }
                    }
                } else if (isInt) {
                    range = es3fFragmentOutputTests.getIntRange(output.precision); // IVec2
                    minVal = [range[0], range[0], range[0], range[0]]; // IVec4
                    maxVal = [range[1], range[1], range[1], range[1]]; // IVec4

                    if (deMath.deInBounds32(output.location + vecNdx, 0, attachments.length)) {
                        // Limit to range of output format as conversion mode is not specified.
                        fmtBits = tcuTextureUtil.getTextureFormatBitDepth(attachments[output.location + vecNdx].format); // IVec4
                        /** @type {Array<boolean>} */ var isZero = deMath.lessThanEqual(fmtBits, [0, 0, 0, 0]); // BVec4, array of booleans, size = 4

                        /** @type {Array<number>} */ var fmtMinVal = []; // IVec4

                        for (var i = 0; i < 4; i++) {

                            // const IVec4 fmtMinVal = (-(tcu::Vector<deInt64, 4>(1) << (fmtBits - 1 ).cast<deInt64>())).asInt();
                            fmtMinVal[i] = -1 * Math.pow(2, fmtBits[i] - 1); // TODO: check implementation, original above
                            // const IVec4 fmtMaxVal = ((tcu::Vector<deInt64, 4>(1) << (fmtBits - 1 ).cast<deInt64>()) - deInt64(1)).asInt();
                            fmtMaxVal[i] = Math.pow(2, fmtBits[i] - 1) - 1; // TODO: check implementation, original above
                        }

                        minVal = tcuTextureUtil.select(minVal, deMath.max(minVal, fmtMinVal), isZero);
                        maxVal = tcuTextureUtil.select(maxVal, deMath.min(maxVal, fmtMaxVal), isZero);
                    }

                    console.log('out ' + curInVec + ' value range: ' + minVal + ' -> ' + maxVal);

                    rangeDiv = es3fFragmentOutputTests.swizzleVec([gridWidth - 1, gridHeight - 1, gridWidth - 1, gridHeight - 1], curInVec); // IVec4
                    for (var i = 0; i < 4; i++) {
                        // const IVec4 step = ((maxVal.cast<deInt64>() - minVal.cast<deInt64>()) / (rangeDiv.cast<deInt64>())).asInt();
                        step[i] = Math.floor((maxVal[i] - minVal[i]) / rangeDiv[i]); // TODO: check with the above line of code
                    }

                    for (var y = 0; y < gridHeight; y++) {
                        for (var x = 0; x < gridWidth; x++) {
                            ix = gridWidth - x - 1;
                            iy = gridHeight - y - 1;
                            c = deMath.add(minVal, deMath.multiply(step, es3fFragmentOutputTests.swizzleVec([x, y, ix, iy], curInVec))); // IVec4

                            pos = (y * gridWidth + x) * numScalars;

                            for (var ndx = 0; ndx < numScalars; ndx++)
                                inputs[curInVec][pos + ndx] = c[ndx];
                        }
                    }
                } else if (isUint) {
                    range = es3fFragmentOutputTests.getUintRange(output.precision); // UVec2
                    maxVal = [range[1], range[1], range[1], range[1]]; // UVec4

                    if (deMath.deInBounds32(output.location + vecNdx, 0, attachments.length)) {
                        // Limit to range of output format as conversion mode is not specified.
                        fmtBits = tcuTextureUtil.getTextureFormatBitDepth(attachments[output.location + vecNdx].format); // IVec4

                        for (var i = 0; i < 4; i++) {
                            fmtMaxVal[i] = Math.pow(2, fmtBits[i]) - 1;
                        }

                        maxVal = deMath.min(maxVal, fmtMaxVal);
                    }

                    console.log('out ' + curInVec + ' value range: ' + minVal + ' -> ' + maxVal);

                    rangeDiv = es3fFragmentOutputTests.swizzleVec([gridWidth - 1, gridHeight - 1, gridWidth - 1, gridHeight - 1], curInVec); // IVec4

                    for (var stepPos = 0; stepPos < maxVal.length; stepPos++) {
                        step[stepPos] = Math.floor(maxVal[stepPos] / rangeDiv[stepPos]);
                    }

                    DE_ASSERT(range[0] == 0);

                    for (var y = 0; y < gridHeight; y++) {
                        for (var x = 0; x < gridWidth; x++) {
                            ix = gridWidth - x - 1;
                            iy = gridHeight - y - 1;
                            c = deMath.multiply(step, es3fFragmentOutputTests.swizzleVec([x, y, ix, iy], curInVec)); // UVec4
                            pos = (y * gridWidth + x) * numScalars;

                            DE_ASSERT(deMath.boolAll(deMath.lessThanEqual(c, maxVal))); // TODO: sometimes crashes here, condition not asserted

                            for (var ndx = 0; ndx < numScalars; ndx++)
                                inputs[curInVec][pos + ndx] = c[ndx];
                        }
                    }
                } else
                    DE_ASSERT(false);

                curInVec += 1;
            }
        }

        // Render using gl.
        gl.useProgram(this.m_program.getProgram());
        gl.bindFramebuffer(gl.FRAMEBUFFER, this.m_framebuffer);
        gl.viewport(0, 0, viewportW, viewportH);
        gl.drawBuffers(drawBuffers);
        gl.disable(gl.DITHER); // Dithering causes issues with unorm formats. Those issues could be worked around in threshold, but it makes validation less accurate.

        /** @type {WebGLBuffer} */ var buffer = null;
        /** @type {string} */ var name;
        curInVec = 0;
        for (var outputNdx = 0; outputNdx < this.m_outputs.length; outputNdx++) {
            output = this.m_outputs[outputNdx];
            isArray = output.arrayLength > 0;
            isFloat = gluShaderUtil.isDataTypeFloatOrVec(output.type);
            isInt = gluShaderUtil.isDataTypeIntOrIVec(output.type);
            isUint = gluShaderUtil.isDataTypeUintOrUVec(output.type);
            /** @type {number} */ var scalarSize = gluShaderUtil.getDataTypeScalarSize(output.type);
            /** @type {number} */ var glScalarType = isFloat ? /* gluShaderUtil.DataType.FLOAT */ gl.FLOAT :
                                                     isInt ? /* gluShaderUtil.DataType.INT */ gl.INT :
                                                     isUint ? /* gluShaderUtil.DataType.UINT */ gl.UNSIGNED_INT : /* gluShaderUtil.DataType.INVALID */ gl.NONE;
            numVecs = isArray ? output.arrayLength : 1;

            for (var vecNdx = 0; vecNdx < numVecs; vecNdx++) {
                name = 'in' + outputNdx + (isArray ? '_' + vecNdx : '');
                /** @type {number} */ var loc = gl.getAttribLocation(this.m_program.getProgram(), name);

                if (loc >= 0) {
                    buffer = gl.createBuffer();
                    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);

                    gl.enableVertexAttribArray(loc);
                    if (isFloat) {
                        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(inputs[curInVec]), gl.STATIC_DRAW);
                        // KHRONOS WebGL 1.0 specification:
                        // void vertexAttribPointer(GLuint indx, GLint size, GLenum type, GLboolean normalized, GLsizei stride, GLintptr offset);
                        gl.vertexAttribPointer(loc, scalarSize, glScalarType, false, 0, 0); // offset = 0
                    } else {
                        gl.bufferData(gl.ARRAY_BUFFER, new Int32Array(inputs[curInVec]), gl.STATIC_DRAW);
                        // KHRONOS WebGL 2.0 specification:
                        // void vertexAttribIPointer(GLuint index, GLint size, GLenum type, GLsizei stride, GLintptr offset)
                        gl.vertexAttribIPointer(loc, scalarSize, glScalarType, 0, 0); // offset = 0
                    }
                } else
                    bufferedLogToConsole('Warning: No location for attribute "' + name + '" found.');

                curInVec += 1;
            }
        }

        /** @type {number} */ var posLoc = gl.getAttribLocation(this.m_program.getProgram(), 'a_position');
        // TCU_CHECK(posLoc >= 0);
        buffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);

        gl.enableVertexAttribArray(posLoc);
        gl.vertexAttribPointer(posLoc, 4, gl.FLOAT, false, 0, 0); // offset = 0

        /** @type {WebGLBuffer} */ var indexObject = gl.createBuffer();
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexObject);
        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);

        gl.drawElements(gl.TRIANGLES, numIndices, gl.UNSIGNED_SHORT, 0); // offset = 0

        // Render reference images.

        var curInNdx = 0;
        for (var outputNdx = 0; outputNdx < this.m_outputs.length; outputNdx++) {
            output = this.m_outputs[outputNdx];
            isArray = output.arrayLength > 0;
            isFloat = gluShaderUtil.isDataTypeFloatOrVec(output.type);
            isInt = gluShaderUtil.isDataTypeIntOrIVec(output.type);
            isUint = gluShaderUtil.isDataTypeUintOrUVec(output.type);
            scalarSize = gluShaderUtil.getDataTypeScalarSize(output.type);
            numVecs = isArray ? output.arrayLength : 1;

            for (var vecNdx = 0; vecNdx < numVecs; vecNdx++) {
                /** @type {number} */ var location = output.location + vecNdx;
                /** @type {Array<number>} */ var inputData = inputs[curInNdx];

                DE_ASSERT(deMath.deInBounds32(location, 0, this.m_fboSpec.length));

                /** @type {number} */ var bufW = this.m_fboSpec[location].width;
                /** @type {number} */ var bufH = this.m_fboSpec[location].height;
                /** @type {Object} */ var descriptor = {
                        format: attachments[location].referenceFormat,
                        width: bufW,
                        height: bufH,
                        depth: 1,
                        data: attachments[location].referenceData // ArrayBuffer
                };
                /** @type {tcuTexture.PixelBufferAccess} */ var buf = new tcuTexture.PixelBufferAccess(descriptor);
                /** @type {tcuTexture.PixelBufferAccess} */ var viewportBuf = tcuTextureUtil.getSubregion(buf, 0, 0, 0, viewportW, viewportH, 1);

                if (isInt || isUint)
                    es3fFragmentOutputTests.renderIntReference(viewportBuf, gridWidth, gridHeight, scalarSize, inputData);
                else if (isFloat)
                    es3fFragmentOutputTests.renderFloatReference(viewportBuf, gridWidth, gridHeight, scalarSize, inputData);
                else
                    DE_ASSERT(false);

                curInNdx += 1;
            }
        }

        // Compare all images.
        /** @type {boolean} */ var allLevelsOk = true;
        for (var attachNdx = 0; attachNdx < numAttachments; attachNdx++) {
            attachmentW = this.m_fboSpec[attachNdx].width;
            attachmentH = this.m_fboSpec[attachNdx].height;
            /** @type {number} */ var numValidChannels = attachments[attachNdx].numWrittenChannels;
            /** @type {Array<boolean>} */ var cmpMask = [numValidChannels >= 1, numValidChannels >= 2, numValidChannels >= 3, numValidChannels >= 4];
            /** @type {gluShaderUtil.precision} */ var outPrecision = attachments[attachNdx].outPrecision;
            /** @type {tcuTexture.TextureFormat} */ var format = attachments[attachNdx].format;
            /** @type {Object} */
            var renderedDescriptor = {
                    format: attachments[attachNdx].readFormat,
                    width: attachmentW,
                    height: attachmentH,
                    depth: 1,
                    rowPitch: deMath.deAlign32(attachments[attachNdx].readFormat.getPixelSize() * attachmentW, readAlignment),
                    slicePitch: 0,
                    data: attachments[attachNdx].renderedData // ArrayBuffer
            };
            /** @type {tcuTexture.PixelBufferAccess} */ var rendered = new tcuTexture.PixelBufferAccess(renderedDescriptor);
            /** @type {gluTextureUtil.TransferFormat} */ var transferFmt = gluTextureUtil.getTransferFormat(attachments[attachNdx].readFormat);
            gl.readBuffer(gl.COLOR_ATTACHMENT0 + attachNdx);
            gl.readPixels(0, 0, attachmentW, attachmentH, transferFmt.format, transferFmt.dataType, rendered.getDataPtr());

            /** @type {Object} */
            var referenceDescriptor = {
                    format: attachments[attachNdx].referenceFormat,
                    width: attachmentW,
                    height: attachmentH,
                    depth: 1,
                    data: attachments[attachNdx].referenceData // ArrayBuffer
            };
            /** @type {tcuTexture.ConstPixelBufferAccess} */ var reference = new tcuTexture.ConstPixelBufferAccess(referenceDescriptor);
            /** @type {tcuTexture.TextureChannelClass} */ var texClass = tcuTexture.getTextureChannelClass(format.type);
            /** @type {boolean} */ var isOk = true;
            name = 'Attachment ' + attachNdx;
            /** @type {string} */ var desc = 'Color attachment ' + attachNdx;
            /** @type {Array<number>} */ var threshold;

            bufferedLogToConsole('Attachment ' + attachNdx + ': ' + numValidChannels + ' channels have defined values and used for comparison');

            switch (texClass) {
                case tcuTexture.TextureChannelClass.FLOATING_POINT: {
                    /** @type {Array<number>} */ var formatThreshold = []; // UVec4 //!< Threshold computed based on format.
                    formatThreshold.length = 4;
                    /** @type {number} */ var precThreshold = 0; // deUint32 //!< Threshold computed based on output type precision
                    /** @type {Array<number>} */ var finalThreshold = []; // UVec4
                    finalThreshold.length = 4;

                    switch (format.type) {
                        case tcuTexture.ChannelType.FLOAT:
                            formatThreshold = [4, 4, 4, 4]; // UVec4
                            break;
                        case tcuTexture.ChannelType.HALF_FLOAT:
                            formatThreshold = [(1 << 13) + 4, (1 << 13) + 4, (1 << 13) + 4, (1 << 13) + 4]; // UVec4
                            break;
                        case tcuTexture.ChannelType.UNSIGNED_INT_11F_11F_10F_REV:
                            formatThreshold = [(1 << 17) + 4, (1 << 17) + 4, (1 << 18) + 4, 4]; // UVec4
                            break;
                        default:
                            DE_ASSERT(false);
                            break;
                    }

                    switch (outPrecision) {
                        case gluShaderUtil.precision.PRECISION_LOWP:
                            precThreshold = (1 << 21);
                            break;
                        case gluShaderUtil.precision.PRECISION_MEDIUMP:
                            precThreshold = (1 << 13);
                            break;
                        case gluShaderUtil.precision.PRECISION_HIGHP:
                            precThreshold = 0;
                            break;
                        default:
                            DE_ASSERT(false);
                    }

                    finalThreshold = tcuTextureUtil.select(
                                    deMath.max(formatThreshold, [precThreshold, precThreshold, precThreshold, precThreshold]),
                                    [0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff], // C++ version: UVec4(~0u) bitwise not, all bits in the integer will be flipped
                                    cmpMask);

                    isOk = tcuImageCompare.floatUlpThresholdCompare(name, desc, reference, rendered, finalThreshold /*, tcu::COMPARE_LOG_RESULT*/);
                    break;
                }

                case tcuTexture.TextureChannelClass.UNSIGNED_FIXED_POINT: {
                    // \note glReadPixels() allows only 8 bits to be read. This means that RGB10_A2 will loose some
                    // bits in the process and it must be taken into account when computing threshold.
                    /** @type {Array<number>} */ var bits = deMath.min([8, 8, 8, 8], tcuTextureUtil.getTextureFormatBitDepth(format)); // IVec4

                    /** @type {Array<number>} */ var baseThreshold = []; // Vec4
                    baseThreshold.length = 4;
                    for (var inc = 0; inc < baseThreshold.length; inc++) {
                        // TODO: check the operation below: baseThreshold = 1.0f / ((IVec4(1) << bits)-1).asFloat();
                        baseThreshold[inc] = 1.0 / ((1 << bits[inc]) - 1);
                    }

                    threshold = tcuTextureUtil.select(baseThreshold, [2.0, 2.0, 2.0, 2.0], cmpMask); // Vec4

                    isOk = tcuImageCompare.floatThresholdCompare(name, desc, reference, rendered, threshold/*, tcu::COMPARE_LOG_RESULT*/);
                    break;
                }

                case tcuTexture.TextureChannelClass.SIGNED_INTEGER:
                case tcuTexture.TextureChannelClass.UNSIGNED_INTEGER: {
                    threshold = tcuTextureUtil.select(
                                    [0, 0, 0, 0],
                                    [1, 1, 1, 1],
                                    cmpMask
                                    ); // UVec4
                    isOk = tcuImageCompare.intThresholdCompare(name, desc, reference, rendered, threshold/*, tcu::COMPARE_LOG_RESULT*/);
                    break;
                }

                default:
                    testFailedOptions('Unsupported comparison', true);
                    break;
            }

            if (!isOk)
                allLevelsOk = false;
        }

        if (numAttachments > 1) {
            if (allLevelsOk)
                testPassed('Image comparison passed for ' +  numAttachments + ' attachments');
            else
                testFailed('Image comparison failed for some of ' +  numAttachments + ' attachments');
        } else {
            if (allLevelsOk)
                testPassed('Image comparison passed');
            else
                testFailed('Image comparison failed');
        }

        return tcuTestCase.IterateResult.STOP;
    };

    /**
     * es3fFragmentOutputTests.createRandomCase. Constructs the es3fFragmentOutputTests.createRandomCase, child class of es3fFragmentOutputTests.FragmentOutputCase
     * @constructor
     * @param {number} minRenderTargets
     * @param {number} maxRenderTargets
     * @param {number} seed
     * @return {es3fFragmentOutputTests.FragmentOutputCase} The currently modified object
     */
    es3fFragmentOutputTests.createRandomCase = function(minRenderTargets, maxRenderTargets, seed, colorBufferFloatSupported) {

        /** @type {Array<gluShaderUtil.DataType>} */
        var outputTypes = [
                           gluShaderUtil.DataType.FLOAT,
                           gluShaderUtil.DataType.FLOAT_VEC2,
                           gluShaderUtil.DataType.FLOAT_VEC3,
                           gluShaderUtil.DataType.FLOAT_VEC4,
                           gluShaderUtil.DataType.INT,
                           gluShaderUtil.DataType.INT_VEC2,
                           gluShaderUtil.DataType.INT_VEC3,
                           gluShaderUtil.DataType.INT_VEC4,
                           gluShaderUtil.DataType.UINT,
                           gluShaderUtil.DataType.UINT_VEC2,
                           gluShaderUtil.DataType.UINT_VEC3,
                           gluShaderUtil.DataType.UINT_VEC4
                           ];

        /** @type {Array<gluShaderUtil.precision>} */
        var precisions = [
                          gluShaderUtil.precision.PRECISION_LOWP,
                          gluShaderUtil.precision.PRECISION_MEDIUMP,
                          gluShaderUtil.precision.PRECISION_HIGHP
                          ];

        /** @type {Array<WebGLRenderingContextBase.GLenum>} */
        var floatFormats = [
                            gl.RGBA32F,
                            gl.RGBA16F,
                            gl.R11F_G11F_B10F,
                            gl.RG32F,
                            gl.RG16F,
                            gl.R32F,
                            gl.R16F,
                            gl.RGBA8,
                            gl.SRGB8_ALPHA8,
                            gl.RGB10_A2,
                            gl.RGBA4,
                            gl.RGB5_A1,
                            gl.RGB8,
                            gl.RGB565,
                            gl.RG8,
                            gl.R8
                            ];

        /** @type {Array<WebGLRenderingContextBase.GLenum>} */
        var colorBufferFloatFormats = [
                                       gl.RGBA32F,
                                       gl.RGBA16F,
                                       gl.R11F_G11F_B10F,
                                       gl.RG32F,
                                       gl.RG16F,
                                       gl.R32F,
                                       gl.R16F
        ];


        /** @type {Array<WebGLRenderingContextBase.GLenum>} */
        var intFormats = [
                            gl.RGBA32I,
                            gl.RGBA16I,
                            gl.RGBA8I,
                            gl.RG32I,
                            gl.RG16I,
                            gl.RG8I,
                            gl.R32I,
                            gl.R16I,
                            gl.R8I
                            ];

        /** @type {Array<WebGLRenderingContextBase.GLenum>} */
        var uintFormats = [
                           gl.RGBA32UI,
                           gl.RGBA16UI,
                           gl.RGBA8UI,
                           gl.RGB10_A2UI,
                           gl.RG32UI,
                           gl.RG16UI,
                           gl.RG8UI,
                           gl.R32UI,
                           gl.R16UI,
                           gl.R8UI
                           ];

        /** @type {deRandom.Random} */ var rnd = new deRandom.Random(seed);
        /** @type {Array<es3fFragmentOutputTests.FragmentOutput>} */ var outputs = [];
        /** @type {Array<es3fFragmentOutputTests.BufferSpec>} */ var targets = [];
        /** @type {Array<gluShaderUtil.DataType>} */ var outTypes = [];

        /** @type {number} */ var numTargets = rnd.getInt(minRenderTargets, maxRenderTargets);
        /** @type {number} */ var width = 128; // \todo [2012-04-10 pyry] Separate randomized sizes per target?
        /** @type {number} */ var height = 64;
        /** @type {number} */ var samples = 0;

        // Compute outputs.
        /** @type {number} */ var curLoc = 0;
        while (curLoc < numTargets) {
            /** @type {boolean} */ var useArray = rnd.getFloat() < 0.3;
            /** @type {number} */ var maxArrayLen = numTargets - curLoc;
            /** @type {number} */ var arrayLen = useArray ? rnd.getInt(1, maxArrayLen) : 0;
            /** @type {Array<gluShaderUtil.DataType>} */ var basicTypeArray = rnd.choose(outputTypes, undefined, 1);
            /** @type {gluShaderUtil.DataType} */ var basicType = basicTypeArray[0];
            /** @type {Array<gluShaderUtil.precision>} */ var precisionArray = rnd.choose(precisions, undefined, 1);
            /** @type {gluShaderUtil.precision} */ var precision = precisionArray[0];
            /** @type {number} */ var numLocations = useArray ? arrayLen : 1;

            outputs.push(new es3fFragmentOutputTests.FragmentOutput(basicType, precision, curLoc, arrayLen));

            for (var ndx = 0; ndx < numLocations; ndx++)
                outTypes.push(basicType);

            curLoc += numLocations;
        }
        DE_ASSERT(curLoc == numTargets);
        DE_ASSERT(outTypes.length == numTargets);

        // Compute buffers.
        while (targets.length < numTargets) {
            /** @type {gluShaderUtil.DataType} */ var outType = outTypes[targets.length];
            /** @type {boolean} */ var isFloat = gluShaderUtil.isDataTypeFloatOrVec(outType);
            /** @type {boolean} */ var isInt = gluShaderUtil.isDataTypeIntOrIVec(outType);
            /** @type {boolean} */ var isUint = gluShaderUtil.isDataTypeUintOrUVec(outType);
            /** @type {Array} */ var formatArray = [];
            /** @type {number} */ var format = 0;

            if (isFloat) {
                formatArray = rnd.choose(floatFormats, undefined, 1);
                format = formatArray[0];
                if (colorBufferFloatFormats.indexOf(format) >= 0 && !colorBufferFloatSupported)
                    return null;
            } else if (isInt) {
                formatArray = rnd.choose(intFormats, undefined, 1);
                format = formatArray[0];
            } else if (isUint) {
                formatArray = rnd.choose(uintFormats, undefined, 1);
                format = formatArray[0];
            } else
                DE_ASSERT(false);

            targets.push(new es3fFragmentOutputTests.BufferSpec(format, width, height, samples));
        }

        return new es3fFragmentOutputTests.FragmentOutputCase(seed.toString(), '', targets, outputs);

    };

    es3fFragmentOutputTests.init = function(gl) {
        var state = tcuTestCase.runner;
        state.testCases = tcuTestCase.newTest('fragment_outputs', 'Top level');
        /** @const @type {tcuTestCase.DeqpTest} */ var testGroup = state.testCases;

        /** @type {Array<WebGLRenderingContextBase.GLenum>} */
        var requiredFloatFormats = [
            gl.RGBA32F,
            gl.RGBA16F,
            gl.R11F_G11F_B10F,
            gl.RG32F,
            gl.RG16F,
            gl.R32F,
            gl.R16F
        ];

        /** @type {Array<WebGLRenderingContextBase.GLenum>} */
        var requiredFixedFormats = [
            gl.RGBA8,
            gl.SRGB8_ALPHA8,
            gl.RGB10_A2,
            gl.RGBA4,
            gl.RGB5_A1,
            gl.RGB8,
            gl.RGB565,
            gl.RG8,
            gl.R8
        ];

        /** @type {Array<WebGLRenderingContextBase.GLenum>} */
        var requiredIntFormats = [
            gl.RGBA32I,
            gl.RGBA16I,
            gl.RGBA8I,
            gl.RG32I,
            gl.RG16I,
            gl.RG8I,
            gl.R32I,
            gl.R16I,
            gl.R8I
        ];

        /** @type {Array<WebGLRenderingContextBase.GLenum>} */
        var requiredUintFormats = [
            gl.RGBA32UI,
            gl.RGBA16UI,
            gl.RGBA8UI,
            gl.RGB10_A2UI,
            gl.RG32UI,
            gl.RG16UI,
            gl.RG8UI,
            gl.R32UI,
            gl.R16UI,
            gl.R8UI
        ];

        /** @type {Array<gluShaderUtil.precision>} */
        var precisions = [

            gluShaderUtil.precision.PRECISION_LOWP,
            gluShaderUtil.precision.PRECISION_MEDIUMP,
            gluShaderUtil.precision.PRECISION_HIGHP

        ];

     // .basic.

        /** @type {tcuTestCase.DeqpTest} */ var basicGroup = tcuTestCase.newTest('basic', 'Basic fragment output tests');
        testGroup.addChild(basicGroup);

        /** @const @type {number} */ var width = 64;
        /** @const @type {number} */ var height = 64;
        /** @const @type {number} */ var samples = 0;
        /** @type {Array<es3fFragmentOutputTests.BufferSpec>} */ var fboSpec = null;
        /** @type {gluShaderUtil.precision} */ var prec;
        /** @type {string} */ var precName;

    // .float
        if (gl.getExtension('EXT_color_buffer_float')) {
            /** @type {tcuTestCase.DeqpTest} */ var floatGroup = tcuTestCase.newTest('float', 'Floating-point output tests');
            basicGroup.addChild(floatGroup);

            for (var fmtNdx = 0; fmtNdx < requiredFloatFormats.length; fmtNdx++) {
                var format = requiredFloatFormats[fmtNdx];
                var fmtName = es3fFboTestUtil.getFormatName(format);
                fboSpec = [];

                fboSpec.push(new es3fFragmentOutputTests.BufferSpec(format, width, height, samples));

                for (var precNdx = 0; precNdx < precisions.length; precNdx++) {
                    prec = precisions[precNdx];
                    precName = gluShaderUtil.getPrecisionName(prec);

                    // NOTE: Eliminated original OutputVec and toVec(), as it only returned an element of the outputs array in OutputVec
                    floatGroup.addChild(new es3fFragmentOutputTests.FragmentOutputCase(fmtName + '_' + precName + '_float', '', fboSpec, [new es3fFragmentOutputTests.FragmentOutput(gluShaderUtil.DataType.FLOAT, prec, 0)]));
                    floatGroup.addChild(new es3fFragmentOutputTests.FragmentOutputCase(fmtName + '_' + precName + '_vec2', '', fboSpec, [new es3fFragmentOutputTests.FragmentOutput(gluShaderUtil.DataType.FLOAT_VEC2, prec, 0)]));
                    floatGroup.addChild(new es3fFragmentOutputTests.FragmentOutputCase(fmtName + '_' + precName + '_vec3', '', fboSpec, [new es3fFragmentOutputTests.FragmentOutput(gluShaderUtil.DataType.FLOAT_VEC3, prec, 0)]));
                    floatGroup.addChild(new es3fFragmentOutputTests.FragmentOutputCase(fmtName + '_' + precName + '_vec4', '', fboSpec, [new es3fFragmentOutputTests.FragmentOutput(gluShaderUtil.DataType.FLOAT_VEC4, prec, 0)]));
                }
            }
        }

        // .fixed
        /** @type {tcuTestCase.DeqpTest} */ var fixedGroup = tcuTestCase.newTest('fixed', 'Fixed-point output tests');
        basicGroup.addChild(fixedGroup);
        for (var fmtNdx = 0; fmtNdx < requiredFixedFormats.length; fmtNdx++) {
            var format = requiredFixedFormats[fmtNdx];
            var fmtName = es3fFboTestUtil.getFormatName(format);
            fboSpec = [];

            fboSpec.push(new es3fFragmentOutputTests.BufferSpec(format, width, height, samples));

            for (var precNdx = 0; precNdx < precisions.length; precNdx++) {
                prec = precisions[precNdx];
                precName = gluShaderUtil.getPrecisionName(prec);

                fixedGroup.addChild(new es3fFragmentOutputTests.FragmentOutputCase(fmtName + '_' + precName + '_float', '', fboSpec, [new es3fFragmentOutputTests.FragmentOutput(gluShaderUtil.DataType.FLOAT, prec, 0)]));
                fixedGroup.addChild(new es3fFragmentOutputTests.FragmentOutputCase(fmtName + '_' + precName + '_vec2', '', fboSpec, [new es3fFragmentOutputTests.FragmentOutput(gluShaderUtil.DataType.FLOAT_VEC2, prec, 0)]));
                fixedGroup.addChild(new es3fFragmentOutputTests.FragmentOutputCase(fmtName + '_' + precName + '_vec3', '', fboSpec, [new es3fFragmentOutputTests.FragmentOutput(gluShaderUtil.DataType.FLOAT_VEC3, prec, 0)]));
                fixedGroup.addChild(new es3fFragmentOutputTests.FragmentOutputCase(fmtName + '_' + precName + '_vec4', '', fboSpec, [new es3fFragmentOutputTests.FragmentOutput(gluShaderUtil.DataType.FLOAT_VEC4, prec, 0)]));
            }
        }

        // .int
        /** @type {tcuTestCase.DeqpTest} */ var intGroup = tcuTestCase.newTest('int', 'Integer output tests');
        basicGroup.addChild(intGroup);
        for (var fmtNdx = 0; fmtNdx < requiredIntFormats.length; fmtNdx++) {
            var format = requiredIntFormats[fmtNdx];
            var fmtName = es3fFboTestUtil.getFormatName(format);
            fboSpec = [];

            fboSpec.push(new es3fFragmentOutputTests.BufferSpec(format, width, height, samples));

            for (var precNdx = 0; precNdx < precisions.length; precNdx++) {
                prec = precisions[precNdx];
                precName = gluShaderUtil.getPrecisionName(prec);

                intGroup.addChild(new es3fFragmentOutputTests.FragmentOutputCase(fmtName + '_' + precName + '_int', '', fboSpec, [new es3fFragmentOutputTests.FragmentOutput(gluShaderUtil.DataType.INT, prec, 0)]));
                intGroup.addChild(new es3fFragmentOutputTests.FragmentOutputCase(fmtName + '_' + precName + '_ivec2', '', fboSpec, [new es3fFragmentOutputTests.FragmentOutput(gluShaderUtil.DataType.INT_VEC2, prec, 0)]));
                intGroup.addChild(new es3fFragmentOutputTests.FragmentOutputCase(fmtName + '_' + precName + '_ivec3', '', fboSpec, [new es3fFragmentOutputTests.FragmentOutput(gluShaderUtil.DataType.INT_VEC3, prec, 0)]));
                intGroup.addChild(new es3fFragmentOutputTests.FragmentOutputCase(fmtName + '_' + precName + '_ivec4', '', fboSpec, [new es3fFragmentOutputTests.FragmentOutput(gluShaderUtil.DataType.INT_VEC4, prec, 0)]));
            }
        }

        // .uint
        /** @type {tcuTestCase.DeqpTest} */ var uintGroup = tcuTestCase.newTest('uint', 'Usigned integer output tests');
        basicGroup.addChild(uintGroup);
        for (var fmtNdx = 0; fmtNdx < requiredUintFormats.length; fmtNdx++) {
            var format = requiredUintFormats[fmtNdx];
            var fmtName = es3fFboTestUtil.getFormatName(format);
            fboSpec = [];

            fboSpec.push(new es3fFragmentOutputTests.BufferSpec(format, width, height, samples));

            for (var precNdx = 0; precNdx < precisions.length; precNdx++) {
                prec = precisions[precNdx];
                precName = gluShaderUtil.getPrecisionName(prec);

                uintGroup.addChild(new es3fFragmentOutputTests.FragmentOutputCase(fmtName + '_' + precName + '_uint', '', fboSpec, [new es3fFragmentOutputTests.FragmentOutput(gluShaderUtil.DataType.UINT, prec, 0)]));
                uintGroup.addChild(new es3fFragmentOutputTests.FragmentOutputCase(fmtName + '_' + precName + '_uvec2', '', fboSpec, [new es3fFragmentOutputTests.FragmentOutput(gluShaderUtil.DataType.UINT_VEC2, prec, 0)]));
                uintGroup.addChild(new es3fFragmentOutputTests.FragmentOutputCase(fmtName + '_' + precName + '_uvec3', '', fboSpec, [new es3fFragmentOutputTests.FragmentOutput(gluShaderUtil.DataType.UINT_VEC3, prec, 0)]));
                uintGroup.addChild(new es3fFragmentOutputTests.FragmentOutputCase(fmtName + '_' + precName + '_uvec4', '', fboSpec, [new es3fFragmentOutputTests.FragmentOutput(gluShaderUtil.DataType.UINT_VEC4, prec, 0)]));

            }
        }

     // .array

        /** @type {tcuTestCase.DeqpTest} */ var arrayGroup = tcuTestCase.newTest('array', 'Array outputs');
        testGroup.addChild(arrayGroup);

        /** @type {number} */ var numTargets = 3;

        // .float
        if (gl.getExtension('EXT_color_buffer_float')) {
            /** @type {tcuTestCase.DeqpTest} */ var arrayFloatGroup = tcuTestCase.newTest('float', 'Floating-point output tests');
            arrayGroup.addChild(arrayFloatGroup);
            for (var fmtNdx = 0; fmtNdx < requiredFloatFormats.length; fmtNdx++) {
                var format = requiredFloatFormats[fmtNdx];
                var fmtName = es3fFboTestUtil.getFormatName(format);
                fboSpec = [];

                for (var ndx = 0; ndx < numTargets; ndx++)
                    fboSpec.push(new es3fFragmentOutputTests.BufferSpec(format, width, height, samples));

                for (var precNdx = 0; precNdx < precisions.length; precNdx++) {
                    prec = precisions[precNdx];
                    precName = gluShaderUtil.getPrecisionName(prec);

                    arrayFloatGroup.addChild(new es3fFragmentOutputTests.FragmentOutputCase(fmtName + '_' + precName + '_float', '', fboSpec, [new es3fFragmentOutputTests.FragmentOutput(gluShaderUtil.DataType.FLOAT, prec, 0, numTargets)]));
                    arrayFloatGroup.addChild(new es3fFragmentOutputTests.FragmentOutputCase(fmtName + '_' + precName + '_vec2', '', fboSpec, [new es3fFragmentOutputTests.FragmentOutput(gluShaderUtil.DataType.FLOAT_VEC2, prec, 0, numTargets)]));
                    arrayFloatGroup.addChild(new es3fFragmentOutputTests.FragmentOutputCase(fmtName + '_' + precName + '_vec3', '', fboSpec, [new es3fFragmentOutputTests.FragmentOutput(gluShaderUtil.DataType.FLOAT_VEC3, prec, 0, numTargets)]));
                    arrayFloatGroup.addChild(new es3fFragmentOutputTests.FragmentOutputCase(fmtName + '_' + precName + '_vec4', '', fboSpec, [new es3fFragmentOutputTests.FragmentOutput(gluShaderUtil.DataType.FLOAT_VEC4, prec, 0, numTargets)]));
                }
            }
        }

        // .fixed
        /** @type {tcuTestCase.DeqpTest} */ var arrayFixedGroup = tcuTestCase.newTest('fixed', 'Fixed-point output tests');
        arrayGroup.addChild(arrayFixedGroup);
        for (var fmtNdx = 0; fmtNdx < requiredFixedFormats.length; fmtNdx++) {
            var format = requiredFixedFormats[fmtNdx];
            var fmtName = es3fFboTestUtil.getFormatName(format);
            fboSpec = [];

            for (var ndx = 0; ndx < numTargets; ndx++)
                fboSpec.push(new es3fFragmentOutputTests.BufferSpec(format, width, height, samples));

            for (var precNdx = 0; precNdx < precisions.length; precNdx++) {
                prec = precisions[precNdx];
                precName = gluShaderUtil.getPrecisionName(prec);

                arrayFixedGroup.addChild(new es3fFragmentOutputTests.FragmentOutputCase(fmtName + '_' + precName + '_float', '', fboSpec, [new es3fFragmentOutputTests.FragmentOutput(gluShaderUtil.DataType.FLOAT, prec, 0, numTargets)]));
                arrayFixedGroup.addChild(new es3fFragmentOutputTests.FragmentOutputCase(fmtName + '_' + precName + '_vec2', '', fboSpec, [new es3fFragmentOutputTests.FragmentOutput(gluShaderUtil.DataType.FLOAT_VEC2, prec, 0, numTargets)]));
                arrayFixedGroup.addChild(new es3fFragmentOutputTests.FragmentOutputCase(fmtName + '_' + precName + '_vec3', '', fboSpec, [new es3fFragmentOutputTests.FragmentOutput(gluShaderUtil.DataType.FLOAT_VEC3, prec, 0, numTargets)]));
                arrayFixedGroup.addChild(new es3fFragmentOutputTests.FragmentOutputCase(fmtName + '_' + precName + '_vec4', '', fboSpec, [new es3fFragmentOutputTests.FragmentOutput(gluShaderUtil.DataType.FLOAT_VEC4, prec, 0, numTargets)]));
            }
        }

        // .int
        /** @type {tcuTestCase.DeqpTest} */ var arrayIntGroup = tcuTestCase.newTest('int', 'Integer output tests');
        arrayGroup.addChild(arrayIntGroup);
        for (var fmtNdx = 0; fmtNdx < requiredIntFormats.length; fmtNdx++) {
            var format = requiredIntFormats[fmtNdx];
            var fmtName = es3fFboTestUtil.getFormatName(format);
            fboSpec = [];

            for (var ndx = 0; ndx < numTargets; ndx++)
                fboSpec.push(new es3fFragmentOutputTests.BufferSpec(format, width, height, samples));

            for (var precNdx = 0; precNdx < precisions.length; precNdx++) {
                prec = precisions[precNdx];
                precName = gluShaderUtil.getPrecisionName(prec);

                arrayIntGroup.addChild(new es3fFragmentOutputTests.FragmentOutputCase(fmtName + '_' + precName + '_int', '', fboSpec, [new es3fFragmentOutputTests.FragmentOutput(gluShaderUtil.DataType.INT, prec, 0, numTargets)]));
                arrayIntGroup.addChild(new es3fFragmentOutputTests.FragmentOutputCase(fmtName + '_' + precName + '_ivec2', '', fboSpec, [new es3fFragmentOutputTests.FragmentOutput(gluShaderUtil.DataType.INT_VEC2, prec, 0, numTargets)]));
                arrayIntGroup.addChild(new es3fFragmentOutputTests.FragmentOutputCase(fmtName + '_' + precName + '_ivec3', '', fboSpec, [new es3fFragmentOutputTests.FragmentOutput(gluShaderUtil.DataType.INT_VEC3, prec, 0, numTargets)]));
                arrayIntGroup.addChild(new es3fFragmentOutputTests.FragmentOutputCase(fmtName + '_' + precName + '_ivec4', '', fboSpec, [new es3fFragmentOutputTests.FragmentOutput(gluShaderUtil.DataType.INT_VEC4, prec, 0, numTargets)]));
            }
        }

        // .uint
        /** @type {tcuTestCase.DeqpTest} */ var arrayUintGroup = tcuTestCase.newTest('uint', 'Usigned integer output tests');
        arrayGroup.addChild(arrayUintGroup);
        for (var fmtNdx = 0; fmtNdx < requiredUintFormats.length; fmtNdx++) {
            var format = requiredUintFormats[fmtNdx];
            var fmtName = es3fFboTestUtil.getFormatName(format);
            fboSpec = [];

            for (var ndx = 0; ndx < numTargets; ndx++)
                fboSpec.push(new es3fFragmentOutputTests.BufferSpec(format, width, height, samples));

            for (var precNdx = 0; precNdx < precisions.length; precNdx++) {
                prec = precisions[precNdx];
                precName = gluShaderUtil.getPrecisionName(prec);

                arrayUintGroup.addChild(new es3fFragmentOutputTests.FragmentOutputCase(fmtName + '_' + precName + '_uint', '', fboSpec, [new es3fFragmentOutputTests.FragmentOutput(gluShaderUtil.DataType.UINT, prec, 0, numTargets)]));
                arrayUintGroup.addChild(new es3fFragmentOutputTests.FragmentOutputCase(fmtName + '_' + precName + '_uvec2', '', fboSpec, [new es3fFragmentOutputTests.FragmentOutput(gluShaderUtil.DataType.UINT_VEC2, prec, 0, numTargets)]));
                arrayUintGroup.addChild(new es3fFragmentOutputTests.FragmentOutputCase(fmtName + '_' + precName + '_uvec3', '', fboSpec, [new es3fFragmentOutputTests.FragmentOutput(gluShaderUtil.DataType.UINT_VEC3, prec, 0, numTargets)]));
                arrayUintGroup.addChild(new es3fFragmentOutputTests.FragmentOutputCase(fmtName + '_' + precName + '_uvec4', '', fboSpec, [new es3fFragmentOutputTests.FragmentOutput(gluShaderUtil.DataType.UINT_VEC4, prec, 0, numTargets)]));
            }
        }

    // .random

        /** @type {tcuTestCase.DeqpTest} */ var randomGroup = tcuTestCase.newTest('random', 'Random fragment output cases');
        testGroup.addChild(randomGroup);

        /** @type {boolean} */ var colorBufferFloatSupported = (gl.getExtension('EXT_color_buffer_float') != null);
        for (var seed = 0; seed < 100; seed++) {
            var test = es3fFragmentOutputTests.createRandomCase(2, 4, seed, colorBufferFloatSupported);
            if (test !== null) {
                randomGroup.addChild(test);
            }
        }

    };

    /**
     * Create and execute the test cases
     */
    es3fFragmentOutputTests.run = function(context) {
        gl = context;
      //Set up Test Root parameters
        var testName = 'fragment_output';
        var testDescription = 'Fragment Output Tests';
        var state = tcuTestCase.runner;

        state.testName = testName;
        state.testCases = tcuTestCase.newTest(testName, testDescription, null);

      //Set up name and description of this test series.
        setCurrentTestName(testName);
        description(testDescription);

        try {
            es3fFragmentOutputTests.init(gl);
            tcuTestCase.runTestCases();
        } catch (err) {
            testFailedOptions('Failed to es3fFragmentOutputTests.run tests', false);
            console.log(err);
            tcuTestCase.runner.terminate();
        }

    };

});
