﻿<!--

/*
** Copyright (c) 2012 The Khronos Group Inc.
**
** Permission is hereby granted, free of charge, to any person obtaining a
** copy of this software and/or associated documentation files (the
** "Materials"), to deal in the Materials without restriction, including
** without limitation the rights to use, copy, modify, merge, publish,
** distribute, sublicense, and/or sell copies of the Materials, and to
** permit persons to whom the Materials are furnished to do so, subject to
** the following conditions:
**
** The above copyright notice and this permission notice shall be included
** in all copies or substantial portions of the Materials.
**
** THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
** MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
*/

-->

<!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>
<title>WebGL WEBGL_compressed_texture_s3tc Conformance Tests</title>
<style>
img {
 border: 1px solid black;
 margin-right: 1em;
}
.testimages {
}

.testimages br {
  clear: both;
}

.testimages > div {
  float: left;
  margin: 1em;
}
</style>
</head>
<body>
<div id="description"></div>
<canvas id="canvas" width="8" height="8" style="width: 8px; height: 8px;"></canvas>
<div id="console"></div>
<script>
"use strict";
description("This test verifies the functionality of the WEBGL_compressed_texture_s3tc extension, if it is available.");

debug("");

var img_4x4_rgba_raw = new Uint8Array([
    0xff,0x00,0x00,0x69,0x00,0xff,0x00,0xff,0xff,0x00,0x00,0xff,0x00,0xff,0x00,0xff,0x00,0xff,0x00,0xff,0x00,0xff,0x00,0xff,0xff,0x00,0x00,0xff,0x00,0xff,0x00,0xff,0xff,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0x00,0xff,0x00,0xff,0x00,0xff,0x00,0xff,0x00,0xff,0x00,0xff,0x00,0xff,0x00,0xff,0x00,0xff,0x00,0xff,
]);
var img_4x4_rgb_dxt1 = new Uint8Array([
    0xe0,0x07,0x00,0xf8,0x11,0x10,0x15,0x00,
]);
var img_4x4_rgba_dxt1 = new Uint8Array([
    0xe0,0x07,0x00,0xf8,0x13,0x10,0x15,0x00,
]);
var img_4x4_rgba_dxt3 = new Uint8Array([
    0xf6,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x00,0xf8,0xe0,0x07,0x44,0x45,0x40,0x55,
]);
var img_4x4_rgba_dxt5 = new Uint8Array([
    0xf6,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x00,0xf8,0xe0,0x07,0x44,0x45,0x40,0x55,
]);
var img_8x8_rgba_raw = new Uint8Array([
    0xff,0x00,0x00,0x69,0x00,0xff,0x00,0xff,0xff,0x00,0x00,0xff,0x00,0xff,0x00,0xff,0xff,0xff,0x00,0xff,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0xff,0x00,0x00,0xff,0xff,0x00,0xff,0x00,0x69,0x00,0xff,0x00,0xff,0xff,0x00,0x00,0xff,0x00,0xff,0x00,0xff,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0xff,0x00,0x00,0xff,0xff,0xff,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0x00,0xff,0x00,0xff,0xff,0xff,0x00,0xff,0xff,0xff,0x00,0xff,0xff,0xff,0x00,0xff,0x00,0x00,0xff,0xff,0x00,0xff,0x00,0xff,0x00,0xff,0x00,0xff,0x00,0xff,0x00,0xff,0x00,0xff,0x00,0xff,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0xff,0x00,0xff,0x00,0xff,0xff,0x00,0xff,0xff,0x00,0xff,0x00,0xff,0xff,0x00,0xff,0xff,0x00,0x00,0xff,0xff,0x00,0xff,0xff,0xff,0x00,0x00,0xff,0xff,0x00,0xff,0xff,0xff,0xff,0x00,0xff,0xff,0xff,0x00,0xff,0xff,0x00,0xff,0x00,0xff,0xff,0x00,0xff,0xff,0x00,0xff,0xff,0xff,0x00,0xff,0xff,0xff,0x00,0x00,0xff,0xff,0x00,0xff,0xff,0xff,0x00,0xff,0x00,0x69,0x00,0xff,0x00,0xff,0x00,0xff,0x00,0xff,0xff,0x00,0xff,0xff,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0xff,0x00,0x00,0xff,0xff,0x00,0xff,0xff,0xff,0xff,0x00,0xff,0x69,0xff,0x00,0xff,0xff,0xff,0x00,0xff,0xff,0xff,0x00,0xff,0xff,0x00,0xff,0xff,0xff,0x00,0xff,0xff,0xff,0x00,0xff,0xff,0xff,0x00,0xff,0xff,0xff,
]);
var img_8x8_rgb_dxt1 = new Uint8Array([
    0xe0,0x07,0x00,0xf8,0x11,0x10,0x15,0x00,0x1f,0x00,0xe0,0xff,0x11,0x10,0x15,0x00,0xe0,0x07,0x1f,0xf8,0x44,0x45,0x40,0x55,0x1f,0x00,0xff,0x07,0x44,0x45,0x40,0x55,
]);
var img_8x8_rgba_dxt1 = new Uint8Array([
    0xe0,0x07,0x00,0xf8,0x13,0x13,0x15,0x00,0x1f,0x00,0xe0,0xff,0x11,0x10,0x15,0x00,0xe0,0x07,0x1f,0xf8,0x44,0x45,0x43,0x57,0x1f,0x00,0xff,0x07,0x44,0x45,0x40,0x55,
]);
var img_8x8_rgba_dxt3 = new Uint8Array([
    0xf6,0xff,0xf6,0xff,0xff,0xff,0xff,0xff,0x00,0xf8,0xe0,0x07,0x44,0x45,0x40,0x55,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xe0,0xff,0x1f,0x00,0x44,0x45,0x40,0x55,0xff,0xff,0xff,0xff,0xf6,0xff,0xf6,0xff,0x1f,0xf8,0xe0,0x07,0x11,0x10,0x15,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x07,0x1f,0x00,0x11,0x10,0x15,0x00,
]);
var img_8x8_rgba_dxt5 = new Uint8Array([
    0xff,0x69,0x01,0x10,0x00,0x00,0x00,0x00,0x00,0xf8,0xe0,0x07,0x44,0x45,0x40,0x55,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0xe0,0xff,0x1f,0x00,0x44,0x45,0x40,0x55,0xff,0x69,0x00,0x00,0x00,0x01,0x10,0x00,0x1f,0xf8,0xe0,0x07,0x11,0x10,0x15,0x00,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0x07,0x1f,0x00,0x11,0x10,0x15,0x00,
]);

