<!--
Copyright (c) 2020 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">
<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>
<style>
.spinner {
    width: 100px;
    height: 100px;
    border: 20px solid transparent;
    border-top: 20px solid black;
    border-radius: 100%;
    text-align: center;
    padding: 10px;
}
@keyframes rotation {
    from { transorm: rotate(0); }
    to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div id="description"></div>
<button onclick='compileShaders()'>The spinners below should not stutter when you click this button.</button>
<div class="spinner" style="animation: rotation 2s infinite linear;">CSS</div>
<div class="spinner" id=spinner>JS</div>
<div id="console"></div>
<canvas id=canvas></canvas>
<script>
"use strict";
description("Test KHR_parallel_shader_compile");

function spinSpinner() {
    let degrees = (performance.now() / 1000 / 2 % 1.) * 360;
    spinner.style.transform = `rotate(${degrees}deg)`;
    requestAnimationFrame(spinSpinner);
}
spinSpinner();

const wtu = WebGLTestUtils;

const gl = wtu.create3DContext();
const loseContext = wtu.getExtensionWithKnownPrefixes(gl, "WEBGL_lose_context");

let counter = 0;
const vertexSource = (extra) => `
void main() {
    vec4 result = vec4(0.${counter++});
${extra || ''}
    gl_Position = result;
}`;
const fragmentSource = (extra) => `
precision highp float;
void main() {
    vec4 result = vec4(0.${counter++});
${extra || ''}
    gl_FragColor = result;
}`;

let vs = gl.createShader(gl.VERTEX_SHADER);
let fs = gl.createShader(gl.FRAGMENT_SHADER);
let program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);

const COMPLETION_STATUS_KHR = 0x91B1;

gl.shaderSource(vs, vertexSource());
gl.compileShader(vs);
let status = gl.getShaderParameter(vs, COMPLETION_STATUS_KHR);
if (status !== null) testFailed('Extension disabled, status should be null');
wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "extension disabled");

gl.shaderSource(fs, fragmentSource());
gl.compileShader(fs);

gl.linkProgram(program);
status = gl.getProgramParameter(program, COMPLETION_STATUS_KHR);
if (status !== null) testFailed('Extension disabled, status should be null');
wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "extension disabled");

const ext = wtu.getExtensionWithKnownPrefixes(gl, "KHR_parallel_shader_compile");

let successfullyParsed = false;

let extraCode = '';

