<!--
Copyright (c) 2019 The Khronos Group Inc.
Use of this source code is governed by an MIT-style license that can be
found in the LICENSE.txt file.
-->

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>OES_texture_float/WEBGL_color_buffer_float</title>
<link rel="stylesheet" href="../../resources/js-test-style.css"/>
<script src="../../js/js-test-pre.js"></script>
<script src="../../js/webgl-test-utils.js"></script>
<script src="../../js/tests/ext-float-blend.js"></script>
</head>
<body>
<div id="description"></div>
<canvas id="canvas" style="width: 50px; height: 50px;"> </canvas>
<div id="console"></div>
<!-- Shaders for testing floating-point textures -->
<script id="testFragmentShader" type="x-shader/x-fragment">
precision mediump float;
uniform sampler2D tex;
uniform vec4 subtractor;
varying vec2 texCoord;
void main()
{
    vec4 color = texture2D(tex, texCoord);
    if (abs(color.r - subtractor.r) +
        abs(color.g - subtractor.g) +
        abs(color.b - subtractor.b) +
        abs(color.a - subtractor.a) < 8.0) {
        gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0);
    } else {
        gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
    }
}
</script>
<!-- Shaders for testing floating-point render targets -->
<script id="positionVertexShader" type="x-shader/x-vertex">
attribute vec4 vPosition;
void main()
{
    gl_Position = vPosition;
}
</script>
<script id="floatingPointFragmentShader" type="x-shader/x-fragment">
void main()
{
    gl_FragColor = vec4(10000.0, 10000.0, 10000.0, 10000.0);
}
</script>
<script>
"use strict";
description("This test verifies the functionality of the OES_texture_float and WEBGL_color_buffer_float extensions, if available.");

debug("");

var wtu = WebGLTestUtils;
var canvas = document.getElementById("canvas");
var gl = wtu.create3DContext(canvas);

if (!gl) {
    testFailed("WebGL context does not exist");
} else {
    testPassed("WebGL context exists");

    var texturedShaders = [
        wtu.simpleTextureVertexShader,
        "testFragmentShader"
    ];
    var testProgram =
        wtu.setupProgram(gl,
                         texturedShaders,
                         ['vPosition', 'texCoord0'],
                         [0, 1]);
    var quadParameters = wtu.setupUnitQuad(gl, 0, 1);

    // First verify that allocation of floating-point textures fails if
    // the extension has not been enabled yet.
    runTextureCreationTest(testProgram, false);

    if (!gl.getExtension("OES_texture_float")) {
        testPassed("No OES_texture_float support -- this is legal");
    } else {
        testPassed("Successfully enabled OES_texture_float extension");
        // If alpha value is missing from a texture it gets filled to 1 when sampling according to GLES2.0 table 3.12
        runTextureCreationTest(testProgram, true, gl.RGBA, 4, [10000, 10000, 10000, 10000]);
        runTextureCreationTest(testProgram, true, gl.RGB, 3, [10000, 10000, 10000, 1]);
        runTextureCreationTest(testProgram, true, gl.LUMINANCE, 1, [10000, 10000, 10000, 1]);
        runTextureCreationTest(testProgram, true, gl.ALPHA, 1, [0, 0, 0, 10000]);
        runTextureCreationTest(testProgram, true, gl.LUMINANCE_ALPHA, 2, [10000, 10000, 10000, 10000]);

        (function() {
            debug("");
            var renderable = isRenderable(gl);
            var renderableExtName = "WEBGL_color_buffer_float";
            var supported = gl.getSupportedExtensions().includes(renderableExtName);
            if (renderable && !supported) {
                testFailed("RGBA/FLOAT is color renderable but " + renderableExtName + " not exposed");
            } else if (supported && !renderable) {
                testFailed(renderableExtName + " is exposed but RGBA/FLOAT is not color renderable");
            }
            if (supported) {
                runRenderTargetAndReadbackTest(testProgram, gl.RGBA, 4, [10000, 10000, 10000, 10000], 0, true);
                runRenderTargetAndReadbackTest(testProgram, gl.RGB, 3, [10000, 10000, 10000, 1], 0, false);
                runRenderTargetAndReadbackTest(testProgram, gl.RGBA, 4, [10000, 10000, 10000, 10000], 1, true);
                runRenderTargetAndReadbackTest(testProgram, gl.RGBA, 4, [10000, 10000, 10000, 10000], 0.5, true);
                runFramebufferTest();

                debug("");
                debug("Test float32 blending without EXT_float_blend.");
                testExtFloatBlend(gl.RGBA);

                debug("");
                debug("Testing that float32 blending succeeds with EXT_float_blend.");
                if (!gl.getExtension("EXT_float_blend")) {
                    testPassed("No EXT_float_blend support -- this is legal");
                    return;
                }
                testExtFloatBlend(gl.RGBA);
            }
        })();

        runUniqueObjectTest();
    }
}

