655 lines
18 KiB
JavaScript
655 lines
18 KiB
JavaScript
// 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
|
|
})
|