var wtu = WebGLTestUtils;
var contextVersion = wtu.getDefault3DContextVersion();
var canvas = document.getElementById("canvas");
var gl = wtu.create3DContext(canvas, {antialias: false});
var program = wtu.setupTexturedQuad(gl);
var ext = null;
var vao = null;
var validFormats = {
    COMPRESSED_RGB_S3TC_DXT1_EXT        : 0x83F0,
    COMPRESSED_RGBA_S3TC_DXT1_EXT       : 0x83F1,
    COMPRESSED_RGBA_S3TC_DXT3_EXT       : 0x83F2,
    COMPRESSED_RGBA_S3TC_DXT5_EXT       : 0x83F3,
};
var name;
var supportedFormats;

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

    // Run tests with extension disabled
    runTestDisabled();

    // Query the extension and store globally so shouldBe can access it
    ext = wtu.getExtensionWithKnownPrefixes(gl, "WEBGL_compressed_texture_s3tc");
    if (!ext) {
        testPassed("No WEBGL_compressed_texture_s3tc support -- this is legal");
        runSupportedTest(false);
    } else {
        testPassed("Successfully enabled WEBGL_compressed_texture_s3tc extension");

        runSupportedTest(true);
        runTestExtension();
    }
}

function runSupportedTest(extensionEnabled) {
    var name = wtu.getSupportedExtensionWithKnownPrefixes(gl, "WEBGL_compressed_texture_s3tc");
    if (name !== undefined) {
        if (extensionEnabled) {
            testPassed("WEBGL_compressed_texture_s3tc listed as supported and getExtension succeeded");
        } else {
            testFailed("WEBGL_compressed_texture_s3tc listed as supported but getExtension failed");
        }
    } else {
        if (extensionEnabled) {
            testFailed("WEBGL_compressed_texture_s3tc not listed as supported but getExtension succeeded");
        } else {
            testPassed("WEBGL_compressed_texture_s3tc not listed as supported and getExtension failed -- this is legal");
        }
    }
}