function isRenderable(gl) {
    var tex = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, tex);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.FLOAT, null);

    var fb = gl.createFramebuffer();
    gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);

    var status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
    gl.deleteFramebuffer(fb);
    gl.deleteTexture(tex);

    return status == gl.FRAMEBUFFER_COMPLETE;
}

function allocateTexture()
{
    var texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, texture);
    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);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "texture parameter setup should succeed");
    return texture;
}

function checkRenderingResults()
{
    wtu.checkCanvas(gl, [0, 255, 0, 255], "should be green");
}

function runTextureCreationTest(testProgram, extensionEnabled, opt_format, opt_numChannels, opt_subtractor)
{
    var format = opt_format || gl.RGBA;
    var numberOfChannels = opt_numChannels || 4;
    var expectFailure = !extensionEnabled;
    var subtractor = opt_subtractor || [10000, 10000, 10000, 10000];

    debug("");
    debug("testing format: " + wtu.glEnumToString(gl, format) +
          " expect:" + (extensionEnabled ? "success" : "failure"));

    var texture = allocateTexture();
    // Generate data.
    var width = 2;
    var height = 2;
    var data = new Float32Array(width * height * numberOfChannels);
    for (var ii = 0; ii < data.length; ++ii) {
        data[ii] = 10000;
    }
    gl.texImage2D(gl.TEXTURE_2D, 0, format, width, height, 0, format, gl.FLOAT, data);
    if (expectFailure) {
        wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "floating-point texture allocation must be disallowed if OES_texture_float isn't enabled");
        return;
    } else {
        wtu.glErrorShouldBe(gl, gl.NO_ERROR, "floating-point texture allocation should succeed if OES_texture_float is enabled");
    }
    // Verify that the texture actually works for sampling and contains the expected data.
    gl.uniform4fv(gl.getUniformLocation(testProgram, "subtractor"), subtractor);
    wtu.clearAndDrawUnitQuad(gl);
    checkRenderingResults();

    // Check that linear fails.
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    wtu.clearAndDrawUnitQuad(gl);
    wtu.checkCanvas(gl, [255, 0, 0, 255], "should be red");
}

function arrayToString(arr, size) {
    var mySize;
    if (!size)
        mySize = arr.length;
    else
        mySize = size;
    var out = "[";
    for (var ii = 0; ii < mySize; ++ii) {
    if (ii > 0) {
        out += ", ";
    }
    out += arr[ii];
    }
    return out + "]";
}

