<!--
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>Buffer allocation 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="canvasParent"></div>
<div id="description"></div>
<div id="console"></div>
<script id="vshader" type="x-shader/x-vertex">
attribute vec2 inPosition;
attribute vec4 inColor0;
attribute vec4 inColor1;
attribute vec4 inColor2;
attribute vec4 inColor3;
attribute vec4 inColor4;
attribute vec4 inColor5;
attribute vec4 inColor6;
attribute vec4 inColor7;

varying vec4 color;

void main()
{
    color = abs(inColor0) + abs(inColor1) + abs(inColor2) + abs(inColor3) +
            abs(inColor4) + abs(inColor5) + abs(inColor6) + abs(inColor7);

    color = clamp(color, vec4(0.0), vec4(1.0));

    gl_Position = vec4(inPosition, 0.0, 1.0);
}
</script>
<script id="fshader" type="x-shader/x-fragment">
precision mediump float;

varying vec4 color;

void main()
{
    if (color == vec4(0.0))
        discard;

    gl_FragColor = color;
}
</script>

<script>
description("Allocates a number of different sized buffers and checks that the buffers are cleared " +
            "OR that the allocation results in gl.OUT_OF_MEMORY or context loss.");
var wtu = WebGLTestUtils;

// The shader processes eight vec4 attributes at once to reduce the amount of
// draw calls.
var numColorAttrs = 8;

// Process 64 squares at once to also reduce the amount of draw calls.
var vertices = [];
var w = 0.25;
for (var x = -1; x < 1; x += w) {
    for (var y = -1; y < 1; y += w) {
        vertices.push(x + w, y + w);
        vertices.push(x,     y + w);
        vertices.push(x,     y    );

        vertices.push(x + w, y + w);
        vertices.push(x,     y    );
        vertices.push(x + w, y    );
    }
}
var numVertices = (vertices.length / 2);

var gl;
var squareBuffer;
var error = 0;
var expectContextLost = false;

function initGLForBufferSizesTest() {
    var canvas = document.createElement("canvas");
    canvas.width = 40;
    canvas.height = 40;
    var parent = document.getElementById("canvasParent");
    parent.innerHTML = '';
    parent.appendChild(canvas);
    gl = wtu.create3DContext(canvas);
    var attribs = ["inPosition", "inColor0", "inColor1", "inColor2", "inColor3",
                   "inColor4", "inColor5", "inColor6", "inColor7"];
    wtu.setupProgram(gl, ["vshader", "fshader"], attribs);
    gl.enableVertexAttribArray(0);
    for (var i = 0; i < numColorAttrs; i++) {
        gl.enableVertexAttribArray(1 + i);
    }
    gl.disable(gl.DEPTH_TEST);
    gl.disable(gl.BLEND);

    squareBuffer = gl.createBuffer();

    gl.bindBuffer(gl.ARRAY_BUFFER, squareBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
}

function createBuffer(size, allowedToFail) {
    var msg = "Calling bufferData with size=" + size;
    var buffer = gl.createBuffer();

    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, size, gl.STATIC_DRAW);

    error = gl.getError();
    if (error !== gl.NO_ERROR) {
        gl.deleteBuffer(buffer);
        if (allowedToFail) {
            if (error === gl.OUT_OF_MEMORY) {
                testPassed(msg + " failed with gl.OUT_OF_MEMORY (this is allowed)");
                return null;
            } else if (error === gl.CONTEXT_LOST_WEBGL) {
                testPassed(msg + " failed with gl.CONTEXT_LOST_WEBGL (this is allowed)");
                return null;
            }
        }
        testFailed(msg + " failed with error " + wtu.glEnumToString(gl, error));
        return null;
    }

    testPassed(msg + " did not result in any errors");
    var reportedSize = gl.getBufferParameter(gl.ARRAY_BUFFER, gl.BUFFER_SIZE);
    expectContextLost = false;
    if (reportedSize === null) {
        testPassed("Null size reported by gl, this should happen if the context is lost which is allowed.");
        expectContextLost = true;
    } else if (reportedSize !== size) {
        if (size > Math.pow(2, 32)) {
            testPassed("gl reported different size " + reportedSize + " for the buffer, but this is expected since " +
                       "the requested size was above what the return value of getBufferParameter can represent.");
        } else {
            testFailed("gl reported different size " + reportedSize + " for the buffer.");
        }
    } else {
        testPassed("Size reported by gl was the same as the requested size.");
    }

    return buffer;
}

