Impact

This forum is read only and just serves as an archive. If you have any questions, please post them on github.com/phoboslab/impact

1 decade ago by HawkleyFox

It seems ImpactJS is misusing the requestAnimationFrame() feature by having game logic execute during the callback. Whenever the callback is triggered, the browser passes in a rough estimate of how much time the callback has to draw before it misses the paint event.

Since requestAnimationFrame() will not occur again until after the previously delivered frame is painted, failing to deliver a frame in time means the game will be suspended until the next paint event.

This ends up slowing down the game drastically if its logic eats up enough time. The rAF callback should only be used for rendering to reduce the likelihood of this deadlock occurring.

My adjusted game loop works a little something like this:
doUpdate() is called, starting the loop.
requestAnimationFrame(doDraw)
doDraw() is called via callback.
setTimeout(doUpdate, 0)
doUpdate() is called via callback.
(...)

On slower systems, I have observed a 1.25 to 3 times increase in frame rate, dependent on how bad the performance was prior to the change. If you have a game that has some heavy logic in it, you may want to consider making this change yourself.

1 decade ago by Joncom

Interesting find... I wonder if you could provide a less "psudeo code" example so that people could more readily try out your tweak?

1 decade ago by HawkleyFox

I guess I can put it together. It does make changes to the way ig.Game interacts with ig.System, though. Classically, your game's run() function is where update+draw occurs, but since those had to be separated out, run() is no longer a good thing to use.

Also, my tweaks are in CoffeeScript, but I can compile them into JavaScript before hand.

Here is the changes to ig.System.
ig.module('game.system').requires('impact.system').defines(function() {
  var anims, curCallback, current, next, onInterval;
  ig.System.inject({
    setDelegate: function(object) {
      if (typeof object.update !== 'function') {
        throw 'System.setDelegate: No update() function in object';
      } else if (typeof object.draw !== 'function') {
        throw 'System.setDelegate: No draw() function in object';
      } else {
        this.delegate = object;
        return this.startRunLoop();
      }
    },
    stopRunLoop: function() {
      if (this.updateId != null) {
        ig.clearUpdate(this.updateId);
      }
      if (this.drawId != null) {
        ig.clearDraw(this.drawId);
      }
      return this.running = false;
    },
    startRunLoop: function() {
      var _this = this;
      this.stopRunLoop();
      this.updateId = ig.queueUpdate(function() {
        return _this.doUpdate();
      });
      this.drawId = null;
      return this.running = true;
    },
    doUpdate: function() {
      var _base,
        _this = this;
      if (!this.running) {
        return;
      }
      ig.Timer.step();
      this.tick = this.clock.tick();
      if (typeof (_base = this.delegate).frameStart === "function") {
        _base.frameStart();
      }
      this.delegate.update();
      ig.input.clearPressed();
      this.updateId = null;
      return ig.queueDraw((function() {
        return _this.doDraw();
      }), this.canvas);
    },
    doDraw: function() {
      var _base,
        _this = this;
      if (!this.running) {
        return;
      }
      this.delegate.draw();
      if (typeof (_base = this.delegate).frameEnd === "function") {
        _base.frameEnd();
      }
      this.drawId = null;
      return ig.queueUpdate(function() {
        if (_this.newGameClass) {
          _this.setGameNow(_this.newGameClass);
          _this.newGameClass = null;
        }
        return _this.doUpdate();
      });
    },
    run: function() {}
  });
  ig.queueUpdate = function(callback) {
    return window.setTimeout(callback, 0);
  };
  ig.clearUpdate = function(id) {
    window.clearTimeout(id);
  };
  next = 1;
  anims = {};
  if (window.requestAnimationFrame) {
    ig.queueDraw = function(callback, element) {
      var animate, current;
      current = next++;
      if (next > 20) {
        next = 1;
      }
      anims[current] = true;
      animate = function() {
        if (!anims[current]) {
          return;
        }
        callback();
        return delete anims[current];
      };
      window.requestAnimationFrame(animate, element);
      return current;
    };
  } else {
    current = 0;
    curCallback = function() {};
    onInterval = function() {
      if (!anims[current]) {
        return;
      }
      curCallback();
      return delete anims[current];
    };
    ig.queueDraw = function(callback) {
      current = next++;
      if (next > 20) {
        next = 1;
      }
      anims[current] = true;
      curCallback = callback;
      return current;
    };
    window.setInterval(onInterval, 1000 / 60);
  }
  return ig.clearDraw = function(id) {
    return delete anims[id];
  };
});