function runRenderTargetAndReadbackTest(testProgram, format, numberOfChannels, subtractor, texSubImageCover, requireRenderable)
{
    var formatString = wtu.glEnumToString(gl, format);
    debug("");
    debug("testing floating-point " + formatString + " render target" + (texSubImageCover > 0 ? " after calling texSubImage" : ""));

    var texture = allocateTexture();
    var width = 2;
    var height = 2;
    gl.texImage2D(gl.TEXTURE_2D, 0, format, width, height, 0, format, gl.FLOAT, null);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "floating-point texture allocation should succeed if OES_texture_float is enabled");

    // Try to use this texture as a render target.
    var fbo = gl.createFramebuffer();
    gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
    gl.bindTexture(gl.TEXTURE_2D, null);
    // It is legal for a WebGL implementation exposing the OES_texture_float extension to
    // support floating-point textures but not as attachments to framebuffer objects.
    if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE) {
        if (requireRenderable)
            testFailed("floating-point " + formatString + " render target not supported");
        else
            debug("floating-point " + formatString + " render target not supported -- this is legal");
        return;
    }

    if (texSubImageCover > 0) {
        // Ensure that replacing the whole texture or a part of it with texSubImage2D doesn't affect renderability
        gl.bindTexture(gl.TEXTURE_2D, texture);
        var data = new Float32Array(width * height * numberOfChannels * texSubImageCover);
        gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width, height * texSubImageCover, format, gl.FLOAT, data);
        wtu.glErrorShouldBe(gl, gl.NO_ERROR, "texSubImage2D should succeed if OES_texture_float is enabled");
        gl.bindTexture(gl.TEXTURE_2D, null);
        if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE) {
            testFailed("render target support changed after calling texSubImage2D");
            return;
        }
    }

    var renderProgram =
        wtu.setupProgram(gl,
                         ["positionVertexShader", "floatingPointFragmentShader"],
                         ['vPosition'],
                         [0]);
    wtu.clearAndDrawUnitQuad(gl);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "rendering to floating-point texture should succeed");

    // Now sample from the floating-point texture and verify we got the correct values.
    gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.useProgram(testProgram);
    gl.uniform1i(gl.getUniformLocation(testProgram, "tex"), 0);
    gl.uniform4fv(gl.getUniformLocation(testProgram, "subtractor"), subtractor);
    wtu.clearAndDrawUnitQuad(gl);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "rendering from floating-point texture should succeed");
    checkRenderingResults();

    // Finally, if the implementation supports floating-point readback, verify it.
    gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
    var implFormat = gl.getParameter(gl.IMPLEMENTATION_COLOR_READ_FORMAT);
    var implType = gl.getParameter(gl.IMPLEMENTATION_COLOR_READ_TYPE);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "getParameter of IMPLEMENTATION_COLOR_READ_{FORMAT|TYPE} should succeed");
    if ((implFormat == gl.RGBA || implFormat == gl.RGB) && implType == gl.FLOAT) {
    debug("Checking readback of floating-point values");
    var arraySize = (implFormat == gl.RGBA) ? 4 : 3
    var buf = new Float32Array(arraySize);
    gl.readPixels(0, 0, 1, 1, implFormat, implType , buf);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "readPixels from floating-point renderbuffer should succeed");
    var ok = true;
    var tolerance = 8.0; // TODO: factor this out from both this test and the subtractor shader above.
    for (var ii = 0; ii < buf.length; ++ii) {
        if (Math.abs(buf[ii] - subtractor[ii]) > tolerance) {
        ok = false;
        break;
        }
    }
    if (ok) {
        testPassed("readPixels of float-type data from floating-point renderbuffer succeeded");
    } else {
        testFailed("readPixels of float-type data from floating-point renderbuffer failed: expected "
               + arrayToString(subtractor, arraySize) + ", got " + arrayToString(buf));
    }
    }
}

function runUniqueObjectTest()
{
    debug("");
    debug("Testing that getExtension() returns the same object each time");
    gl.getExtension("OES_texture_float").myProperty = 2;
    webglHarnessCollectGarbage();
    shouldBe('gl.getExtension("OES_texture_float").myProperty', '2');
}

// Make sure we can call readPixels with the passed in arrayBufferConstructor and that the color
// channels are the ones we expect. If there is a mismatch between the glType and arrayBuffer type,
// fail the test.
function verifyReadPixelsColors(red, green, blue, alpha, alphaRGB, glFormat, glType, arrayBufferConstructor) {
    var typeName = wtu.glEnumToString(gl, glType);

    debug(wtu.glEnumToString(gl, glFormat) + " framebuffer with " + typeName + " readback.");

    var arrayBuffer = new arrayBufferConstructor(4);
    gl.readPixels(0, 0, 1, 1, gl.RGBA, glType, arrayBuffer);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "readPixels should return NO_ERROR when reading " + typeName + " data.");

    assertMsg(arrayBuffer[0] === red, "Red channel should be " + red + " for " + typeName + " readPixels. Received: " + arrayBuffer[0]);
    assertMsg(arrayBuffer[1] === green, "Green channel should be " + green + " for " + typeName + " readPixels. Received: " + arrayBuffer[1]);
    assertMsg(arrayBuffer[2] === blue, "Blue channel should be " + blue + " for " + typeName + " readPixels. Received: " + arrayBuffer[2]);
    if (glFormat === gl.RGBA) {
        assertMsg(arrayBuffer[3] === alpha, "Alpha channel should be " + alpha + " for " + typeName + " readPixels. Received: " + arrayBuffer[3]);
    } else if (glFormat === gl.RGB) {
        assertMsg(arrayBuffer[3] === alphaRGB, "Alpha channel should be " + alphaRGB + " for " + typeName + " readPixels. Received: " + arrayBuffer[3]);
    }

    // Make sure any arrayBuffer types that are not equal to arrayBufferConstructor fail readPixels.
    if (arrayBufferConstructor !== Uint8Array) {
        arrayBuffer = new Uint8Array(4);
        gl.readPixels(0, 0, 1, 1, gl.RGBA, glType, arrayBuffer);
        wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "readPixels should return INVALID_OPERATION when reading mismatched types. " + Uint8Array.toString());
    }
    if (arrayBufferConstructor !== Float32Array) {
        arrayBuffer = new Float32Array(4);
        gl.readPixels(0, 0, 1, 1, gl.RGBA, glType, arrayBuffer);
        wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "readPixels should return INVALID_OPERATION when reading mismatched types. " + Float32Array.toString());
    }
    if (arrayBufferConstructor !== Uint16Array) {
        arrayBuffer = new Uint16Array(4);
        gl.readPixels(0, 0, 1, 1, gl.RGBA, glType, arrayBuffer);
        wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "readPixels should return INVALID_OPERATION when reading mismatched types. " + Uint16Array.toString());
    }
}

