Saturday, August 28, 2010

Stressed - Asynchronously

In a previous post I discussed the improvement in performance to be able to display 1600 cubes on the screen at a time at 32fps. It has been quite a while since that post, and until recently I've been busy with infrastructure cleanup and changes and just recently cycled back around to take another look at performance.

One of the main goals of Sgine from the beginning has been to be a multi-threaded 3d engine. However, early on there were some problems with updating content asynchronously that were causing visual issues so I modified all non-renderer updates to occur in the rendering thread (like every other 3d engine out there). After remembering that I had never switched this back over (yeah, just took a single line of code: Updatable.useWorkManager = true), I re-ran the stress test:



Yes, you read that right, I went from 32fps all the way up to 296fps. That's a pretty substantial jump if I do say so myself. There are additional tweaks I can make to make it even faster I think, but I'll save that for another day.

While we're on the topic of multi-threading 3d engines I'd like to share another recent development. As anyone ever developing multi-threaded support in a 3d engine is aware, there are many times you find yourself outside of the rendering thread, but you need to invoke some rendering logic. This was a big problem in jME and Renanse (Joshua Slack) and I came up with the GameTaskQueue that would allow you to inject "work" to be done into a queue to be done later in the rendering thread.

To some degree I make use of the same concept in Sgine, but have created a trait "org.sgine.work.Worker" to represent this functionality. Either a Function0[Unit] or curried function can be passed to invokeLater or invokeAndWait like so:

renderer.invokeLater {
    glPolygonMode(GL_FRONT, GL_POINT)
}

This is on par, but slightly easier to read than passing a Runnable to a queue, but still not too far off. However, I remember being incredibly frustrated in jME having to do this all the time when I needed to inject some logic into the renderer. Now, in Sgine that shouldn't usually be necessary as its purpose is to abstract the rendering layer, but still I wanted to take this a step further (primarily for internal use).

So here's a real-world example of a problem that was created by asynchronous updates. In the Debug trait it adds a keyboard listener to do specific things upon key press. For example, hitting Escape will shutdown the renderer. This shouldn't be a problem as the Renderer.shutdown() method can be invoked asynchronously with any trouble, but unfortunately I have no reference to the current Renderer, so I have to use Renderer() to get the ThreadLocal Renderer. Obviously if keyboard events are being thrown outside of the renderer thread this causes a problem.

So, in an effort to simplify this process, a new feature has been added to EventHandler to allow a "worker" to be specified. If a worker is specified then all calls to invoke the wrapped listener will be "invokeAndWait" through that worker.

In the case of Debug:

Keyboard.listeners += EventHandler(handleKey, ProcessingMode.Blocking)

Now becomes:

Keyboard.listeners += EventHandler(handleKey, ProcessingMode.Blocking, worker = renderer)

From now on I don't have to worry about finding the right renderer and then invokeAndWait within that renderer for anything within the handleKey function, I can rely on the fact that handleKey is always going to be run within the rendering thread.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.

Scala Engine for high-performance interactive applications.