And to keep the built-in debugging going, I created a module to update ig.Debug.
ig.module('game.debug').requires('impact.debug.debug').defines(function() {
  return ig.System.inject({
    stopRunLoop: function() {
      if (this.updateId != null) {
        ig.debug.afterRun();
      }
      return this.parent();
    },
    doUpdate: function() {
      ig.debug.beforeRun();
      return this.parent();
    },
    doDraw: function() {
      this.parent();
      return ig.debug.afterRun();
    }
  });
});

One consequence of this change to ig.Debug is that the "frame time" readout will always read around 15ms if your computer can reach 60 fps. That's because, to the debug system, the frame isn't finished until its actually drawn, which may occur many milliseconds after update() has completed.

So, "frame time" only becomes relevant if the frame rate dips below 60fps.

Changes should be made to your ig.Game class:
Anything special you were doing in run() (beyond updating and drawing) should be moved into a frameStart() function. If you have any tasks that should run immediately after drawing, you can put them into frameEnd().

EDIT: Altered the way it works when requestAnimationFrame() is not available. Previous version locked IE9 to 30fps.

EDIT: Bug fixes, including removing a memory leak.

1 decade ago by HawkleyFox

And I'll throw in the original CoffeeScript too. It may be clearer to read for some people.
ig.module('game.system').requires('impact.system').defines ->
	ig.System.inject
		setDelegate: (object) ->
			if typeof object.update isnt 'function'
				throw 'System.setDelegate: No update() function in object'
			else if typeof object.draw isnt 'function'
				throw 'System.setDelegate: No draw() function in object'
			else
				@delegate = object
				@startRunLoop()

		stopRunLoop: ->
			ig.clearUpdate(@updateId) if @updateId?
			ig.clearDraw(@drawId) if @drawId?
			@running = false

		startRunLoop: ->
			@stopRunLoop()
			@updateId = ig.queueUpdate => @doUpdate()
			@drawId = null
			@running = true

		doUpdate: ->
			return unless @running
			ig.Timer.step()
			@tick = @clock.tick()
			@delegate.frameStart?()
			@delegate.update()
			ig.input.clearPressed()
			@updateId = null
			ig.queueDraw (=> @doDraw()), @canvas

		doDraw: ->
			return unless @running
			@delegate.draw()
			@delegate.frameEnd?()
			@drawId = null
			ig.queueUpdate =>
				if @newGameClass
					@setGameNow @newGameClass
					@newGameClass = null
				@doUpdate()

		run: -> return # no-op

	ig.queueUpdate = (callback) -> return window.setTimeout(callback, 0)
	ig.clearUpdate = (id) -> window.clearTimeout(id); return

	next = 1; anims = {}
	if window.requestAnimationFrame
		ig.queueDraw = (callback, element) ->
			current = next++
			next = 1 if next > 20
			anims[current] = true
			animate = ->
				return unless anims[current]
				callback()
				delete anims[current]
			window.requestAnimationFrame(animate, element)
			return current
	else
		current = 0
		curCallback = -> return
		onInterval = ->
			return unless anims[current]
			curCallback()
			delete anims[current]
		ig.queueDraw = (callback) ->
			current = next++
			next = 1 if next > 20
			anims[current] = true
			curCallback = callback
			return current
		window.setInterval(onInterval, 1000/60)
	ig.clearDraw = (id) -> delete anims[id]