function runTestDisabled() {
    debug("Testing binding enum with extension disabled");

    supportedFormats = gl.getParameter(gl.COMPRESSED_TEXTURE_FORMATS);
    shouldBe("supportedFormats", "[]");
}

function formatExists(format, supportedFormats) {
    for (var ii = 0; ii < supportedFormats.length; ++ii) {
        if (format == supportedFormats[ii]) {
            testPassed("supported format " + formatToString(format) + " is exists");
            return;
        }
    }
    testFailed("supported format " + formatToString(format) + " does not exist");
}

function formatToString(format) {
    for (var p in ext) {
        if (ext[p] == format) {
            return p;
        }
    }
    return "0x" + format.toString(16);
}

function runTestExtension() {
    debug("Testing WEBGL_compressed_texture_s3tc");

    // check that all format enums exist.
    for (name in validFormats) {
        var expected = "0x" + validFormats[name].toString(16);
        var actual = "ext['" + name + "']";
        shouldBe(actual, expected);
    }

    supportedFormats = gl.getParameter(gl.COMPRESSED_TEXTURE_FORMATS);
    // There should be exactly 4 formats for both WebGL 1.0 and WebGL 2.0.
    shouldBe("supportedFormats.length", "4");

    // check that all 4 formats exist
    for (var name in validFormats.length) {
        formatExists(validFormats[name], supportedFormats);
    }

    // Test each format
    testDXT1_RGB();
    testDXT1_RGBA();
    testDXT3_RGBA();
    testDXT5_RGBA();
}

function testDXT1_RGB() {
    var tests = [
        {   width: 4,
            height: 4,
            channels: 3,
            data: img_4x4_rgb_dxt1,
            format: ext.COMPRESSED_RGB_S3TC_DXT1_EXT,
            hasAlpha: false,
        },
        {   width: 8,
            height: 8,
            channels: 3,
            data: img_8x8_rgb_dxt1,
            format: ext.COMPRESSED_RGB_S3TC_DXT1_EXT,
            hasAlpha: false,
            subX0: 0,
            subY0: 0,
            subWidth: 4,
            subHeight: 4,
            subData: img_4x4_rgb_dxt1
        }
    ];
    testDXTTextures(tests);
}

function testDXT1_RGBA() {
    var tests = [
        {   width: 4,
            height: 4,
            channels: 4,
            data: img_4x4_rgba_dxt1,
            format: ext.COMPRESSED_RGBA_S3TC_DXT1_EXT,
            // This is a special case -- the texture is still opaque
            // though it's RGBA.
            hasAlpha: false,
        },
        {   width: 8,
            height: 8,
            channels: 4,
            data: img_8x8_rgba_dxt1,
            format: ext.COMPRESSED_RGBA_S3TC_DXT1_EXT,
            // This is a special case -- the texture is still opaque
            // though it's RGBA.
        }
    ];
    testDXTTextures(tests);
}

function testDXT3_RGBA() {
    var tests = [
        {   width: 4,
            height: 4,
            channels: 4,
            data: img_4x4_rgba_dxt3,
            format: ext.COMPRESSED_RGBA_S3TC_DXT3_EXT,
            hasAlpha: true,
        },
        {   width: 8,
            height: 8,
            channels: 4,
            data: img_8x8_rgba_dxt3,
            format: ext.COMPRESSED_RGBA_S3TC_DXT3_EXT,
            hasAlpha: true,
            subX0: 0,
            subY0: 0,
            subWidth: 4,
            subHeight: 4,
            subData: img_4x4_rgba_dxt3
        }
    ];
    testDXTTextures(tests);
}

