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.