ig.module('game.debug').requires('impact.debug.debug').defines ->
	ig.System.inject
		stopRunLoop: ->
			ig.debug.afterRun() if @updateId?
			@parent()

		doUpdate: ->
			ig.debug.beforeRun()
			@parent()

		doDraw: ->
			@parent()
			ig.debug.afterRun()

1 decade ago by dominic

Oh, wow. This is interesting. Do you know of any articles that document this behavior? The docs on MDN just state:
The callback method is passed a single argument, a DOMHighResTimeStamp, which indicates the time, in milliseconds but with a minimal precision of 10 µs, at which the repaint is scheduled to occur.


Pretty vague, but it sure sounds like this timestamp is indeed in the future. So, Impact's actual timestep should be the one provided in the rAF callback and not Date.now().

Wouldn't it make sense for the browser to call the rAF callback immediately after the last display refresh, so that the JS code has the full 16ms to do its work? I was previously under the impression that this is exactly how it behaves.

I'll play around with this all for a bit. Thanks for reporting this issue!

Edit:

Well, it seems MDN is lying. The timestamp passed in to the rAF callback is actually the current timestamp - it's not a future timestamp "at which the repaint is scheduled to occur".

var draw = function( timestamp ) {
	requestAnimationFrame(draw);
	
	// this always logs a negative value (about -1ms), 
	// meaning the rAF timestamp lies in the past.
	console.log(timestamp-performance.now());
};
requestAnimationFrame(draw);

Edit2:

Even if the execution time of the callback is close to 16ms, the browser still doesn't miss a frame. Meaning my initial assumption was correct and the implementation in Impact is fine!? I'm not completely sure what to make of this...

See this example; it busy-waits 14ms in the rAF callback before drawing. The framerate is still steady at 60fps.
http://phoboslab.org/crap/raf-test.html

1 decade ago by drhayes

Looks like MDN's wording is weird, because that's the behavior I've always seen: it's basically handing you "now".

From W3: http://www.w3.org/TR/animation-timing/#processingmodel

Let time be the result of invoking the now method of the Performance interface within this context.


I've read other articles about this and I'm kind of torn: yes, RAF should probably only be used for animations but I don't want my game logic running in a background tab sucking up battery either.

Also, setTimeout doesn't pass in a high-resolution time and, AFAIK, doesn't have performance guarantees around when it executes. Sure, you probably only need it to run every 16ms... but isn't that what RAF is gonna do for you anyway?

Incidentally, here's a cool article about 0ms loops using window.postMessage. But I digress.

1 decade ago by HawkleyFox

Quote from dominic
See this example; it busy-waits 14ms in the rAF callback before drawing. The framerate is still steady at 60fps.

All this stuff really only comes into play if your frame rate is below 60.
Quote from dominic
Wouldn't it make sense for the browser to call the rAF callback immediately after the last display refresh, so that the JS code has the full 16ms to do its work? I was previously under the impression that this is exactly how it behaves.

You have to remember, the web browser is doing a lot more above and beyond your own code. Web browsers these days are on the same level of complexity as an operating system. Resources must be shared with other windows, tabs, extensions... rAF is there to help the browser manage its time more intelligently.

In Chrome, all tabs must sync up at some point to be painted by the single GPU resource. Those tabs that miss the paint state are forced to wait until the paint state comes back around again. You're either on that train or you aren't.

In your rAF-Test, if I make the busy-wait 17ms instead, Chrome will slow to 30fps. Other browsers that still use CPU drawing will slow to the expected ~50fps. I was able to get Chrome running at 50fps again by implementing the rAF/setTimout pattern used in my plugin above. It was configured as follows:
var fpsInput = document.getElementById('fps');

var fps = 60;
var last = performance.now();
var draw = function( timestamp ) {
	var now = performance.now();
	fps = fps * 0.9 + (1000/(now-last)) * 0.1;
	last = now;
	
	while( performance.now() - now < 3 ) {};

	fpsInput.value = fps;
	
	window.setTimeout(update, 0);
};
var update = function() {
	var now = performance.now();
	while( performance.now() - now < 14 ) {};
	requestAnimationFrame(draw);
};