function testDXT5_RGBA() {
    var tests = [
        {   width: 4,
            height: 4,
            channels: 4,
            data: img_4x4_rgba_dxt5,
            format: ext.COMPRESSED_RGBA_S3TC_DXT5_EXT,
            hasAlpha: true,
        },
        {   width: 8,
            height: 8,
            channels: 4,
            data: img_8x8_rgba_dxt5,
            format: ext.COMPRESSED_RGBA_S3TC_DXT5_EXT,
            hasAlpha: true,
            subX0: 0,
            subY0: 0,
            subWidth: 4,
            subHeight: 4,
            subData: img_4x4_rgba_dxt5
        }
    ];
    testDXTTextures(tests);
}

function testDXTTextures(tests) {
    debug("<hr/>");
    for (var ii = 0; ii < tests.length; ++ii) {
        testDXTTexture(tests[ii], false);
        if (contextVersion >= 2) {
            debug("<br/>");
            testDXTTexture(tests[ii], true);
        }
    }
}

function uncompressDXTBlock(
    destBuffer, destX, destY, destWidth, src, srcOffset, format) {
    function make565(src, offset) {
        return src[offset + 0] + src[offset + 1] * 256;
    }
    function make8888From565(c) {
        return [
                Math.floor(((c >> 11) & 0x1F) * 255 / 31),
                Math.floor(((c >>    5) & 0x3F) * 255 / 63),
                Math.floor(((c >>    0) & 0x1F) * 255 / 31),
                255
            ];
    }
    function mix(mult, c0, c1, div) {
        var r = [];
        for (var ii = 0; ii < c0.length; ++ii) {
            r[ii] = Math.floor((c0[ii] * mult + c1[ii]) / div);
        }
        return r;
    }
    var isDXT1 = format == ext.COMPRESSED_RGB_S3TC_DXT1_EXT ||
                 format == ext.COMPRESSED_RGBA_S3TC_DXT1_EXT;
    var colorOffset = srcOffset + (isDXT1 ? 0 : 8);
    var color0 = make565(src, colorOffset + 0);
    var color1 = make565(src, colorOffset + 2);
    var c0gtc1 = color0 > color1 || !isDXT1;
    var rgba0 = make8888From565(color0);
    var rgba1 = make8888From565(color1);
    var colors = [
            rgba0,
            rgba1,
            c0gtc1 ? mix(2, rgba0, rgba1, 3) : mix(1, rgba0, rgba1, 2),
            c0gtc1 ? mix(2, rgba1, rgba0, 3) : [0, 0, 0, 255]
        ];

    // yea I know there is a lot of math in this inner loop.
    // so sue me.
    for (var yy = 0; yy < 4; ++yy) {
        var pixels = src[colorOffset + 4 + yy];
        for (var xx = 0; xx < 4; ++xx) {
            var dstOff = ((destY + yy) * destWidth + destX + xx) * 4;
            var code = (pixels >> (xx * 2)) & 0x3;
            var srcColor = colors[code];
            var alpha;
            switch (format) {
            case ext.COMPRESSED_RGB_S3TC_DXT1_EXT:
                alpha = 255;
                break;
            case ext.COMPRESSED_RGBA_S3TC_DXT1_EXT:
                alpha = (code == 3 && !c0gtc1) ? 0 : 255;
                break;
            case ext.COMPRESSED_RGBA_S3TC_DXT3_EXT:
                {
                    var alpha0 = src[srcOffset + yy * 2 + Math.floor(xx / 2)];
                    var alpha1 = (alpha0 >> ((xx % 2) * 4)) & 0xF;
                    alpha = alpha1 | (alpha1 << 4);
                }
                break;
            case ext.COMPRESSED_RGBA_S3TC_DXT5_EXT:
                {
                    var alpha0 = src[srcOffset + 0];
                    var alpha1 = src[srcOffset + 1];
                    var alphaOff = Math.floor(yy / 2) * 3 + 2;
                    var alphaBits =
                         src[srcOffset + alphaOff + 0] +
                         src[srcOffset + alphaOff + 1] * 256 +
                         src[srcOffset + alphaOff + 2] * 65536;
                    var alphaShift = (yy % 2) * 12 + xx * 3;
                    var alphaCode = (alphaBits >> alphaShift) & 0x7;
                    if (alpha0 > alpha1) {
                        switch (alphaCode) {
                        case 0:
                            alpha = alpha0;
                            break;
                        case 1:
                            alpha = alpha1;
                            break;
                        default:
                            alpha = ((8 - alphaCode) * alpha0 + (alphaCode - 1) * alpha1) / 7;
                            break;
                        }
                    } else {
                        switch (alphaCode) {
                        case 0:
                            alpha = alpha0;
                            break;
                        case 1:
                            alpha = alpha1;
                            break;
                        case 6:
                            alpha = 0;
                            break;
                        case 7:
                            alpha = 255;
                            break;
                        default:
                            alpha = ((6 - alphaCode) * alpha0 + (alphaCode - 1) * alpha1) / 5;
                            break;
                        }
                    }
                }
                break;
            default:
                throw "bad format";
            }
            destBuffer[dstOff + 0] = srcColor[0];
            destBuffer[dstOff + 1] = srcColor[1];
            destBuffer[dstOff + 2] = srcColor[2];
            destBuffer[dstOff + 3] = alpha;
        }
    }
}

