<!--
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>WebGL EXT_disjoint_timer_query Conformance Tests</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>
</head>
<body>
<div id="description"></div>
<canvas id="canvas" style="width: 50px; height: 50px;"> </canvas>
<div id="console"></div>

<script>
"use strict";
description("This test verifies the functionality of the EXT_disjoint_timer_query extension, if it is available.");

var wtu = WebGLTestUtils;
var canvas = document.getElementById("canvas");
var gl = wtu.create3DContext(canvas);
var ext = null;
var query = null;
var query2 = null;
var elapsed_query = null;
var timestamp_query1 = null;
var timestamp_query2 = null;
var availability_retry = 500;
var timestamp_counter_bits = 0;

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

    // Query the extension and store globally so shouldBe can access it
    ext = wtu.getExtensionWithKnownPrefixes(gl, "EXT_disjoint_timer_query");
    if (!ext) {
        testPassed("No EXT_disjoint_timer_query support -- this is legal");
        finishTest();
    } else {
        if (wtu.getDefault3DContextVersion() > 1) {
            testFailed("EXT_disjoint_timer_query must not be advertised on WebGL 2.0 contexts");
            finishTest();
        } else {
            runSanityTests();

            // Clear disjoint value.
            gl.getParameter(ext.GPU_DISJOINT_EXT);

            runElapsedTimeTest();
            timestamp_counter_bits = ext.getQueryEXT(ext.TIMESTAMP_EXT, ext.QUERY_COUNTER_BITS_EXT);
            if (timestamp_counter_bits > 0) {
                runTimeStampTest();
            }
            verifyQueryResultsNotAvailable();

            window.requestAnimationFrame(checkQueryResults);
        }
    }
}

