diff --git a/lib/roles/app.js b/lib/roles/app.js index 4c3410e2..1bd33a4a 100644 --- a/lib/roles/app.js +++ b/lib/roles/app.js @@ -20,6 +20,8 @@ var dbapi = require('../db/api') var auth = require('../middleware/auth') var webpack = require('../middleware/webpack') +var cors = require('cors') + module.exports = function(options) { var log = logger.createLogger('app') , app = express() @@ -36,6 +38,8 @@ module.exports = function(options) { io.set('log level', 1) io.set('browser client', false) + app.use(cors()) + app.use('/static/lib', express.static(pathutil.resource('lib'))) app.use('/static', express.static(pathutil.resource('app'))) diff --git a/res/app/components/stf/screen/fast-image-render/index.js b/res/app/components/stf/screen/fast-image-render/index.js index 0c78f0dc..62d2255b 100644 --- a/res/app/components/stf/screen/fast-image-render/index.js +++ b/res/app/components/stf/screen/fast-image-render/index.js @@ -3,35 +3,7 @@ // See http://jsperf.com/canvas-drawimage-vs-webgl-drawarrays -function FastImageLoader(url) { - var that = this - this.loader = new Image() - - if (url) { - this.load(url) - } - - this.loader.onload = function () { - if (typeof(that.onLoad) === 'function') { - that.onLoad(this) - } - } - - this.loader.onerror = function () { - - if (typeof(that.onError) === 'function') { - that.onError(this) - } - } -} - -/** - * Loads an URL - * @param {string} url - */ -FastImageLoader.prototype.load = function (url) { - this.loader.src = url -} +// ------------------------------------------------------------------------------------------------- function FastPixiRender(canvasElement, options) { @@ -79,6 +51,7 @@ FastPixiRender.prototype.draw = function (image) { this.renderer.render(this.stage) } +// ------------------------------------------------------------------------------------------------- function CanvasRender(canvasElement, options) { var checkForCanvasElement = function checkForCanvasElement() { @@ -107,6 +80,215 @@ CanvasRender.prototype.clear = function () { } +// ------------------------------------------------------------------------------------------------- + +// Based on http://www-cs-students.stanford.edu/~eparker/files/crunch/renderer.js + +/** + * Constructs a renderer object. + * @param {WebGLRenderingContext} gl The GL context. + * @constructor + */ +var Renderer = function (gl) { + /** + * The GL context. + * @type {WebGLRenderingContext} + * @private + */ + this.gl_ = gl; + + /** + * The WebGLProgram. + * @type {WebGLProgram} + * @private + */ + this.program_ = gl.createProgram(); + + /** + * @type {WebGLShader} + * @private + */ + this.vertexShader_ = this.compileShader_( + Renderer.vertexShaderSource_, gl.VERTEX_SHADER); + + /** + * @type {WebGLShader} + * @private + */ + this.fragmentShader_ = this.compileShader_( + Renderer.fragmentShaderSource_, gl.FRAGMENT_SHADER); + + /** + * Cached uniform locations. + * @type {Object.} + * @private + */ + this.uniformLocations_ = {}; + + /** + * Cached attribute locations. + * @type {Object.} + * @private + */ + this.attribLocations_ = {}; + + /** + * A vertex buffer containing a single quad with xy coordinates from [-1,-1] + * to [1,1] and uv coordinates from [0,0] to [1,1]. + * @private + */ + this.quadVertexBuffer_ = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, this.quadVertexBuffer_); + var vertices = new Float32Array( + [-1.0, -1.0, 0.0, 1.0, + +1.0, -1.0, 1.0, 1.0, + -1.0, +1.0, 0.0, 0.0, + 1.0, +1.0, 1.0, 0.0]); + gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); + + + // Init shaders + gl.attachShader(this.program_, this.vertexShader_); + gl.attachShader(this.program_, this.fragmentShader_); + gl.bindAttribLocation(this.program_, 0, 'vert'); + gl.linkProgram(this.program_); + gl.useProgram(this.program_); + gl.enableVertexAttribArray(0); + + gl.enable(gl.DEPTH_TEST); + gl.disable(gl.CULL_FACE); + + var count = gl.getProgramParameter(this.program_, gl.ACTIVE_UNIFORMS); + for (var i = 0; i < /** @type {number} */(count); i++) { + var infoU = gl.getActiveUniform(this.program_, i); + this.uniformLocations_[infoU.name] = gl.getUniformLocation(this.program_, infoU.name); + } + + count = gl.getProgramParameter(this.program_, gl.ACTIVE_ATTRIBUTES); + for (var j = 0; j < /** @type {number} */(count); j++) { + var infoA = gl.getActiveAttrib(this.program_, j); + this.attribLocations_[infoA.name] = gl.getAttribLocation(this.program_, infoA.name); + } +}; + + +Renderer.prototype.finishInit = function () { + this.draw(); +}; + + +Renderer.prototype.createDxtTexture = function (dxtData, width, height, format) { + var gl = this.gl_; + var tex = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, tex); + gl.compressedTexImage2D( + gl.TEXTURE_2D, + 0, + format, + width, + height, + 0, + dxtData); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + 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.generateMipmap(gl.TEXTURE_2D) + gl.bindTexture(gl.TEXTURE_2D, null); + return tex; +}; + + +Renderer.prototype.createRgb565Texture = function (rgb565Data, width, height) { + var gl = this.gl_; + var tex = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, tex); + gl.texImage2D( + gl.TEXTURE_2D, + 0, + gl.RGB, + width, + height, + 0, + gl.RGB, + gl.UNSIGNED_SHORT_5_6_5, + rgb565Data); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + 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.generateMipmap(gl.TEXTURE_2D) + gl.bindTexture(gl.TEXTURE_2D, null); + return tex; +}; + + +Renderer.prototype.drawTexture = function (texture, width, height) { + var gl = this.gl_; + // draw scene + gl.clearColor(0, 0, 0, 1); + gl.clearDepth(1.0); + gl.viewport(0, 0, width, height); + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT); + + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.uniform1i(this.uniformLocations_.texSampler, 0); + + gl.enableVertexAttribArray(this.attribLocations_.vert); + gl.bindBuffer(gl.ARRAY_BUFFER, this.quadVertexBuffer_); + gl.vertexAttribPointer(this.attribLocations_.vert, 4, gl.FLOAT, + false, 0, 0); + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); +}; + + +/** + * Compiles a GLSL shader and returns a WebGLShader. + * @param {string} shaderSource The shader source code string. + * @param {number} type Either VERTEX_SHADER or FRAGMENT_SHADER. + * @return {WebGLShader} The new WebGLShader. + * @private + */ +Renderer.prototype.compileShader_ = function (shaderSource, type) { + var gl = this.gl_; + var shader = gl.createShader(type); + gl.shaderSource(shader, shaderSource); + gl.compileShader(shader); + return shader; +}; + + +/** + * @type {string} + * @private + */ +Renderer.vertexShaderSource_ = [ + 'attribute vec4 vert;', + 'varying vec2 v_texCoord;', + 'void main() {', + ' gl_Position = vec4(vert.xy, 0.0, 1.0);', + ' v_texCoord = vert.zw;', + '}' +].join('\n'); + + +/** + * @type {string} + * @private + */ +Renderer.fragmentShaderSource_ = [ + 'precision highp float;', + 'uniform sampler2D texSampler;', + 'varying vec2 v_texCoord;', + 'void main() {', + ' gl_FragColor = texture2D(texSampler, v_texCoord);', + '}' +].join('\n'); + +// ------------------------------------------------------------------------------------------------- + + function WebGLRender(canvasElement, options) { var checkForCanvasElement = function checkForCanvasElement() { if (!canvasElement) { @@ -139,6 +321,13 @@ function WebGLRender(canvasElement, options) { } } + if (!this.ctx.getExtension('WEBKIT_WEBGL_compressed_texture_s3tc')) { + this.dxtSupported = false + } + + this.renderer = new Renderer(this.ctx) + + this.contextLost = false // gl.useProgram(this.shaderManager.defaultShader.program) @@ -146,7 +335,7 @@ function WebGLRender(canvasElement, options) { //this.ctx.disable(this.ctx.DEPTH_TEST) //this.ctx.disable(this.ctx.CULL_FACE) - this.setup() + //this.setup() } WebGLRender.prototype.setup = function () { @@ -207,6 +396,13 @@ WebGLRender.prototype.setup = function () { } WebGLRender.prototype.draw = function (image) { +// this.renderer.drawTexture(image, image.width, image.height) + this.renderer.drawTexture(image, 643, 1149) + +} + + +WebGLRender.prototype.drawOld = function (image) { var tex = this.ctx.createTexture() this.ctx.bindTexture(this.ctx.TEXTURE_2D, tex) this.ctx.texParameteri(this.ctx.TEXTURE_2D, this.ctx.TEXTURE_MIN_FILTER, this.ctx.NEAREST) @@ -233,10 +429,29 @@ WebGLRender.prototype.clear = function () { } + +// ------------------------------------------------------------------------------------------------- + + function FastImageRender(canvasElement, options) { + var that = this this.options = options || {} this.canvasElement = canvasElement + if (true) { + this.loader = new Image() + this.loader.onload = function () { + if (typeof(that.onLoad) === 'function') { + that.onLoad(this) + } + } + this.loader.onerror = function () { + if (typeof(that.onError) === 'function') { + that.onError(this) + } + } + } + if (this.options.render === 'webgl') { this.render = new WebGLRender(canvasElement, options) } else if (this.options.render.indexOf('pixi') !== -1) { @@ -244,6 +459,36 @@ function FastImageRender(canvasElement, options) { } else { this.render = new CanvasRender(canvasElement, options) } + + +} + +FastImageRender.prototype.load = function (url, type) { + var that = this + + if (this.options.textureLoader) { + if (!this.textureLoader) { + this.textureLoader = new TextureUtil.TextureLoader(this.render.ctx) + } + var texture = null + if (type) { + texture = this.render.ctx.createTexture(); + this.textureLoader.loadEx(url, texture, true, function (tex) { + if (typeof(that.onLoad) === 'function') { + that.onLoad(texture) + } + }, type) + } else { + this.textureLoader.load(url, function (texture) { + if (typeof(that.onLoad) === 'function') { + that.onLoad(texture) + } + }) + } + + } else { + this.loader.src = url + } } FastImageRender.prototype.draw = function (image) { @@ -335,10 +580,12 @@ Object.defineProperty(FastImageRender.prototype, 'canvasStyleHeight', { }) +// ------------------------------------------------------------------------------------------------- + + // Check for Non CommonJS world if (typeof module !== 'undefined') { module.exports = { - FastImageRender: FastImageRender, - FastImageLoader: FastImageLoader + FastImageRender: FastImageRender } } diff --git a/res/app/components/stf/screen/fast-image-render/test/images/screen.dds b/res/app/components/stf/screen/fast-image-render/test/images/screen.dds new file mode 100644 index 00000000..f85ef616 Binary files /dev/null and b/res/app/components/stf/screen/fast-image-render/test/images/screen.dds differ diff --git a/res/app/components/stf/screen/fast-image-render/test/images/screen.dds.gz b/res/app/components/stf/screen/fast-image-render/test/images/screen.dds.gz new file mode 100644 index 00000000..69f878e4 Binary files /dev/null and b/res/app/components/stf/screen/fast-image-render/test/images/screen.dds.gz differ diff --git a/res/app/components/stf/screen/fast-image-render/test/screen.jpg b/res/app/components/stf/screen/fast-image-render/test/images/screen.jpg similarity index 100% rename from res/app/components/stf/screen/fast-image-render/test/screen.jpg rename to res/app/components/stf/screen/fast-image-render/test/images/screen.jpg diff --git a/res/app/components/stf/screen/fast-image-render/test/screen.png b/res/app/components/stf/screen/fast-image-render/test/images/screen.png similarity index 100% rename from res/app/components/stf/screen/fast-image-render/test/screen.png rename to res/app/components/stf/screen/fast-image-render/test/images/screen.png diff --git a/res/app/components/stf/screen/fast-image-render/test/images/screen.webp b/res/app/components/stf/screen/fast-image-render/test/images/screen.webp new file mode 100644 index 00000000..ed5ec839 Binary files /dev/null and b/res/app/components/stf/screen/fast-image-render/test/images/screen.webp differ diff --git a/res/app/components/stf/screen/fast-image-render/test/images/texture24.crn b/res/app/components/stf/screen/fast-image-render/test/images/texture24.crn new file mode 100644 index 00000000..5fe97a2d Binary files /dev/null and b/res/app/components/stf/screen/fast-image-render/test/images/texture24.crn differ diff --git a/res/app/components/stf/screen/fast-image-render/test/index.html b/res/app/components/stf/screen/fast-image-render/test/index.html index eeb78847..3479923a 100644 --- a/res/app/components/stf/screen/fast-image-render/test/index.html +++ b/res/app/components/stf/screen/fast-image-render/test/index.html @@ -21,7 +21,8 @@ + - \ No newline at end of file + diff --git a/res/app/components/stf/screen/fast-image-render/test/performance_test.js b/res/app/components/stf/screen/fast-image-render/test/performance_test.js index 77a1a4d4..f01f100b 100644 --- a/res/app/components/stf/screen/fast-image-render/test/performance_test.js +++ b/res/app/components/stf/screen/fast-image-render/test/performance_test.js @@ -3,13 +3,11 @@ var frameNumberElement = document.querySelector('#frame-number') var totalTimeElement = document.querySelector('#total-time') var frame = { - total: 5000, + total: 50, current: 0 } -var imageLoader = new FastImageLoader() - -var imageRender = new FastImageRender(canvasElement, {render: 'canvas'}) +var imageRender = new FastImageRender(canvasElement, {render: 'canvas', textureLoader: false}) function loadNext() { console.time('load') @@ -17,14 +15,15 @@ function loadNext() { // var height = 300 // loader.src = 'http://placehold.it/' + width + 'x' + height + '?' + Date.now() // loader.src = 'http://lorempixel.com/' + width + '/' + height + '/abstract/Frame-' + frames.current + '/?' + Date.now() - imageLoader.load('screen.jpg?' + Date.now()) + imageRender.load('images/screen.webp?' + Date.now()) +// imageRender.load('images/screen.jpg') } var startTime = new Date().getTime() loadNext() -imageLoader.onLoad = function (image) { +imageRender.onLoad = function (image) { console.timeEnd('load') console.time('draw') imageRender.draw(image) diff --git a/res/app/components/stf/screen/fast-image-render/test/results.md b/res/app/components/stf/screen/fast-image-render/test/results.md new file mode 100644 index 00000000..5d410450 --- /dev/null +++ b/res/app/components/stf/screen/fast-image-render/test/results.md @@ -0,0 +1,103 @@ +# Benchmark results (internal data) + +----- + +### Versions +- Canary 35 +- Safari 7 +- Firefox 27 + + +### Time to load and draw 5000 frames + +Hardware | Browser | Render | Time | ms +-------- | ------- | ------ | ---- | -- +iMac | Canary | pixi-canvas | 54s | +iMac | Canary | pixi-webgl | 58s | +iMac | Canary | canvas | 54s | 5.8ms +iMac | Firefox | pixi-canvas | 75s | 8.3ms +iMac | Firefox | pixi-webgl | 82s | 11.3ms +iMac | Firefox | canvas | 76s | 8.1ms +MacBook | Canary | pixi-canvas | 68s +MacBook | Canary | pixi-webgl | 83s +MacBook | Canary | canvas | 76s + + +### Time to just draw 5000 frames + +Hardware | Browser | Format | Render | Time | ms +-------- | ------- | ------ | ------ | ---- | -- +iMac | Canary | DDS DXT1 | webgl | 55s | + + +### Time to render 1 frame +Hardware | Browser | Format | Size | Function | ms +-------- | ------- | ------ | ---- | -------- | -- +iMac | Canary | DDS DXT1 | 2.1MB | load | 20-100ms +iMac | Canary | DDS DXT1 | 2.1MB | draw | **0.04ms** +iMac | Canary | DDS GZIP | 271KB | load | +iMac | Canary | DDS GZIP | 271KB | draw | +iMac | Canary | WEBP | 70KB | load | 9ms +iMac | Canary | WEBP | 70KB | draw | 24ms +iMac | Canary | DDS CRN | 70KB | load | 30ms +iMac | Canary | DDS CRN | 70KB | draw | **0.04ms** +iMac | Canary | JPEG | 94KB | load | 25ms +iMac | Canary | JPEG | 94KB | draw | 6ms +iMac | Canary | PNG | 590KB | load | 6ms +iMac | Canary | PNG | 590KB | draw | 30ms + + +# About ST3C DXT1 DDS CRN + +### ST3C (S3 Texture Compression) +Group of related lossy texture compression algorithms, supported by most GPUs in Mac/PCs + +### DXT1 (Block Compression 1) +Smallest ST3C algorithm, 1-bit Alpha, compression ratio 6:1 + +### DDS (DirectDraw Surface) +Container file format for storing ST3C textures + +### CRN (DXTn Real-time Transcoding) +Container file format for transcoding crunched ST3C textures + + + +# DDS vs JPEG + +- Drawing a DXT1 texture is over 100 times faster than JPEG +- The DXT1 texture is transferred directly to the GPU, unlike JPEG +- The DXT1 uses 6 times less GPU memory +- File size is big, but when gzipped it's smaller than PNG + +# CRN vs JPEG +- It transcodes to DXT1 so has all DXT1 benefits +- File size is very small, even smaller than JPEG (!) +- Requires to transcode on client side, so there is a CPU penalty +- However transcoding CRN->DXT1 is faster than decoding and tranfering JPEG->GPU +- JPEG->GPU texture uploading blocks the main thread and is slow +- Transcoding a CRN can be done async, and even offloaded to a WebWorker +- Currently the Crunch library is compiled to JS with Emscripten, so enabling asm.js would make the transcoding even faster +- Compressing CRN vs libjpeg-turbo benchmarks still need to be done + +# Links + + +ECT1 texture format works on all Android devices +http://developer.android.com/tools/help/etc1tool.html + +WebGL also +http://blog.tojicode.com/2011/12/compressed-textures-in-webgl.html + +Crunch +https://code.google.com/p/crunch/ + +Fast compression +http://www.fastcompression.com/ + +DXT1 crunch WebGL +http://www-cs-students.stanford.edu/~eparker/files/crunch/decode_test.html + +WebGL Texture Utils +https://github.com/gunta/webgl-texture-utils#webgl-texture-utils +