Source: framework/application.js

Object.assign(pc, function () {
    /**
     * @name pc.Application
     * @class Default application which performs general setup code and initiates the main game loop.
     * @description Create a new Application.
     * @param {Element} canvas The canvas element
     * @param {Object} options
     * @param {pc.Keyboard} [options.keyboard] Keyboard handler for input
     * @param {pc.Mouse} [options.mouse] Mouse handler for input
     * @param {pc.TouchDevice} [options.touch] TouchDevice handler for input
     * @param {pc.GamePads} [options.gamepads] Gamepad handler for input
     * @param {String} [options.scriptPrefix] Prefix to apply to script urls before loading
     * @param {String} [options.assetPrefix] Prefix to apply to asset urls before loading
     * @param {Object} [options.graphicsDeviceOptions] Options object that is passed into the {@link pc.GraphicsDevice} constructor
     *
     * @example
     * // Create application
     * var app = new pc.Application(canvas, options);
     * // Start game loop
     * app.start()
     */

    // PROPERTIES

    /**
     * @name pc.Application#scene
     * @type {pc.Scene}
     * @description The current {@link pc.Scene}
     */

    /**
     * @name pc.Application#timeScale
     * @type {Number}
     * @description Scales the global time delta.
     */

    /**
     * @name pc.Application#maxDeltaTime
     * @type {Number}
     * @description Clamps per-frame delta time to an upper bound. Useful since returning from a tab
     * deactivation can generate huge values for dt, which can adversely affect game state. Defaults
     * to 0.1 (seconds).
     */

    /**
     * @name pc.Application#assets
     * @type {pc.AssetRegistry}
     * @description The assets available to the application.
     */

    /**
     * @name pc.Application#graphicsDevice
     * @type {pc.GraphicsDevice}
     * @description The graphics device used by the application.
     */

    /**
     * @name pc.Application#systems
     * @type {pc.ComponentSystemRegistry}
     * @description The component systems.
     */

    /**
     * @name pc.Application#loader
     * @type {pc.ResourceLoader}
     * @description The resource loader.
     */

    /**
     * @name pc.Application#root
     * @type {pc.Entity}
     * @description The root {@link pc.Entity} of the application.
     */

    /**
     * @name pc.Application#keyboard
     * @type {pc.Keyboard}
     * @description The keyboard device.
     */

    /**
     * @name pc.Application#mouse
     * @type {pc.Mouse}
     * @description The mouse device.
     */

    /**
     * @name pc.Application#touch
     * @type {pc.TouchDevice}
     * @description Used to get touch events input.
     */

    /**
     * @name pc.Application#gamepads
     * @type {pc.GamePads}
     * @description Used to access GamePad input.
     */

    /**
     * @name pc.Application#elementInput
     * @type {pc.ElementInput}
     * @description Used to handle input for {@link pc.ElementComponent}s.
     */

    /**
     * @name pc.Application#scripts
     * @type pc.ScriptRegistry
     * @description The Script Registry of the Application
     */

    /**
     * @name pc.Application#batcher
     * @type pc.BatchManager
     * @description The Batch Manager of the Application
     */

    /**
     * @name pc.Application#autoRender
     * @type Boolean
     * @description When true (the default) the application's render function is called every frame.
     */

    /**
     * @name pc.Application#renderNextFrame
     * @type Boolean
     * @description If {@link pc.Application#autoRender} is false, set `app.renderNextFrame` true to force application to render the scene once next frame.
     * @example
     * // render the scene only while space key is pressed
     * if (this.app.keyboard.isPressed(pc.KEY_SPACE)) {
     *    this.app.renderNextFrame = true;
     * }
     */

     /**
     * @private
     * @name pc.Application#i18n
     * @type {pc.I18n}
     * @description Handles localization
     */

    var Application = function (canvas, options) {
        options = options || {};

        // Open the log
        pc.log.open();
        // Add event support
        pc.events.attach(this);

        // Store application instance
        Application._applications[canvas.id] = this;
        Application._currentApplication = this;
        pc.app = this;

        this._time = 0;
        this.timeScale = 1;
        this.maxDeltaTime = 0.1; // Maximum delta is 0.1s or 10 fps.

        this.frame = 0; // the total number of frames the application has updated since start() was called

        this.autoRender = true;
        this.renderNextFrame = false;

        // enable if you want entity type script attributes
        // to not be re-mapped when an entity is cloned
        this.useLegacyScriptAttributeCloning = pc.script.legacy;

        this._librariesLoaded = false;
        this._fillMode = pc.FILLMODE_KEEP_ASPECT;
        this._resolutionMode = pc.RESOLUTION_FIXED;
        this._allowResize = true;

        // for compatibility
        this.context = this;

        this.graphicsDevice = new pc.GraphicsDevice(canvas, options.graphicsDeviceOptions);
        this.stats = new pc.ApplicationStats(this.graphicsDevice);
        this._audioManager = new pc.SoundManager(options);
        this.loader = new pc.ResourceLoader(this);

        // stores all entities that have been created
        // for this app by guid
        this._entityIndex = {};

        this.scene = new pc.Scene();
        this.root = new pc.Entity(this);
        this.root._enabledInHierarchy = true;
        this._enableList = [];
        this._enableList.size = 0;
        this.assets = new pc.AssetRegistry(this.loader);
        if (options.assetPrefix) this.assets.prefix = options.assetPrefix;
        this.bundles = new pc.BundleRegistry(this.assets);
        // set this to false if you want to run without using bundles
        // We set it to true only if TextDecoder is available because we currently
        // rely on it for untarring.
        this.enableBundles = (typeof TextDecoder !== 'undefined');
        this.scriptsOrder = options.scriptsOrder || [];
        this.scripts = new pc.ScriptRegistry(this);

        this.i18n = new pc.I18n(this);

        this._sceneRegistry = new pc.SceneRegistry(this);

        var self = this;
        this.defaultLayerWorld = new pc.Layer({
            name: "World",
            id: pc.LAYERID_WORLD
        });

        if (this.graphicsDevice.webgl2) {
            // WebGL 2 depth layer just copies existing depth
            this.defaultLayerDepth = new pc.Layer({
                enabled: false,
                name: "Depth",
                id: pc.LAYERID_DEPTH,

                onEnable: function () {
                    if (this.renderTarget) return;
                    var depthBuffer = new pc.Texture(self.graphicsDevice, {
                        format: pc.PIXELFORMAT_DEPTHSTENCIL,
                        width: self.graphicsDevice.width,
                        height: self.graphicsDevice.height
                    });
                    depthBuffer.name = 'rt-depth2';
                    depthBuffer.minFilter = pc.FILTER_NEAREST;
                    depthBuffer.magFilter = pc.FILTER_NEAREST;
                    depthBuffer.addressU = pc.ADDRESS_CLAMP_TO_EDGE;
                    depthBuffer.addressV = pc.ADDRESS_CLAMP_TO_EDGE;
                    this.renderTarget = new pc.RenderTarget({
                        colorBuffer: null,
                        depthBuffer: depthBuffer,
                        autoResolve: false
                    });
                    self.graphicsDevice.scope.resolve("uDepthMap").setValue(depthBuffer);
                },

                onDisable: function () {
                    if (!this.renderTarget) return;
                    this.renderTarget._depthBuffer.destroy();
                    this.renderTarget.destroy();
                    this.renderTarget = null;
                },

                onPreRenderOpaque: function (cameraPass) { // resize depth map if needed
                    var gl = self.graphicsDevice.gl;
                    this.srcFbo = gl.getParameter(gl.FRAMEBUFFER_BINDING);

                    if (!this.renderTarget || (this.renderTarget.width !== self.graphicsDevice.width || this.renderTarget.height !== self.graphicsDevice.height)) {
                        this.onDisable();
                        this.onEnable();
                    }

                    // disable clearing
                    this.oldClear = this.cameras[cameraPass].camera._clearOptions;
                    this.cameras[cameraPass].camera._clearOptions = this.depthClearOptions;
                },

                onPostRenderOpaque: function (cameraPass) { // copy depth
                    if (!this.renderTarget) return;

                    this.cameras[cameraPass].camera._clearOptions = this.oldClear;

                    var gl = self.graphicsDevice.gl;

                    self.graphicsDevice.setRenderTarget(this.renderTarget);
                    self.graphicsDevice.updateBegin();

                    gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.srcFbo);
                    gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.renderTarget._glFrameBuffer);
                    gl.blitFramebuffer( 0, 0, this.renderTarget.width, this.renderTarget.height,
                                        0, 0, this.renderTarget.width, this.renderTarget.height,
                                        gl.DEPTH_BUFFER_BIT,
                                        gl.NEAREST);
                }

            });
            this.defaultLayerDepth.depthClearOptions = {
                flags: 0
            };
        } else {
            // WebGL 1 depth layer just renders same objects as in World, but with RGBA-encoded depth shader
            this.defaultLayerDepth = new pc.Layer({
                enabled: false,
                name: "Depth",
                id: pc.LAYERID_DEPTH,
                shaderPass: pc.SHADER_DEPTH,

                onEnable: function () {
                    if (this.renderTarget) return;
                    var colorBuffer = new pc.Texture(self.graphicsDevice, {
                        format: pc.PIXELFORMAT_R8_G8_B8_A8,
                        width: self.graphicsDevice.width,
                        height: self.graphicsDevice.height
                    });
                    colorBuffer.name = 'rt-depth1';
                    colorBuffer.minFilter = pc.FILTER_NEAREST;
                    colorBuffer.magFilter = pc.FILTER_NEAREST;
                    colorBuffer.addressU = pc.ADDRESS_CLAMP_TO_EDGE;
                    colorBuffer.addressV = pc.ADDRESS_CLAMP_TO_EDGE;
                    this.renderTarget = new pc.RenderTarget(self.graphicsDevice, colorBuffer, {
                        depth: true,
                        stencil: self.graphicsDevice.supportsStencil
                    });
                    self.graphicsDevice.scope.resolve("uDepthMap").setValue(colorBuffer);
                },

                onDisable: function () {
                    if (!this.renderTarget) return;
                    this.renderTarget._colorBuffer.destroy();
                    this.renderTarget.destroy();
                    this.renderTarget = null;
                },

                onPostCull: function (cameraPass) {
                    // Collect all rendered mesh instances with the same render target as World has, depthWrite == true and prior to this layer to replicate blitFramebuffer on WebGL2
                    var visibleObjects = this.instances.visibleOpaque[cameraPass];
                    var visibleList = visibleObjects.list;
                    var visibleLength = 0;
                    var layers = self.scene.layers.layerList;
                    var subLayerEnabled = self.scene.layers.subLayerEnabled;
                    var isTransparent = self.scene.layers.subLayerList;
                    var rt = self.defaultLayerWorld.renderTarget;
                    var cam = this.cameras[cameraPass];
                    var layer;
                    var j;
                    var layerVisibleList, layerCamId, layerVisibleListLength, drawCall, transparent;
                    for (var i = 0; i < layers.length; i++) {
                        layer = layers[i];
                        if (layer === this) break;
                        if (layer.renderTarget !== rt || !layer.enabled || !subLayerEnabled[i]) continue;
                        layerCamId = layer.cameras.indexOf(cam);
                        if (layerCamId < 0) continue;
                        transparent = isTransparent[i];
                        layerVisibleList = transparent ? layer.instances.visibleTransparent[layerCamId] : layer.instances.visibleOpaque[layerCamId];
                        layerVisibleListLength = layerVisibleList.length;
                        layerVisibleList = layerVisibleList.list;
                        for (j = 0; j < layerVisibleListLength; j++) {
                            drawCall = layerVisibleList[j];
                            if (drawCall.material && drawCall.material.depthWrite && !drawCall._noDepthDrawGl1) {
                                visibleList[visibleLength] = drawCall;
                                visibleLength++;
                            }
                        }
                    }
                    visibleObjects.length = visibleLength;
                },

                onPreRenderOpaque: function (cameraPass) { // resize depth map if needed
                    if (!this.renderTarget || (this.renderTarget.width !== self.graphicsDevice.width || this.renderTarget.height !== self.graphicsDevice.height)) {
                        this.onDisable();
                        this.onEnable();
                    }
                    this.oldClear = this.cameras[cameraPass].camera._clearOptions;
                    this.cameras[cameraPass].camera._clearOptions = this.rgbaDepthClearOptions;
                },

                onDrawCall: function () {
                    self.graphicsDevice.setColorWrite(true, true, true, true);
                },

                onPostRenderOpaque: function (cameraPass) {
                    if (!this.renderTarget) return;
                    this.cameras[cameraPass].camera._clearOptions = this.oldClear;
                }

            });
            this.defaultLayerDepth.rgbaDepthClearOptions = {
                color: [254.0 / 255, 254.0 / 255, 254.0 / 255, 254.0 / 255],
                depth: 1.0,
                flags: pc.CLEARFLAG_COLOR | pc.CLEARFLAG_DEPTH
            };
        }

        this.defaultLayerSkybox = new pc.Layer({
            enabled: false,
            name: "Skybox",
            id: pc.LAYERID_SKYBOX,
            opaqueSortMode: pc.SORTMODE_NONE
        });
        this.defaultLayerUi = new pc.Layer({
            enabled: true,
            name: "UI",
            id: pc.LAYERID_UI,
            transparentSortMode: pc.SORTMODE_MANUAL,
            passThrough: false
        });
        this.defaultLayerImmediate = new pc.Layer({
            enabled: true,
            name: "Immediate",
            id: pc.LAYERID_IMMEDIATE,
            opaqueSortMode: pc.SORTMODE_NONE,
            passThrough: true
        });
        this.defaultLayerComposition = new pc.LayerComposition();

        this.defaultLayerComposition.pushOpaque(this.defaultLayerWorld);
        this.defaultLayerComposition.pushOpaque(this.defaultLayerDepth);
        this.defaultLayerComposition.pushOpaque(this.defaultLayerSkybox);
        this.defaultLayerComposition.pushTransparent(this.defaultLayerWorld);
        this.defaultLayerComposition.pushOpaque(this.defaultLayerImmediate);
        this.defaultLayerComposition.pushTransparent(this.defaultLayerImmediate);
        this.defaultLayerComposition.pushTransparent(this.defaultLayerUi);

        this.scene.layers = this.defaultLayerComposition;

        this._immediateLayer = this.defaultLayerImmediate;

        // Default layers patch
        this.scene.on('set:layers', function (oldComp, newComp) {
            var list = newComp.layerList;
            var layer;
            for (var i = 0; i < list.length; i++) {
                layer = list[i];
                switch (layer.id) {
                    case pc.LAYERID_DEPTH:
                        layer.onEnable = self.defaultLayerDepth.onEnable;
                        layer.onDisable = self.defaultLayerDepth.onDisable;
                        layer.onPreRenderOpaque = self.defaultLayerDepth.onPreRenderOpaque;
                        layer.onPostRenderOpaque = self.defaultLayerDepth.onPostRenderOpaque;
                        layer.depthClearOptions = self.defaultLayerDepth.depthClearOptions;
                        layer.rgbaDepthClearOptions = self.defaultLayerDepth.rgbaDepthClearOptions;
                        layer.shaderPass = self.defaultLayerDepth.shaderPass;
                        layer.onPostCull = self.defaultLayerDepth.onPostCull;
                        layer.onDrawCall = self.defaultLayerDepth.onDrawCall;
                        break;
                    case pc.LAYERID_UI:
                        layer.passThrough = self.defaultLayerUi.passThrough;
                        break;
                    case pc.LAYERID_IMMEDIATE:
                        layer.passThrough = self.defaultLayerImmediate.passThrough;
                        break;
                }
            }
        });

        this.renderer = new pc.ForwardRenderer(this.graphicsDevice);
        this.renderer.scene = this.scene;
        this.lightmapper = new pc.Lightmapper(this.graphicsDevice, this.root, this.scene, this.renderer, this.assets);
        this.once('prerender', this._firstBake, this);
        this.batcher = new pc.BatchManager(this.graphicsDevice, this.root, this.scene);
        this.once('prerender', this._firstBatch, this);

        this.keyboard = options.keyboard || null;
        this.mouse = options.mouse || null;
        this.touch = options.touch || null;
        this.gamepads = options.gamepads || null;
        this.elementInput = options.elementInput || null;
        if (this.elementInput)
            this.elementInput.app = this;

        this.vr = null;
        // you can enable vr here, or in application properties
        if (options.vr) {
            this._onVrChange(options.vr);
        }

        this._inTools = false;

        this._skyboxLast = 0;

        this._scriptPrefix = options.scriptPrefix || '';

        if (this.enableBundles) {
            this.loader.addHandler("bundle", new pc.BundleHandler(this.assets));
        }

        this.loader.addHandler("animation", new pc.AnimationHandler());
        this.loader.addHandler("model", new pc.ModelHandler(this.graphicsDevice, this.scene.defaultMaterial));
        this.loader.addHandler("material", new pc.MaterialHandler(this));
        this.loader.addHandler("texture", new pc.TextureHandler(this.graphicsDevice, this.assets, this.loader));
        this.loader.addHandler("text", new pc.TextHandler());
        this.loader.addHandler("json", new pc.JsonHandler());
        this.loader.addHandler("audio", new pc.AudioHandler(this._audioManager));
        this.loader.addHandler("script", new pc.ScriptHandler(this));
        this.loader.addHandler("scene", new pc.SceneHandler(this));
        this.loader.addHandler("cubemap", new pc.CubemapHandler(this.graphicsDevice, this.assets, this.loader));
        this.loader.addHandler("html", new pc.HtmlHandler());
        this.loader.addHandler("css", new pc.CssHandler());
        this.loader.addHandler("shader", new pc.ShaderHandler());
        this.loader.addHandler("hierarchy", new pc.HierarchyHandler(this));
        this.loader.addHandler("scenesettings", new pc.SceneSettingsHandler(this));
        this.loader.addHandler("folder", new pc.FolderHandler());
        this.loader.addHandler("font", new pc.FontHandler(this.loader));
        this.loader.addHandler("binary", new pc.BinaryHandler());
        this.loader.addHandler("textureatlas", new pc.TextureAtlasHandler(this.loader));
        this.loader.addHandler("sprite", new pc.SpriteHandler(this.assets, this.graphicsDevice));

        this.systems = new pc.ComponentSystemRegistry();
        this.systems.add(new pc.RigidBodyComponentSystem(this));
        this.systems.add(new pc.CollisionComponentSystem(this));
        this.systems.add(new pc.AnimationComponentSystem(this));
        this.systems.add(new pc.ModelComponentSystem(this));
        this.systems.add(new pc.CameraComponentSystem(this));
        this.systems.add(new pc.LightComponentSystem(this));
        if (pc.script.legacy) {
            this.systems.add(new pc.ScriptLegacyComponentSystem(this));
        } else {
            this.systems.add(new pc.ScriptComponentSystem(this));
        }
        this.systems.add(new pc.AudioSourceComponentSystem(this, this._audioManager));
        this.systems.add(new pc.SoundComponentSystem(this, this._audioManager));
        this.systems.add(new pc.AudioListenerComponentSystem(this, this._audioManager));
        this.systems.add(new pc.ParticleSystemComponentSystem(this));
        this.systems.add(new pc.ScreenComponentSystem(this));
        this.systems.add(new pc.ElementComponentSystem(this));
        this.systems.add(new pc.ButtonComponentSystem(this));
        this.systems.add(new pc.ScrollViewComponentSystem(this));
        this.systems.add(new pc.ScrollbarComponentSystem(this));
        this.systems.add(new pc.SpriteComponentSystem(this));
        this.systems.add(new pc.LayoutGroupComponentSystem(this));
        this.systems.add(new pc.LayoutChildComponentSystem(this));
        this.systems.add(new pc.ZoneComponentSystem(this));

        this._visibilityChangeHandler = this.onVisibilityChange.bind(this);

        // Depending on browser add the correct visibiltychange event and store the name of the hidden attribute
        // in this._hiddenAttr.
        if (document.hidden !== undefined) {
            this._hiddenAttr = 'hidden';
            document.addEventListener('visibilitychange', this._visibilityChangeHandler, false);
        } else if (document.mozHidden !== undefined) {
            this._hiddenAttr = 'mozHidden';
            document.addEventListener('mozvisibilitychange', this._visibilityChangeHandler, false);
        } else if (document.msHidden !== undefined) {
            this._hiddenAttr = 'msHidden';
            document.addEventListener('msvisibilitychange', this._visibilityChangeHandler, false);
        } else if (document.webkitHidden !== undefined) {
            this._hiddenAttr = 'webkitHidden';
            document.addEventListener('webkitvisibilitychange', this._visibilityChangeHandler, false);
        }

        // bind tick function to current scope

        /* eslint-disable-next-line no-use-before-define */
        this.tick = makeTick(this); // Circular linting issue as makeTick and Application reference each other
    };

    Application._currentApplication = null;
    Application._applications = {};
    Application.getApplication = function (id) {
        return id ? Application._applications[id] : Application._currentApplication;
    };

    // Mini-object used to measure progress of loading sets
    var Progress = function (length) {
        this.length = length;
        this.count = 0;

        this.inc = function () {
            this.count++;
        };

        this.done = function () {
            return (this.count === this.length);
        };
    };

    Object.assign(Application.prototype, {
        /**
         * @function
         * @name pc.Application#configure
         * @description Load the application configuration file and apply application properties and fill the asset registry
         * @param {String} url The URL of the configuration file to load
         * @param {Function} callback The Function called when the configuration file is loaded and parsed
         */
        configure: function (url, callback) {
            var self = this;
            pc.http.get(url, function (err, response) {
                if (err) {
                    callback(err);
                    return;
                }

                var props = response.application_properties;
                var scenes = response.scenes;
                var assets = response.assets;

                self._parseApplicationProperties(props, function (err) {
                    self._onVrChange(props.vr);
                    self._parseScenes(scenes);
                    self._parseAssets(assets);
                    if (!err) {
                        callback(null);
                    } else {
                        callback(err);
                    }
                });
            });
        },

        /**
         * @function
         * @name pc.Application#preload
         * @description Load all assets in the asset registry that are marked as 'preload'
         * @param {Function} callback Function called when all assets are loaded
         */
        preload: function (callback) {
            var self = this;
            var i, total;

            self.fire("preload:start");

            // get list of assets to preload
            var assets = this.assets.list({
                preload: true
            });

            var _assets = new Progress(assets.length);

            var _done = false;

            // check if all loading is done
            var done = function () {
                // do not proceed if application destroyed
                if (!self.graphicsDevice) {
                    return;
                }

                if (!_done && _assets.done()) {
                    _done = true;
                    self.fire("preload:end");
                    callback();
                }
            };

            // totals loading progress of assets
            total = assets.length;
            var count = function () {
                return _assets.count;
            };

            if (_assets.length) {
                var onAssetLoad = function (asset) {
                    _assets.inc();
                    self.fire('preload:progress', count() / total);

                    if (_assets.done())
                        done();
                };

                var onAssetError = function (err, asset) {
                    _assets.inc();
                    self.fire('preload:progress', count() / total);

                    if (_assets.done())
                        done();
                };

                // for each asset
                for (i = 0; i < assets.length; i++) {
                    if (!assets[i].loaded) {
                        assets[i].once('load', onAssetLoad);
                        assets[i].once('error', onAssetError);

                        this.assets.load(assets[i]);
                    } else {
                        _assets.inc();
                        self.fire("preload:progress", count() / total);

                        if (_assets.done())
                            done();
                    }
                }
            } else {
                done();
            }
        },

        /**
         * @function
         * @name pc.Application#getSceneUrl
         * @description Look up the URL of the scene hierarchy file via the name given to the scene in the editor. Use this to in {@link pc.Application#loadSceneHierarchy}.
         * @param {String} name The name of the scene file given in the Editor
         * @returns {String} The URL of the scene file
         */
        getSceneUrl: function (name) {
            var entry = this._sceneRegistry.find(name);
            if (entry) {
                return entry.url;
            }
            return null;

        },

        /**
         * @function
         * @name pc.Application#loadSceneHierarchy
         * @description Load a scene file, create and initialize the Entity hierarchy
         * and add the hierarchy to the application root Entity.
         * @param {String} url The URL of the scene file. Usually this will be "scene_id.json"
         * @param {Function} callback The function to call after loading, passed (err, entity) where err is null if no errors occurred.
         * @example
         *
         * app.loadSceneHierarchy("1000.json", function (err, entity) {
         *     if (!err) {
         *       var e = app.root.find("My New Entity");
         *     } else {
         *       // error
         *     }
         *   }
         * });
         */
        loadSceneHierarchy: function (url, callback) {
            this._sceneRegistry.loadSceneHierarchy(url, callback);
        },

        /**
         * @function
         * @name pc.Application#loadSceneSettings
         * @description Load a scene file and apply the scene settings to the current scene
         * @param {String} url The URL of the scene file. Usually this will be "scene_id.json"
         * @param {Function} callback The function called after the settings are applied. Passed (err) where err is null if no error occurred.
         * @example
         * app.loadSceneSettings("1000.json", function (err) {
         *     if (!err) {
         *       // success
         *     } else {
         *       // error
         *     }
         *   }
         * });
         */
        loadSceneSettings: function (url, callback) {
            this._sceneRegistry.loadSceneSettings(url, callback);
        },

        loadScene: function (url, callback) {
            this._sceneRegistry.loadScene(url, callback);
        },

        _preloadScripts: function (sceneData, callback) {
            if (!pc.script.legacy) {
                callback();
                return;
            }

            var self = this;

            self.systems.script.preloading = true;

            var scripts = this._getScriptReferences(sceneData);

            var i = 0, l = scripts.length;
            var progress = new Progress(l);
            var scriptUrl;
            var regex = /^http(s)?:\/\//;

            if (l) {
                var onLoad = function (err, ScriptType) {
                    if (err)
                        console.error(err);

                    progress.inc();
                    if (progress.done()) {
                        self.systems.script.preloading = false;
                        callback();
                    }
                };

                for (i = 0; i < l; i++) {
                    scriptUrl = scripts[i];
                    // support absolute URLs (for now)
                    if (!regex.test(scriptUrl.toLowerCase()) && self._scriptPrefix)
                        scriptUrl = pc.path.join(self._scriptPrefix, scripts[i]);

                    this.loader.load(scriptUrl, 'script', onLoad);
                }
            } else {
                self.systems.script.preloading = false;
                callback();
            }
        },

        // set application properties from data file
        _parseApplicationProperties: function (props, callback) {
            var i;
            var len;

            // TODO: remove this temporary block after migrating properties
            if (!props.useDevicePixelRatio)
                props.useDevicePixelRatio = props.use_device_pixel_ratio;
            if (!props.resolutionMode)
                props.resolutionMode = props.resolution_mode;
            if (!props.fillMode)
                props.fillMode = props.fill_mode;
            if (!props.vrPolyfillUrl)
                props.vrPolyfillUrl = props.vr_polyfill_url;

            this._width = props.width;
            this._height = props.height;
            if (props.useDevicePixelRatio) {
                this.graphicsDevice.maxPixelRatio = window.devicePixelRatio;
            }

            this.setCanvasResolution(props.resolutionMode, this._width, this._height);
            this.setCanvasFillMode(props.fillMode, this._width, this._height);

            // if VR is enabled in the project and there is no native VR support
            // load the polyfill
            if (props.vr && props.vrPolyfillUrl) {
                if (!pc.VrManager.isSupported || pc.platform.android) {
                    props.libraries.push(props.vrPolyfillUrl);
                }
            }

            // set up layers
            if (props.layers && props.layerOrder) {
                var composition = new pc.LayerComposition();

                var layers = {};
                for (var key in props.layers) {
                    var data = props.layers[key];
                    data.id = parseInt(key, 10);
                    // depth layer should only be enabled when needed
                    // by incrementing its ref counter
                    data.enabled = data.id !== pc.LAYERID_DEPTH;
                    layers[key] = new pc.Layer(data);
                }

                for (i = 0, len = props.layerOrder.length; i < len; i++) {
                    var sublayer = props.layerOrder[i];
                    var layer = layers[sublayer.layer];
                    if (!layer) continue;

                    if (sublayer.transparent) {
                        composition.pushTransparent(layer);
                    } else {
                        composition.pushOpaque(layer);
                    }

                    composition.subLayerEnabled[i] = sublayer.enabled;
                }

                this.scene.layers = composition;
            }

            // add batch groups
            if (props.batchGroups) {
                for (i = 0, len = props.batchGroups.length; i < len; i++) {
                    var grp = props.batchGroups[i];
                    this.batcher.addGroup(grp.name, grp.dynamic, grp.maxAabbSize, grp.id, grp.layers);
                }

            }

            // set localization assets
            if (props.i18nAssets) {
                this.i18n.assets = props.i18nAssets;
            }

            this._loadLibraries(props.libraries, callback);
        },

        _loadLibraries: function (urls, callback) {
            var len = urls.length;
            var count = len;
            var self = this;

            var regex = /^http(s)?:\/\//;

            if (len) {
                var onLoad = function (err, script) {
                    count--;
                    if (err) {
                        callback(err);
                    } else if (count === 0) {
                        self.onLibrariesLoaded();
                        callback(null);
                    }
                };

                for (var i = 0; i < len; ++i) {
                    var url = urls[i];

                    if (!regex.test(url.toLowerCase()) && self._scriptPrefix)
                        url = pc.path.join(self._scriptPrefix, url);

                    this.loader.load(url, 'script', onLoad);
                }
            } else {
                self.onLibrariesLoaded();
                callback(null);
            }
        },

        // insert scene name/urls into the registry
        _parseScenes: function (scenes) {
            if (!scenes) return;

            for (var i = 0; i < scenes.length; i++) {
                this._sceneRegistry.add(scenes[i].name, scenes[i].url);
            }
        },

        // insert assets into registry
        _parseAssets: function (assets) {
            var i, id;
            var list = [];

            var scriptsIndex = {};
            var bundlesIndex = {};

            if (!pc.script.legacy) {
                // add scripts in order of loading first
                for (i = 0; i < this.scriptsOrder.length; i++) {
                    id = this.scriptsOrder[i];
                    if (!assets[id])
                        continue;

                    scriptsIndex[id] = true;
                    list.push(assets[id]);
                }

                // then add bundles
                if (this.enableBundles) {
                    for (id in assets) {
                        if (assets[id].type === 'bundle') {
                            bundlesIndex[id] = true;
                            list.push(assets[id]);
                        }
                    }
                }

                // then add rest of assets
                for (id in assets) {
                    if (scriptsIndex[id] || bundlesIndex[id])
                        continue;

                    list.push(assets[id]);
                }
            } else {
                if (this.enableBundles) {
                    // add bundles
                    for (id in assets) {
                        if (assets[id].type === 'bundle') {
                            bundlesIndex[id] = true;
                            list.push(assets[id]);
                        }
                    }
                }


                // then add rest of assets
                for (id in assets) {
                    if (bundlesIndex[id])
                        continue;

                    list.push(assets[id]);
                }
            }

            for (i = 0; i < list.length; i++) {
                var data = list[i];
                var asset = new pc.Asset(data.name, data.type, data.file, data.data);
                asset.id = parseInt(data.id, 10);
                asset.preload = data.preload ? data.preload : false;
                // if this is a script asset and has already been embedded in the page then
                // mark it as loaded
                asset.loaded = data.type === 'script' && data.data && data.data.loadingType > 0;
                // tags
                asset.tags.add(data.tags);
                // i18n
                if (data.i18n) {
                    for (var locale in data.i18n) {
                        asset.addLocalizedAssetId(locale, data.i18n[locale]);
                    }
                }
                // registry
                this.assets.add(asset);
            }
        },

        _getScriptReferences: function (scene) {
            var i, key;

            var priorityScripts = [];
            if (scene.settings.priority_scripts) {
                priorityScripts = scene.settings.priority_scripts;
            }

            var _scripts = [];
            var _index = {};

            // first add priority scripts
            for (i = 0; i < priorityScripts.length; i++) {
                _scripts.push(priorityScripts[i]);
                _index[priorityScripts[i]] = true;
            }

            // then iterate hierarchy to get referenced scripts
            var entities = scene.entities;
            for (key in entities) {
                if (!entities[key].components.script) {
                    continue;
                }

                var scripts = entities[key].components.script.scripts;
                for (i = 0; i < scripts.length; i++) {
                    if (_index[scripts[i].url])
                        continue;
                    _scripts.push(scripts[i].url);
                    _index[scripts[i].url] = true;
                }
            }

            return _scripts;
        },

        /**
         * @function
         * @name pc.Application#start
         * @description Start the Application updating
         */
        start: function () {
            this.frame = 0;

            this.fire("start", {
                timestamp: pc.now(),
                target: this
            });

            if (!this._librariesLoaded) {
                this.onLibrariesLoaded();
            }

            pc.ComponentSystem.initialize(this.root);
            this.fire("initialize");

            pc.ComponentSystem.postInitialize(this.root);
            this.fire("postinitialize");

            this.tick();
        },

        /**
         * @function
         * @name pc.Application#update
         * @description Application specific update method. Override this if you have a custom Application
         * @param {Number} dt The time delta since the last frame.
         */
        update: function (dt) {
            this.frame++;

            this.graphicsDevice.updateClientRect();

            if (this.vr) this.vr.poll();

            // #ifdef PROFILER
            this.stats.frame.updateStart = pc.now();
            // #endif

            // Perform ComponentSystem update
            if (pc.script.legacy)
                pc.ComponentSystem.fixedUpdate(1.0 / 60.0, this._inTools);

            pc.ComponentSystem.update(dt, this._inTools);
            pc.ComponentSystem.postUpdate(dt, this._inTools);

            // fire update event
            this.fire("update", dt);

            if (this.controller) {
                this.controller.update(dt);
            }
            if (this.mouse) {
                this.mouse.update(dt);
            }
            if (this.keyboard) {
                this.keyboard.update(dt);
            }
            if (this.gamepads) {
                this.gamepads.update(dt);
            }

            // #ifdef PROFILER
            this.stats.frame.updateTime = pc.now() - this.stats.frame.updateStart;
            // #endif
        },

        /**
         * @function
         * @name pc.Application#render
         * @description Application specific render method. Override this if you have a custom Application
         */
        render: function () {
            // #ifdef PROFILER
            this.stats.frame.renderStart = pc.now();
            // #endif

            this.fire("prerender");
            this.root.syncHierarchy();
            this.batcher.updateAll();
            pc._skipRenderCounter = 0;
            this.renderer.renderComposition(this.scene.layers);
            this.fire("postrender");

            // #ifdef PROFILER
            this.stats.frame.renderTime = pc.now() - this.stats.frame.renderStart;
            // #endif
        },

        _fillFrameStats: function (now, dt, ms) {
            // Timing stats
            var stats = this.stats.frame;
            stats.dt = dt;
            stats.ms = ms;
            if (now > stats._timeToCountFrames) {
                stats.fps = stats._fpsAccum;
                stats._fpsAccum = 0;
                stats._timeToCountFrames = now + 1000;
            } else {
                stats._fpsAccum++;
            }

            // Render stats
            stats.cameras = this.renderer._camerasRendered;
            stats.materials = this.renderer._materialSwitches;
            stats.shaders = this.graphicsDevice._shaderSwitchesPerFrame;
            stats.shadowMapUpdates = this.renderer._shadowMapUpdates;
            stats.shadowMapTime = this.renderer._shadowMapTime;
            stats.depthMapTime = this.renderer._depthMapTime;
            stats.forwardTime = this.renderer._forwardTime;
            var prims = this.graphicsDevice._primsPerFrame;
            stats.triangles = prims[pc.PRIMITIVE_TRIANGLES] / 3 +
                Math.max(prims[pc.PRIMITIVE_TRISTRIP] - 2, 0) +
                Math.max(prims[pc.PRIMITIVE_TRIFAN] - 2, 0);
            stats.cullTime = this.renderer._cullTime;
            stats.sortTime = this.renderer._sortTime;
            stats.skinTime = this.renderer._skinTime;
            stats.morphTime = this.renderer._morphTime;
            stats.instancingTime = this.renderer._instancingTime;
            stats.otherPrimitives = 0;
            for (var i = 0; i < prims.length; i++) {
                if (i < pc.PRIMITIVE_TRIANGLES) {
                    stats.otherPrimitives += prims[i];
                }
                prims[i] = 0;
            }
            this.renderer._camerasRendered = 0;
            this.renderer._materialSwitches = 0;
            this.renderer._shadowMapUpdates = 0;
            this.graphicsDevice._shaderSwitchesPerFrame = 0;
            this.renderer._cullTime = 0;
            this.renderer._sortTime = 0;
            this.renderer._skinTime = 0;
            this.renderer._morphTime = 0;
            this.renderer._instancingTime = 0;
            this.renderer._shadowMapTime = 0;
            this.renderer._depthMapTime = 0;
            this.renderer._forwardTime = 0;

            // Draw call stats
            stats = this.stats.drawCalls;
            stats.forward = this.renderer._forwardDrawCalls;
            stats.culled = this.renderer._numDrawCallsCulled;
            stats.depth = 0;
            stats.shadow = this.renderer._shadowDrawCalls;
            stats.skinned = this.renderer._skinDrawCalls;
            stats.immediate = 0;
            stats.instanced = 0;
            stats.removedByInstancing = 0;
            stats.total = this.graphicsDevice._drawCallsPerFrame;
            stats.misc = stats.total - (stats.forward + stats.shadow);
            this.renderer._depthDrawCalls = 0;
            this.renderer._shadowDrawCalls = 0;
            this.renderer._forwardDrawCalls = 0;
            this.renderer._numDrawCallsCulled = 0;
            this.renderer._skinDrawCalls = 0;
            this.renderer._immediateRendered = 0;
            this.renderer._instancedDrawCalls = 0;
            this.renderer._removedByInstancing = 0;
            this.graphicsDevice._drawCallsPerFrame = 0;

            this.stats.misc.renderTargetCreationTime = this.graphicsDevice.renderTargetCreationTime;

            stats = this.stats.particles;
            stats.updatesPerFrame = stats._updatesPerFrame;
            stats.frameTime = stats._frameTime;
            stats._updatesPerFrame = 0;
            stats._frameTime = 0;
        },

        /**
         * @function
         * @name pc.Application#setCanvasFillMode
         * @description Controls how the canvas fills the window and resizes when the window changes.
         * @param {String} mode The mode to use when setting the size of the canvas. Can be:
         * <ul>
         *     <li>pc.FILLMODE_NONE: the canvas will always match the size provided.</li>
         *     <li>pc.FILLMODE_FILL_WINDOW: the canvas will simply fill the window, changing aspect ratio.</li>
         *     <li>pc.FILLMODE_KEEP_ASPECT: the canvas will grow to fill the window as best it can while maintaining the aspect ratio.</li>
         * </ul>
         * @param {Number} [width] The width of the canvas (only used when mode is pc.FILLMODE_NONE).
         * @param {Number} [height] The height of the canvas (only used when mode is pc.FILLMODE_NONE).
         */
        setCanvasFillMode: function (mode, width, height) {
            this._fillMode = mode;
            this.resizeCanvas(width, height);
        },

        /**
         * @function
         * @name pc.Application#setCanvasResolution
         * @description Change the resolution of the canvas, and set the way it behaves when the window is resized
         * @param {String} mode The mode to use when setting the resolution. Can be:
         * <ul>
         *     <li>pc.RESOLUTION_AUTO: if width and height are not provided, canvas will be resized to match canvas client size.</li>
         *     <li>pc.RESOLUTION_FIXED: resolution of canvas will be fixed.</li>
         * </ul>
         * @param {Number} [width] The horizontal resolution, optional in AUTO mode, if not provided canvas clientWidth is used
         * @param {Number} [height] The vertical resolution, optional in AUTO mode, if not provided canvas clientHeight is used
         */
        setCanvasResolution: function (mode, width, height) {
            this._resolutionMode = mode;

            // In AUTO mode the resolution is the same as the canvas size, unless specified
            if (mode === pc.RESOLUTION_AUTO && (width === undefined)) {
                width = this.graphicsDevice.canvas.clientWidth;
                height = this.graphicsDevice.canvas.clientHeight;
            }

            this.graphicsDevice.resizeCanvas(width, height);
        },

        /**
         * @function
         * @name pc.Application#isHidden
         * @description Queries the visibility of the window or tab in which the application is running.
         * @returns {Boolean} True if the application is not visible and false otherwise.
         */
        isHidden: function () {
            return document[this._hiddenAttr];
        },

        /**
         * @private
         * @function
         * @name pc.Application#onVisibilityChange
         * @description Called when the visibility state of the current tab/window changes
         */
        onVisibilityChange: function () {
            if (this.isHidden()) {
                this._audioManager.suspend();
            } else {
                this._audioManager.resume();
            }
        },

        /**
         * @function
         * @name pc.Application#resizeCanvas
         * @description Resize the canvas in line with the current FillMode
         * In KEEP_ASPECT mode, the canvas will grow to fill the window as best it can while maintaining the aspect ratio
         * In FILL_WINDOW mode, the canvas will simply fill the window, changing aspect ratio
         * In NONE mode, the canvas will always match the size provided
         * @param {Number} [width] The width of the canvas, only used in NONE mode
         * @param {Number} [height] The height of the canvas, only used in NONE mode
         * @returns {Object} A object containing the values calculated to use as width and height
         */
        resizeCanvas: function (width, height) {
            if (!this._allowResize) return; // prevent resizing (e.g. if presenting in VR HMD)

            var windowWidth = window.innerWidth;
            var windowHeight = window.innerHeight;

            if (navigator.isCocoonJS) {
                width = windowWidth;
                height = windowHeight;

                this.graphicsDevice.resizeCanvas(width, height);
            } else {
                if (this._fillMode === pc.FILLMODE_KEEP_ASPECT) {
                    var r = this.graphicsDevice.canvas.width / this.graphicsDevice.canvas.height;
                    var winR = windowWidth / windowHeight;

                    if (r > winR) {
                        width = windowWidth;
                        height = width / r;
                    } else {
                        height = windowHeight;
                        width = height * r;
                    }
                } else if (this._fillMode === pc.FILLMODE_FILL_WINDOW) {
                    width = windowWidth;
                    height = windowHeight;
                } else {
                    // FILLMODE_NONE use width and height that are provided
                }

                this.graphicsDevice.canvas.style.width = width + 'px';
                this.graphicsDevice.canvas.style.height = height + 'px';

                // In AUTO mode the resolution is changed to match the canvas size
                if (this._resolutionMode === pc.RESOLUTION_AUTO) {
                    this.setCanvasResolution(pc.RESOLUTION_AUTO);
                }
            }

            // return the final values calculated for width and height
            return {
                width: width,
                height: height
            };
        },

        /**
         * @private
         * @name pc.Application#onLibrariesLoaded
         * @description Event handler called when all code libraries have been loaded
         * Code libraries are passed into the constructor of the Application and the application won't start running or load packs until all libraries have
         * been loaded
         */
        onLibrariesLoaded: function () {
            this._librariesLoaded = true;
            this.systems.rigidbody.onLibraryLoaded();
            this.systems.collision.onLibraryLoaded();
        },

        applySceneSettings: function (settings) {
            var asset;

            if (this.systems.rigidbody && typeof Ammo !== 'undefined') {
                var gravity = settings.physics.gravity;
                this.systems.rigidbody.gravity.set(gravity[0], gravity[1], gravity[2]);
            }

            this.scene.applySettings(settings);

            if (settings.render.hasOwnProperty('skybox')) {
                if (settings.render.skybox) {
                    asset = this.assets.get(settings.render.skybox);

                    if (asset) {
                        this.setSkybox(asset);
                    } else {
                        this.assets.once('add:' + settings.render.skybox, this.setSkybox, this);
                    }
                } else {
                    this.setSkybox(null);
                }
            }
        },

        /**
         * @function
         * @name pc.Application#setSkybox
         * @description Sets the skybox asset to current scene, and subscribes to asset load/change events
         * @param {pc.Asset} asset Asset of type `skybox` to be set to, or null to remove skybox
         */
        setSkybox: function (asset) {
            if (asset) {
                if (this._skyboxLast === asset.id) {
                    if (this.scene.skyboxMip === 0 && !asset.loadFaces) {
                        this._skyboxLoad(asset);
                    } else {
                        this._onSkyboxChange(asset);
                    }
                    return;
                }

                if (this._skyboxLast) {
                    this.assets.off('add:' + this._skyboxLast, this.setSkybox, this);
                    this.assets.off('load:' + this._skyboxLast, this._onSkyboxChange, this);
                    this.assets.off('remove:' + this._skyboxLast, this._skyboxRemove, this);
                }

                this._skyboxLast = asset.id;

                this.assets.on('load:' + asset.id, this._onSkyboxChange, this);
                this.assets.once('remove:' + asset.id, this._skyboxRemove, this);

                if (asset.resource)
                    this.scene.setSkybox(asset.resources);

                this._skyboxLoad(asset);
            } else {
                if (!this._skyboxLast)
                    return;

                this._skyboxRemove({
                    id: this._skyboxLast
                });
            }
        },

        _onVrChange: function (enabled) {
            if (enabled) {
                if (!this.vr) {
                    this.vr = new pc.VrManager(this);
                }
            } else {
                if (this.vr) {
                    this.vr.destroy();
                    this.vr = null;
                }
            }
        },

        _onSkyboxChange: function (asset) {
            this.scene.setSkybox(asset.resources);
        },

        _skyboxLoad: function (asset) {
            if (this.scene.skyboxMip === 0)
                asset.loadFaces = true;

            this.assets.load(asset);

            this._onSkyboxChange(asset);
        },

        _skyboxRemove: function (asset) {
            if (!this._skyboxLast)
                return;

            this.assets.off('add:' + asset.id, this.setSkybox, this);
            this.assets.off('load:' + asset.id, this._onSkyboxChange, this);
            this.assets.off('remove:' + asset.id, this._skyboxRemove, this);
            this.scene.setSkybox(null);
            this._skyboxLast = null;
        },

        _firstBake: function () {
            this.lightmapper.bake(null, this.scene.lightmapMode);
        },

        _firstBatch: function () {
            if (this.scene._needsStaticPrepare) {
                this.renderer.prepareStaticMeshes(this.graphicsDevice, this.scene);
                this.scene._needsStaticPrepare = false;
            }
            this.batcher.generate();
        },

        _processTimestamp: function (timestamp) {
            return timestamp;
        },

        /**
         * @function
         * @name pc.Application#destroy
         * @description Destroys application and removes all event listeners.
         */
        destroy: function () {
            var i, l;
            var canvasId = this.graphicsDevice.canvas.id;

            this.off('librariesloaded');
            document.removeEventListener('visibilitychange', this._visibilityChangeHandler, false);
            document.removeEventListener('mozvisibilitychange', this._visibilityChangeHandler, false);
            document.removeEventListener('msvisibilitychange', this._visibilityChangeHandler, false);
            document.removeEventListener('webkitvisibilitychange', this._visibilityChangeHandler, false);
            this._visibilityChangeHandler = null;
            this.onVisibilityChange = null;

            this.root.destroy();
            this.root = null;

            if (this.mouse) {
                this.mouse.off();
                this.mouse.detach();
                this.mouse = null;
            }

            if (this.keyboard) {
                this.keyboard.off();
                this.keyboard.detach();
                this.keyboard = null;
            }

            if (this.touch) {
                this.touch.off();
                this.touch.detach();
                this.touch = null;
            }

            if (this.elementInput) {
                this.elementInput.detach();
                this.elementInput = null;
            }

            if (this.controller) {
                this.controller = null;
            }

            var systems = this.systems.list;
            for (i = 0, l = systems.length; i < l; i++) {
                systems[i].destroy();
            }

            pc.ComponentSystem.destroy();

            // destroy all texture resources
            var assets = this.assets.list();
            for (i = 0; i < assets.length; i++) {
                assets[i].unload();
                assets[i].off();
            }
            this.assets.off();


            // destroy bundle registry
            this.bundles.destroy();
            this.bundles = null;

            this.i18n.destroy();
            this.i18n = null;

            for (var key in this.loader.getHandler('script')._cache) {
                var element = this.loader.getHandler('script')._cache[key];
                var parent = element.parentNode;
                if (parent) parent.removeChild(element);
            }
            this.loader.getHandler('script')._cache = {};

            this.loader.destroy();
            this.loader = null;

            this.scene.destroy();
            this.scene = null;

            this.systems = [];
            this.context = null;

            // script registry
            this.scripts.destroy();
            this.scripts = null;

            this._sceneRegistry.destroy();
            this._sceneRegistry = null;

            this.lightmapper.destroy();
            this.lightmapper = null;

            this.batcher.destroyManager();
            this.batcher = null;

            this._entityIndex = {};

            this.defaultLayerDepth.onPreRenderOpaque = null;
            this.defaultLayerDepth.onPostRenderOpaque = null;
            this.defaultLayerDepth.onDisable = null;
            this.defaultLayerDepth.onEnable = null;
            this.defaultLayerDepth = null;
            this.defaultLayerWorld = null;

            pc.destroyPostEffectQuad();

            this.graphicsDevice.destroy();
            this.graphicsDevice = null;

            this.renderer = null;
            this.tick = null;

            this.off(); // remove all events

            if (this._audioManager) {
                this._audioManager.destroy();
                this._audioManager = null;
            }

            pc.http = new pc.Http();
            pc.script.app = null;
            // remove default particle texture
            pc.ParticleEmitter.DEFAULT_PARAM_TEXTURE = null;

            Application._applications[canvasId] = null;

            if (Application._currentApplication === this) {
                Application._currentApplication = null;
            }
        }
    });

    // static data
    var _frameEndData = {};

    // create tick function to be wrapped in closure
    var makeTick = function (_app) {
        var app = _app;
        return function (timestamp) {
            if (!app.graphicsDevice) {
                return;
            }

            Application._currentApplication = app;

            // have current application pointer in pc
            pc.app = app;

            var now = app._processTimestamp(timestamp) || pc.now();
            var ms = now - (app._time || now);
            var dt = ms / 1000.0;
            dt = pc.math.clamp(dt, 0, app.maxDeltaTime);
            dt *= app.timeScale;

            app._time = now;

            // Submit a request to queue up a new animation frame immediately
            if (app.vr && app.vr.display) {
                app.vr.display.requestAnimationFrame(app.tick);
            } else {
                window.requestAnimationFrame(app.tick);
            }

            if (app.graphicsDevice.contextLost) {
                return;
            }

            // #ifdef PROFILER
            app._fillFrameStats(now, dt, ms);
            // #endif

            app.update(dt);

            if (app.autoRender || app.renderNextFrame) {
                app.render();
                app.renderNextFrame = false;
            }

            // set event data
            _frameEndData.timestamp = pc.now();
            _frameEndData.target = app;

            app.fire("frameend", _frameEndData);
            app.fire("frameEnd", _frameEndData);// deprecated old event, remove when editor updated

            if (app.vr && app.vr.display && app.vr.display.presenting) {
                app.vr.display.submitFrame();
            }
        };
    };

    return {
        /**
         * @enum pc.FILLMODE
         * @name pc.FILLMODE_NONE
         * @description When resizing the window the size of the canvas will not change.
         */
        FILLMODE_NONE: 'NONE',
        /**
         * @enum pc.FILLMODE
         * @name pc.FILLMODE_FILL_WINDOW
         * @description When resizing the window the size of the canvas will change to fill the window exactly.
         */
        FILLMODE_FILL_WINDOW: 'FILL_WINDOW',
        /**
         * @enum pc.FILLMODE
         * @name pc.FILLMODE_KEEP_ASPECT
         * @description When resizing the window the size of the canvas will change to fill the window as best it can, while maintaining the same aspect ratio.
         */
        FILLMODE_KEEP_ASPECT: 'KEEP_ASPECT',
        /**
         * @enum pc.RESOLUTION
         * @name pc.RESOLUTION_AUTO
         * @description When the canvas is resized the resolution of the canvas will change to match the size of the canvas.
         */
        RESOLUTION_AUTO: 'AUTO',
        /**
         * @enum pc.RESOLUTION
         * @name pc.RESOLUTION_FIXED
         * @description When the canvas is resized the resolution of the canvas will remain at the same value and the output will just be scaled to fit the canvas.
         */
        RESOLUTION_FIXED: 'FIXED',

        Application: Application
    };
}());