pex-renderer
Version:
Physically Based Renderer for Pex
504 lines (481 loc) • 14.7 kB
JavaScript
const isBrowser = require('is-browser')
let canvas = null
let ctx2d = null
const W = 250
const H = 860
const M = 10
const LINE_H = 16
const FONT_H = 11
const FONT = FONT_H + 'px Droid Sans Mono, Andale Mono, monospace'
function ms(time) {
var f = Math.floor(time * 10) / 10
var s = '' + f
if (f % 1 === 0) s += '.0'
if (f < 10) s = ' ' + s
return s + ''
}
function pa3(f) {
if (f < 10) return ' ' + f
if (f < 100) return ' ' + f
return f
}
function prop(name) {
return function(o) {
return o[name]
}
}
function groupBy(fn, list) {
return list.reduce((acc, o) => {
var val = fn(o)
if (!acc[val]) {
acc[val] = []
}
acc[val].push(o)
return acc
}, {})
}
function createProfiler(ctx, renderer) {
const gl = ctx.gl
if (isBrowser && !canvas) {
canvas = document.createElement('canvas')
canvas.id = 'pex-renderer-profiler'
canvas.width = W * 2
canvas.height = H * 2
ctx.gl.canvas.parentElement.appendChild(canvas)
canvas.style.position = 'absolute'
canvas.style.width = W + 'px'
canvas.style.height = H + 'px'
canvas.style.top = M + 'px'
canvas.style.right = M + 'px'
canvas.style.zIndex = 1000
ctx2d = canvas.getContext('2d')
}
const profiler = {
canvas: canvas,
frame: 0,
flush: true,
measurements: {},
commands: [],
ctx2d: ctx2d,
bufferCount: 0,
totalBufferCount: 0,
textureCount: 0,
totalTextureCount: 0,
programCount: 0,
totalProgramCount: 0,
framebufferCount: 0,
totalFramebufferCount: 0,
bindTextureCount: 0,
useProgramCount: 0,
setUniformCount: 0,
trianglesCount: 0,
drawElementsCount: 0,
drawElementsInstancedCount: 0,
drawArraysCount: 0,
drawArraysInstancedCount: 0,
linesCount: 0,
time: function(label, gpu) {
if (this.flush) gl.finish()
if (this.flush) gl.flush()
let m = this.measurements[label]
if (!m) {
m = this.measurements[label] = {
begin: 0,
end: 0,
last: 0,
total: 0,
count: 0,
avg: 0,
max: 0
}
if (gpu) {
m.query = ctx.query()
}
}
this.measurements[label].start = window.performance
? window.performance.now()
: Date.now()
if (m.query && m.query.result) m.gpu = m.query.result / 1000000
if (m.query) ctx.beginQuery(m.query)
},
timeEnd: function(label) {
if (this.flush) gl.finish()
if (this.flush) gl.flush()
const m = this.measurements[label]
if (!m) {
return
}
m.end = window.performance ? window.performance.now() : Date.now()
m.last = m.end - m.start
m.total += m.last
m.max = Math.max(m.max, m.last)
m.count++
m.avg = (m.avg * 9 + m.last * 1) / 10
if (m.query) ctx.endQuery(m.query)
},
add: function(command, label) {
let callStack = null
try {
throw new Error('Call stack capture')
} catch (e) {
callStack = e.stack
}
this.commands.push({
command: command,
stack: callStack,
label: label
})
},
setFlush: function(state) {
this.flush = state
},
summary: function() {
var lines = []
const frameMeasurement = this.measurements['Frame']
if (frameMeasurement) {
lines.push(`FPS: ${(1000 / frameMeasurement.avg).toFixed(3)}`)
}
const frameRAFMeasurement = this.measurements['FrameRAF']
if (frameRAFMeasurement) {
lines.push(`FPS (RAF): ${(1000 / frameRAFMeasurement.avg).toFixed(3)}`)
}
lines.push('------')
lines = lines.concat(
Object.keys(this.measurements)
.sort((a, b) => {
return this.measurements[a].start - this.measurements[b].start
})
.map((label) => {
const m = this.measurements[label]
return `${label}: ${ms(m.avg)} ${
m.gpu ? ' / ' + (Math.floor(m.gpu * 10) / 10).toFixed(1) : ''
}`
})
)
var ctx = renderer._ctx
var textures = ctx.resources.filter((r) => r.class === 'texture')
var textureVRAM = 0
var texture2DByPixelFormat = groupBy(
prop('pixelFormat'),
textures.filter((tex) => tex.target === ctx.gl.TEXTURE_2D)
)
var textureCubeByPixelFormat = groupBy(
prop('pixelFormat'),
textures.filter((tex) => tex.target === ctx.gl.TEXTURE_CUBE_MAP)
)
textures.forEach((texture) => {
var bits = 8
var channels = 4
if (texture.pixelFormat === ctx.PixelFormat.RGBA32F) {
bits = 32
}
if (texture.pixelFormat === ctx.PixelFormat.RGBA16F) {
bits = 16
}
if (texture.pixelFormat === ctx.PixelFormat.Depth) {
bits = 24 // estimate
}
var bpp = (bits / 8) * channels
if (texture.target === ctx.gl.TEXTURE_2D) {
textureVRAM += texture.width * texture.height * bpp
} else if (texture.target === ctx.gl.TEXTURE_CUBE_MAP) {
textureVRAM += texture.width * texture.height * bpp * 6
}
})
lines.push('------')
lines.push(`Entities: ${pa3(renderer.entities.length)}`)
lines.push(
`Geometries: ${pa3(renderer.getComponents('Geometry').length)}`
)
lines.push(`Materials: ${pa3(renderer.getComponents('Material').length)}`)
lines.push(`Skins: ${pa3(renderer.getComponents('Skin').length)}`)
lines.push(
`Animations: ${pa3(renderer.getComponents('Animation').length)}`
)
lines.push(`Morphs: ${pa3(renderer.getComponents('Morph').length)}`)
lines.push(`Cameras: ${pa3(renderer.getComponents('Camera').length)}`)
lines.push(`Orbiters: ${pa3(renderer.getComponents('Orbiter').length)}`)
lines.push(
`Reflection Probes: ${pa3(
renderer.getComponents('ReflectionProbe').length
)}`
)
lines.push(`Skyboxes: ${pa3(renderer.getComponents('Skybox').length)}`)
lines.push(
`Ambient Lights: ${pa3(renderer.getComponents('AmbientLight').length)}`
)
lines.push(
`Point Lights: ${pa3(renderer.getComponents('PointLight').length)}`
)
lines.push(
`Directional Lights: ${pa3(
renderer.getComponents('DirectionalLight').length
)}`
)
lines.push(
`Spot Lights: ${pa3(renderer.getComponents('SpotLight').length)}`
)
lines.push(
`Area Lights: ${pa3(renderer.getComponents('AreaLight').length)}`
)
lines.push('------')
lines.push(
`Programs: ${pa3(
renderer._ctx.resources.filter((r) => r.class === 'program').length
)}`
)
lines.push(
`Passes: ${pa3(
renderer._ctx.resources.filter((r) => r.class === 'pass').length
)}`
)
lines.push(
`Pipelines: ${pa3(
renderer._ctx.resources.filter((r) => r.class === 'pipeline').length
)}`
)
lines.push(
`Textures 2D: ${pa3(
renderer._ctx.resources.filter(
(r) => r.class === 'texture' && r.target === ctx.gl.TEXTURE_2D
).length
)}`
)
Object.keys(texture2DByPixelFormat).forEach((format) => {
lines.push(
`${format.toUpperCase()}: ${pa3(
texture2DByPixelFormat[format].length
)}`
)
})
lines.push(
`Textures Cube: ${pa3(
renderer._ctx.resources.filter(
(r) => r.class === 'texture' && r.target === ctx.gl.TEXTURE_CUBE_MAP
).length
)}`
)
Object.keys(textureCubeByPixelFormat).forEach((format) => {
lines.push(
`${format.toUpperCase()}: ${pa3(
textureCubeByPixelFormat[format].length
)}`
)
})
lines.push(`Texture VRAM: ${(textureVRAM / (1024 * 1024)).toFixed(0)}MB`)
lines.push('------')
lines.push(
`Buffers: ${pa3(profiler.bufferCount)} / ${pa3(
profiler.totalBufferCount
)}`
)
lines.push(
`Textures: ${pa3(profiler.textureCount)} / ${pa3(
profiler.totalTextureCount
)}`
)
lines.push(
`Programs: ${pa3(profiler.programCount)} / ${pa3(
profiler.totalProgramCount
)}`
)
lines.push(
`FBOs: ${pa3(profiler.framebufferCount)} / ${pa3(
profiler.totalFramebufferCount
)}`
)
lines.push('------')
lines.push(`Lines: ${profiler.linesCount}`)
lines.push(`Triangles: ${profiler.trianglesCount}`)
lines.push(`Instanced Lines: ${profiler.instancedLinesCount}`)
lines.push(`Instanced Triangles: ${profiler.instancedTrianglesCount}`)
lines.push('------')
lines.push(`Bind Texture: ${pa3(profiler.bindTextureCount)}`)
lines.push(`Use Program: ${pa3(profiler.useProgramCount)}`)
lines.push(`Set Uniform: ${pa3(profiler.setUniformCount)}`)
lines.push(`Draw Elements : ${pa3(profiler.drawElementsCount)}`)
lines.push(
`Instanced Draw Elements: ${pa3(profiler.drawElementsInstancedCount)}`
)
lines.push(`Draw Arrays : ${pa3(profiler.drawArraysCount)}`)
lines.push(
`Instanced Draw Arrays: ${pa3(profiler.drawArraysInstancedCount)}`
)
lines.push('------')
lines = lines.concat(
this.commands.map((cmd) => {
// const cpu = cmd.command.stats.cpuTime / cmd.command.stats.count
// const gpu = cmd.command.stats.gpuTime / cmd.command.stats.count
// if (cmd.command.stats.count >= 30) {
// cmd.command.stats.gpuTime = 0
// cmd.command.stats.cpuTime = 0
// cmd.command.stats.count = 0
// }
// return `${cmd.label}: ${ms(cpu)} ${ms(gpu)}`
return `${cmd.label}: N/A`
})
)
return lines
},
setEnabled: function(state) {
if (isBrowser) {
canvas.style.display = state ? 'block' : 'none'
}
},
startFrame: function() {
this.timeEnd('FrameRAF')
this.time('FrameRAF')
this.time('Frame')
resetFrameStats()
},
endFrame: function() {
this.timeEnd('Frame')
draw()
}
}
function draw() {
profiler.frame++
if (!ctx2d) {
if (profiler.frame % 30 === 0) {
console.log('profiler', profiler.summary())
}
return
}
const lines = profiler.summary()
ctx2d.save()
ctx2d.scale(2, 2)
ctx2d.fillStyle = 'rgba(0, 0, 0, 0.5)'
ctx2d.clearRect(0, 0, canvas.width, canvas.height)
ctx2d.fillRect(0, 0, canvas.width, canvas.height)
ctx2d.font = FONT
ctx2d.fillStyle = '#FFF'
lines.forEach((line, index) => {
const w = ctx2d.measureText(line).width
ctx2d.fillText(line, W - M - w, M + FONT_H + LINE_H * index)
})
ctx2d.restore()
}
// function wrapRes (fn, counter) {
// const ctxFn = ctx[fn]
// ctx[fn] = function () {
// profiler[counter]++
// return ctxFn.apply(this, arguments)
// }
// }
// TODO:
// pipelines, passes, etc
// wrapRes('vertexBuffer', 'bufferCount')
// wrapRes('elementsBuffer', 'elementsCount')
// wrapRes('texture2D', 'text')
// wrapRes('cube', 'totalTextureCubeCount')
// wrapRes('framebuffer', 'totalFramebufferCount')
function wrapGLCall(fn, callback) {
const glFn = gl[fn]
gl[fn] = function() {
callback(arguments)
return glFn.apply(this, arguments)
}
}
// TODO
wrapGLCall('createBuffer', () => {
profiler.bufferCount++
profiler.totalBufferCount++
})
wrapGLCall('deleteBuffer', () => {
profiler.bufferCount--
})
wrapGLCall('createProgram', () => {
profiler.programCount++
profiler.totalProgramCount++
})
wrapGLCall('deleteProgram', () => {
profiler.programCount--
})
wrapGLCall('createTexture', () => {
profiler.textureCount++
profiler.totalTextureCount++
})
wrapGLCall('deleteTexture', () => {
profiler.textureCount--
})
wrapGLCall('createFramebuffer', () => {
profiler.framebufferCount++
profiler.totalFramebufferCount++
})
wrapGLCall('deleteFramebuffer', () => {
profiler.framebufferCount--
})
wrapGLCall('bindTexture', () => profiler.bindTextureCount++)
wrapGLCall('useProgram', () => profiler.useProgramCount++)
wrapGLCall('drawElements', (args) => {
const mode = args[0]
const count = args[1]
if (mode === gl.LINES) profiler.linesCount += count
if (mode === gl.TRIANGLES) profiler.trianglesCount += count
profiler.drawElementsCount++
})
wrapGLCall('drawArrays', (args) => {
const mode = args[0]
const count = args[2]
if (mode === gl.LINES) profiler.linesCount += count
if (mode === gl.TRIANGLES) profiler.trianglesCount += count
profiler.drawArraysCount++
})
for (let prop in gl) {
if (prop.indexOf('uniform') === 0) {
wrapGLCall(prop, () => profiler.setUniformCount++)
}
}
function wrapGLExtCall(ext, fn, callback) {
if (!ext) {
console.log('profiler', `Ext ${ext} it not available`)
return
}
const extFn = ext[fn]
ext[fn] = function() {
callback(arguments)
return extFn.apply(ext, arguments)
}
}
// TODO: what about webgl2?
wrapGLCall(
'drawElementsInstanced',
(args) => {
const mode = args[0]
const count = args[1]
const primcount = args[4]
if (mode === gl.LINES) profiler.instancedLinesCount += count * primcount // assuming divisor 1
if (mode === gl.TRIANGLES)
profiler.instancedTrianglesCount += count * primcount // assuming divisor 1
profiler.drawElementsInstancedCount++
}
)
wrapGLCall(
'drawArraysInstanced',
(args) => {
const mode = args[0]
const count = args[2]
const primcount = args[3]
if (mode === gl.LINES)
profiler['instancedLinesCount'] += count * primcount // assuming divisor 1
if (mode === gl.TRIANGLES)
profiler['instancedTrianglesCount'] += count * primcount // assuming divisor 1
profiler.drawArraysInstancedCount++
}
)
function resetFrameStats() {
profiler.bindTextureCount = 0
profiler.useProgramCount = 0
profiler.setUniformCount = 0
profiler.linesCount = 0
profiler.trianglesCount = 0
profiler.instancedLinesCount = 0
profiler.instancedTrianglesCount = 0
profiler.drawElementsCount = 0
profiler.drawElementsInstancedCount = 0
profiler.drawArraysCount = 0
profiler.drawArraysInstancedCount = 0
}
return profiler
}
module.exports = createProfiler