// Verify that float textures attached to frame buffers function correctly with regard to framebuffer
// completness, IMPLEMENTATION_COLOR_READ_FORMAT/TYPE and readPixels
function runFramebufferTest() {
    debug("");
    debug("Framebuffer Tests");

    var texture = allocateTexture();
    gl.bindTexture(gl.TEXTURE_2D, texture);

    var fbo = gl.createFramebuffer();
    gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);

    debug("Ensure non-color-renderable formats [LUMINANCE, LUMINANCE_ALPHA, ALPHA] fail.");
    var arrayBufferFloatOutput = new Float32Array(4); // 4 color channels
    [gl.LUMINANCE, gl.LUMINANCE_ALPHA, gl.ALPHA].forEach(function(badFormat) {
        debug(wtu.glEnumToString(gl, badFormat) + " framebuffer");

        gl.texImage2D(gl.TEXTURE_2D, 0, badFormat, 1, 1, 0, badFormat, gl.FLOAT, null);
        shouldBe("gl.checkFramebufferStatus(gl.FRAMEBUFFER)", "gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT");

        shouldBeNull("gl.getParameter(gl.IMPLEMENTATION_COLOR_READ_FORMAT)");
        wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "IMPLEMENTATION_COLOR_READ_FORMAT should fail for incomplete framebuffers.");

        shouldBeNull("gl.getParameter(gl.IMPLEMENTATION_COLOR_READ_TYPE)");
        wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "IMPLEMENTATION_COLOR_READ_TYPE should fail for incomplete framebuffers.");

        gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.FLOAT, arrayBufferFloatOutput);
        wtu.glErrorShouldBe(gl, gl.INVALID_FRAMEBUFFER_OPERATION , "readPixels should fail on incomplete framebuffers.");
        debug("");
    });

    debug("Ensure color renderable formats [RGBA, RGB] succeed.");
    var arrayBufferFloatInput = new Float32Array(4); // 4 color channels
    arrayBufferFloatInput[0] = 0;
    arrayBufferFloatInput[1] = .25;
    arrayBufferFloatInput[2] = .50;
    arrayBufferFloatInput[3] = .75;

    [gl.RGBA, gl.RGB].forEach(function(goodFormat) {
        debug("");
        debug(wtu.glEnumToString(gl, goodFormat) + " framebuffer tests");

        gl.texImage2D(gl.TEXTURE_2D, 0, goodFormat, 1, 1, 0, goodFormat, gl.FLOAT, arrayBufferFloatInput);
        if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE) {
            testPassed("Format is not renderable. This is allowed.");
            return;
        }
        verifyReadPixelsColors(
            0.00, // red
            0.25, // green
            0.50, // blue
            0.75, // alpha
            1.0,  // alphaRGB
            goodFormat,
            gl.FLOAT,
            Float32Array);

        var implementationColorReadFormat = gl.getParameter(gl.IMPLEMENTATION_COLOR_READ_FORMAT);
        assertMsg(implementationColorReadFormat === gl.RGBA || implementationColorReadFormat === gl.RGB,
            "IMPLEMENTATION_COLOR_READ_FORMAT should be color renderable: RGBA or RGB. Received: " + wtu.glEnumToString(gl, implementationColorReadFormat));

        var implementationColorReadType = gl.getParameter(gl.IMPLEMENTATION_COLOR_READ_TYPE);
        assertMsg(implementationColorReadType === gl.UNSIGNED_BYTE ||
                  implementationColorReadType === gl.FLOAT ||
                  "IMPLEMENTATION_COLOR_READ_TYPE must be one of UNSIGNED_BYTE or FLOAT " +
                  "Received: " + wtu.glEnumToString(gl, implementationColorReadType));
    });
}

debug("");
var successfullyParsed = true;
</script>
<script src="../../js/js-test-post.js"></script>

</body>
</html>
