// Error checking and parameter validation. // // Statements for the form `check.someProcedure(...)` get removed by // a browserify transform for optimized/minified bundles. // /* globals atob */ var isTypedArray = require('./is-typed-array') var extend = require('./extend') var endl = '\n' // only used for extracting shader names. if atob not present, then errors // will be slightly crappier function decodeB64 (str) { if (typeof atob !== 'undefined') { return atob(str) } return 'base64:' + str } function raise (message) { var error = new Error('(regl) ' + message) console.error(error) throw error } function check (pred, message) { if (!pred) { raise(message) } } function encolon (message) { if (message) { return ': ' + message } return '' } function checkParameter (param, possibilities, message) { if (!(param in possibilities)) { raise('unknown parameter (' + param + ')' + encolon(message) + '. possible values: ' + Object.keys(possibilities).join()) } } function checkIsTypedArray (data, message) { if (!isTypedArray(data)) { raise( 'invalid parameter type' + encolon(message) + '. must be a typed array') } } function standardTypeEh (value, type) { switch (type) { case 'number': return typeof value === 'number' case 'object': return typeof value === 'object' case 'string': return typeof value === 'string' case 'boolean': return typeof value === 'boolean' case 'function': return typeof value === 'function' case 'undefined': return typeof value === 'undefined' case 'symbol': return typeof value === 'symbol' } } function checkTypeOf (value, type, message) { if (!standardTypeEh(value, type)) { raise( 'invalid parameter type' + encolon(message) + '. expected ' + type + ', got ' + (typeof value)) } } function checkNonNegativeInt (value, message) { if (!((value >= 0) && ((value | 0) === value))) { raise('invalid parameter type, (' + value + ')' + encolon(message) + '. must be a nonnegative integer') } } function checkOneOf (value, list, message) { if (list.indexOf(value) < 0) { raise('invalid value' + encolon(message) + '. must be one of: ' + list) } } var constructorKeys = [ 'gl', 'canvas', 'container', 'attributes', 'pixelRatio', 'extensions', 'optionalExtensions', 'profile', 'onDone', 'cachedCode' ] function checkConstructor (obj) { Object.keys(obj).forEach(function (key) { if (constructorKeys.indexOf(key) < 0) { raise('invalid regl constructor argument "' + key + '". must be one of ' + constructorKeys) } }) } function leftPad (str, n) { str = str + '' while (str.length < n) { str = ' ' + str } return str } function ShaderFile () { this.name = 'unknown' this.lines = [] this.index = {} this.hasErrors = false } function ShaderLine (number, line) { this.number = number this.line = line this.errors = [] } function ShaderError (fileNumber, lineNumber, message) { this.file = fileNumber this.line = lineNumber this.message = message } function guessCommand () { var error = new Error() var stack = (error.stack || error).toString() var pat = /compileProcedure.*\n\s*at.*\((.*)\)/.exec(stack) if (pat) { return pat[1] } var pat2 = /compileProcedure.*\n\s*at\s+(.*)(\n|$)/.exec(stack) if (pat2) { return pat2[1] } return 'unknown' } function guessCallSite () { var error = new Error() var stack = (error.stack || error).toString() var pat = /at REGLCommand.*\n\s+at.*\((.*)\)/.exec(stack) if (pat) { return pat[1] } var pat2 = /at REGLCommand.*\n\s+at\s+(.*)\n/.exec(stack) if (pat2) { return pat2[1] } return 'unknown' } function parseSource (source, command) { var lines = source.split('\n') var lineNumber = 1 var fileNumber = 0 var files = { unknown: new ShaderFile(), 0: new ShaderFile() } files.unknown.name = files[0].name = command || guessCommand() files.unknown.lines.push(new ShaderLine(0, '')) for (var i = 0; i < lines.length; ++i) { var line = lines[i] var parts = /^\s*#\s*(\w+)\s+(.+)\s*$/.exec(line) if (parts) { switch (parts[1]) { case 'line': var lineNumberInfo = /(\d+)(\s+\d+)?/.exec(parts[2]) if (lineNumberInfo) { lineNumber = lineNumberInfo[1] | 0 if (lineNumberInfo[2]) { fileNumber = lineNumberInfo[2] | 0 if (!(fileNumber in files)) { files[fileNumber] = new ShaderFile() } } } break case 'define': var nameInfo = /SHADER_NAME(_B64)?\s+(.*)$/.exec(parts[2]) if (nameInfo) { files[fileNumber].name = (nameInfo[1] ? decodeB64(nameInfo[2]) : nameInfo[2]) } break } } files[fileNumber].lines.push(new ShaderLine(lineNumber++, line)) } Object.keys(files).forEach(function (fileNumber) { var file = files[fileNumber] file.lines.forEach(function (line) { file.index[line.number] = line }) }) return files } function parseErrorLog (errLog) { var result = [] errLog.split('\n').forEach(function (errMsg) { if (errMsg.length < 5) { return } var parts = /^ERROR:\s+(\d+):(\d+):\s*(.*)$/.exec(errMsg) if (parts) { result.push(new ShaderError( parts[1] | 0, parts[2] | 0, parts[3].trim())) } else if (errMsg.length > 0) { result.push(new ShaderError('unknown', 0, errMsg)) } }) return result } function annotateFiles (files, errors) { errors.forEach(function (error) { var file = files[error.file] if (file) { var line = file.index[error.line] if (line) { line.errors.push(error) file.hasErrors = true return } } files.unknown.hasErrors = true files.unknown.lines[0].errors.push(error) }) } function checkShaderError (gl, shader, source, type, command) { if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { var errLog = gl.getShaderInfoLog(shader) var typeName = type === gl.FRAGMENT_SHADER ? 'fragment' : 'vertex' checkCommandType(source, 'string', typeName + ' shader source must be a string', command) var files = parseSource(source, command) var errors = parseErrorLog(errLog) annotateFiles(files, errors) Object.keys(files).forEach(function (fileNumber) { var file = files[fileNumber] if (!file.hasErrors) { return } var strings = [''] var styles = [''] function push (str, style) { strings.push(str) styles.push(style || '') } push('file number ' + fileNumber + ': ' + file.name + '\n', 'color:red;text-decoration:underline;font-weight:bold') file.lines.forEach(function (line) { if (line.errors.length > 0) { push(leftPad(line.number, 4) + '| ', 'background-color:yellow; font-weight:bold') push(line.line + endl, 'color:red; background-color:yellow; font-weight:bold') // try to guess token var offset = 0 line.errors.forEach(function (error) { var message = error.message var token = /^\s*'(.*)'\s*:\s*(.*)$/.exec(message) if (token) { var tokenPat = token[1] message = token[2] switch (tokenPat) { case 'assign': tokenPat = '=' break } offset = Math.max(line.line.indexOf(tokenPat, offset), 0) } else { offset = 0 } push(leftPad('| ', 6)) push(leftPad('^^^', offset + 3) + endl, 'font-weight:bold') push(leftPad('| ', 6)) push(message + endl, 'font-weight:bold') }) push(leftPad('| ', 6) + endl) } else { push(leftPad(line.number, 4) + '| ') push(line.line + endl, 'color:red') } }) if (typeof document !== 'undefined' && !window.chrome) { styles[0] = strings.join('%c') console.log.apply(console, styles) } else { console.log(strings.join('')) } }) check.raise('Error compiling ' + typeName + ' shader, ' + files[0].name) } } function checkLinkError (gl, program, fragShader, vertShader, command) { if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { var errLog = gl.getProgramInfoLog(program) var fragParse = parseSource(fragShader, command) var vertParse = parseSource(vertShader, command) var header = 'Error linking program with vertex shader, "' + vertParse[0].name + '", and fragment shader "' + fragParse[0].name + '"' if (typeof document !== 'undefined') { console.log('%c' + header + endl + '%c' + errLog, 'color:red;text-decoration:underline;font-weight:bold', 'color:red') } else { console.log(header + endl + errLog) } check.raise(header) } } function saveCommandRef (object) { object._commandRef = guessCommand() } function saveDrawCommandInfo (opts, uniforms, attributes, stringStore) { saveCommandRef(opts) function id (str) { if (str) { return stringStore.id(str) } return 0 } opts._fragId = id(opts.static.frag) opts._vertId = id(opts.static.vert) function addProps (dict, set) { Object.keys(set).forEach(function (u) { dict[stringStore.id(u)] = true }) } var uniformSet = opts._uniformSet = {} addProps(uniformSet, uniforms.static) addProps(uniformSet, uniforms.dynamic) var attributeSet = opts._attributeSet = {} addProps(attributeSet, attributes.static) addProps(attributeSet, attributes.dynamic) opts._hasCount = ( 'count' in opts.static || 'count' in opts.dynamic || 'elements' in opts.static || 'elements' in opts.dynamic) } function commandRaise (message, command) { var callSite = guessCallSite() raise(message + ' in command ' + (command || guessCommand()) + (callSite === 'unknown' ? '' : ' called from ' + callSite)) } function checkCommand (pred, message, command) { if (!pred) { commandRaise(message, command || guessCommand()) } } function checkParameterCommand (param, possibilities, message, command) { if (!(param in possibilities)) { commandRaise( 'unknown parameter (' + param + ')' + encolon(message) + '. possible values: ' + Object.keys(possibilities).join(), command || guessCommand()) } } function checkCommandType (value, type, message, command) { if (!standardTypeEh(value, type)) { commandRaise( 'invalid parameter type' + encolon(message) + '. expected ' + type + ', got ' + (typeof value), command || guessCommand()) } } function checkOptional (block) { block() } function checkFramebufferFormat (attachment, texFormats, rbFormats) { if (attachment.texture) { checkOneOf( attachment.texture._texture.internalformat, texFormats, 'unsupported texture format for attachment') } else { checkOneOf( attachment.renderbuffer._renderbuffer.format, rbFormats, 'unsupported renderbuffer format for attachment') } } var GL_CLAMP_TO_EDGE = 0x812F var GL_NEAREST = 0x2600 var GL_NEAREST_MIPMAP_NEAREST = 0x2700 var GL_LINEAR_MIPMAP_NEAREST = 0x2701 var GL_NEAREST_MIPMAP_LINEAR = 0x2702 var GL_LINEAR_MIPMAP_LINEAR = 0x2703 var GL_BYTE = 5120 var GL_UNSIGNED_BYTE = 5121 var GL_SHORT = 5122 var GL_UNSIGNED_SHORT = 5123 var GL_INT = 5124 var GL_UNSIGNED_INT = 5125 var GL_FLOAT = 5126 var GL_UNSIGNED_SHORT_4_4_4_4 = 0x8033 var GL_UNSIGNED_SHORT_5_5_5_1 = 0x8034 var GL_UNSIGNED_SHORT_5_6_5 = 0x8363 var GL_UNSIGNED_INT_24_8_WEBGL = 0x84FA var GL_HALF_FLOAT_OES = 0x8D61 var TYPE_SIZE = {} TYPE_SIZE[GL_BYTE] = TYPE_SIZE[GL_UNSIGNED_BYTE] = 1 TYPE_SIZE[GL_SHORT] = TYPE_SIZE[GL_UNSIGNED_SHORT] = TYPE_SIZE[GL_HALF_FLOAT_OES] = TYPE_SIZE[GL_UNSIGNED_SHORT_5_6_5] = TYPE_SIZE[GL_UNSIGNED_SHORT_4_4_4_4] = TYPE_SIZE[GL_UNSIGNED_SHORT_5_5_5_1] = 2 TYPE_SIZE[GL_INT] = TYPE_SIZE[GL_UNSIGNED_INT] = TYPE_SIZE[GL_FLOAT] = TYPE_SIZE[GL_UNSIGNED_INT_24_8_WEBGL] = 4 function pixelSize (type, channels) { if (type === GL_UNSIGNED_SHORT_5_5_5_1 || type === GL_UNSIGNED_SHORT_4_4_4_4 || type === GL_UNSIGNED_SHORT_5_6_5) { return 2 } else if (type === GL_UNSIGNED_INT_24_8_WEBGL) { return 4 } else { return TYPE_SIZE[type] * channels } } function isPow2 (v) { return !(v & (v - 1)) && (!!v) } function checkTexture2D (info, mipData, limits) { var i var w = mipData.width var h = mipData.height var c = mipData.channels // Check texture shape check(w > 0 && w <= limits.maxTextureSize && h > 0 && h <= limits.maxTextureSize, 'invalid texture shape') // check wrap mode if (info.wrapS !== GL_CLAMP_TO_EDGE || info.wrapT !== GL_CLAMP_TO_EDGE) { check(isPow2(w) && isPow2(h), 'incompatible wrap mode for texture, both width and height must be power of 2') } if (mipData.mipmask === 1) { if (w !== 1 && h !== 1) { check( info.minFilter !== GL_NEAREST_MIPMAP_NEAREST && info.minFilter !== GL_NEAREST_MIPMAP_LINEAR && info.minFilter !== GL_LINEAR_MIPMAP_NEAREST && info.minFilter !== GL_LINEAR_MIPMAP_LINEAR, 'min filter requires mipmap') } } else { // texture must be power of 2 check(isPow2(w) && isPow2(h), 'texture must be a square power of 2 to support mipmapping') check(mipData.mipmask === (w << 1) - 1, 'missing or incomplete mipmap data') } if (mipData.type === GL_FLOAT) { if (limits.extensions.indexOf('oes_texture_float_linear') < 0) { check(info.minFilter === GL_NEAREST && info.magFilter === GL_NEAREST, 'filter not supported, must enable oes_texture_float_linear') } check(!info.genMipmaps, 'mipmap generation not supported with float textures') } // check image complete var mipimages = mipData.images for (i = 0; i < 16; ++i) { if (mipimages[i]) { var mw = w >> i var mh = h >> i check(mipData.mipmask & (1 << i), 'missing mipmap data') var img = mipimages[i] check( img.width === mw && img.height === mh, 'invalid shape for mip images') check( img.format === mipData.format && img.internalformat === mipData.internalformat && img.type === mipData.type, 'incompatible type for mip image') if (img.compressed) { // TODO: check size for compressed images } else if (img.data) { // check(img.data.byteLength === mw * mh * // Math.max(pixelSize(img.type, c), img.unpackAlignment), var rowSize = Math.ceil(pixelSize(img.type, c) * mw / img.unpackAlignment) * img.unpackAlignment check(img.data.byteLength === rowSize * mh, 'invalid data for image, buffer size is inconsistent with image format') } else if (img.element) { // TODO: check element can be loaded } else if (img.copy) { // TODO: check compatible format and type } } else if (!info.genMipmaps) { check((mipData.mipmask & (1 << i)) === 0, 'extra mipmap data') } } if (mipData.compressed) { check(!info.genMipmaps, 'mipmap generation for compressed images not supported') } } function checkTextureCube (texture, info, faces, limits) { var w = texture.width var h = texture.height var c = texture.channels // Check texture shape check( w > 0 && w <= limits.maxTextureSize && h > 0 && h <= limits.maxTextureSize, 'invalid texture shape') check( w === h, 'cube map must be square') check( info.wrapS === GL_CLAMP_TO_EDGE && info.wrapT === GL_CLAMP_TO_EDGE, 'wrap mode not supported by cube map') for (var i = 0; i < faces.length; ++i) { var face = faces[i] check( face.width === w && face.height === h, 'inconsistent cube map face shape') if (info.genMipmaps) { check(!face.compressed, 'can not generate mipmap for compressed textures') check(face.mipmask === 1, 'can not specify mipmaps and generate mipmaps') } else { // TODO: check mip and filter mode } var mipmaps = face.images for (var j = 0; j < 16; ++j) { var img = mipmaps[j] if (img) { var mw = w >> j var mh = h >> j check(face.mipmask & (1 << j), 'missing mipmap data') check( img.width === mw && img.height === mh, 'invalid shape for mip images') check( img.format === texture.format && img.internalformat === texture.internalformat && img.type === texture.type, 'incompatible type for mip image') if (img.compressed) { // TODO: check size for compressed images } else if (img.data) { check(img.data.byteLength === mw * mh * Math.max(pixelSize(img.type, c), img.unpackAlignment), 'invalid data for image, buffer size is inconsistent with image format') } else if (img.element) { // TODO: check element can be loaded } else if (img.copy) { // TODO: check compatible format and type } } } } } module.exports = extend(check, { optional: checkOptional, raise: raise, commandRaise: commandRaise, command: checkCommand, parameter: checkParameter, commandParameter: checkParameterCommand, constructor: checkConstructor, type: checkTypeOf, commandType: checkCommandType, isTypedArray: checkIsTypedArray, nni: checkNonNegativeInt, oneOf: checkOneOf, shaderError: checkShaderError, linkError: checkLinkError, callSite: guessCallSite, saveCommandRef: saveCommandRef, saveDrawInfo: saveDrawCommandInfo, framebufferFormat: checkFramebufferFormat, guessCommand: guessCommand, texture2D: checkTexture2D, textureCube: checkTextureCube })