function getBlockSize(format) {
  var isDXT1 = format == ext.COMPRESSED_RGB_S3TC_DXT1_EXT ||
               format == ext.COMPRESSED_RGBA_S3TC_DXT1_EXT;
  return isDXT1 ? 8 : 16;
}

function uncompressDXT(width, height, data, format) {
    if (width % 4 || height % 4) throw "bad width or height";

    var dest = new Uint8Array(width * height * 4);
    var blocksAcross = width / 4;
    var blocksDown = height / 4;
    var blockSize = getBlockSize(format);
    for (var yy = 0; yy < blocksDown; ++yy) {
        for (var xx = 0; xx < blocksAcross; ++xx) {
            uncompressDXTBlock(
                dest, xx * 4, yy * 4, width, data,
                (yy * blocksAcross + xx) * blockSize, format);
        }
    }
    return dest;
}

function uncompressDXTIntoSubRegion(width, height, subX0, subY0, subWidth, subHeight, data, format)
{
    if (width % 4 || height % 4 || subX0 % 4 || subY0 % 4 || subWidth % 4 || subHeight % 4)
        throw "bad dimension";

    var dest = new Uint8Array(width * height * 4);
    var blocksAcross = subWidth / 4;
    var blocksDown = subHeight / 4;
    var blockSize = getBlockSize(format);
    for (var yy = 0; yy < blocksDown; ++yy) {
        for (var xx = 0; xx < blocksAcross; ++xx) {
            uncompressDXTBlock(
                dest, subX0 + xx * 4, subY0 + yy * 4, width, data,
                (yy * blocksAcross + xx) * blockSize, format);
        }
    }
    return dest;
}

function copyRect(data, srcX, srcY, dstX, dstY, width, height, stride) {
  var bytesPerLine = width * 4;
  var srcOffset = srcX * 4 + srcY * stride;
  var dstOffset = dstX * 4 + dstY * stride;
  for (; height > 0; --height) {
    for (var ii = 0; ii < bytesPerLine; ++ii) {
      data[dstOffset + ii] = data[srcOffset + ii];
    }
    srcOffset += stride;
    dstOffset += stride;
  }
}