update();

As more browsers begin to rely on the GPU for almost all of its drawing tasks, this issue will crop up in other browsers as well. No matter what: draw in rAF, update someplace else.
Quote from drhayes
yes, RAF should probably only be used for animations but I don't want my game logic running in a background tab sucking up battery either.

The way I have it setup above, game logic will not execute at full speed unless the tab is in the foreground. The last thing the rAF callback does is prime the next game update.

Game updates are still tied to rAF, in other words. Your battery will not suffer. :P

Quote from drhayes
Also, setTimeout (...) doesn't have performance guarantees around when it executes.

The point of using setTimeout at all is to clear the current execution context so the browser can paint the frame. As far as I know, the setTimout callback fires the next time the browser begins consuming JavaScript events, which is (some time) after the paint.

I'd have to experiment to see if postMessage clears the execution context, as setTimeout does.

1 decade ago by quidmonkey

Something's not adding up...you haven't completely decoupled your run loop. So let's say your update does take ~17ms each frame. Yes, you'll drop to rendering every other frame using rAF only because of blowing your frame buffer; but the same would still apply using your setTImeout/rAF method, because the update would run, costing 17ms, and then the rAF would be scheduled for the next frame.

Are you doing any other processing that's causing delays? Say Ajax or event listeners? If you've got a lot of other concurrent processes on your Javascript thread, then that approach would make sense.

1 decade ago by HawkleyFox

What do you mean by 'not completely decoupled your run loop'? My goal isn't to decouple my updates from the run loop. If I were to do that, I'd be doing something very complicated with WebWorkers, instead. I'm really just modifying my code to not perform game logic updates during the rAF callback.

There may be some additional voodoo going on in the bowels of the browser that I haven't recognized yet, but I have explained it as I currently understand it, and frankly, it gets me a better frame rate, so I'm not complaining.

1 decade ago by drhayes

Hawkley: Ah, okay, I see the call to update at the end of the draw call. I wasn't reading closely enough before. That's really excellent.

postMessage is asynchronous, which should be good enough to get the JS event loop to tick another frame. But I'd love to see any results of your experiments.

How did you discover this? That (basically) means every requestAnimationFrame tutorial I've ever read is missing a subtle but very important point.

1 decade ago by HawkleyFox

Quote from drhayes
How did you discover this? That (basically) means every requestAnimationFrame tutorial I've ever read is missing a subtle but very important point.

That MDN article made me curious about it when I was hunting for ways to unlock some more performance. I decided to go research best practices regarding rAF, and learned that it was accepted that game updates should not be done in a rAF callback. So, I made a naive change to my run loop to see if it made a difference and saw a huge difference in performance on my ultrabook.

It was kind of a surprise; I thought it worked like Dominic assumed it worked (and on some browsers, it does), but once I saw the results, I decided to implement a more permanent solution to my game: the one I posted above.
Quote from drhayes
postMessage is asynchronous, which should be good enough to get the JS event loop to tick another frame. But I'd love to see any results of your experiments.

Like, truly asynchronous? That seems unlikely seeing as JavaScript has no facilities to handle multi-threaded processing. WebWorkers basically exist in their own, completely separate, JavaScript context to prevent race-conditions and other threading anomalies (though there are unofficial APIs that allow you to hand 'ownership' of an object to one thread or another).

1 decade ago by quidmonkey

Quote from HawkleyFox
What do you mean by 'not completely decoupled your run loop'? My goal isn't to decouple my updates from the run loop. If I were to do that, I'd be doing something very complicated with WebWorkers, instead. I'm really just modifying my code to not perform game logic updates during the rAF callback.

There may be some additional voodoo going on in the bowels of the browser that I haven't recognized yet, but I have explained it as I currently understand it, and frankly, it gets me a better frame rate, so I'm not complaining.


Oh, I don't doubt you see an improvement, and this is an interesting find; but I'm trying to wrap my head around what's happening. By decoupling, I mean having update call itself, and draw call itself, allowing you to have multiple update loops per draw. With this implementation you have a update process calling the draw process which calls the update process once more.