// Draw a square on the canvas using attributes from the clear buffer created with bufferData.
function drawWithBuffer(buffer, allowedToFail) {
    gl.clearColor(0, 1, 0, 1);
    gl.clear(gl.COLOR_BUFFER_BIT);

    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    var size = gl.getBufferParameter(gl.ARRAY_BUFFER, gl.BUFFER_SIZE);
    // Each vec4 is 16 bytes
    var increment = numVertices * numColorAttrs * 16;
    for (var offset = 0; offset + increment <= size; offset += increment) {
        gl.bindBuffer(gl.ARRAY_BUFFER, squareBuffer);
        gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);

        for (var i = 0; i < numColorAttrs; i++) {
            gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
            gl.vertexAttribPointer(1 + i, 4, gl.FLOAT, false, 0,
                                   offset + increment * i / numColorAttrs);
        }
        gl.drawArrays(gl.TRIANGLES, 0, numVertices);
        error = gl.getError();

        if (error !== gl.NO_ERROR) {
            if (allowedToFail) {
                if (error === gl.OUT_OF_MEMORY) {
                    testPassed("drawArrays failed with gl.OUT_OF_MEMORY (this is allowed)");
                    return;
                } else if (error === gl.CONTEXT_LOST_WEBGL) {
                    testPassed("drawArrays failed with gl.CONTEXT_LOST_WEBGL (this is allowed)");
                    return;
                }
            }
            testFailed("drawArrays failed with error " + wtu.glEnumToString(gl, error));
            return;
        }
    }
    wtu.checkCanvas(gl, [0, 255, 0, 255], "should be green");
}


// To be able to confirm the whole buffer has been cleared, the size needs to
// be divisible by the amount of vertices. Thus most sizes are multiples of 3.
var tests = [
    // Reasonable sized buffers.
    { size: 3 * 1024,              allowedToFail: false, tryDrawing: true },
    { size: 3 * 1024 * 1024,       allowedToFail: false, tryDrawing: true },
    { size: 3 * 1024 * 1024 * 16,  allowedToFail: false, tryDrawing: true },

    // Huge buffers, which are meant to test out of memory handling.
    // Allowed failures are gl.OUT_OF_MEMORY or context loss.
    // Succeeding in the allocations is allowed as well for forward compatibility.

    // 1.5 GB allocation for stressing lower-end 32-bit systems.
    // Allocation is likely to succeed on higher-end hardware.
    { size: 3 * 1024 * 1024 * 512, allowedToFail: true,  tryDrawing: true },
    // A buffer that no implementation will be able to allocate for some time
    // to come. To do this, we use half of the lower 43-bit half of a 44-bit
    // memory address space, so that the size is still valid on current common
    // 64-bit implementations, and also below 52-bit limit for exact conversion
    // from float to long long in WebIDL (though 2^n should be safe anyway).
    // The 4 TB size is large enough that even extrapolating the historical
    // exponential growth trend of memory sizes, hardware in 2020's should
    // still have some trouble actually doing the allocation.
    { size: (1 << 12) * (1 << 30), allowedToFail: true,  tryDrawing: false }
];

function finishBufferSizesTest() {
    gl.deleteBuffer(squareBuffer);
    finishTest();
}

var testIndex = -1;
function runNextTest() {
    ++testIndex;
    if (testIndex > 0 && tests[testIndex - 1].allowedToFail) {
        if (gl.isContextLost() || error === gl.OUT_OF_MEMORY) {
            initGLForBufferSizesTest();
        } else if (expectContextLost) {
            testFailed("Context was not lost after timeout even though gl.getBufferParameter returned null.");
        }
    }
    var buffer = createBuffer(tests[testIndex].size, tests[testIndex].allowedToFail);
    if (buffer) {
        if (tests[testIndex].tryDrawing) {
            drawWithBuffer(buffer, tests[testIndex].allowedToFail);
        }
        gl.deleteBuffer(buffer);
    }

    if (testIndex + 1 >= tests.length) {
        finishBufferSizesTest();
    } else {
        if (tests[testIndex + 1].allowedToFail && !tests[testIndex].allowedToFail) {
            if (!confirm("The following tests can cause unresponsiveness or instability. Press OK to continue.")) {
                testFailed("Tests aborted");
                return;
            }
        }
        if (tests[testIndex].allowedToFail) {
            // Give plenty of time for possible context loss
            setTimeout(runNextTest(), 5000);
        } else {
            runNextTest();
        }
    }
};

initGLForBufferSizesTest();
runNextTest();

var successfullyParsed = true;
</script>

</body>
</html>