function runSanityTests() {
    debug("");
    debug("Testing timer query expectations");

    shouldBe("ext.QUERY_COUNTER_BITS_EXT", "0x8864");
    shouldBe("ext.CURRENT_QUERY_EXT", "0x8865");
    shouldBe("ext.QUERY_RESULT_EXT", "0x8866");
    shouldBe("ext.QUERY_RESULT_AVAILABLE_EXT", "0x8867");
    shouldBe("ext.TIME_ELAPSED_EXT", "0x88BF");
    shouldBe("ext.TIMESTAMP_EXT", "0x8E28");
    shouldBe("ext.GPU_DISJOINT_EXT", "0x8FBB");

    shouldBe("ext.isQueryEXT(null)", "false");

    shouldBeTrue("ext.getQueryEXT(ext.TIME_ELAPSED_EXT, ext.CURRENT_QUERY_EXT) === null");
    wtu.glErrorShouldBe(gl, gl.NO_ERROR);
    shouldBeTrue("ext.getQueryEXT(ext.TIME_ELAPSED_EXT, ext.QUERY_COUNTER_BITS_EXT) >= 30");
    wtu.glErrorShouldBe(gl, gl.NO_ERROR);

    shouldBeTrue("ext.getQueryEXT(ext.TIMESTAMP_EXT, ext.CURRENT_QUERY_EXT) === null");
    wtu.glErrorShouldBe(gl, gl.NO_ERROR);

    // Certain drivers set timestamp counter bits to 0 as they don't support timestamps
    shouldBeTrue("ext.getQueryEXT(ext.TIMESTAMP_EXT, ext.QUERY_COUNTER_BITS_EXT) >= 30 || " +
             "ext.getQueryEXT(ext.TIMESTAMP_EXT, ext.QUERY_COUNTER_BITS_EXT) === 0");
    wtu.glErrorShouldBe(gl, gl.NO_ERROR);

    debug("");
    debug("Testing time elapsed query lifecycle");
    query = ext.createQueryEXT();
    shouldBe("ext.isQueryEXT(query)", "false");
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "Query creation must succeed.");
    shouldThrow("ext.beginQueryEXT(ext.TIMESTAMP_EXT, null)");
    ext.beginQueryEXT(ext.TIMESTAMP_EXT, query);
    wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "Beginning a timestamp query should fail.");
    ext.beginQueryEXT(ext.TIME_ELAPSED_EXT, query);
    shouldBe("ext.isQueryEXT(query)", "true");
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "Beginning an inactive time elapsed query should succeed.");
    ext.beginQueryEXT(ext.TIME_ELAPSED_EXT, query);
    wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "Attempting to begin an active query should fail.");
    ext.getQueryObjectEXT(query, ext.QUERY_RESULT_AVAILABLE_EXT);
    wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "Fetching query result availability of an active query should fail.");
    ext.getQueryObjectEXT(query, ext.QUERY_RESULT_EXT);
    wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "Fetching query result of an active query should fail.");
    shouldBe("ext.getQueryEXT(ext.TIME_ELAPSED_EXT, ext.CURRENT_QUERY_EXT)", "query");
    ext.endQueryEXT(ext.TIME_ELAPSED_EXT);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "Ending an active time elapsed query should succeed.");
    shouldThrow("ext.getQueryObjectEXT(null, ext.QUERY_RESULT_AVAILABLE_EXT)");
    ext.getQueryObjectEXT(query, ext.QUERY_RESULT_AVAILABLE_EXT);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "Fetching query result availability after query end should succeed.");
    ext.endQueryEXT(ext.TIME_ELAPSED_EXT);
    wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "Attempting to end an inactive query should fail.");
    ext.queryCounterEXT(query, ext.TIMESTAMP_EXT);
    wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "Should not be able to use time elapsed query to store a timestamp.");
    ext.deleteQueryEXT(query);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "Query deletion must succeed.");
    ext.beginQueryEXT(ext.TIME_ELAPSED_EXT, query);
    wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "Beginning a deleted query must fail.");
    ext.getQueryObjectEXT(query, ext.QUERY_RESULT_AVAILABLE_EXT);
    wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "Fetching query result availability after query deletion should fail.");
    shouldBe("ext.isQueryEXT(query)", "false");

    debug("");
    debug("Testing timestamp counter");
    query = ext.createQueryEXT();
    shouldThrow("ext.queryCounterEXT(null, ext.TIMESTAMP_EXT)");
    ext.queryCounterEXT(query, ext.TIMESTAMP_EXT);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "Timestamp counter queries should work.");
    ext.deleteQueryEXT(query);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR);

    debug("");
    debug("Performing parameter sanity checks");
    gl.getParameter(ext.TIMESTAMP_EXT);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "getParameter timestamp calls should work.");
    gl.getParameter(ext.GPU_DISJOINT_EXT);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "getParameter disjoint calls should work.");

    debug("");
    debug("Testing current query conditions");
    query = ext.createQueryEXT();
    query2 = ext.createQueryEXT();
    shouldBe("ext.getQueryEXT(ext.TIME_ELAPSED_EXT, ext.CURRENT_QUERY_EXT)", "null");
    ext.beginQueryEXT(ext.TIME_ELAPSED_EXT, query);
    shouldBe("ext.getQueryEXT(ext.TIME_ELAPSED_EXT, ext.CURRENT_QUERY_EXT)", "query");
    wtu.glErrorShouldBe(gl, gl.NO_ERROR);

    debug("");
    debug("Testing failed begin query should not change the current query.");
    ext.beginQueryEXT(ext.TIME_ELAPSED_EXT, query2);
    wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "Beginning an elapsed query without ending should fail.");
    shouldBe("ext.getQueryEXT(ext.TIME_ELAPSED_EXT, ext.CURRENT_QUERY_EXT)", "query");
    wtu.glErrorShouldBe(gl, gl.NO_ERROR);

    debug("");
    debug("Testing beginning a timestamp query is invalid and should not change the elapsed query.");
    ext.beginQueryEXT(ext.TIMESTAMP_EXT, query2)
    wtu.glErrorShouldBe(gl, gl.INVALID_ENUM);
    shouldBe("ext.getQueryEXT(ext.TIME_ELAPSED_EXT, ext.CURRENT_QUERY_EXT)", "query");
    wtu.glErrorShouldBe(gl, gl.NO_ERROR);

    debug("");
    debug("Testing timestamp queries end immediately so are never current.");
    ext.queryCounterEXT(query2, ext.TIMESTAMP_EXT);
    shouldBe("ext.getQueryEXT(ext.TIMESTAMP_EXT, ext.CURRENT_QUERY_EXT)", "null");
    wtu.glErrorShouldBe(gl, gl.NO_ERROR);

    debug("");
    debug("Testing ending the query should clear the current query.");
    ext.endQueryEXT(ext.TIME_ELAPSED_EXT);
    shouldBe("ext.getQueryEXT(ext.TIME_ELAPSED_EXT, ext.CURRENT_QUERY_EXT)", "null");
    wtu.glErrorShouldBe(gl, gl.NO_ERROR);

    debug("");
    debug("Testing beginning a elapsed query using a timestamp query should fail and not affect current query.")
    ext.beginQueryEXT(ext.TIME_ELAPSED_EXT, query2);
    wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "Switching query targets should fail.");
    shouldBe("ext.getQueryEXT(ext.TIME_ELAPSED_EXT, ext.CURRENT_QUERY_EXT)", "null");
    wtu.glErrorShouldBe(gl, gl.NO_ERROR);

    ext.deleteQueryEXT(query);
    ext.deleteQueryEXT(query2);

    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "Should be no errors at end of sanity tests");
}

