olofson / audiality2

A realtime scripted modular audio engine for video games and musical applications.
http://audiality.org/
zlib License
79 stars 5 forks source link

Clean up the object property interface #40

Open olofson opened 11 years ago

olofson commented 11 years ago

Currently, object properties are addressed using a single enumeration of integer names, covering the properties of all object types. We're sticking with the single enumeration/integer name idea, but we need to extend the interface to support applications and language bindings better. We need calls to check what properties an object actually has, and a call to translate an integer property name into a string.

Should we desire to add support for run-time defined properties (for example, A2S programs defining custom properties), we can easily add that on top of this interface by adding a state global property registry.

There is a more serious problem, though: Synchronization! As it is, we'll have to hold the engine lock to touch any interesting properties, and that's something you should NEVER do during normal operation. Everything needs to be lock free to ensure glitch free operation on all platforms.

There are a few approaches:

  1. Lock free techniques. This gets very complicated with objects that may be destroyed by the realtime context at any time.
  2. Caching. Quickly becomes extremely expensive! We can't know ahead of time which properties of what objects an application is going to read, or how often, so we'd essentially have to send them all to the API side after each realtime engine cycle...
  3. Realtime-only API. Keep the API, but allow calls only from the context of the realtime engine. Thus, any application code dealing directly with properties will have to be moved into one or more callbacks, moving the synchronization problem to the application side.
  4. Request/response events. Use the existing API/engine message queues to pass batch requests for property reads and writes. The actual operations would be performed in the realtime context, and values and errors would be sent back to the API as messages.

Alternative 1 would be really hard to get right, and 2 is simply not viable for performance reasons. (We can have thousands of objects, most of which will have several readable properties!)

Alternative 3 might be of interest if there's a case for realtime application code that needs to access properties. Can't really think of any valid reasons to do that at this point, though. It should be simple enough to add such an interface if needed, as we'll probably need to implement something much like that internally anyway.

So, it looks like 4 - request/response events. It's probably the only proper way of doing it, at least on mainstream operating systems (an RTOS with tools for dealing with priority inversion is another matter), and the engine is already operated like that during normal operation, so it makes perfect sense to build on that.

olofson commented 10 years ago

Actually, approach 1 may be viable, at least for some properties...! As long as we're only ever touching objects that are never physically destroyed (which would result in segfaults), the worst we can get is corrupt data due to dealing with objects while they're modified or sent back to their respective pools. As long as we can detect that, we can just retry the operation. (This is how a2_Now() is implemented already.)

The major problem here is granularity. Putting guards around the whole engine callback is not an option, because that blocks the property API from accessing realtime objects for the whole duration of every engine process cycle...! That is, practically all the time under near maximum engine CPU load.

However, most of the things we'll be accessing are naturally atomic (VM program counter, VM registers etc), so all we really need to know is that we're not reading garbage from a voice that was sent to the free pool while we were reading it.

There are various fields we could theoretically (ab)use as guard for lock-free voice access, but the problem with that is that there's a tiny chance of the voice being freed and reused in a way that restores the guard fields to their former values, meaning we fail to detect that anything happened. (The new one could be running the same program, it could end up right before the same physical voice struct in whatever list it's in, etc.) The chance of that happening should be very small, but but we cannot rely on this to never ever fail.