function testDXTTexture(test, useTexStorage) {
    var data = new Uint8Array(test.data);
    var width = test.width;
    var height = test.height;
    var format = test.format;

    var uncompressedData = uncompressDXT(width, height, data, format);

    canvas.width = width;
    canvas.height = height;
    gl.viewport(0, 0, width, height);
    debug("testing " + formatToString(format) + " " + width + "x" + height +
          (useTexStorage ? " via texStorage2D" : " via compressedTexImage2D"));

    var tex = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, tex);
    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);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
    if (useTexStorage) {
        if (test.subData) {
            var uncompressedDataSub = uncompressDXTIntoSubRegion(
                width, height, test.subX0, test.subY0, test.subWidth, test.subHeight, test.subData, format);
            var tex1 = gl.createTexture();
            gl.bindTexture(gl.TEXTURE_2D, tex1);
            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);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

            gl.texStorage2D(gl.TEXTURE_2D, 1, format, width, height);
            wtu.glErrorShouldBe(gl, gl.NO_ERROR, "allocating compressed texture via texStorage2D");
            gl.compressedTexSubImage2D(
                gl.TEXTURE_2D, 0, test.subX0, test.subY0, test.subWidth, test.subHeight, format, test.subData);
            wtu.glErrorShouldBe(gl, gl.NO_ERROR, "uploading compressed texture data via compressedTexSubImage2D");

            wtu.clearAndDrawUnitQuad(gl);
            wtu.glErrorShouldBe(gl, gl.NO_ERROR, "drawing unit quad 1");
            compareRect(width, height, test.channels, uncompressedDataSub, "NEAREST");

            // Clean up and recover
            gl.deleteTexture(tex1);
            gl.bindTexture(gl.TEXTURE_2D, tex);
        }

        gl.texStorage2D(gl.TEXTURE_2D, 1, format, width, height);
        wtu.glErrorShouldBe(gl, gl.NO_ERROR, "allocating compressed texture via texStorage2D");
        wtu.clearAndDrawUnitQuad(gl);
        wtu.glErrorShouldBe(gl, gl.NO_ERROR, "drawing unit quad");
        var clearColor = (test.hasAlpha ? [0, 0, 0, 0] : [0, 0, 0, 255]);
        wtu.checkCanvas(gl, clearColor, "texture should be initialized to black");
        gl.compressedTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width, height, format, data);
        wtu.glErrorShouldBe(gl, gl.NO_ERROR, "uploading compressed texture data via compressedTexSubImage2D");
    } else {
        gl.compressedTexImage2D(gl.TEXTURE_2D, 0, format, width, height, 0, data);
        wtu.glErrorShouldBe(gl, gl.NO_ERROR, "uploading compressed texture");
    }
    gl.generateMipmap(gl.TEXTURE_2D);
    wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "trying to generate mipmaps from compressed texture");
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "after clearing generateMipmap error");
    wtu.clearAndDrawUnitQuad(gl);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "drawing unit quad 1");
    compareRect(width, height, test.channels, uncompressedData, "NEAREST");
    // Test again with linear filtering.
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    wtu.clearAndDrawUnitQuad(gl);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "drawing unit quad 2");
    compareRect(width, height, test.channels, uncompressedData, "LINEAR");

    if (!useTexStorage) {
        // It's not allowed to redefine textures defined via texStorage2D.
        gl.compressedTexImage2D(gl.TEXTURE_2D, 0, format, width, height, 1, data);
        wtu.glErrorShouldBe(gl, gl.INVALID_VALUE, "non 0 border");

        gl.compressedTexImage2D(gl.TEXTURE_2D, 0, format, width + 4, height, 0, data);
        wtu.glErrorShouldBe(gl, gl.INVALID_VALUE, "data size does not match dimensions");
        gl.compressedTexImage2D(gl.TEXTURE_2D, 0, format, width, height + 4, 0, data);
        wtu.glErrorShouldBe(gl, gl.INVALID_VALUE, "data size does not match dimensions");
        gl.compressedTexImage2D(gl.TEXTURE_2D, 0, format, width - 4, height, 0, data);
        wtu.glErrorShouldBe(gl, gl.INVALID_VALUE, "data size does not match dimensions");
        gl.compressedTexImage2D(gl.TEXTURE_2D, 0, format, width, height - 4, 0, data);
        wtu.glErrorShouldBe(gl, gl.INVALID_VALUE, "data size does not match dimensions");

        gl.compressedTexImage2D(gl.TEXTURE_2D, 0, format, width - 1, height, 0, data);
        wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "invalid dimensions");
        gl.compressedTexImage2D(gl.TEXTURE_2D, 0, format, width - 2, height, 0, data);
        wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "invalid dimensions");
        gl.compressedTexImage2D(gl.TEXTURE_2D, 0, format, width, height - 1, 0, data);
        wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "invalid dimensions");
        gl.compressedTexImage2D(gl.TEXTURE_2D, 0, format, width, height - 2, 0, data);
        wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "invalid dimensions");

        if (width == 4) {
            gl.compressedTexImage2D(gl.TEXTURE_2D, 1, format, 1, height, 0, data);
            wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "invalid dimensions for level > 0");
            gl.compressedTexImage2D(gl.TEXTURE_2D, 1, format, 2, height, 0, data);
            wtu.glErrorShouldBe(gl, gl.NO_ERROR, "valid dimensions for level > 0");
        }
        if (height == 4) {
            gl.compressedTexImage2D(gl.TEXTURE_2D, 1, format, width, 1, 0, data);
            wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "invalid dimensions for level > 0");
            gl.compressedTexImage2D(gl.TEXTURE_2D, 1, format, width, 2, 0, data);
            wtu.glErrorShouldBe(gl, gl.NO_ERROR, "valid dimensions for level > 0");
        }
    }

    // pick a wrong format that uses the same amount of data.
    var wrongFormat;
    switch (format) {
    case ext.COMPRESSED_RGB_S3TC_DXT1_EXT:
      wrongFormat = ext.COMPRESSED_RGBA_S3TC_DXT1_EXT;
      break;
    case ext.COMPRESSED_RGBA_S3TC_DXT1_EXT:
      wrongFormat = ext.COMPRESSED_RGB_S3TC_DXT1_EXT;
      break;
    case ext.COMPRESSED_RGBA_S3TC_DXT3_EXT:
      wrongFormat = ext.COMPRESSED_RGBA_S3TC_DXT5_EXT;
      break;
    case ext.COMPRESSED_RGBA_S3TC_DXT5_EXT:
      wrongFormat = ext.COMPRESSED_RGBA_S3TC_DXT3_EXT;
      break;
    }

    gl.compressedTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width, height, wrongFormat, data);
    wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "format does not match");

    gl.compressedTexSubImage2D(gl.TEXTURE_2D, 0, 4, 0, width, height, format, data);
    wtu.glErrorShouldBe(gl, gl.INVALID_VALUE, "dimension out of range");
    gl.compressedTexSubImage2D(gl.TEXTURE_2D, 0, 0, 4, width, height, format, data);
    wtu.glErrorShouldBe(gl, gl.INVALID_VALUE, "dimension out of range");

    gl.compressedTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width + 4, height, format, data);
    wtu.glErrorShouldBe(gl, gl.INVALID_VALUE, "data size does not match dimensions");
    gl.compressedTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width, height + 4, format, data);
    wtu.glErrorShouldBe(gl, gl.INVALID_VALUE, "data size does not match dimensions");
    gl.compressedTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width - 4, height, format, data);
    wtu.glErrorShouldBe(gl, gl.INVALID_VALUE, "data size does not match dimensions");
    gl.compressedTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width, height - 4, format, data);
    wtu.glErrorShouldBe(gl, gl.INVALID_VALUE, "data size does not match dimensions");

    gl.compressedTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width - 1, height, format, data);
    wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "invalid dimensions");
    gl.compressedTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width - 2, height, format, data);
    wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "invalid dimensions");
    gl.compressedTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width, height - 1, format, data);
    wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "invalid dimensions");
    gl.compressedTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width, height - 2, format, data);
    wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "invalid dimensions");

    var subData = new Uint8Array(data.buffer, 0, getBlockSize(format));

    if (width == 8 && height == 8) {
        gl.compressedTexSubImage2D(gl.TEXTURE_2D, 0, 1, 0, 4, 4, format, subData);
        wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "invalid offset");
        gl.compressedTexSubImage2D(gl.TEXTURE_2D, 0, 0, 1, 4, 4, format, subData);
        wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "invalid offset");
    }

    var stride = width * 4;
    for (var yoff = 0; yoff < height; yoff += 4) {
        for (var xoff = 0; xoff < width; xoff += 4) {
            copyRect(uncompressedData, 0, 0, xoff, yoff, 4, 4, stride);
            gl.compressedTexSubImage2D(gl.TEXTURE_2D, 0, xoff, yoff, 4, 4, format, subData);
            wtu.glErrorShouldBe(gl, gl.NO_ERROR, "uploading compressed texture");
            // First test NEAREST filtering.
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
            wtu.clearAndDrawUnitQuad(gl);
            compareRect(width, height, test.channels, uncompressedData, "NEAREST");
            wtu.glErrorShouldBe(gl, gl.NO_ERROR, "drawing unit quad");
            // Next test LINEAR filtering.
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
            wtu.clearAndDrawUnitQuad(gl);
            wtu.glErrorShouldBe(gl, gl.NO_ERROR, "drawing unit quad");
            compareRect(width, height, test.channels, uncompressedData, "LINEAR");
        }
    }
}

