diff --git a/src/createView.js b/src/createView.js index 09e1510..41d376e 100644 --- a/src/createView.js +++ b/src/createView.js @@ -1,61 +1,7 @@ const invariant = require("invariant"); - -function ContentTextureObject (id) { - return { type: "content", id }; -} - -function NDArrayTextureObject (ndarray) { - return { type: "ndarray", ndarray }; -} - -function URITextureObject (obj) { - return { type: "uri", ...obj }; -} - -function FramebufferTextureObject (id) { - return { type: "fbo", id }; -} - -function withOpts (obj, opts) { - return { ...obj, opts }; -} - -function extractImages (uniforms) { - const images = []; - for (let u in uniforms) { - let value = uniforms[u]; - if (value && - typeof value === "object" && - value.type === "image" && - value.value && - typeof value.value.uri === "string") { - images.push(value.value); - } - } - return images; -} - -function uniqImages (arr) { - var uris = []; - var coll = []; - arr.forEach(function (item) { - if (uris.indexOf(item.uri) === -1) { - uris.push(item.uri); - coll.push(item); - } - }); - return coll; -} - -function isNonSamplerUniformValue (obj) { - let typ = typeof obj; - if (typ==="number" || typ==="boolean") return true; - if (obj !== null && typ === "object" && obj instanceof Array) { - typ = typeof obj[0]; - return typ==="number" || typ==="boolean"; - } - return false; -} +const resolveData = require("./data/resolve"); +const fillDataWithVDOMDescendants = require("./data/fillWithVDOMDescendants"); +const createBuildData = require("./data/build"); module.exports = function (React, Shaders, Uniform, GLComponent, renderVcontainer, renderVcontent, renderVGL) { const { @@ -63,279 +9,7 @@ module.exports = function (React, Shaders, Uniform, GLComponent, renderVcontaine PropTypes } = React; - function pickReactFirstChild (children) { - return React.Children.count(children) === 1 ? - (children instanceof Array ? children[0] : children) : - null; - } - - function unfoldGLComponent (c) { // FIXME: React might eventually improve to ease the work done here. see https://github.com/facebook/react/issues/4697#issuecomment-134335822 - const instance = new c.type(); - if (!(instance instanceof GLComponent)) return; // FIXME: can we check this without instanciating it? - instance.props = c.props; - return pickReactFirstChild(instance.render()); - } - - function findGLViewInGLComponentChildren (children) { - // going down the VDOM tree, while we can unfold GLComponent - for (let c = children; c && typeof c.type === "function"; c = unfoldGLComponent(c)) - if (c.type === GLView) - return c; // found a GLView - } - - - - //// buildData : traverses the Virtual DOM to generates a data tree - - function buildData (shader, glViewUniforms, width, height, glViewChildren, preload) { - invariant(Shaders.exists(shader), "Shader #%s does not exists", shader); - - const shaderName = Shaders.getName(shader); - - const uniforms = { ...glViewUniforms }; - const children = []; - const contents = []; - - React.Children.forEach(glViewChildren, child => { - invariant(child.type === Uniform, "(Shader '%s') GL.View can only contains children of type GL.Uniform. Got '%s'", shaderName, child.type && child.type.displayName || child); - const { name, children, ...opts } = child.props; - invariant(typeof name === "string" && name, "(Shader '%s') GL.Uniform must define an name String", shaderName); - invariant(!glViewUniforms || !(name in glViewUniforms), "(Shader '%s') The uniform '%s' set by GL.Uniform must not be in {uniforms} props", shaderName); - invariant(!(name in uniforms), "(Shader '%s') The uniform '%s' set by GL.Uniform must not be defined in another GL.Uniform", shaderName); - uniforms[name] = children.value ? children : { value: children, opts }; // eslint-disable-line no-undef - }); - - Object.keys(uniforms) - .forEach(name => { - let value = uniforms[name]; - if (isNonSamplerUniformValue(value)) return; - - let opts, typ = typeof value; - - if (value && typ === "object" && !value.prototype && "value" in value) { - // if value has a value field, we tread this field as the value, but keep opts in memory if provided - if (typeof value.opts === "object") { - opts = value.opts; - } - value = value.value; - typ = typeof value; - } - - if (!value) { - // falsy value are accepted to indicate blank texture - uniforms[name] = value; - } - else if (typ === "string") { - // uri specified as a string - uniforms[name] = withOpts(URITextureObject({ uri: value }), opts); - } - else if (typ === "object" && typeof value.uri === "string") { - // uri specified in an object, we keep all other fields for RN "local" image use-case - uniforms[name] = withOpts(URITextureObject(value), opts); - } - else if (typ === "object" && value.data && value.shape && value.stride) { - // ndarray kind of texture - uniforms[name] = withOpts(NDArrayTextureObject(value), opts); - } - else if(typ === "object" && (value instanceof Array ? React.isValidElement(value[0]) : React.isValidElement(value))) { - // value is a VDOM or array of VDOM - const childGLView = findGLViewInGLComponentChildren(value); - if (childGLView) { - // We have found a GL.View children, we integrate it in the tree and recursively do the same - const childProps = childGLView.props; - children.push({ - vdom: value, - uniform: name, - data: buildData( - childProps.shader, - childProps.uniforms, - childProps.width || width, - childProps.height || height, - childProps.children, - "preload" in childProps ? childProps.preload : preload) - }); - } - else { - // in other cases VDOM, we will use child as a content - contents.push({ - vdom: value, - uniform: name, - opts - }); - } - } - else { - // in any other case, it is an unrecognized invalid format - delete uniforms[name]; - if (typeof console !== "undefined" && console.error) console.error("invalid uniform '"+name+"' value:", value); // eslint-disable-line no-console - invariant(false, "Shader #%s: Unrecognized format for uniform '%s'", shader, name); - } - }); - - return { - shader, - uniforms, - width, - height, - children, - contents, - preload - }; - } - - - - ///// resolveData : takes the output of buildData to generate the final data tree - // that have resolved framebuffers and shared computation of duplicate uniforms (e.g: content / GL.View) - - function resolveData (data) { - - let imagesToPreload = []; - - // contents are view/canvas/image/video to be rasterized "globally" - const contentsMeta = findContentsUniq(data); - const contentsVDOM = contentsMeta.map(({vdom}) => vdom); - - // recursively find all contents but without duplicates by comparing VDOM reference - function findContentsUniq (data) { - const vdoms = []; - const contents = []; - function rec (data) { - data.contents.forEach(content => { - if (vdoms.indexOf(content.vdom) === -1) { - vdoms.push(content.vdom); - contents.push(content); - } - }); - data.children.forEach(child => { - rec(child.data); - }); - } - rec(data); - return contents; - } - - // recursively find shared VDOM across direct children. - // if a VDOM is used in 2 different children, it means we can share its computation in contextChildren - function findChildrenDuplicates (data, toIgnore) { - // FIXME the code here is a bit complex and not so performant. - // We should see if we can precompute some data once before - function childVDOMs ({vdom,data}, arrVdom, arrData) { - if (toIgnore.indexOf(vdom) === -1 && arrVdom.indexOf(vdom) === -1) { - arrVdom.push(vdom); - arrData.push(data); - } - data.children.forEach(child => childVDOMs(child, arrVdom, arrData)); - } - let allVdom = []; - let allData = []; - const childrenVDOMs = data.children.map(child => { - const arrVdom = []; - const arrData = []; - childVDOMs(child, arrVdom, arrData); - allVdom = allVdom.concat(arrVdom); - allData = allData.concat(arrData); - return arrVdom; - }); - return allVdom.map((vdom, allIndex) => { - let occ = 0; - for (let i=0; i 1) return { vdom: vdom, data: allData[allIndex] }; - } - } - }).filter(obj => obj); - } - - // Recursively "resolve" the data to assign fboId and factorize duplicate uniforms to shared uniforms. - function rec (data, fboId, parentContext, parentFbos) { - const parentContextVDOM = parentContext.map(({vdom}) => vdom); - - const genFboId = (fboIdCounter => - () => { - fboIdCounter ++; - while ( - fboIdCounter === fboId || - parentFbos.indexOf(fboIdCounter)!==-1) // ensure fbo is not already taken in parents - fboIdCounter ++; - return fboIdCounter; - } - )(-1); - - const { uniforms: dataUniforms, children: dataChildren, contents: dataContents, preload, ...dataRest } = data; - const uniforms = {...dataUniforms}; - - const shared = findChildrenDuplicates(data, parentContextVDOM); - const childrenContext = shared.map(({vdom}) => { - const fboId = genFboId(); - return { vdom, fboId }; - }); - - const context = parentContext.concat(childrenContext); - const contextVDOM = context.map(({vdom}) => vdom); - const contextFbos = context.map(({fboId}) => fboId); - - const contextChildren = []; - const children = []; - - const toRecord = dataChildren.concat(shared).map(child => { - const { data: childData, uniform, vdom } = child; - let i = contextVDOM.indexOf(vdom); - let fboId, addToCollection; - if (i===-1) { - fboId = genFboId(); - addToCollection = children; - } - else { - fboId = context[i].fboId; - if (i >= parentContext.length) {// is a new context children - addToCollection = contextChildren; - } - } - if (uniform) uniforms[uniform] = FramebufferTextureObject(fboId); - return { fboId, childData, addToCollection }; - }); - - const childrenFbos = toRecord.map(({fboId})=>fboId); - const allFbos = parentFbos.concat(contextFbos).concat(childrenFbos); - - const recorded = []; - toRecord.forEach(({ fboId, childData, addToCollection }) => { - if (recorded.indexOf(fboId) === -1) { - recorded.push(fboId); - if (addToCollection) addToCollection.push(rec(childData, fboId, context, allFbos)); - } - }); - - dataContents.forEach(({ uniform, vdom, opts }) => { - const id = contentsVDOM.indexOf(vdom); - invariant(id!==-1, "contents was discovered by findContentsMeta"); - uniforms[uniform] = withOpts(ContentTextureObject(id), opts); - }); - - // Check images to preload - if (preload) { - imagesToPreload = imagesToPreload.concat(extractImages(dataUniforms)); - } - - return { - ...dataRest, // eslint-disable-line no-undef - uniforms, - contextChildren, - children, - fboId - }; - } - - return { - data: rec(data, -1, [], []), - contentsVDOM, - imagesToPreload: uniqImages(imagesToPreload) - }; - } - - + let buildData; // will be set after GLView class defined. class GLView extends Component { constructor (props, context) { @@ -348,7 +22,11 @@ module.exports = function (React, Shaders, Uniform, GLComponent, renderVcontaine const { width, height, children, shader, uniforms, debug, preload, opaque, ...restProps } = props; invariant(width && height && width>0 && height>0, "width and height are required for the root GLView"); - const {data, contentsVDOM, imagesToPreload} = resolveData(buildData(shader, uniforms, width, height, children, preload||false)); + const {data, contentsVDOM, imagesToPreload} = + resolveData( + fillDataWithVDOMDescendants( + buildData( + shader, uniforms, width, height, children, preload||false))); const contents = contentsVDOM.map((vdom, i) => renderVcontent(data.width, data.height, i, vdom)); if (debug && @@ -391,5 +69,7 @@ module.exports = function (React, Shaders, Uniform, GLComponent, renderVcontaine opaque: true }; + buildData = createBuildData(React, Shaders, Uniform, GLComponent, GLView); + return GLView; }; diff --git a/src/data/TextureObjects.js b/src/data/TextureObjects.js new file mode 100644 index 0000000..d53c93b --- /dev/null +++ b/src/data/TextureObjects.js @@ -0,0 +1,28 @@ + +function Content (id) { + return { type: "content", id }; +} + +function NDArray (ndarray) { + return { type: "ndarray", ndarray }; +} + +function URI (obj) { + return { type: "uri", ...obj }; +} + +function Framebuffer (id) { + return { type: "fbo", id }; +} + +function withOpts (obj, opts) { + return { ...obj, opts }; +} + +module.exports = { + Content, + NDArray, + URI, + Framebuffer, + withOpts +}; diff --git a/src/data/build.js b/src/data/build.js new file mode 100644 index 0000000..ea4a1cb --- /dev/null +++ b/src/data/build.js @@ -0,0 +1,125 @@ +const invariant = require("invariant"); +const TextureObjects = require("./TextureObjects"); +const isNonSamplerUniformValue = require("./isNonSamplerUniformValue"); + +module.exports = function (React, Shaders, Uniform, GLComponent, GLView) { + // FIXME: maybe with React 0.14, we will be able to make this library depending on React so we don't have to do this closure + + function pickReactFirstChild (children) { + return React.Children.count(children) === 1 ? + (children instanceof Array ? children[0] : children) : + null; + } + + function unfoldGLComponent (c) { // FIXME: React might eventually improve to ease the work done here. see https://github.com/facebook/react/issues/4697#issuecomment-134335822 + const instance = new c.type(); + if (!(instance instanceof GLComponent)) return; // FIXME: can we check this without instanciating it? + instance.props = c.props; + return pickReactFirstChild(instance.render()); + } + + function findGLViewInGLComponentChildren (children) { + // going down the VDOM tree, while we can unfold GLComponent + for (let c = children; c && typeof c.type === "function"; c = unfoldGLComponent(c)) + if (c.type === GLView) + return c; // found a GLView + } + + //// buildData : traverses the Virtual DOM to generates a data tree + + return function buildData (shader, glViewUniforms, width, height, glViewChildren, preload) { + invariant(Shaders.exists(shader), "Shader #%s does not exists", shader); + + const shaderName = Shaders.getName(shader); + + const uniforms = { ...glViewUniforms }; + const children = []; + const contents = []; + + React.Children.forEach(glViewChildren, child => { + invariant(child.type === Uniform, "(Shader '%s') GL.View can only contains children of type GL.Uniform. Got '%s'", shaderName, child.type && child.type.displayName || child); + const { name, children, ...opts } = child.props; + invariant(typeof name === "string" && name, "(Shader '%s') GL.Uniform must define an name String", shaderName); + invariant(!glViewUniforms || !(name in glViewUniforms), "(Shader '%s') The uniform '%s' set by GL.Uniform must not be in {uniforms} props", shaderName); + invariant(!(name in uniforms), "(Shader '%s') The uniform '%s' set by GL.Uniform must not be defined in another GL.Uniform", shaderName); + uniforms[name] = children.value ? children : { value: children, opts }; // eslint-disable-line no-undef + }); + + Object.keys(uniforms) + .forEach(name => { + let value = uniforms[name]; + if (isNonSamplerUniformValue(value)) return; + + let opts, typ = typeof value; + + if (value && typ === "object" && !value.prototype && "value" in value) { + // if value has a value field, we tread this field as the value, but keep opts in memory if provided + if (typeof value.opts === "object") { + opts = value.opts; + } + value = value.value; + typ = typeof value; + } + + if (!value) { + // falsy value are accepted to indicate blank texture + uniforms[name] = value; + } + else if (typ === "string") { + // uri specified as a string + uniforms[name] = TextureObjects.withOpts(TextureObjects.URI({ uri: value }), opts); + } + else if (typ === "object" && typeof value.uri === "string") { + // uri specified in an object, we keep all other fields for RN "local" image use-case + uniforms[name] = TextureObjects.withOpts(TextureObjects.URI(value), opts); + } + else if (typ === "object" && value.data && value.shape && value.stride) { + // ndarray kind of texture + uniforms[name] = TextureObjects.withOpts(TextureObjects.NDArray(value), opts); + } + else if(typ === "object" && (value instanceof Array ? React.isValidElement(value[0]) : React.isValidElement(value))) { + // value is a VDOM or array of VDOM + const childGLView = findGLViewInGLComponentChildren(value); + if (childGLView) { + // We have found a GL.View children, we integrate it in the tree and recursively do the same + const childProps = childGLView.props; + children.push({ + vdom: value, + uniform: name, + data: buildData( + childProps.shader, + childProps.uniforms, + childProps.width || width, + childProps.height || height, + childProps.children, + "preload" in childProps ? childProps.preload : preload) + }); + } + else { + // in other cases VDOM, we will use child as a content + contents.push({ + vdom: value, + uniform: name, + opts + }); + } + } + else { + // in any other case, it is an unrecognized invalid format + delete uniforms[name]; + if (typeof console !== "undefined" && console.error) console.error("invalid uniform '"+name+"' value:", value); // eslint-disable-line no-console + invariant(false, "Shader #%s: Unrecognized format for uniform '%s'", shader, name); + } + }); + + return { + shader, + uniforms, + width, + height, + children, + contents, + preload + }; + }; +}; diff --git a/src/data/extractImages.js b/src/data/extractImages.js new file mode 100644 index 0000000..3d228d5 --- /dev/null +++ b/src/data/extractImages.js @@ -0,0 +1,16 @@ +function extractImages (uniforms) { + const images = []; + for (let u in uniforms) { + let value = uniforms[u]; + if (value && + typeof value === "object" && + value.type === "image" && + value.value && + typeof value.value.uri === "string") { + images.push(value.value); + } + } + return images; +} + +module.exports = extractImages; diff --git a/src/data/fillWithVDOMDescendants.js b/src/data/fillWithVDOMDescendants.js new file mode 100644 index 0000000..0a32950 --- /dev/null +++ b/src/data/fillWithVDOMDescendants.js @@ -0,0 +1,29 @@ + +function fillWithVDOMDescendants (dataTree) { + function rec (node) { + let descendantsVDOM = [], descendantsVDOMData = []; + const newChildren = node.data.children.map(node => { + const res = rec(node); + if (descendantsVDOM.indexOf(res.vdom) === -1) { + descendantsVDOM.push(res.vdom); + descendantsVDOMData.push(res.data); + } + res.descendantsVDOM.forEach((vdom, i) => { + if (descendantsVDOM.indexOf(vdom) === -1) { + descendantsVDOM.push(vdom); + descendantsVDOMData.push(res.descendantsVDOMData[i]); + } + }); + return res; + }); + return { + ...node, + data: { ...node.data, children: newChildren }, + descendantsVDOM, + descendantsVDOMData + }; + } + return rec({ data: dataTree }).data; +} + +module.exports = fillWithVDOMDescendants; diff --git a/src/data/findChildrenDuplicates.js b/src/data/findChildrenDuplicates.js new file mode 100644 index 0000000..da748b9 --- /dev/null +++ b/src/data/findChildrenDuplicates.js @@ -0,0 +1,26 @@ + +// recursively find shared VDOM across direct children. +// if a VDOM is used in 2 different children, it means we can share its computation in contextChildren +function findChildrenDuplicates (data, toIgnore) { + let descendantsVDOM = []; + let descendantsVDOMData = []; + data.children.map(child => { + descendantsVDOM = descendantsVDOM.concat(child.descendantsVDOM); + descendantsVDOMData = descendantsVDOMData.concat(child.descendantsVDOMData); + }); + return descendantsVDOM.map((vdom, allIndex) => { + if (toIgnore.indexOf(vdom) !== -1) return; + let occ = 0; + for (let i=0; i 1) return { + data: descendantsVDOMData[allIndex], + vdom + }; + } + } + }).filter(obj => obj); +} + +module.exports = findChildrenDuplicates; diff --git a/src/data/findContentsUniq.js b/src/data/findContentsUniq.js new file mode 100644 index 0000000..4fcd3da --- /dev/null +++ b/src/data/findContentsUniq.js @@ -0,0 +1,20 @@ +// recursively find all contents but without duplicates by comparing VDOM reference +function findContentsUniq (data) { + const vdoms = []; + const contents = []; + function rec (data) { + data.contents.forEach(content => { + if (vdoms.indexOf(content.vdom) === -1) { + vdoms.push(content.vdom); + contents.push(content); + } + }); + data.children.forEach(child => { + rec(child.data); + }); + } + rec(data); + return contents; +} + +module.exports = findContentsUniq; diff --git a/src/data/isNonSamplerUniformValue.js b/src/data/isNonSamplerUniformValue.js new file mode 100644 index 0000000..8360437 --- /dev/null +++ b/src/data/isNonSamplerUniformValue.js @@ -0,0 +1,11 @@ +// heuristic to determine if a uniform value is not a texture kind +function isNonSamplerUniformValue (obj) { + let typ = typeof obj; + if (typ==="number" || typ==="boolean") return true; + if (obj !== null && typ === "object" && obj instanceof Array) { + typ = typeof obj[0]; + return typ==="number" || typ==="boolean"; + } + return false; +} +module.exports = isNonSamplerUniformValue; diff --git a/src/data/resolve.js b/src/data/resolve.js new file mode 100644 index 0000000..ff429df --- /dev/null +++ b/src/data/resolve.js @@ -0,0 +1,113 @@ +const invariant = require("invariant"); + +const findContentsUniq = require("./findContentsUniq"); +const findChildrenDuplicates = require("./findChildrenDuplicates"); +const TextureObjects = require("./TextureObjects"); +const extractImages = require("./extractImages"); +const uniqImages = require("./uniqImages"); + +///// resolveData : takes the output of buildData to generate the final data tree +// that have resolved framebuffers and shared computation of duplicate uniforms (e.g: content / GL.View) + +function resolve (dataTree) { + let imagesToPreload = []; + + // contents are view/canvas/image/video to be rasterized "globally" + const contentsMeta = findContentsUniq(dataTree); + const contentsVDOM = contentsMeta.map(({vdom}) => vdom); + + // Recursively "resolve" the data to assign fboId and factorize duplicate uniforms to shared uniforms. + function rec (data, fboId, parentContext, parentFbos) { + const { uniforms: dataUniforms, children: dataChildren, contents: dataContents, preload, ...dataRest } = data; + const uniforms = {...dataUniforms}; + const parentContextVDOM = parentContext.map(({vdom}) => vdom); + + // A function to generate a free FBO id for this node + const genFboId = (fboIdCounter => + () => { + fboIdCounter ++; + while ( + fboIdCounter === fboId || // fbo should not take the current one + parentFbos.indexOf(fboIdCounter)!==-1) // ensure fbo is not already taken in parents + fboIdCounter ++; + return fboIdCounter; + } + )(-1); + + // shared contains all nodes that are contains in more than one direct children. + const shared = findChildrenDuplicates(data, parentContextVDOM); + + // We assign fboIds to shared + const childrenContext = shared.map(({vdom}) => { + const fboId = genFboId(); + return { vdom, fboId }; + }); + + // We accumulate into context the childrenContext and the parentContext + const context = parentContext.concat(childrenContext); + const contextVDOM = context.map(({vdom}) => vdom); + const contextFbos = context.map(({fboId}) => fboId); + + // contextChildren and children are field to fill for this node + // We traverse the dataChildren to resolve where each child should go: + // either we create a new child, a we create context child or we use an existing parent context + const contextChildren = []; + const children = []; + + const toRecord = dataChildren.concat(shared).map(child => { + const { uniform, vdom, data } = child; + let i = contextVDOM.indexOf(vdom); + let fboId, addToCollection; + if (i===-1) { + fboId = genFboId(); + addToCollection = children; + } + else { + fboId = context[i].fboId; + if (i >= parentContext.length) {// is a new context children + addToCollection = contextChildren; + } + } + if (uniform) uniforms[uniform] = TextureObjects.Framebuffer(fboId); + return { data, fboId, addToCollection }; + }); + + const childrenFbos = toRecord.map(({fboId})=>fboId); + const allFbos = parentFbos.concat(contextFbos).concat(childrenFbos); + + const recorded = []; + toRecord.forEach(({ data, fboId, addToCollection }) => { + if (recorded.indexOf(fboId) === -1) { + recorded.push(fboId); + if (addToCollection) addToCollection.push(rec(data, fboId, context, allFbos)); + } + }); + + dataContents.forEach(({ uniform, vdom, opts }) => { + const id = contentsVDOM.indexOf(vdom); + invariant(id!==-1, "contents was discovered by findContentsMeta"); + uniforms[uniform] = TextureObjects.withOpts(TextureObjects.Content(id), opts); + }); + + // Check images to preload + if (preload) { + imagesToPreload = imagesToPreload.concat(extractImages(dataUniforms)); + } + + return { + ...dataRest, // eslint-disable-line no-undef + uniforms, + contextChildren, + children, + fboId + }; + } + + return { + data: rec(dataTree, -1, [], []), + contentsVDOM, + imagesToPreload: uniqImages(imagesToPreload) + }; +} + +module.exports = resolve; diff --git a/src/data/uniqImages.js b/src/data/uniqImages.js new file mode 100644 index 0000000..9ee056c --- /dev/null +++ b/src/data/uniqImages.js @@ -0,0 +1,14 @@ + +function uniqImages (arr) { + var uris = []; + var coll = []; + arr.forEach(function (item) { + if (uris.indexOf(item.uri) === -1) { + uris.push(item.uri); + coll.push(item); + } + }); + return coll; +} + +module.exports = uniqImages;