function runElapsedTimeTest() {
    debug("");
    debug("Testing elapsed time query");

    elapsed_query = ext.createQueryEXT();
    ext.beginQueryEXT(ext.TIME_ELAPSED_EXT, elapsed_query);
    gl.clearColor(0, 0, 1, 1);
    gl.clear(gl.COLOR_BUFFER_BIT);
    ext.endQueryEXT(ext.TIME_ELAPSED_EXT);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "Time elapsed query should have no errors");
}

function runTimeStampTest() {
    debug("");
    debug("Testing timestamp query");

    timestamp_query1 = ext.createQueryEXT();
    timestamp_query2 = ext.createQueryEXT();
    ext.queryCounterEXT(timestamp_query1, ext.TIMESTAMP_EXT);
    gl.clearColor(1, 0, 0, 1);
    gl.clear(gl.COLOR_BUFFER_BIT);
    ext.queryCounterEXT(timestamp_query2, ext.TIMESTAMP_EXT);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "Timestamp queries should have no errors");
}

function verifyQueryResultsNotAvailable() {
    debug("");
    debug("Verifying queries' results don't become available too early");

    // Verify as best as possible that the implementation doesn't
    // allow a query's result to become available the same frame, by
    // spin-looping for some time and ensuring that none of the
    // queries' results become available.
    var startTime = Date.now();
    while (Date.now() - startTime < 2000) {
        gl.finish();
        if (ext.getQueryObjectEXT(elapsed_query, ext.QUERY_RESULT_AVAILABLE_EXT)) {
            testFailed("One of the queries' results became available too early");
            return;
        }
        if (timestamp_counter_bits > 0) {
            if (ext.getQueryObjectEXT(timestamp_query1, ext.QUERY_RESULT_AVAILABLE_EXT) ||
                ext.getQueryObjectEXT(timestamp_query2, ext.QUERY_RESULT_AVAILABLE_EXT)) {
                testFailed("One of the queries' results became available too early");
                return;
            }
        }
    }

    testPassed("Queries' results didn't become available in a spin loop");
}

function checkQueryResults() {
    if (availability_retry > 0) {
        // Make a reasonable attempt to wait for the queries' results to become available.
        if (!ext.getQueryObjectEXT(elapsed_query, ext.QUERY_RESULT_AVAILABLE_EXT) ||
            (timestamp_counter_bits > 0 && !ext.getQueryObjectEXT(timestamp_query2, ext.QUERY_RESULT_AVAILABLE_EXT))) {
            var error = gl.getError();
            if (error != gl.NO_ERROR) {
                testFailed("getQueryObjectEXT should have no errors: " + wtu.glEnumToString(gl, error));
                debug("");
                finishTest();
                return;
            }
            availability_retry--;
            window.requestAnimationFrame(checkQueryResults);
            return;
        }
    }

    debug("");
    debug("Testing query results");

    // Make sure queries are available.
    shouldBe("ext.getQueryObjectEXT(elapsed_query, ext.QUERY_RESULT_AVAILABLE_EXT)", "true");
    if (timestamp_counter_bits > 0) {
        shouldBe("ext.getQueryObjectEXT(timestamp_query1, ext.QUERY_RESULT_AVAILABLE_EXT)", "true");
        shouldBe("ext.getQueryObjectEXT(timestamp_query2, ext.QUERY_RESULT_AVAILABLE_EXT)", "true");
    }

    var disjoint_value = gl.getParameter(ext.GPU_DISJOINT_EXT);
    if (disjoint_value) {
        // Cannot validate results make sense, but this is okay.
        testPassed("Disjoint triggered.");
    } else {
        var elapsed_result = ext.getQueryObjectEXT(elapsed_query, ext.QUERY_RESULT_EXT);
        if (timestamp_counter_bits > 0) {
            var timestamp_result1 = ext.getQueryObjectEXT(timestamp_query1, ext.QUERY_RESULT_EXT);
            var timestamp_result2 = ext.getQueryObjectEXT(timestamp_query2, ext.QUERY_RESULT_EXT);
        }
        // Do some basic validity checking of the elapsed time query. There's no way it should
        // take more than about half a second for a no-op query.
        var halfSecondInNanos = 0.5 * 1000 * 1000 * 1000;
        if (elapsed_result < 0 || elapsed_result > halfSecondInNanos) {
            testFailed("Time elapsed query returned invalid data: " + elapsed_result);
        } else {
            testPassed("Time elapsed query results were valid.");
        }

        if (timestamp_counter_bits > 0) {
            if (timestamp_result1 <= 0 ||
                timestamp_result2 <= 0 ||
                timestamp_result2 <= timestamp_result1) {
                testFailed("Timestamp queries returned invalid data: timestamp_result1 = " +
                           timestamp_result1 + ", timestamp_result2 = " + timestamp_result2);
            } else {
                testPassed("Timestamp query results were valid.");
            }
        }
    }

    debug("");
    finishTest();
}
</script>
</body>
</html>
