// Fixed synthetic WebGL1 workloads only. No model, camera frame or face data. // A smaller FBO than the canvas also verifies native readback coordinates. module.exports = function nativeWebGLProbe(gl, options) { var strip = options && options.drawStrategy === 'quad_strip'; var status = { stage: 'not_run', code: 'NONE', clear: 'not_run', constantArrays: 'not_run', constantIndexed: 'not_run', constantQuad: 'not_run', shader: 'not_run', byteTexture: 'not_run', floatTexture: 'not_run', vertexBufferBytes: 0, indexBufferBytes: 0, changedBytes: 0, mismatchCount: 0, expectedChecksum: 0, actualChecksum: 0, pixel0: 0, pixel1: 0, pixel2: 0, pixel3: 0, glError: 0, framebufferStatus: 0, readReturn: 'not_run' }; var resources = [], attributes = []; function fail(code) { status.code = code; throw new Error('Native WebGL synthetic probe failed'); } function integer(value) { return typeof value === 'number' && isFinite(value) && value >= 0 && Math.floor(value) === value ? value : 0; } function own(kind, argument) { var value = kind === 'Shader' ? gl.createShader(argument) : gl['create' + kind](); if (!value) fail('API_FAILED'); resources.push({ kind: kind, value: value }); return value; } function inspect() { var error = gl.getError(), framebuffer = gl.checkFramebufferStatus(gl.FRAMEBUFFER); status.glError = integer(error); status.framebufferStatus = integer(framebuffer); if (error !== gl.NO_ERROR) fail('GL_ERROR'); if (framebuffer !== gl.FRAMEBUFFER_COMPLETE) fail('FRAMEBUFFER_INCOMPLETE'); } function texture(data, type) { var value = own('Texture'); gl.bindTexture(gl.TEXTURE_2D, value); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 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, 2, 2, 0, gl.RGBA, type, data); return value; } function compiled(type, source) { var shader = own('Shader', type); gl.shaderSource(shader, source); gl.compileShader(shader); var state = gl.getShaderParameter(shader, gl.COMPILE_STATUS); // Some native bindings expose GLboolean as 0/1 rather than JS boolean. if (state !== true && state !== 1) fail('SHADER_COMPILE_FAILED'); return shader; } function program(fragment) { var vertex = compiled(gl.VERTEX_SHADER, 'attribute vec2 position; varying vec2 uv; void main(){uv=(position+1.0)*0.5;gl_Position=vec4(position,0.0,1.0);}'); var shader = compiled(gl.FRAGMENT_SHADER, fragment), value = own('Program'); gl.attachShader(value, vertex); gl.attachShader(value, shader); gl.linkProgram(value); var state = gl.getProgramParameter(value, gl.LINK_STATUS); if (state !== true && state !== 1) fail('PROGRAM_LINK_FAILED'); gl.useProgram(value); var attribute = gl.getAttribLocation(value, 'position'); if (typeof attribute !== 'number' || attribute < 0) fail('API_FAILED'); if (attributes.indexOf(attribute) < 0) attributes.push(attribute); gl.enableVertexAttribArray(attribute); gl.vertexAttribPointer(attribute, 2, gl.FLOAT, false, 0, 0); return value; } function clearForDraw() { gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); } function drawQuad() { if (strip) gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); else { // Rebind immediately before every indexed draw: drawArrays and other // native host work must not leave this probe relying on stale binding. gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indices); gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0); } } function bufferSize(target) { if (typeof gl.getBufferParameter !== 'function' || typeof gl.BUFFER_SIZE !== 'number') return 0; // Preserve upload errors, but an unsupported optional metadata query must // not become a new numerical admission requirement. var pending = gl.getError(); if (pending !== gl.NO_ERROR) { status.glError = integer(pending); fail('GL_ERROR'); } try { var value = gl.getBufferParameter(target, gl.BUFFER_SIZE); return gl.getError() === gl.NO_ERROR ? integer(value) : 0; } catch (_) { try { gl.getError(); } catch (_) {} return 0; } } function read(expected) { var pixels = new Uint8Array(16); pixels.fill(165); var returned = gl.readPixels(0, 0, 2, 2, gl.RGBA, gl.UNSIGNED_BYTE, pixels); status.readReturn = returned === undefined ? 'undefined' : returned instanceof ArrayBuffer ? 'arraybuffer' : ArrayBuffer.isView(returned) ? 'typedarray' : returned instanceof Promise ? 'promise' : 'other'; if (returned instanceof Promise) returned.catch(function () {}); status.changedBytes = 0; status.mismatchCount = 0; status.expectedChecksum = 0; status.actualChecksum = 0; for (var index = 0; index < 16; index++) { if (pixels[index] !== 165) status.changedBytes++; if (Math.abs(pixels[index] - expected[index]) > 1) status.mismatchCount++; status.expectedChecksum += expected[index]; status.actualChecksum += pixels[index]; } // Big-endian RGBA uint32 values from fixed synthetic input only. for (var pixel = 0; pixel < 4; pixel++) { var offset = pixel * 4; status['pixel' + pixel] = ((pixels[offset] * 256 + pixels[offset + 1]) * 256 + pixels[offset + 2]) * 256 + pixels[offset + 3]; } // Capture GL state even when all returned values are wrong. inspect(); if (!status.changedBytes) fail('READ_UNCHANGED'); if (status.mismatchCount) fail('VALUE_MISMATCH'); } try { status.stage = 'clear'; var methods = ['createTexture', 'deleteTexture', 'bindTexture', 'texParameteri', 'texImage2D', 'createFramebuffer', 'deleteFramebuffer', 'bindFramebuffer', 'framebufferTexture2D', 'checkFramebufferStatus', 'getError', 'viewport', 'clearColor', 'clear', 'createBuffer', 'deleteBuffer', 'bindBuffer', 'bufferData', 'createShader', 'deleteShader', 'shaderSource', 'compileShader', 'getShaderParameter', 'createProgram', 'deleteProgram', 'attachShader', 'linkProgram', 'getProgramParameter', 'useProgram', 'getAttribLocation', 'enableVertexAttribArray', 'disableVertexAttribArray', 'vertexAttribPointer', 'drawElements', 'drawArrays', 'disable', 'colorMask', 'activeTexture', 'getUniformLocation', 'uniform1i', 'readPixels', 'getExtension']; for (var index = 0; index < methods.length; index++) if (!gl || typeof gl[methods[index]] !== 'function') fail('API_MISSING'); ['BLEND', 'CULL_FACE', 'DEPTH_TEST', 'STENCIL_TEST', 'SCISSOR_TEST', 'DITHER', 'POLYGON_OFFSET_FILL', 'SAMPLE_COVERAGE'].forEach(function (name) { if (typeof gl[name] === 'number') gl.disable(gl[name]); }); gl.colorMask(true, true, true, true); gl.activeTexture(gl.TEXTURE0); var target = texture(null, gl.UNSIGNED_BYTE), framebuffer = own('Framebuffer'); gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, target, 0); gl.bindTexture(gl.TEXTURE_2D, null); gl.viewport(0, 0, 2, 2); inspect(); gl.clearColor(17 / 255, 85 / 255, 153 / 255, 1); gl.clear(gl.COLOR_BUFFER_BIT); read([17, 85, 153, 255, 17, 85, 153, 255, 17, 85, 153, 255, 17, 85, 153, 255]); status.clear = 'passed'; status.stage = 'constant_arrays'; var vertices = own('Buffer'); gl.bindBuffer(gl.ARRAY_BUFFER, vertices); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW); status.vertexBufferBytes = bufferSize(gl.ARRAY_BUFFER); var indices = own('Buffer'); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indices); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([0, 1, 2, 2, 1, 3]), gl.STATIC_DRAW); status.indexBufferBytes = bufferSize(gl.ELEMENT_ARRAY_BUFFER); var constant = [31, 127, 223, 255, 31, 127, 223, 255, 31, 127, 223, 255, 31, 127, 223, 255]; program('precision highp float;void main(){gl_FragColor=vec4(31.0/255.0,127.0/255.0,223.0/255.0,1.0);}'); clearForDraw(); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); read(constant); status.constantArrays = 'passed'; status.stage = strip ? 'constant_quad' : 'constant_indexed'; clearForDraw(); drawQuad(); read(constant); if (strip) status.constantQuad = 'passed'; else status.constantIndexed = 'passed'; status.stage = 'shader'; program('precision highp float;void main(){float n=step(1.0,gl_FragCoord.x)+2.0*step(1.0,gl_FragCoord.y);gl_FragColor=vec4((17.0+34.0*n)/255.0,85.0/255.0,153.0/255.0,1.0);}'); clearForDraw(); drawQuad(); read([17, 85, 153, 255, 51, 85, 153, 255, 85, 85, 153, 255, 119, 85, 153, 255]); status.shader = 'passed'; var expected = [31, 63, 95, 255, 127, 159, 191, 255, 223, 47, 79, 255, 111, 143, 175, 255]; var sampling = 'precision highp float;varying vec2 uv;uniform sampler2D inputTexture;void main(){gl_FragColor=texture2D(inputTexture,uv);}'; status.stage = 'byte_texture'; var sampler = program(sampling); texture(new Uint8Array(expected), gl.UNSIGNED_BYTE); var location = gl.getUniformLocation(sampler, 'inputTexture'); if (location === null || location === undefined) fail('API_FAILED'); gl.uniform1i(location, 0); clearForDraw(); drawQuad(); read(expected); status.byteTexture = 'passed'; status.stage = 'float_texture'; if (!gl.getExtension('OES_texture_float')) fail('API_MISSING'); var floats = new Float32Array(expected.length); for (var index = 0; index < expected.length; index++) floats[index] = expected[index] / 255; texture(floats, gl.FLOAT); clearForDraw(); drawQuad(); read(expected); status.floatTexture = 'passed'; status.stage = 'passed'; } catch (_) { if (status.code === 'NONE') status.code = 'API_FAILED'; } finally { attributes.forEach(function (attribute) { try { gl.disableVertexAttribArray(attribute); } catch (_) {} }); try { gl.useProgram(null); gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.bindTexture(gl.TEXTURE_2D, null); gl.bindBuffer(gl.ARRAY_BUFFER, null); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); gl.clearColor(0, 0, 0, 0); } catch (_) {} for (var index = resources.length - 1; index >= 0; index--) { try { gl['delete' + resources[index].kind](resources[index].value); } catch (_) {} } } return status; };