(async () => {

    if (!ext) {
        testPassed("No KHR_parallel_shader_compile support -- this is legal");
    } else {
        testPassed("Successfully enabled KHR_parallel_shader_compile extension");

        shouldBe("ext.COMPLETION_STATUS_KHR", "0x91B1");

        debug("Checking that status is a boolean.");
        gl.shaderSource(vs, vertexSource());
        gl.compileShader(vs);
        let status = gl.getShaderParameter(vs, COMPLETION_STATUS_KHR);
        if (status !== true && status !== false) testFailed("status should be a boolean");

        gl.linkProgram(program);
        status = gl.getProgramParameter(program, COMPLETION_STATUS_KHR);
        if (status !== true && status !== false) testFailed("status should be a boolean");

        const minimumShaderCompileDurationMs = 500;
        debug(`Constructing shader that takes > ${minimumShaderCompileDurationMs} ms to compile.`);
        let measuredCompileDuration = 0;
        extraCode = '\n    if (true) { result += vec4(0.0000001); }';
        for (let i = 0; measuredCompileDuration < minimumShaderCompileDurationMs; i++) {
            extraCode += extraCode;
            extraCode += extraCode;
            if (i < 4) continue;
            gl.shaderSource(vs, vertexSource(extraCode));
            gl.shaderSource(fs, fragmentSource(extraCode));
            gl.compileShader(vs);
            gl.compileShader(fs);
            gl.linkProgram(program);
            const start = performance.now();
            if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
                testFailed(`Shaders failed to compile.
                            program: ${gl.getProgramInfoLog(program)}
                            vs: ${gl.getShaderInfoLog(vs)}
                            fs: ${gl.getShaderInfoLog(fs)}`);
                break;
            }
            measuredCompileDuration = performance.now() - start;
        }

        debug('');
        gl.shaderSource(vs, vertexSource(extraCode));
        gl.shaderSource(fs, fragmentSource(extraCode));
        gl.compileShader(vs);
        gl.compileShader(fs);
        gl.linkProgram(program);

        let start = performance.now();
        gl.getShaderParameter(fs, COMPLETION_STATUS_KHR);
        gl.getShaderParameter(vs, COMPLETION_STATUS_KHR);
        let duration = performance.now() - start;
        if (duration > 100)
            testFailed(`Querying shader status should not wait for compilation. Took ${duration} ms`);

        let frames = 0;
        const maximumTimeToWait = measuredCompileDuration * 4;
        while (!gl.getProgramParameter(program, COMPLETION_STATUS_KHR)
            && performance.now() - start < maximumTimeToWait) {
            frames++;
            await new Promise(requestAnimationFrame);
        }
        duration = performance.now() - start;
        if (!gl.getProgramParameter(program, COMPLETION_STATUS_KHR)) {
            testFailed(`Program took longer than ${maximumTimeToWait} ms to compile. Expected: ${measuredCompileDuration} ms, actual: ${duration} ms`);
        } else if (!gl.getShaderParameter(vs, COMPLETION_STATUS_KHR) || !gl.getShaderParameter(fs, COMPLETION_STATUS_KHR)) {
            testFailed('Program linked before shaders finished compiling.');
        } else if (frames <= 6) {
            testFailed(`Program should have taken many more than 6 frames to compile. Actual value: ${frames} frames, duration ${performance.now() - start} ms.`);
        } else {
            console.log(`COMPLETION_STATUS_KHR sucessfully transitioned from false to true in ${frames} frames and ${duration} ms.`);
            testPassed(`COMPLETION_STATUS_KHR sucessfully transitioned from false to true`);
        }


        debug("Checking that status is true when context is lost.");
        if (loseContext) {
            gl.shaderSource(vs, vertexSource(extraCode));
            gl.shaderSource(fs, fragmentSource(extraCode));
            gl.compileShader(vs);
            gl.compileShader(fs);
            gl.linkProgram(program);
            loseContext.loseContext();
            status = gl.getShaderParameter(vs, COMPLETION_STATUS_KHR);
            if (status !== true) testFailed("shader status should be true when context is lost");
            status = gl.getProgramParameter(program, COMPLETION_STATUS_KHR);
            if (status !== true) testFailed("program status should be true when context is lost");
            loseContext.restoreContext();
            vs = gl.createShader(gl.VERTEX_SHADER);
            fs = gl.createShader(gl.FRAGMENT_SHADER);
            program = gl.createProgram();
        }
    }
    finishTest();
})();

async function compileShaders() {
    console.log('Compiling shaders');
    const gl = canvas.getContext('webgl');
    const vs = gl.createShader(gl.VERTEX_SHADER);
    const fs = gl.createShader(gl.FRAGMENT_SHADER);
    const program = gl.createProgram();
    gl.getExtension(wtu.getExtensionWithKnownPrefixes(gl, "KHR_parallel_shader_compile"));
    gl.attachShader(program, vs);
    gl.attachShader(program, fs);
    gl.shaderSource(vs, vertexSource(extraCode));
    gl.shaderSource(fs, fragmentSource(extraCode));
    gl.compileShader(vs);
    gl.compileShader(fs);
    gl.linkProgram(program);
    while (!gl.getProgramParameter(program, COMPLETION_STATUS_KHR)) {
        gl.getShaderParameter(vs, COMPLETION_STATUS_KHR);
        gl.getShaderParameter(fs, COMPLETION_STATUS_KHR);
        await new Promise(requestAnimationFrame);
    }
    gl.getProgramParameter(program, gl.LINK_STATUS);
    console.log('Compilation finished.');
}

</script>

</body>
</html>
