Saturday, January 18, 2014

Digging Into the Oculus SDK - Part "something": Head Tracking Data

Hardware

The Oculus Rift sensor hardware reports the acceleration and angular velocity as vectors up to 1000 times a second.  It also has a magnetometer and temperature sensor.  The iFixit teardown of the Rift reports the gyro+accelerometer chip to be a MPU-6000 from Invensense, which also includes the temperature sensor.  The same teardown reports that magentometer is believed to be an A983 2206 chip, which they highlight PCB.  All of this hardware is accessed ultimately through the HID interface we discussed in our last post.

Inside the SDK

When tracker data is requested, the SDK opens up the HID device if necessary and configures it with an HID feature report.

Opening the HID device gives you a handle that can be used to read from and write to the device, similar to the kind of handle you'd get from opening a file or a network socket, and typically usable with the same reading and writing APIs.

Feature reports, however, are for reading and writing capabilities and configuration of the device.  These do not go over the normal read and write API, but are instead performed 'out-of-band' using an OS specific API.  On Linux one uses a special ioctl command.  On Win32 based systems it is functions defined by the hid.dll.

The configuration I spoke of consists of issuing a KeepAlive feature report.  This instructs the hardware to start sending messages with the acceleration and gyroscope data.  If this command isn't issued, reading from the HID device handle will never return any data (and if you're doing blocking reads, will hang the thread).  The KeepAlive command includes as a parameter a duration during which it should continue to provide updates.  The parameter is stored in a 16 bit value and interpreted as milliseconds, which means that the maximum value is about 65 seconds if it's treated as an unsigned value, or half that if it's treated as signed.  The SDK code uses a hard coded of 10 seconds, and is set up to send the keep-alive every 3 seconds, which provides plenty of redundancy in ensuring there are no gaps in the data, even if the background processing thread is somehow held in abeyance for several seconds.

The actual reading of the HID device is done through an asynchronous mechanism that is different for each platform.  In an earlier installment we touched on the background thread that handles all SDK commands.  The same thread also handles all the reading of data from the HID device handle.  When data is available it's copied to a buffer and the buffer is passed to a handler object.

SDK Internal Handler

For once we have a single place to look in the SDK for the implementation.  While the code to asynchronously read data and copy it into a buffer is platform specific, the code for interpreting the buffer, a simple array of bytes, is thankfully platform neutral.  It is technically located in the SensorDeviceImpl::OnInputReport method, but this is really just a thin wrapper around a non-class function DecodeTrackerMessage located in the same file.

DecodeTrackerMessage converts the incoming data from a byte array into an actual C structure called TrackerSensors, again defined in the same file.  This conversion mostly consists of simply copying the bytes to the appropriate location in the structure.  The only unusual part of this decoding is that the actual gyroscope and accelerometer vectors are not stored as conventionally sized values.  Typically integer values are stored in either 8, 16, 32, or 64 bits, i.e. sizes that are powers of 2.  However, for whatever reason, 16 bit values for the individual components of the vectors were deemed insufficiently precise, while 32 bit values were deemed overkill.  This makes sense when you look at the units.

Acceleration is typically measured in meters per second squared or in Gs.  1 G is about 9.8 meters per second squared.  However, in order to avoid dealing with floating point numbers, this close to the hardware, the numbers are scaled by 1/1000 and reported as integers, so what the hardware is actually reporting is millimeters per second squared.  If we were limited to 16 bits, the most acceleration we could represent in any given axis would be about +/- 3 Gs, something that's pretty easy to exceed, particularly on small timescales.  On other hand if we use 32 bits we have space for representing about 200,000 Gs.  This is starting to approach the surface gravity of a white dwarf star.  Perhaps that range has some value in the fields of super-villainy and cartoons, it's certainly more than we need.

...or could possibly survive
The compromise is to use a non-standard bit length.  By encoding each axis in 21 bits, we can fit the entire vector into a single 64 bit integer, and still have 1 bit left over.  21 bits allows us to encode about +/- 100 Gs in any direction, which provides plenty of leeway for rapid movements, and probably allows the chip to be used in a variety of more interesting applications that might involve high, though not ludicrous, levels of acceleration.

The input byte array and C structure actually contain room for up to 3 samples, each containing one reading from the accelerometer and one from the gyroscope.  It also contains a field for the number of samples that have occurred since the last report.  If this number is between 1 and 3, then that is the number of samples in the structure that are valid.  If this number is greater than 3 then all three samples are valid and there have been N - 3 samples that have been dropped.  The structure only contains 1 magnetic vector and one temperature value, both of which are encoded more conventionally, though still as integers.  

Integers to floats

Once the data has been decoded, it needs to be processed by whatever message handler is attached to the sensor device.  But dealing with integers of non-SI units could easily be a source of bugs if you get the conversions wrong, plus each tracker message can contain up to 3 samples from the sensors.  So the next step is for the sensor device to pass the tracker message to an internal method onTrackerMessage().  This code is responsible for taking the TrackerMessage type, containing ints and up to 3 samples, and converting it into up to 3 individual instances of the MessageBodyFrame class.  MessageBodyFrame is a subclass of the basic Message type in the SDK, part of it's generic event handling system.  If you connect a callback to the SensorDevice, these are the message you would expect to get from it, at a rate of about 1000 per second.  Most people don't do this though, relying instead on a SensorFusion insteance to handle the messages for them and turn them into a continuously available quaternion, representing the current orientation of the Rift.

MessageBodyFrame contains a representation of the acceleration, rotation and magnetic field all as 3 dimensional vectors composed of floating point values, and represented in SI units.  Actually the docs say magnetic field is represented in gauss, not teslas, but this isn't really important, because a) the conversion factor is a power of 10, and b) the magnetic field isn't combined with any other units, so it might as well be a unit-less vector.  Indeed, the magnetic calibration utility could potentially apply a scaling value to the value, so that it's set up to be a unit vector at all times, but it doesn't appear that it does this currently.

Well, that's all we have time for today.  More detailed inner workings of the SDK that you don't care about coming soon to a blog near you.




No comments:

Post a Comment