<!--
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>getBufferSubData non-blocking test</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>
<div id="console"></div>
<script>
"use strict";
description("Test that getBufferSubData is non-blocking when used with fenceSync");

var wtu = WebGLTestUtils;

var gl = wtu.create3DContext(undefined, undefined, 2);

const srcData = new Uint8Array([ 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0 ]);
const zeroData = new Uint8Array(8);

const srcBuffer = gl.createBuffer();
gl.bindBuffer(gl.COPY_READ_BUFFER, srcBuffer);
gl.bufferData(gl.COPY_READ_BUFFER, srcData, gl.STATIC_DRAW);

const readbackBuffer = gl.createBuffer();
gl.bindBuffer(gl.COPY_WRITE_BUFFER, readbackBuffer);
gl.bufferData(gl.COPY_WRITE_BUFFER, 8, gl.STREAM_READ);

// unrelated buffers for tests
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); // used as copy dst
gl.bufferData(gl.ARRAY_BUFFER, 8, gl.STATIC_DRAW);
gl.bindBuffer(gl.UNIFORM_BUFFER, gl.createBuffer()); // used as copy src
gl.bufferData(gl.UNIFORM_BUFFER, 8, gl.STATIC_DRAW);

const dest = new Uint8Array(8);

// Makes a new "resolvable" Promise
function resolvable() {
    let resolve;
    const promise = new Promise(res => { resolve = res; });
    promise.resolve = resolve;
    return promise;
}

function fence() {
    const promise = resolvable();

    const sync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0);
    gl.flush();
    function check() {
        const status = gl.clientWaitSync(sync, 0, 0);
        if (status == gl.ALREADY_SIGNALED || status == gl.CONDITION_SATISFIED) {
            gl.deleteSync(sync);
            promise.resolve();
        } else {
            setTimeout(check, 0);
        }
    }
    setTimeout(check, 0);

    return promise;
}

function writeToReadbackBuffer() {
    gl.copyBufferSubData(gl.COPY_READ_BUFFER, gl.COPY_WRITE_BUFFER, 0, 0, 8);
}

function timedGetBufferSubData() {
    dest.fill(0);
    const t0 = performance.now();
    gl.getBufferSubData(gl.COPY_WRITE_BUFFER, 0, dest);
    return (performance.now() - t0);
}

function timeBlockingReadback() {
    const promise = resolvable();
    setTimeout(() => {
        writeToReadbackBuffer();
        const tBlocking = timedGetBufferSubData();
        const tLatency = tBlocking;
        promise.resolve({latency: tLatency, blocking: tBlocking});
    }, 0);
    return promise;
}

function timeNonblockingReadback() {
    writeToReadbackBuffer();
    const tLatency0 = performance.now();
    return fence().then(() => {
        const tBlocking = timedGetBufferSubData();
        const tLatency = performance.now() - tLatency0;
        return {latency: tLatency, blocking: tBlocking};
    });
}

function timeReadbackWithUnrelatedCopy() {
    writeToReadbackBuffer();
    const tLatency0 = performance.now();
    const f = fence();
    // copy to a buffer unrelated to the readback
    gl.copyBufferSubData(gl.COPY_READ_BUFFER, gl.ARRAY_BUFFER, 0, 0, 8);
    return f.then(() => {
        const tBlocking = timedGetBufferSubData();
        const tLatency = performance.now() - tLatency0;
        return {latency: tLatency, blocking: tBlocking};
    });
}

function timeReadbackInterrupted() {
    writeToReadbackBuffer();
    const tLatency0 = performance.now();
    const f = fence();
    // interrupt the readback by inserting another write
    gl.copyBufferSubData(gl.UNIFORM_BUFFER, gl.COPY_WRITE_BUFFER, 0, 0, 8);
    return f.then(() => {
        const tBlocking = timedGetBufferSubData();
        const tLatency = performance.now() - tLatency0;
        return {latency: tLatency, blocking: tBlocking};
    });
}

function computeMean(timings) {
    let total = 0;
    for (let i = 0; i < timings.length; ++i) {
        total += timings[i];
    }
    return total / timings.length;
}

function measureMean(fn, iterations) {
    const timingsLatency = Array(iterations);
    const timingsBlocking = Array(iterations);

    // Chain together `iterations` promises to call `fn` sequentially.
    let promise = Promise.resolve();
    for (let i = 0; i < iterations; ++i) {
        promise = promise
            .then(fn)
            .then(t => {
                timingsLatency[i] = t.latency;
                timingsBlocking[i] = t.blocking;
            });
    }

    return promise.then(() => {
        const meanLatency = computeMean(timingsLatency);
        const meanBlocking = computeMean(timingsBlocking);
        return { latency: meanLatency, blocking: meanBlocking };
    });
}

let t_blocking, t_nonblocking;
let t_unrelated;
let t_interrupted;
Promise.resolve()
    .then(() => {
        let iterations = 500;
        debug(`blocking readback: mean over ${iterations} iterations...`);
        return measureMean(timeBlockingReadback, iterations);
    })
    .then(t => {
        t_blocking = t;
        debug(`... latency = ${t.latency}ms, blocking = ${t.blocking}ms`);
    })
    .then(() => shouldBeTrue("areArraysEqual(dest, srcData)"))

    .then(() => debug(""))
    .then(() => {
        let iterations = 500;
        debug(`nonblocking readback: mean over ${iterations} iterations...`);
        return measureMean(timeNonblockingReadback, iterations);
    })
    .then(t => {
        t_nonblocking = t;
        debug(`... latency = ${t.latency}ms, blocking = ${t.blocking}ms`);
    })
    .then(() => shouldBeTrue("areArraysEqual(dest, srcData)"))

    .then(() => debug(""))
    .then(() => {
        let iterations = 500;
        debug(`readback interrupted by unrelated read from copy source: mean over ${iterations} iterations...`);
        return measureMean(timeReadbackWithUnrelatedCopy, iterations);
    })
    .then(t => {
        t_unrelated = t;
        debug(`... latency = ${t.latency}ms, blocking = ${t.blocking}ms`);
    })
    .then(() => shouldBeTrue("areArraysEqual(dest, srcData)"))

    .then(() => debug(""))
    .then(() => {
        let iterations = 500;
        debug(`readback interrupted by write to readback source: mean over ${iterations} iterations...`);
        return measureMean(timeReadbackInterrupted, iterations);
    })
    .then(t => {
        t_interrupted = t;
        debug(`... latency = ${t.latency}ms, blocking = ${t.blocking}ms`);
    })
    .then(() => shouldBeTrue("areArraysEqual(dest, zeroData)"))

    .then(() => {
        debug("");
        shouldBeTrue("t_nonblocking.blocking < t_blocking.blocking");
        shouldBeTrue("t_unrelated.blocking < t_blocking.blocking");
        shouldBeTrue("t_nonblocking.blocking < t_interrupted.blocking");
    })
    .then(finishTest);

var successfullyParsed = true;
</script>
</body>
</html>
