package.lib.util.check.js Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of regl Show documentation
Show all versions of regl Show documentation
regl is a fast functional WebGL framework.
The newest version!
// 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'
]
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
})