From bb8bf127faba6fd8899cdebd011f7ecae42cd24e Mon Sep 17 00:00:00 2001 From: David Manthey Date: Tue, 8 Nov 2016 14:35:15 -0500 Subject: [PATCH] Remove the lag between layers. Pedantically, this wasn't a lag, but rather missed frames. A variety of actions can trigger rerendering. We want to perform animations before other updates. To ensure that renderings are well ordered and not called too often, we were removing an animation frame request and then creating a new one to ensure that the callback happened at the end of the animation frame cycle. However, sometimes this would defer the animation frame callback until a later cycle rather than just at the end of the next cycle. By using a single animation queue, we ensure that get all of the callbacks we want in the animation cycle. This has the fringe benefit that it has less overhead in Chrome. Furthermore, by using a shared array for the animation queue, two map instances can be synchronized, whereas, before, there was likely to be a lag between them. A few other minor optimizations have been made: vgl textures are built in the _build step not in the animation frame. The vgl camera is only updated just as needed, so if two pan events happen in a single frame, only one update will occur. --- src/d3/d3Renderer.js | 10 ++---- src/gl/polygonFeature.js | 4 +-- src/gl/quadFeature.js | 3 +- src/gl/vglRenderer.js | 25 +++++++++------ src/map.js | 58 ++++++++++++++++++++++++++++++++-- src/mapInteractor.js | 4 +-- src/util/init.js | 2 +- tests/cases/d3GraphFeature.js | 2 +- tests/cases/d3PointFeature.js | 2 +- tests/cases/d3VectorFeature.js | 2 +- tests/cases/map.js | 2 +- tests/cases/mapInteractor.js | 7 ++-- tests/test-utils.js | 1 + 13 files changed, 90 insertions(+), 32 deletions(-) diff --git a/src/d3/d3Renderer.js b/src/d3/d3Renderer.js index e24b90d8c6..3573bf1471 100644 --- a/src/d3/d3Renderer.js +++ b/src/d3/d3Renderer.js @@ -40,7 +40,6 @@ var d3Renderer = function (arg) { m_diagonal = null, m_scale = 1, m_transform = {dx: 0, dy: 0, rx: 0, ry: 0, rotation: 0}, - m_renderAnimFrameRef = null, m_renderIds = {}, m_removeIds = {}, m_svg = null, @@ -509,9 +508,7 @@ var d3Renderer = function (arg) { m_this._renderFeature(id, parentId); } else { m_renderIds[id] = true; - if (m_renderAnimFrameRef === null) { - m_renderAnimFrameRef = window.requestAnimationFrame(m_this._renderFrame); - } + m_this.layer().map().scheduleAnimationFrame(m_this._renderFrame); } }; @@ -524,7 +521,6 @@ var d3Renderer = function (arg) { m_removeIds = {}; var ids = m_renderIds; m_renderIds = {}; - m_renderAnimFrameRef = null; for (id in ids) { if (ids.hasOwnProperty(id)) { m_this._renderFeature(id); @@ -578,9 +574,7 @@ var d3Renderer = function (arg) { //////////////////////////////////////////////////////////////////////////// this._removeFeature = function (id) { m_removeIds[id] = true; - if (m_renderAnimFrameRef === null) { - m_renderAnimFrameRef = window.requestAnimationFrame(m_this._renderFrame); - } + m_this.layer().map().scheduleAnimationFrame(m_this._renderFrame); delete m_features[id]; if (m_renderIds[id]) { delete m_renderIds[id]; diff --git a/src/gl/polygonFeature.js b/src/gl/polygonFeature.js index 443827e3c7..8c12bd6b66 100644 --- a/src/gl/polygonFeature.js +++ b/src/gl/polygonFeature.js @@ -340,11 +340,11 @@ var gl_polygonFeature = function (arg) { //////////////////////////////////////////////////////////////////////////// this._update = function (opts) { if (opts && opts.mayDelay) { - m_updateAnimFrameRef = window.requestAnimationFrame(this._update); + m_updateAnimFrameRef = m_this.layer().map().scheduleAnimationFrame(m_this._update); return; } if (m_updateAnimFrameRef) { - window.cancelAnimationFrame(m_updateAnimFrameRef); + m_this.layer().map().scheduleAnimationFrame(m_this._update, 'remove'); m_updateAnimFrameRef = null; } s_update.call(m_this); diff --git a/src/gl/quadFeature.js b/src/gl/quadFeature.js index e0f105362b..54c594daca 100644 --- a/src/gl/quadFeature.js +++ b/src/gl/quadFeature.js @@ -239,6 +239,7 @@ var gl_quadFeature = function (arg) { if (m_clrModelViewUniform) { m_clrModelViewUniform.setOrigin(m_quads.origin); } + m_this._updateTextures(); m_this.buildTime().modified(); }; @@ -330,8 +331,6 @@ var gl_quadFeature = function (arg) { opacity = 1, crop = {x: 1, y: 1}, quadcrop; - m_this._updateTextures(); - context.bindBuffer(vgl.GL.ARRAY_BUFFER, m_glBuffers.imgQuadsPosition); $.each(m_quads.imgQuads, function (idx, quad) { if (!quad.image) { diff --git a/src/gl/vglRenderer.js b/src/gl/vglRenderer.js index 0dd94c5a10..be93d53938 100644 --- a/src/gl/vglRenderer.js +++ b/src/gl/vglRenderer.js @@ -32,8 +32,8 @@ var vglRenderer = function (arg) { m_viewer = null, m_width = 0, m_height = 0, - m_renderAnimFrameRef = null, m_lastZoom, + m_updateCamera = false, s_init = this._init, s_exit = this._exit; @@ -121,7 +121,7 @@ var vglRenderer = function (arg) { m_this.canvas().attr('height', h); renderWindow.positionAndResize(x, y, w, h); - m_this._updateRendererCamera(); + m_updateCamera = true; m_this._render(); return m_this; @@ -133,10 +133,13 @@ var vglRenderer = function (arg) { */ //////////////////////////////////////////////////////////////////////////// this._render = function () { - if (m_renderAnimFrameRef) { - window.cancelAnimationFrame(m_renderAnimFrameRef); - } - m_renderAnimFrameRef = window.requestAnimationFrame(this._renderFrame); + /* If we are already scheduled to render, don't schedule again. Rather, + * mark that we should render after other animation frame requests occur. + * It would be nice if we could just reschedule the call by removing and + * readding the animation frame request, but this doesn't work for if the + * reschedule occurs during another animation frame callback (it then waits + * until a subsequent frame). */ + m_this.layer().map().scheduleAnimationFrame(this._renderFrame, true); return m_this; }; @@ -144,7 +147,10 @@ var vglRenderer = function (arg) { * This clears the render timer and actually renders. */ this._renderFrame = function () { - m_renderAnimFrameRef = null; + if (m_updateCamera) { + m_updateCamera = false; + m_this._updateRendererCamera(); + } m_viewer.render(); }; @@ -217,7 +223,7 @@ var vglRenderer = function (arg) { // produce a pan m_this.layer().geoOn(geo_event.pan, function (evt) { void (evt); - m_this._updateRendererCamera(); + m_updateCamera = true; }); // Connect to parallelprojection event @@ -230,10 +236,11 @@ var vglRenderer = function (arg) { if (!vglRenderer || !vglRenderer.camera()) { console.log('Parallel projection event triggered on unconnected VGL ' + 'renderer.'); + return; } camera = vglRenderer.camera(); camera.setEnableParallelProjection(evt.parallelProjection); - m_this._updateRendererCamera(); + m_updateCamera = true; } }); diff --git a/src/map.js b/src/map.js index 8ac5f1f986..e252b341f6 100644 --- a/src/map.js +++ b/src/map.js @@ -57,6 +57,9 @@ var sceneObject = require('./sceneObject'); * @param {geo.camera?} camera The camera to control the view * @param {geo.mapInteractor?} interactor The UI event handler * @param {geo.clock?} clock The clock used to synchronize time events + * @param {array} [animationQueue] An array used to synchonize animations. If + * specified, this should be an empty array or the same array as passed to + * other map instances. * @param {boolean} [autoResize=true] Adjust map size on window resize * @param {boolean} [clampBoundsX=false] Prevent panning outside of the * maximum bounds in the horizontal direction. @@ -127,6 +130,7 @@ var map = function (arg) { m_clampBoundsX, m_clampBoundsY, m_clampZoom, + m_animationQueue = arg.animationQueue || [], m_origin, m_scale = {x: 1, y: 1, z: 1}; // constant and ignored for the moment @@ -1223,7 +1227,7 @@ var map = function (arg) { } m_this.rotation(p[3], undefined, true); - window.requestAnimationFrame(anim); + m_this.scheduleAnimationFrame(anim); } m_this.geoTrigger(geo_event.transitionstart, opts); @@ -1239,7 +1243,7 @@ var map = function (arg) { } else if (animTime) { anim(animTime); } else { - window.requestAnimationFrame(anim); + m_this.scheduleAnimationFrame(anim); } return m_this; }; @@ -1560,6 +1564,56 @@ var map = function (arg) { return m_this; }; + /** + * Instead of each function using window.requestAnimationFrame, schedule all + * such frames here. This allows the callbacks to be reordered or removed as + * needed and reduces overhead in Chrome a small amount. Also, if the + * animation queue is shared between map instances, the callbacks will be + * called as one, providing better synchronization. + * + * @param {function} callback: function to call during the animation frame. + * It is called with an animation epoch, exactly as requestAnimationFrame. + * @param {string|boolean} action: falsy to only add the callback if it is + * not already scheduled. 'remove' to remove the callback (use this + * instead of cancelAnimationFrame). Any other truthy value moves the + * callback to the end of the list. + * @returns {integer} An integer as returned by window.requestAnimationFrame. + */ + this.scheduleAnimationFrame = function (callback, action) { + if (!m_animationQueue.length) { + /* By refering to requestAnimationFrame as a property of window, versus + * explicitly using window.requestAnimationFrame, we prevent the + * stripping of 'window' off of the reference and allow our tests to + * override this if needed. */ + m_animationQueue.push(window['requestAnimationFrame'](processAnimationFrame)); + } + var pos = m_animationQueue.indexOf(callback, 1); + if (pos >= 0) { + if (!action) { + return; + } + m_animationQueue.splice(pos, 1); + if (action === 'remove') { + return; + } + } + m_animationQueue.push(callback); + return m_animationQueue[0]; + }; + + /** + * Sevice the callback during an animation frame. This uses splice to modify + * the animationQueue to allow multiple map instances to share the queue. + */ + function processAnimationFrame() { + var queue = m_animationQueue.splice(0, m_animationQueue.length); + + /* The first entry is the reference to the window.requestAnimationFrame. */ + for (var i = 1; i < queue.length; i += 1) { + queue[i].apply(this, arguments); + } + } + //////////////////////////////////////////////////////////////////////////// // // The following are some private methods for interacting with the camera. diff --git a/src/mapInteractor.js b/src/mapInteractor.js index 82981f8904..fa7ffc64bc 100644 --- a/src/mapInteractor.js +++ b/src/mapInteractor.js @@ -1431,11 +1431,11 @@ var mapInteractor = function (args) { } if (m_state.handler) { - window.requestAnimationFrame(m_state.handler); + m_this.map().scheduleAnimationFrame(m_state.handler); } }; if (m_state.handler) { - window.requestAnimationFrame(m_state.handler); + m_this.map().scheduleAnimationFrame(m_state.handler); } }; diff --git a/src/util/init.js b/src/util/init.js index f187e3aee7..22f0e41feb 100644 --- a/src/util/init.js +++ b/src/util/init.js @@ -781,7 +781,7 @@ } else if (!stop && !m_originalRequestAnimationFrame) { m_originalRequestAnimationFrame = window.requestAnimationFrame; window.requestAnimationFrame = function (callback) { - m_originalRequestAnimationFrame.call(window, function (timestamp) { + return m_originalRequestAnimationFrame.call(window, function (timestamp) { var track = m_timingData.requestAnimationFrame, recent; /* Some environments have unsynchronized performance and time * counters. The nowDelta factor compensates for this. For diff --git a/tests/cases/d3GraphFeature.js b/tests/cases/d3GraphFeature.js index ee4ff01663..104b25392b 100644 --- a/tests/cases/d3GraphFeature.js +++ b/tests/cases/d3GraphFeature.js @@ -20,12 +20,12 @@ describe('d3 graph feature', function () { var map, layer, feature; it('Setup map', function () { + mockAnimationFrame(); map = geo.map({node: '#map-d3-graph-feature', center: [0, 0], zoom: 3}); layer = map.createLayer('feature', {'renderer': 'd3'}); }); it('Add features to a layer', function () { - mockAnimationFrame(); var selection, nodes; nodes = [ diff --git a/tests/cases/d3PointFeature.js b/tests/cases/d3PointFeature.js index d4ca8e5f5b..46a6bf4793 100644 --- a/tests/cases/d3PointFeature.js +++ b/tests/cases/d3PointFeature.js @@ -20,6 +20,7 @@ describe('d3 point feature', function () { var map, width = 800, height = 600, layer, feature1, feature2; it('Setup map', function () { + mockAnimationFrame(); map = geo.map({node: '#map-d3-point-feature', center: [0, 0], zoom: 3}); layer = map.createLayer('feature', {'renderer': 'd3'}); @@ -27,7 +28,6 @@ describe('d3 point feature', function () { }); it('Add features to a layer', function () { - mockAnimationFrame(); var selection; feature1 = layer.createFeature('point', {selectionAPI: true}) .data([{y: 0, x: 0}, {y: 10, x: 0}, {y: 0, x: 10}]) diff --git a/tests/cases/d3VectorFeature.js b/tests/cases/d3VectorFeature.js index aa3f65e500..357bf25cc5 100644 --- a/tests/cases/d3VectorFeature.js +++ b/tests/cases/d3VectorFeature.js @@ -10,6 +10,7 @@ describe('d3 vector feature', function () { var map, layer, feature1; it('Create a map with a d3 feature layer', function () { + mockAnimationFrame(); d3.select('body').append('div').attr('id', 'map-d3-vector'); map = geo.map({node: '#map-d3-vector', center: [0, 0], @@ -36,7 +37,6 @@ describe('d3 vector feature', function () { }); it('Add features to a layer', function () { - mockAnimationFrame(); var vectorLines, featureGroup, markers; feature1 = layer.createFeature('vector') .data([{y: 0, x: 0}, {y: 10, x: 0}, {y: 0, x: 10}]) diff --git a/tests/cases/map.js b/tests/cases/map.js index 3c9115ac7a..d470526a9d 100644 --- a/tests/cases/map.js +++ b/tests/cases/map.js @@ -390,8 +390,8 @@ describe('geo.core.map', function () { expect(closeToEqual(zc.center, {x: 0, y: 0})).toBe(true); }); it('transition', function () { - var m = create_map(), start, wasCalled; mockAnimationFrame(); + var m = create_map(), start, wasCalled; expect(m.transition()).toBe(null); start = new Date().getTime(); m.transition({ diff --git a/tests/cases/mapInteractor.js b/tests/cases/mapInteractor.js index 36fa490ebe..f3ab6585ff 100644 --- a/tests/cases/mapInteractor.js +++ b/tests/cases/mapInteractor.js @@ -136,6 +136,9 @@ describe('mapInteractor', function () { map.gcs = function (arg) { return 'EPSG:3857'; }; + map.scheduleAnimationFrame = function (callback) { + return window['requestAnimationFrame'](callback); + }; return map; } @@ -1327,6 +1330,7 @@ describe('mapInteractor', function () { }); it('Test momentum', function () { + mockAnimationFrame(); var map = mockedMap('#mapNode1'), start; var interactor = geo.mapInteractor({ @@ -1338,7 +1342,6 @@ describe('mapInteractor', function () { }], throttle: false }); - mockAnimationFrame(); mockDate(); // initiate a pan and release interactor.simulateEvent( @@ -1380,6 +1383,7 @@ describe('mapInteractor', function () { }); it('Test springback', function () { + mockAnimationFrame(); $('#mapNode1').css({width: '400px', height: '400px'}); var map = mockedMap('#mapNode1'), start; @@ -1393,7 +1397,6 @@ describe('mapInteractor', function () { }], throttle: false }); - mockAnimationFrame(); mockDate(); // pan past the max bounds interactor.simulateEvent( diff --git a/tests/test-utils.js b/tests/test-utils.js index b480ee64e3..49aecdcb35 100644 --- a/tests/test-utils.js +++ b/tests/test-utils.js @@ -341,6 +341,7 @@ module.exports.mockAnimationFrame = function (mockDate) { animFrameIndex += 1; var id = animFrameIndex; animFrameCallbacks.push({id: id, callback: callback}); + return id; } /* Replace window.cancelAnimationFrame with this function.