function insertImg(element, caption, img) {
    var div = document.createElement("div");
    div.appendChild(img);
    var label = document.createElement("div");
    label.appendChild(document.createTextNode(caption));
    div.appendChild(label);
    element.appendChild(div);
}

function makeImage(imageWidth, imageHeight, data, alpha) {
    var scale = 8;
    var c = document.createElement("canvas");
    c.width = imageWidth * scale;
    c.height = imageHeight * scale;
    var ctx = c.getContext("2d");
    for (var yy = 0; yy < imageHeight; ++yy) {
        for (var xx = 0; xx < imageWidth; ++xx) {
            var offset = (yy * imageWidth + xx) * 4;
            ctx.fillStyle = "rgba(" +
                    data[offset + 0] + "," +
                    data[offset + 1] + "," +
                    data[offset + 2] + "," +
                    (alpha ? data[offset + 3] / 255 : 1) + ")";
            ctx.fillRect(xx * scale, yy * scale, scale, scale);
        }
    }
    return wtu.makeImageFromCanvas(c);
}

function compareRect(width, height, channels, expectedData, filteringMode) {
    var actual = new Uint8Array(width * height * 4);
    gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, actual);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "reading back pixels");

    var div = document.createElement("div");
    div.className = "testimages";
    insertImg(div, "expected", makeImage(width, height, expectedData, channels == 4));
    insertImg(div, "actual", makeImage(width, height, actual, channels == 4));
    div.appendChild(document.createElement('br'));
    document.getElementById("console").appendChild(div);

    var failed = false;
    for (var yy = 0; yy < height; ++yy) {
        for (var xx = 0; xx < width; ++xx) {
            var offset = (yy * width + xx) * 4;
            var expected = [
                expectedData[offset + 0],
                expectedData[offset + 1],
                expectedData[offset + 2],
                (channels == 3 ? 255 : expectedData[offset + 3])
            ];
            for (var jj = 0; jj < 4; ++jj) {
                if (actual[offset + jj] != expected[jj]) {
                    failed = true;
                    var was = actual[offset + 0].toString();
                    for (var j = 1; j < 4; ++j) {
                        was += "," + actual[offset + j];
                    }
                    testFailed('at (' + xx + ', ' + yy +
                                         ') expected: ' + expected + ' was ' + was);
                }
            }
        }
    }
    if (!failed) {
        testPassed("texture rendered correctly with " + filteringMode + " filtering");
    }
}

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

</body>
</html>
