/* 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. */ GLSLGenerator = (function() { var vertexShaderTemplate = [ "attribute vec4 aPosition;", "", "varying vec4 vColor;", "", "$(extra)", "$(emu)", "", "void main()", "{", " gl_Position = aPosition;", " vec2 texcoord = vec2(aPosition.xy * 0.5 + vec2(0.5, 0.5));", " vec4 color = vec4(", " texcoord,", " texcoord.x * texcoord.y,", " (1.0 - texcoord.x) * texcoord.y * 0.5 + 0.5);", " $(test)", "}" ].join("\n"); var fragmentShaderTemplate = [ "precision mediump float;", "", "varying vec4 vColor;", "", "$(extra)", "$(emu)", "", "void main()", "{", " $(test)", "}" ].join("\n"); var baseVertexShader = [ "attribute vec4 aPosition;", "", "varying vec4 vColor;", "", "void main()", "{", " gl_Position = aPosition;", " vec2 texcoord = vec2(aPosition.xy * 0.5 + vec2(0.5, 0.5));", " vColor = vec4(", " texcoord,", " texcoord.x * texcoord.y,", " (1.0 - texcoord.x) * texcoord.y * 0.5 + 0.5);", "}" ].join("\n"); var baseVertexShaderWithColor = [ "attribute vec4 aPosition;", "attribute vec4 aColor;", "", "varying vec4 vColor;", "", "void main()", "{", " gl_Position = aPosition;", " vColor = aColor;", "}" ].join("\n"); var baseFragmentShader = [ "precision mediump float;", "varying vec4 vColor;", "", "void main()", "{", " gl_FragColor = vColor;", "}" ].join("\n"); var types = [ { type: "float", code: [ "float $(func)_emu($(args)) {", " return $(func)_base($(baseArgs));", "}"].join("\n") }, { type: "vec2", code: [ "vec2 $(func)_emu($(args)) {", " return vec2(", " $(func)_base($(baseArgsX)),", " $(func)_base($(baseArgsY)));", "}"].join("\n") }, { type: "vec3", code: [ "vec3 $(func)_emu($(args)) {", " return vec3(", " $(func)_base($(baseArgsX)),", " $(func)_base($(baseArgsY)),", " $(func)_base($(baseArgsZ)));", "}"].join("\n") }, { type: "vec4", code: [ "vec4 $(func)_emu($(args)) {", " return vec4(", " $(func)_base($(baseArgsX)),", " $(func)_base($(baseArgsY)),", " $(func)_base($(baseArgsZ)),", " $(func)_base($(baseArgsW)));", "}"].join("\n") } ]; var bvecTypes = [ { type: "bvec2", code: [ "bvec2 $(func)_emu($(args)) {", " return bvec2(", " $(func)_base($(baseArgsX)),", " $(func)_base($(baseArgsY)));", "}"].join("\n") }, { type: "bvec3", code: [ "bvec3 $(func)_emu($(args)) {", " return bvec3(", " $(func)_base($(baseArgsX)),", " $(func)_base($(baseArgsY)),", " $(func)_base($(baseArgsZ)));", "}"].join("\n") }, { type: "bvec4", code: [ "vec4 $(func)_emu($(args)) {", " return bvec4(", " $(func)_base($(baseArgsX)),", " $(func)_base($(baseArgsY)),", " $(func)_base($(baseArgsZ)),", " $(func)_base($(baseArgsW)));", "}"].join("\n") } ]; var replaceRE = /\$\((\w+)\)/g; var replaceParams = function(str) { var args = arguments; return str.replace(replaceRE, function(str, p1, offset, s) { for (var ii = 1; ii < args.length; ++ii) { if (args[ii][p1] !== undefined) { return args[ii][p1]; } } throw "unknown string param '" + p1 + "'"; }); }; var generateReferenceShader = function( shaderInfo, template, params, typeInfo, test) { var input = shaderInfo.input; var output = shaderInfo.output; var feature = params.feature; var testFunc = params.testFunc; var emuFunc = params.emuFunc || ""; var extra = params.extra || ''; var args = params.args || "$(type) value"; var type = typeInfo.type; var typeCode = typeInfo.code; var baseArgs = params.baseArgs || "value$(field)"; var baseArgsX = replaceParams(baseArgs, {field: ".x"}); var baseArgsY = replaceParams(baseArgs, {field: ".y"}); var baseArgsZ = replaceParams(baseArgs, {field: ".z"}); var baseArgsW = replaceParams(baseArgs, {field: ".w"}); var baseArgs = replaceParams(baseArgs, {field: ""}); test = replaceParams(test, { input: input, output: output, func: feature + "_emu" }); emuFunc = replaceParams(emuFunc, { func: feature }); args = replaceParams(args, { type: type }); typeCode = replaceParams(typeCode, { func: feature, type: type, args: args, baseArgs: baseArgs, baseArgsX: baseArgsX, baseArgsY: baseArgsY, baseArgsZ: baseArgsZ, baseArgsW: baseArgsW }); var shader = replaceParams(template, { extra: extra, emu: emuFunc + "\n\n" + typeCode, test: test }); return shader; }; var generateTestShader = function( shaderInfo, template, params, test) { var input = shaderInfo.input; var output = shaderInfo.output; var feature = params.feature; var testFunc = params.testFunc; var extra = params.extra || ''; test = replaceParams(test, { input: input, output: output, func: feature }); var shader = replaceParams(template, { extra: extra, emu: '', test: test }); return shader; }; function _reportResults(refData, refImg, testData, testImg, tolerance, width, height, ctx, imgData, wtu, canvas2d, consoleDiv) { var same = true; var firstFailure = null; for (var yy = 0; yy < height; ++yy) { for (var xx = 0; xx < width; ++xx) { var offset = (yy * width + xx) * 4; var imgOffset = ((height - yy - 1) * width + xx) * 4; imgData.data[imgOffset + 0] = 0; imgData.data[imgOffset + 1] = 0; imgData.data[imgOffset + 2] = 0; imgData.data[imgOffset + 3] = 255; if (Math.abs(refData[offset + 0] - testData[offset + 0]) > tolerance || Math.abs(refData[offset + 1] - testData[offset + 1]) > tolerance || Math.abs(refData[offset + 2] - testData[offset + 2]) > tolerance || Math.abs(refData[offset + 3] - testData[offset + 3]) > tolerance) { var detail = 'at (' + xx + ',' + yy + '): ref=(' + refData[offset + 0] + ',' + refData[offset + 1] + ',' + refData[offset + 2] + ',' + refData[offset + 3] + ') test=(' + testData[offset + 0] + ',' + testData[offset + 1] + ',' + testData[offset + 2] + ',' + testData[offset + 3] + ') tolerance=' + tolerance; consoleDiv.appendChild(document.createTextNode(detail)); consoleDiv.appendChild(document.createElement('br')); if (!firstFailure) { firstFailure = ": " + detail; } imgData.data[imgOffset] = 255; same = false; } } } var diffImg = null; if (!same) { ctx.putImageData(imgData, 0, 0); diffImg = wtu.makeImageFromCanvas(canvas2d); } var div = document.createElement("div"); div.className = "testimages"; wtu.insertImage(div, "ref", refImg); wtu.insertImage(div, "test", testImg); if (diffImg) { wtu.insertImage(div, "diff", diffImg); } div.appendChild(document.createElement('br')); consoleDiv.appendChild(div); if (!same) { testFailed("images are different" + (firstFailure ? firstFailure : "")); } else { testPassed("images are the same"); } consoleDiv.appendChild(document.createElement('hr')); } var runFeatureTest = function(params) { var wtu = WebGLTestUtils; var gridRes = params.gridRes; var vertexTolerance = params.tolerance || 0; var fragmentTolerance = params.tolerance || 1; if ('fragmentTolerance' in params) fragmentTolerance = params.fragmentTolerance; description("Testing GLSL feature: " + params.feature); var width = 32; var height = 32; var consoleDiv = document.getElementById("console"); var canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; var gl = wtu.create3DContext(canvas, { premultipliedAlpha: false }); if (!gl) { testFailed("context does not exist"); finishTest(); return; } var canvas2d = document.createElement('canvas'); canvas2d.width = width; canvas2d.height = height; var ctx = canvas2d.getContext("2d"); var imgData = ctx.getImageData(0, 0, width, height); var shaderInfos = [ { type: "vertex", input: "color", output: "vColor", vertexShaderTemplate: vertexShaderTemplate, fragmentShaderTemplate: baseFragmentShader, tolerance: vertexTolerance }, { type: "fragment", input: "vColor", output: "gl_FragColor", vertexShaderTemplate: baseVertexShader, fragmentShaderTemplate: fragmentShaderTemplate, tolerance: fragmentTolerance } ]; for (var ss = 0; ss < shaderInfos.length; ++ss) { var shaderInfo = shaderInfos[ss]; var tests = params.tests; var testTypes = params.emuFuncs || (params.bvecTest ? bvecTypes : types); // Test vertex shaders for (var ii = 0; ii < tests.length; ++ii) { var type = testTypes[ii]; if (params.simpleEmu) { type = { type: type.type, code: params.simpleEmu }; } debug(""); var str = replaceParams(params.testFunc, { func: params.feature, type: type.type, arg0: type.type }); var passMsg = "Testing: " + str + " in " + shaderInfo.type + " shader"; debug(passMsg); var referenceVertexShaderSource = generateReferenceShader( shaderInfo, shaderInfo.vertexShaderTemplate, params, type, tests[ii]); var referenceFragmentShaderSource = generateReferenceShader( shaderInfo, shaderInfo.fragmentShaderTemplate, params, type, tests[ii]); var testVertexShaderSource = generateTestShader( shaderInfo, shaderInfo.vertexShaderTemplate, params, tests[ii]); var testFragmentShaderSource = generateTestShader( shaderInfo, shaderInfo.fragmentShaderTemplate, params, tests[ii]); debug(""); var referenceVertexShader = wtu.loadShader(gl, referenceVertexShaderSource, gl.VERTEX_SHADER, testFailed, true, 'reference'); var referenceFragmentShader = wtu.loadShader(gl, referenceFragmentShaderSource, gl.FRAGMENT_SHADER, testFailed, true, 'reference'); var testVertexShader = wtu.loadShader(gl, testVertexShaderSource, gl.VERTEX_SHADER, testFailed, true, 'test'); var testFragmentShader = wtu.loadShader(gl, testFragmentShaderSource, gl.FRAGMENT_SHADER, testFailed, true, 'test'); debug(""); if (parseInt(wtu.getUrlOptions().dumpShaders)) { var vRefInfo = { shader: referenceVertexShader, shaderSuccess: true, label: "reference vertex shader", source: referenceVertexShaderSource }; var fRefInfo = { shader: referenceFragmentShader, shaderSuccess: true, label: "reference fragment shader", source: referenceFragmentShaderSource }; wtu.dumpShadersInfo(gl, window.location.pathname, passMsg, vRefInfo, fRefInfo); var vTestInfo = { shader: testVertexShader, shaderSuccess: true, label: "test vertex shader", source: testVertexShaderSource }; var fTestInfo = { shader: testFragmentShader, shaderSuccess: true, label: "test fragment shader", source: testFragmentShaderSource }; wtu.dumpShadersInfo(gl, window.location.pathname, passMsg, vTestInfo, fTestInfo); } var refData = draw( referenceVertexShader, referenceFragmentShader); var refImg = wtu.makeImageFromCanvas(canvas); if (ss == 0) { var testData = draw( testVertexShader, referenceFragmentShader); } else { var testData = draw( referenceVertexShader, testFragmentShader); } var testImg = wtu.makeImageFromCanvas(canvas); _reportResults(refData, refImg, testData, testImg, shaderInfo.tolerance, width, height, ctx, imgData, wtu, canvas2d, consoleDiv); } } finishTest(); function draw(vertexShader, fragmentShader) { var program = wtu.createProgram(gl, vertexShader, fragmentShader, testFailed); var posLoc = gl.getAttribLocation(program, "aPosition"); wtu.setupIndexedQuad(gl, gridRes, posLoc); gl.useProgram(program); wtu.clearAndDrawIndexedQuad(gl, gridRes, [0, 0, 255, 255]); wtu.glErrorShouldBe(gl, gl.NO_ERROR, "no errors from draw"); var img = new Uint8Array(width * height * 4); gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, img); return img; } }; var runBasicTest = function(params) { var wtu = WebGLTestUtils; var gridRes = params.gridRes; var vertexTolerance = params.tolerance || 0; var fragmentTolerance = vertexTolerance; if ('fragmentTolerance' in params) fragmentTolerance = params.fragmentTolerance || 0; description("Testing : " + document.getElementsByTagName("title")[0].innerText); var width = 32; var height = 32; var consoleDiv = document.getElementById("console"); var canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; var gl = wtu.create3DContext(canvas); if (!gl) { testFailed("context does not exist"); finishTest(); return; } var canvas2d = document.createElement('canvas'); canvas2d.width = width; canvas2d.height = height; var ctx = canvas2d.getContext("2d"); var imgData = ctx.getImageData(0, 0, width, height); var shaderInfos = [ { type: "vertex", input: "color", output: "vColor", vertexShaderTemplate: vertexShaderTemplate, fragmentShaderTemplate: baseFragmentShader, tolerance: vertexTolerance }, { type: "fragment", input: "vColor", output: "gl_FragColor", vertexShaderTemplate: baseVertexShader, fragmentShaderTemplate: fragmentShaderTemplate, tolerance: fragmentTolerance } ]; for (var ss = 0; ss < shaderInfos.length; ++ss) { var shaderInfo = shaderInfos[ss]; var tests = params.tests; // var testTypes = params.emuFuncs || (params.bvecTest ? bvecTypes : types); // Test vertex shaders for (var ii = 0; ii < tests.length; ++ii) { var test = tests[ii]; debug(""); var passMsg = "Testing: " + test.name + " in " + shaderInfo.type + " shader"; debug(passMsg); function genShader(shaderInfo, template, shader, subs) { shader = replaceParams(shader, subs, { input: shaderInfo.input, output: shaderInfo.output }); shader = replaceParams(template, subs, { test: shader, emu: "", extra: "" }); return shader; } var referenceVertexShaderSource = genShader( shaderInfo, shaderInfo.vertexShaderTemplate, test.reference.shader, test.reference.subs); var referenceFragmentShaderSource = genShader( shaderInfo, shaderInfo.fragmentShaderTemplate, test.reference.shader, test.reference.subs); var testVertexShaderSource = genShader( shaderInfo, shaderInfo.vertexShaderTemplate, test.test.shader, test.test.subs); var testFragmentShaderSource = genShader( shaderInfo, shaderInfo.fragmentShaderTemplate, test.test.shader, test.test.subs); debug(""); var referenceVertexShader = wtu.loadShader(gl, referenceVertexShaderSource, gl.VERTEX_SHADER, testFailed, true, 'reference'); var referenceFragmentShader = wtu.loadShader(gl, referenceFragmentShaderSource, gl.FRAGMENT_SHADER, testFailed, true, 'reference'); var testVertexShader = wtu.loadShader(gl, testVertexShaderSource, gl.VERTEX_SHADER, testFailed, true, 'test'); var testFragmentShader = wtu.loadShader(gl, testFragmentShaderSource, gl.FRAGMENT_SHADER, testFailed, true, 'test'); debug(""); if (parseInt(wtu.getUrlOptions().dumpShaders)) { var vRefInfo = { shader: referenceVertexShader, shaderSuccess: true, label: "reference vertex shader", source: referenceVertexShaderSource }; var fRefInfo = { shader: referenceFragmentShader, shaderSuccess: true, label: "reference fragment shader", source: referenceFragmentShaderSource }; wtu.dumpShadersInfo(gl, window.location.pathname, passMsg, vRefInfo, fRefInfo); var vTestInfo = { shader: testVertexShader, shaderSuccess: true, label: "test vertex shader", source: testVertexShaderSource }; var fTestInfo = { shader: testFragmentShader, shaderSuccess: true, label: "test fragment shader", source: testFragmentShaderSource }; wtu.dumpShadersInfo(gl, window.location.pathname, passMsg, vTestInfo, fTestInfo); } var refData = draw(referenceVertexShader, referenceFragmentShader); var refImg = wtu.makeImageFromCanvas(canvas); if (ss == 0) { var testData = draw(testVertexShader, referenceFragmentShader); } else { var testData = draw(referenceVertexShader, testFragmentShader); } var testImg = wtu.makeImageFromCanvas(canvas); _reportResults(refData, refImg, testData, testImg, shaderInfo.tolerance, width, height, ctx, imgData, wtu, canvas2d, consoleDiv); } } finishTest(); function draw(vertexShader, fragmentShader) { var program = wtu.createProgram(gl, vertexShader, fragmentShader, testFailed); var posLoc = gl.getAttribLocation(program, "aPosition"); wtu.setupIndexedQuad(gl, gridRes, posLoc); gl.useProgram(program); wtu.clearAndDrawIndexedQuad(gl, gridRes, [0, 0, 255, 255]); wtu.glErrorShouldBe(gl, gl.NO_ERROR, "no errors from draw"); var img = new Uint8Array(width * height * 4); gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, img); return img; } }; var runReferenceImageTest = function(params) { var wtu = WebGLTestUtils; var gridRes = params.gridRes; var vertexTolerance = params.tolerance || 0; var fragmentTolerance = vertexTolerance; if ('fragmentTolerance' in params) fragmentTolerance = params.fragmentTolerance || 0; description("Testing GLSL feature: " + params.feature); var width = 32; var height = 32; var consoleDiv = document.getElementById("console"); var canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; var gl = wtu.create3DContext(canvas, { antialias: false, premultipliedAlpha: false }); if (!gl) { testFailed("context does not exist"); finishTest(); return; } var canvas2d = document.createElement('canvas'); canvas2d.width = width; canvas2d.height = height; var ctx = canvas2d.getContext("2d"); var imgData = ctx.getImageData(0, 0, width, height); // State for reference images for vertex shader tests. // These are drawn with the same tessellated grid as the test vertex // shader so that the interpolation is identical. The grid is reused // from test to test; the colors are changed. var indexedQuadForReferenceVertexShader = wtu.setupIndexedQuad(gl, gridRes, 0); var referenceVertexShaderProgram = wtu.setupProgram(gl, [ baseVertexShaderWithColor, baseFragmentShader ], ["aPosition", "aColor"]); var referenceVertexShaderColorBuffer = gl.createBuffer(); var shaderInfos = [ { type: "vertex", input: "color", output: "vColor", vertexShaderTemplate: vertexShaderTemplate, fragmentShaderTemplate: baseFragmentShader, tolerance: vertexTolerance }, { type: "fragment", input: "vColor", output: "gl_FragColor", vertexShaderTemplate: baseVertexShader, fragmentShaderTemplate: fragmentShaderTemplate, tolerance: fragmentTolerance } ]; for (var ss = 0; ss < shaderInfos.length; ++ss) { var shaderInfo = shaderInfos[ss]; var tests = params.tests; var testTypes = params.emuFuncs || (params.bvecTest ? bvecTypes : types); // Test vertex shaders for (var ii = 0; ii < tests.length; ++ii) { var type = testTypes[ii]; var isVertex = (ss == 0); debug(""); var str = replaceParams(params.testFunc, { func: params.feature, type: type.type, arg0: type.type }); var passMsg = "Testing: " + str + " in " + shaderInfo.type + " shader"; debug(passMsg); var referenceVertexShaderSource = generateReferenceShader( shaderInfo, shaderInfo.vertexShaderTemplate, params, type, tests[ii].source); var referenceFragmentShaderSource = generateReferenceShader( shaderInfo, shaderInfo.fragmentShaderTemplate, params, type, tests[ii].source); var testVertexShaderSource = generateTestShader( shaderInfo, shaderInfo.vertexShaderTemplate, params, tests[ii].source); var testFragmentShaderSource = generateTestShader( shaderInfo, shaderInfo.fragmentShaderTemplate, params, tests[ii].source); var referenceTextureOrArray = generateReferenceImage( gl, tests[ii].generator, isVertex ? gridRes : width, isVertex ? gridRes : height, isVertex); debug(""); var testVertexShader = wtu.loadShader(gl, testVertexShaderSource, gl.VERTEX_SHADER, testFailed, true); var testFragmentShader = wtu.loadShader(gl, testFragmentShaderSource, gl.FRAGMENT_SHADER, testFailed, true); debug(""); if (parseInt(wtu.getUrlOptions().dumpShaders)) { var vRefInfo = { shader: referenceVertexShader, shaderSuccess: true, label: "reference vertex shader", source: referenceVertexShaderSource }; var fRefInfo = { shader: referenceFragmentShader, shaderSuccess: true, label: "reference fragment shader", source: referenceFragmentShaderSource }; wtu.dumpShadersInfo(gl, window.location.pathname, passMsg, vRefInfo, fRefInfo); var vTestInfo = { shader: testVertexShader, shaderSuccess: true, label: "test vertex shader", source: testVertexShaderSource }; var fTestInfo = { shader: testFragmentShader, shaderSuccess: true, label: "test fragment shader", source: testFragmentShaderSource }; wtu.dumpShadersInfo(gl, window.location.pathname, passMsg, vTestInfo, fTestInfo); } var refData; if (isVertex) { refData = drawVertexReferenceImage(referenceTextureOrArray); } else { refData = drawFragmentReferenceImage(referenceTextureOrArray); } var refImg = wtu.makeImageFromCanvas(canvas); var testData; if (isVertex) { var referenceFragmentShader = wtu.loadShader(gl, referenceFragmentShaderSource, gl.FRAGMENT_SHADER, testFailed); testData = draw( testVertexShader, referenceFragmentShader); } else { var referenceVertexShader = wtu.loadShader(gl, referenceVertexShaderSource, gl.VERTEX_SHADER, testFailed); testData = draw( referenceVertexShader, testFragmentShader); } var testImg = wtu.makeImageFromCanvas(canvas); var testTolerance = shaderInfo.tolerance; // Provide per-test tolerance so that we can increase it only for those desired. if ('tolerance' in tests[ii]) testTolerance = tests[ii].tolerance || 0; _reportResults(refData, refImg, testData, testImg, testTolerance, width, height, ctx, imgData, wtu, canvas2d, consoleDiv); } } finishTest(); function draw(vertexShader, fragmentShader) { var program = wtu.createProgram(gl, vertexShader, fragmentShader, testFailed); var posLoc = gl.getAttribLocation(program, "aPosition"); wtu.setupIndexedQuad(gl, gridRes, posLoc); gl.useProgram(program); wtu.clearAndDrawIndexedQuad(gl, gridRes, [0, 0, 255, 255]); wtu.glErrorShouldBe(gl, gl.NO_ERROR, "no errors from draw"); var img = new Uint8Array(width * height * 4); gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, img); return img; } function drawVertexReferenceImage(colors) { gl.bindBuffer(gl.ARRAY_BUFFER, indexedQuadForReferenceVertexShader[0]); gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0); gl.bindBuffer(gl.ARRAY_BUFFER, referenceVertexShaderColorBuffer); gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW); gl.enableVertexAttribArray(1); gl.vertexAttribPointer(1, 4, gl.UNSIGNED_BYTE, true, 0, 0); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexedQuadForReferenceVertexShader[1]); gl.useProgram(referenceVertexShaderProgram); wtu.clearAndDrawIndexedQuad(gl, gridRes); gl.disableVertexAttribArray(0); gl.disableVertexAttribArray(1); wtu.glErrorShouldBe(gl, gl.NO_ERROR, "no errors from draw"); var img = new Uint8Array(width * height * 4); gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, img); return img; } function drawFragmentReferenceImage(texture) { var program = wtu.setupTexturedQuad(gl); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, texture); var texLoc = gl.getUniformLocation(program, "tex"); gl.uniform1i(texLoc, 0); wtu.clearAndDrawUnitQuad(gl); wtu.glErrorShouldBe(gl, gl.NO_ERROR, "no errors from draw"); var img = new Uint8Array(width * height * 4); gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, img); return img; } /** * Creates and returns either a Uint8Array (for vertex shaders) or * WebGLTexture (for fragment shaders) containing the reference * image for the function being tested. Exactly how the function is * evaluated, and the size of the returned texture or array, depends on * whether we are testing a vertex or fragment shader. If a fragment * shader, the function is evaluated at the pixel centers. If a * vertex shader, the function is evaluated at the triangle's * vertices. * * @param {!WebGLRenderingContext} gl The WebGLRenderingContext to use to generate texture objects. * @param {!function(number,number,number,number): !Array.} generator The reference image generator function. * @param {number} width The width of the texture to generate if testing a fragment shader; the grid resolution if testing a vertex shader. * @param {number} height The height of the texture to generate if testing a fragment shader; the grid resolution if testing a vertex shader. * @param {boolean} isVertex True if generating a reference image for a vertex shader; false if for a fragment shader. * @return {!WebGLTexture|!Uint8Array} The texture object or array that was generated. */ function generateReferenceImage( gl, generator, width, height, isVertex) { // Note: the math in this function must match that in the vertex and // fragment shader templates above. function computeTexCoord(x) { return x * 0.5 + 0.5; } function computeVertexColor(texCoordX, texCoordY) { return [ texCoordX, texCoordY, texCoordX * texCoordY, (1.0 - texCoordX) * texCoordY * 0.5 + 0.5 ]; } /** * Computes fragment color according to the algorithm used for interpolation * in OpenGL (GLES 2.0 spec 3.5.1, OpenGL 4.3 spec 14.6.1). */ function computeInterpolatedColor(texCoordX, texCoordY) { // Calculate grid line indexes below and to the left from texCoord. var gridBottom = Math.floor(texCoordY * gridRes); if (gridBottom == gridRes) { --gridBottom; } var gridLeft = Math.floor(texCoordX * gridRes); if (gridLeft == gridRes) { --gridLeft; } // Calculate coordinates relative to the grid cell. var cellX = texCoordX * gridRes - gridLeft; var cellY = texCoordY * gridRes - gridBottom; // Barycentric coordinates inside either triangle ACD or ABC // are used as weights for the vertex colors in the corners: // A--B // |\ | // | \| // D--C var aColor = computeVertexColor(gridLeft / gridRes, (gridBottom + 1) / gridRes); var bColor = computeVertexColor((gridLeft + 1) / gridRes, (gridBottom + 1) / gridRes); var cColor = computeVertexColor((gridLeft + 1) / gridRes, gridBottom / gridRes); var dColor = computeVertexColor(gridLeft / gridRes, gridBottom / gridRes); // Calculate weights. var a, b, c, d; if (cellX + cellY < 1) { // In bottom triangle ACD. a = cellY; // area of triangle C-D-(cellX, cellY) relative to ACD c = cellX; // area of triangle D-A-(cellX, cellY) relative to ACD d = 1 - a - c; b = 0; } else { // In top triangle ABC. a = 1 - cellX; // area of the triangle B-C-(cellX, cellY) relative to ABC c = 1 - cellY; // area of the triangle A-B-(cellX, cellY) relative to ABC b = 1 - a - c; d = 0; } var interpolated = []; for (var ii = 0; ii < aColor.length; ++ii) { interpolated.push(a * aColor[ii] + b * bColor[ii] + c * cColor[ii] + d * dColor[ii]); } return interpolated; } function clamp(value, minVal, maxVal) { return Math.max(minVal, Math.min(value, maxVal)); } // Evaluates the function at clip coordinates (px,py), storing the // result in the array "pixel". Each channel's result is clamped // between 0 and 255. function evaluateAtClipCoords(px, py, pixel, colorFunc) { var tcx = computeTexCoord(px); var tcy = computeTexCoord(py); var color = colorFunc(tcx, tcy); var output = generator(color[0], color[1], color[2], color[3]); // Multiply by 256 to get even distribution for all values between 0 and 1. // Use rounding rather than truncation to more closely match the GPU's behavior. pixel[0] = clamp(Math.round(256 * output[0]), 0, 255); pixel[1] = clamp(Math.round(256 * output[1]), 0, 255); pixel[2] = clamp(Math.round(256 * output[2]), 0, 255); pixel[3] = clamp(Math.round(256 * output[3]), 0, 255); } function generateFragmentReference() { var data = new Uint8Array(4 * width * height); var horizTexel = 1.0 / width; var vertTexel = 1.0 / height; var halfHorizTexel = 0.5 * horizTexel; var halfVertTexel = 0.5 * vertTexel; var pixel = new Array(4); for (var yi = 0; yi < height; ++yi) { for (var xi = 0; xi < width; ++xi) { // The function must be evaluated at pixel centers. // Compute desired position in clip space var px = -1.0 + 2.0 * (halfHorizTexel + xi * horizTexel); var py = -1.0 + 2.0 * (halfVertTexel + yi * vertTexel); evaluateAtClipCoords(px, py, pixel, computeInterpolatedColor); var index = 4 * (width * yi + xi); data[index + 0] = pixel[0]; data[index + 1] = pixel[1]; data[index + 2] = pixel[2]; data[index + 3] = pixel[3]; } } var texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 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.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data); return texture; } function generateVertexReference() { // We generate a Uint8Array which contains the evaluation of the // function at the vertices of the triangle mesh. It is expected // that the width and the height are identical, and equivalent // to the grid resolution. if (width != height) { throw "width and height must be equal"; } var texSize = 1 + width; var data = new Uint8Array(4 * texSize * texSize); var step = 2.0 / width; var pixel = new Array(4); for (var yi = 0; yi < texSize; ++yi) { for (var xi = 0; xi < texSize; ++xi) { // The function is evaluated at the triangles' vertices. // Compute desired position in clip space var px = -1.0 + (xi * step); var py = -1.0 + (yi * step); evaluateAtClipCoords(px, py, pixel, computeVertexColor); var index = 4 * (texSize * yi + xi); data[index + 0] = pixel[0]; data[index + 1] = pixel[1]; data[index + 2] = pixel[2]; data[index + 3] = pixel[3]; } } return data; } //---------------------------------------------------------------------- // Body of generateReferenceImage // if (isVertex) { return generateVertexReference(); } else { return generateFragmentReference(); } } }; return { /** * runs a bunch of GLSL tests using the passed in parameters * The parameters are: * * feature: * the name of the function being tested (eg, sin, dot, * normalize) * * testFunc: * The prototype of function to be tested not including the * return type. * * emuFunc: * A base function that can be used to generate emulation * functions. Example for 'ceil' * * float $(func)_base(float value) { * float m = mod(value, 1.0); * return m != 0.0 ? (value + 1.0 - m) : value; * } * * args: * The arguments to the function * * baseArgs: (optional) * The arguments when a base function is used to create an * emulation function. For example 'float sign_base(float v)' * is used to implemenent vec2 sign_emu(vec2 v). * * simpleEmu: * if supplied, the code that can be used to generate all * functions for all types. * * Example for 'normalize': * * $(type) $(func)_emu($(args)) { * return value / length(value); * } * * gridRes: (optional) * The resolution of the mesh to generate. The default is a * 1x1 grid but many vertex shaders need a higher resolution * otherwise the only values passed in are the 4 corners * which often have the same value. * * tests: * The code for each test. It is assumed the tests are for * float, vec2, vec3, vec4 in that order. * * tolerance: (optional) * Allow some tolerance in the comparisons. The tolerance is applied to * both vertex and fragment shaders. The default tolerance is 0, meaning * the values have to be identical. * * fragmentTolerance: (optional) * Specify a tolerance which only applies to fragment shaders. The * fragment-only tolerance will override the shared tolerance for * fragment shaders if both are specified. Fragment shaders usually * use mediump float precision so they sometimes require higher tolerance * than vertex shaders which use highp by default. */ runFeatureTest: runFeatureTest, /* * Runs a bunch of GLSL tests using the passed in parameters * * The parameters are: * * tests: * Array of tests. For each test the following parameters are expected * * name: * some description of the test * reference: * parameters for the reference shader (see below) * test: * parameters for the test shader (see below) * * The parameter for the reference and test shaders are * * shader: the GLSL for the shader * subs: any substitutions you wish to define for the shader. * * Each shader is created from a basic template that * defines an input and an output. You can see the * templates at the top of this file. The input and output * change depending on whether or not we are generating * a vertex or fragment shader. * * All this code function does is a bunch of string substitutions. * A substitution is defined by $(name). If name is found in * the 'subs' parameter it is replaced. 4 special names exist. * * 'input' the input to your GLSL. Always a vec4. All change * from 0 to 1 over the quad to be drawn. * * 'output' the output color. Also a vec4 * * 'emu' a place to insert extra stuff * 'extra' a place to insert extra stuff. * * You can think of the templates like this * * $(extra) * $(emu) * * void main() { * // do math to calculate input * ... * * $(shader) * } * * Your shader first has any subs you provided applied as well * as 'input' and 'output' * * It is then inserted into the template which is also provided * with your subs. * * gridRes: (optional) * The resolution of the mesh to generate. The default is a * 1x1 grid but many vertex shaders need a higher resolution * otherwise the only values passed in are the 4 corners * which often have the same value. * * tolerance: (optional) * Allow some tolerance in the comparisons. The tolerance is applied to * both vertex and fragment shaders. The default tolerance is 0, meaning * the values have to be identical. * * fragmentTolerance: (optional) * Specify a tolerance which only applies to fragment shaders. The * fragment-only tolerance will override the shared tolerance for * fragment shaders if both are specified. Fragment shaders usually * use mediump float precision so they sometimes require higher tolerance * than vertex shaders which use highp. */ runBasicTest: runBasicTest, /** * Runs a bunch of GLSL tests using the passed in parameters. The * expected results are computed as a reference image in JavaScript * instead of on the GPU. The parameters are: * * feature: * the name of the function being tested (eg, sin, dot, * normalize) * * testFunc: * The prototype of function to be tested not including the * return type. * * args: * The arguments to the function * * gridRes: (optional) * The resolution of the mesh to generate. The default is a * 1x1 grid but many vertex shaders need a higher resolution * otherwise the only values passed in are the 4 corners * which often have the same value. * * tests: * Array of tests. It is assumed the tests are for float, vec2, * vec3, vec4 in that order. For each test the following * parameters are expected: * * source: the GLSL source code for the tests * * generator: a JavaScript function taking four parameters * which evaluates the same function as the GLSL source, * returning its result as a newly allocated array. * * tolerance: (optional) a per-test tolerance. * * extra: (optional) * Extra GLSL code inserted at the top of each test's shader. * * tolerance: (optional) * Allow some tolerance in the comparisons. The tolerance is applied to * both vertex and fragment shaders. The default tolerance is 0, meaning * the values have to be identical. * * fragmentTolerance: (optional) * Specify a tolerance which only applies to fragment shaders. The * fragment-only tolerance will override the shared tolerance for * fragment shaders if both are specified. Fragment shaders usually * use mediump float precision so they sometimes require higher tolerance * than vertex shaders which use highp. */ runReferenceImageTest: runReferenceImageTest, none: false }; }());