Are you developing a multiplayer game? Or is your Impact game embedded on a page with lots of other content?

1 decade ago by HawkleyFox

Quote from quidmonkey
By decoupling, I mean having update call itself, and draw call itself, allowing you to have multiple update loops per draw.

Ahh, I wouldn't recommend that. If your updates are already taking way too long, its even more likely that a draw call would go unanswered for longer than it should because of a second update call blocking things.

Besides, Impact's timer system already does a great job of handling long delays between updates. More than one update per frame shouldn't be needed, but you are welcome to try.

And no, my game is currently a no-bells-and-whistles platformer (I haven't coded the bells and whistles yet). It is embedded in the middle of a grey background HTML page like the samples were. What is making my updates relatively long on slow devices is the Box2D simulation. I have it integrated as a system in an entity-component system.

1 decade ago by quidmonkey

Do you know if the Box2D is using setTimeout for its step() loop?

1 decade ago by HawkleyFox

Quote from quidmonkey
Do you know if the Box2D is using setTimeout for its step() loop?

The version I am using does not have a loop. It expects you, the programmer, to call World.Step() when it is appropriate.

1 decade ago by drhayes

Hey Hawkley, sorry to revive a dead thread, but is that "dreta" account on GitHub yours?

1 decade ago by HawkleyFox

Quote from drhayes
Hey Hawkley, sorry to revive a dead thread, but is that "dreta" account on GitHub yours?

Nope. Just found it when looking for JavaScript entity-system libraries. It has been working out pretty well for me. Integrated it into an Impact plugin and gave it an API that takes advantage of many CoffeeScript features.

But, none of my Impact work is on Github yet. Maybe someday. Would love to share my components and systems when I feel they're mature enough.

1 decade ago by drhayes

Dude, just post it. ( =

Open source code is like art; it's never finished, just abandoned. And I think the more code out there the better as far as Impact adoption/learning go.

1 decade ago by dominic

Quote from HawkleyFox
In your rAF-Test, if I make the busy-wait 17ms instead, Chrome will slow to 30fps. Other browsers that still use CPU drawing will slow to the expected ~50fps.

The thing is, 50fps will look worse than 30fps. The reason is simply that your display's refresh rate is 60hz and 60 is not evenly divisible by 50, resulting in choppy movement. Allow me to illustrate with some ASII art:

display refresh (60hz): |||||||||||||||||||||||||||||||||||||||||||||||||
    drawing with 60fps: |||||||||||||||||||||||||||||||||||||||||||||||||
    drawing with 30fps: | | | | | | | | | | | | | | | | | | | | | | | | |
    drawing with 50fps: | |||| |||||| ||||| |||| ||||| |||||| ||||| |||||

So as you can see, while we obviously draw more frames when we draw at 50fps than with 30fps, we have some awkward gaps between drawing frames. This will be perceptible and will look worse than the slower, but steady 30fps drawing.

This is also the reason why all modern games run at either 30 or 60fps, but nothing in between. Games with vsync enabled (which is implicitly true for all browser games) will look choppy at anything else than 30 or 60 fps.

So, requestAnimationFrame was invented for precisely the reason of choosing a steady frame rate for us. We should embrace it, not fight it :)

If you want to draw at the fastest possible framerate and don't care for choppiness, you could simply ditch the requestAnimationFrame hackery and go back to setTimeout(draw, 1).


Also somewhat related, NVIDIA recently introduced their G-Sync solution, which allows games to push frames to the display whenever they are ready, instead of waiting for the next refresh. However, my understanding is that something like this could only be utilized by browsers and browser games if they run in fullscreen.

1 decade ago by HawkleyFox

Quote from dominic
If you want to draw at the fastest possible framerate and don't care for choppiness, you could simply ditch the requestAnimationFrame hackery and go back to setTimeout(draw, 1).

In my change, rAF is still in use. The browser is still choosing when to draw and I do my drawing during the callback. What I am not doing is treating a draw call as an opportunity to perform heavy logic during a potentially time sensitive period.

So, I use a setTimeout to clear the current JavaScript execution context, returning control to the browser, to allow it to get the frame out. I do not want to keep the browser waiting. setTimeout grants the browser an opportunity to return to my code at the beginning of the next frame.

Chrome is the only browser that showed what appeared to be a vertical sync behavior, but only when an inordinately long period of time is consumed during the rAF callback. It isn't a feature; its a symptom of a problem.

1 decade ago by KirbySaysHi

Quote from HawkleyFox
Like, truly asynchronous? That seems unlikely seeing as JavaScript has no facilities to handle multi-threaded processing.


postMessage is often used [1] [2] as a hack to get around lack of native requestAnimationFrame support. So by "asynchronous", I assume @drhayes means "queues a function callback for the next runloop".

So if you do:

window.addEventListener('message', function(msg) { console.log(msg.data) });
window.postMessage('lo', '*');
console.log('yo');

You'll get:

> yo
> lo

By the way, this ping pong between rAF and setTimeout is really cool! I'd love to see it implemented into Impact proper. Question though, why is there a hard coded limit of 20 "update/draw handles"? Both rAF and setTimeout return an integer id that you could use to later cancel either.

[1]: https://github.com/NobleJS/setImmediate/blob/d73deacc3d22a755da42e378c66641b78c4b780f/setImmediate.js#L124
[2]: https://github.com/defunctzombie/node-process/blob/9f9801f97077e9c55e92ec3d6ce41fa99d109bef/browser.js#L16

1 decade ago by drhayes

So by "asynchronous", I assume @drhayes means "queues a function callback for the next runloop".


Sure did.

1 decade ago by HawkleyFox

Quote from KirbySaysHi
Question though, why is there a hard coded limit of 20 "update/draw handles"? Both rAF and setTimeout return an integer id that you could use to later cancel either.

I believe it is because, in the case rAF isn't available, I have to use setInterval to act like rAF in its place. I had to produce my own IDs in that case.

Since vanilla Impact supports canceling a callback, I figured I should retain that functionality. I used an object as a hash-map to track the state and I had a fear that the object would continue to grow with empty buckets if I didn't put a limit on the IDs.

1 decade ago by Jamesu

This is or was possibly a bug in Chrome ? Using Chrome 32 I don't get any problem with the following code:

var fpsInput = document.getElementById('fps');

var fps = 60;
var last = performance.now();

var draw = function( timestamp ) {
	requestAnimationFrame(draw);
	fps = fps * 0.9 + (1000/(timestamp-last)) * 0.1;
	last = timestamp;
	
	while( performance.now() - timestamp < 17 ) {};

	fpsInput.value = fps;
};

requestAnimationFrame(draw);

This is basically the same as the example you posted in comments to prove your point. I removed the setTimeout and used only regular rAF.

FPS is the same for both our methods, i.e. ~50

Can you double check on Chrome 32+ to see if you experience the 30 FPS ? If so, it's a problem on your end. If not, it was a problem that Chrome fixed already as of v32.

7 years ago by trusktr

I don't see how this helps. If you use `setTimeout` to run game logic, you're just moving the logic from one place to another place. If your game logic takes 100 milliseconds to finish, then you're never ever going to have an animation frame fired before those 100 milliseconds unless the game work is launching sub tasks in such a way that no synchronous process ever runs for more than about 9 milliseconds in order to give the animation frame a chance to fire.

The only way you can truly decouple the game updates from rendering is to have the updates in a worker, that way a lightweight animation frame is always guaranteed to fire every 16ms even is "synchronous" game updating is happening in the worker for 100ms.

7 years ago by trusktr

> If you use `setTimeout` to run game logic, you're just moving the logic from one place to another place

I mean, If you use `setTimeout` to run game logic, you're just moving the logic from one place to another place in the same thread.

7 years ago by trusktr

And if logic is in the same thread, then game updates will block rendering.
Page 1 of 1
« first « previous next › last »