From be7bb3cddfcde06ae23e41e6bd481237b1449c48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Fri, 1 Jan 2016 17:37:57 +0100 Subject: [PATCH] Fixes #10 --- src/Node.js | 2 +- src/Shaders.js | 178 ++++++++++++++++++++++++++++++++++++++----- src/createSurface.js | 40 ++++++++-- src/data/build.js | 6 +- 4 files changed, 196 insertions(+), 30 deletions(-) diff --git a/src/Node.js b/src/Node.js index 190712fe..46e779a2 100644 --- a/src/Node.js +++ b/src/Node.js @@ -16,7 +16,7 @@ Node.isGLNode = true; Node.displayName = "GL.Node"; Node.propTypes = { - shader: PropTypes.number.isRequired, + shader: PropTypes.any.isRequired, uniforms: PropTypes.object, children: PropTypes.node, width: PropTypes.number, diff --git a/src/Shaders.js b/src/Shaders.js index 2ca88141..db80ce5e 100644 --- a/src/Shaders.js +++ b/src/Shaders.js @@ -1,46 +1,188 @@ const {EventEmitter} = require("events"); const invariant = require("invariant"); +function defer () { + const deferred = {}; + const promise = new Promise((resolve, reject) => { + deferred.resolve = resolve; + deferred.reject = reject; + }); + deferred.promise = promise; + return deferred; +} + +const INLINE_NAME = ""; + let _uid = 1; -const names = {}; -const shaders = {}; +const names = {}; // keep names +const shaders = {}; // keep shader objects +const shadersCompileResponses = {}; // keep promise of compile responses +const shadersReferenceCounters = {}; // reference count the shaders created with Shaders.create()/used inline so we don't delete them if one of 2 dups is still used + +const surfaceInlines = {}; +const previousSurfaceInlines = {}; + +const add = shader => { + const existingId = findShaderId(shaders, shader); + const id = existingId || _uid ++; + let promise; + if (!existingId) { + const d = defer(); + names[id] = shader.name; + shaders[id] = shader; + shadersReferenceCounters[id] = 0; + shadersCompileResponses[id] = promise = d.promise; + Shaders.emit("add", id, shader, (error, result) => { + if (error) + d.reject(error); + else + d.resolve(result); + }); + } + else { + promise = shadersCompileResponses[id]; + } + return { id, promise }; +}; + +const remove = id => { + delete shaders[id]; + delete names[id]; + delete shadersReferenceCounters[id]; + delete shadersCompileResponses[id]; + Shaders.emit("remove", id); +}; + +const getShadersToRemove = () => + Object.keys(shadersReferenceCounters) + .filter(id => shadersReferenceCounters[id] <= 0) + .map(id => parseInt(id, 10)); + +let scheduled; +const gcNow = () => { + clearTimeout(scheduled); + getShadersToRemove().forEach(remove); +}; +const scheduleGC = () => { + // debounce the shader deletion to let a last chance to a future dup shader to appear + // the idea is also to postpone this operation when the app is not so busy + const noDebounce = getShadersToRemove().length > 20; + if (!noDebounce) clearTimeout(scheduled); + scheduled = setTimeout(gcNow, 500); +}; + +const sameShader = (a, b) => a.frag === b.frag; + +const findShaderId = (shaders, shader) => { + for (let id in shaders) { + if (sameShader(shaders[id], shader)) { + return parseInt(id, 10); + } + } + return null; +}; + +const logError = name => error => + console.error( //eslint-disable-line no-console + "Shader '" + name + "' failed to compile:\n" + error + ); const Shaders = { - create (obj) { + + _onSurfaceWillMount (surfaceId) { + surfaceInlines[surfaceId] = []; + }, + + _onSurfaceWillUnmount (surfaceId) { + surfaceInlines[surfaceId].forEach(id => + shadersReferenceCounters[id]--); + delete surfaceInlines[surfaceId]; + delete previousSurfaceInlines[surfaceId]; + scheduleGC(); + }, + + _beforeSurfaceBuild (surfaceId) { + previousSurfaceInlines[surfaceId] = surfaceInlines[surfaceId]; + surfaceInlines[surfaceId] = []; + }, + + // Resolve the shader field of GL.Node. + // it can be an id (created with Shaders.create) or an inline object. + _resolve (idOrObject, surfaceId, compileHandler) { + if (typeof idOrObject === "number") return idOrObject; + const { id, promise } = add({ name: INLINE_NAME, ...idOrObject }); + if (compileHandler) { + promise.then( + result => compileHandler(null, result), + error => compileHandler(error)); + } + else { + promise.catch(logError(Shaders.getName(id))); + } + const inlines = surfaceInlines[surfaceId]; + inlines.push(id); + return id; + }, + + _afterSurfaceBuild (surfaceId) { + previousSurfaceInlines[surfaceId].forEach(id => + shadersReferenceCounters[id]--); + surfaceInlines[surfaceId].forEach(id => + shadersReferenceCounters[id]++); + delete previousSurfaceInlines[surfaceId]; + scheduleGC(); + }, + + // Exposed methods + + create (obj, onAllCompile) { invariant(typeof obj === "object", "config must be an object"); const result = {}; - for (let key in obj) { + const compileErrors = {}, compileResults = {}; + Promise.all(Object.keys(obj).map(key => { const shader = obj[key]; invariant(typeof shader === "object" && typeof shader.frag === "string", "invalid shader given to Shaders.create(). A valid shader is a { frag: String }"); - const id = _uid ++; - if (!shader.name) shader.name = key; - names[id] = shader.name; - shaders[id] = shader; - this.emit("add", id, shader); + const {id, promise} = add({ name: key, ...shader }); result[key] = id; - } + shadersReferenceCounters[id] ++; + return promise.then( + result => compileResults[key] = result, + error => compileErrors[key] = error + ); + })) + .then(() => { + if (onAllCompile) { + onAllCompile( + Object.keys(compileErrors).length ? compileErrors : null, + compileResults); + } + else { + Object.keys(compileErrors).forEach(key => + logError(Shaders.getName(result[key]))(compileErrors[key])); + } + }); return result; }, - remove (id) { - invariant(id in shaders, "There is no such shader '%s'", id); - delete shaders[id]; - delete names[id]; - this.emit("remove", id); - }, + get (id) { - return shaders[id]; + return Object.freeze(shaders[id]); }, + getName (id) { return names[id]; }, + list () { return Object.keys(shaders); }, + exists (id) { - return typeof id === "number" && id >= 1 && id < _uid; + return id in shaders; }, + gcNow, + ...EventEmitter.prototype }; diff --git a/src/createSurface.js b/src/createSurface.js index 5ffe0743..6ded246b 100644 --- a/src/createSurface.js +++ b/src/createSurface.js @@ -5,9 +5,12 @@ const { } = React; const invariant = require("invariant"); const { fill, resolve, build } = require("./data"); +const Shaders = require("./Shaders"); const findGLNodeInGLComponentChildren = require("./data/findGLNodeInGLComponentChildren"); const invariantStrictPositive = require("./data/invariantStrictPositive"); +let _glSurfaceId = 1; + function logResult (data, contentsVDOM) { if (typeof console !== "undefined" && console.debug // eslint-disable-line @@ -22,6 +25,13 @@ module.exports = function (renderVcontainer, renderVcontent, renderVGL, getPixel constructor (props, context) { super(props, context); this._renderId = 1; + this._id = _glSurfaceId ++; + } + componentWillMount () { + Shaders._onSurfaceWillMount(this._id); + } + componentWillUnmount () { + Shaders._onSurfaceWillUnmount(this._id); } getGLCanvas () { return this.refs.canvas; @@ -32,6 +42,7 @@ module.exports = function (renderVcontainer, renderVcontent, renderVGL, getPixel return c.captureFrame.apply(c, arguments); } render() { + const id = this._id; const renderId = this._renderId ++; const props = this.props; const { @@ -65,14 +76,27 @@ module.exports = function (renderVcontainer, renderVcontent, renderVGL, getPixel const { via, childGLNode } = glNode; - const { data, contentsVDOM, imagesToPreload } = - resolve( - fill( - build( - childGLNode, - context, - preload, - via))); + let resolved; + try { + Shaders._beforeSurfaceBuild(id); + resolved = + resolve( + fill( + build( + childGLNode, + context, + preload, + via, + id))); + } + catch (e) { + throw e; + } + finally { + Shaders._afterSurfaceBuild(id); + } + + const { data, contentsVDOM, imagesToPreload } = resolved; if (debug) logResult(data, contentsVDOM); diff --git a/src/data/build.js b/src/data/build.js index 35846dbb..5108efa3 100644 --- a/src/data/build.js +++ b/src/data/build.js @@ -10,9 +10,9 @@ const invariantStrictPositive = require("./invariantStrictPositive"); //// build: converts the gl-react VDOM DSL into an internal data tree. -module.exports = function build (GLNode, context, parentPreload, via) { +module.exports = function build (GLNode, context, parentPreload, via, surfaceId) { const props = GLNode.props; - const shader = props.shader; + const shader = Shaders._resolve(props.shader, surfaceId, props.onShaderCompile); const GLNodeUniforms = props.uniforms; const { width, @@ -96,7 +96,7 @@ module.exports = function build (GLNode, context, parentPreload, via) { children.push({ vdom: value, uniform: name, - data: build(childGLNode, newContext, preload, via) + data: build(childGLNode, newContext, preload, via, surfaceId) }); } else {