Early on I set about digging into the SDK's inner workings to see how easily it could be ported to Java. Eventually I succeeded in replicating both the head tracker reading code and some of the sensor fusion code in a Java project I called Jocular. It was basically functional, but it didn't have all the same features as the C++ SDK. In particular I never spent a lot of effort on magnetic yaw correction or prediction. Largely I lost interest in it because I didn't want to make myself responsible for constantly porting stuff in order to maintain feature parity with the official codebase.
When the 0.3.x came out it included a simplified C API that provided not only access to the sensors but also included an implementation of the distortion rendering inside the SDK itself. This was something of a boon to developers, because the implementation of distortion is non-trivial. It was also a boon to Java developers, because tooling for creating Java bindings to C functionality is pretty functional. That's not to say that binding the the C++ code would have been impossible, but the C API is much simpler, requiring fewer hurdles to jump to get the same results. So I bumped the Jocular version from 1 to 2, and set about producing just such a binding.
JNA vs JNI
Binding from Java to C can be done in a couple of ways, typically either through JNI (Java Native Interface) or through JNA (Java Native Access).JNI functionality requires that you write C code specifically designed for Java to call. Within this C code you can then call other native C libraries, or C++ code for that matter. This typically produces the fastest implementation, but it's a bit of overhead I wanted to avoid.
JNA on the other hand can be called with pure Java. The magic of loading native libraries, locating the appropriate functions within them and conversion of parameters is all baked inside the JNA library, which internally uses JNI. So really, there's only JNI for accessing C code, but the JNA library makes it easy to do in a non-library specific way.
JNA tends to be slower than JNI, but the difference tends to reflect how much information you're passing through parameters. The Rift API is simple and doesn't require much information to be passed over the API. The head tracking data and timing information is trivial in almost any context, while the OpenGL subsystem actually holds the bulk of the information that is used for distortion. So despite the ostensible speed difference, the ease of use of JNA wins out here.
Building the binding
Actually creating the Java classes to map to the Oculus SDK C API was the next task, and not one I relished. While the functions and structures I wanted to map weren't complex, there are a couple dozen of them, and going through them would have been tedious. Fortunately, there exist tools to do this for me. In particular I found a tool called JNAerator, which would accept as input a C header file and produce as output Java classes. This was suitable for producing a good first pass implementation and was essentially what I used for the first release of Jocular 2.JNAerator is kind of a finicky tool. There's both a command line interface as well as a GUI of sorts, but neither is exactly what you'd think of as polished. It's possible there are better tools out there, but I started working with this one and it seemed to be suitable for doing the bulk of the work I needed done, producing output classes for all the required structures, constants for all the required enums and static methods on a library wrapper for all the functions.
The mapping was imperfect. For instance, the C API header declares the structures with underscores in their names and uses typedefs to produce non-underscored names for use. The generated code didn't recognize the typedefs as important, so the generated types include the underscores.
All of the types also include ovr in the structure name, since C has no concept of namespacing, so all types are always in the global namespace. Java has strong namespacing, so the naming verbosity is unnecessary.
So for instance, the OVR C API has a raw type name ovrSensorState_. The Java name should be SensorState. Fortunately, JNA doesn't care about the type names, just the signatures, so it was a simple matter of going through the API and using Eclipse to refactor the names.
The next problem was the access pattern for using the ovrHmd handle provided by the SDK. This handle is essentially a pointer to a class in the C API implementation (which is written in C++), and most of the functions in the C API take the handle as their first parameter. This pattern indicates that the best mapping for the type was to create a class that wrapped the SDK functions internally. Fortunately the generator for the Java code was smart enough to recognize this pattern and create members on the Hmd type that wrapped calls to the static members in the library. However, some of the calls were ripe for some improvement.
Some of the functions needed to return complex data but also indicate error state. In these cases the C API provides an function parameter which is used for output, and the function return value itself is a flag, where a non-zero value indicates success. In Java, it's preferable to use the exception handling in the language to indicate error conditions. So I modified the wrappers for these functions to no longer require the user to pass in output parameters, and to raise an exception in the case of failure.
Consider the case of the rendering config function. In the C API it's declared like this
ovrBool ovrHmd_ConfigureRendering(
ovrHmd hmd,
const ovrRenderAPIConfig* apiConfig,
unsigned int distortionCaps,
const ovrFovPort[2] eyeFovIn,
ovrEyeRenderDesc[2] eyeRenderDescOut)
The generated code produces this equivalent (after my refactoring of the naming conventions):
byte ovrHmd_ConfigureRendering( Hmd hmd, RenderAPIConfig apiConfig, int distortionCaps, FovPort eyeFovIn[], EyeRenderDesc eyeRenderDescOut[]);
This is made much more use friendly in the Hmd wrapper class like so:
public EyeRenderDesc[] configureRendering( RenderAPIConfig apiConfig, int distortionCaps, FovPort eyeFovIn[]) { EyeRenderDesc eyeRenderDescs[] = (EyeRenderDesc[]) new EyeRenderDesc().toArray(2); if (0 == OvrLibrary.INSTANCE.ovrHmd_ConfigureRendering( this, apiConfig, distortionCaps, eyeFovIn, eyeRenderDescs)) { throw new IllegalStateException( "Unable to configure rendering"); } configuredRendering = true; return eyeRenderDescs;}
The end user no longer needs to allocate the EyeRenderDesc array and pass it in. The wrapper function handles this and simply returns the results, or throws an exception in the case of failure.
Finally, in order to make the API more completely accessible through the Hmd class alone, I created static methods in the Hmd type to wrap the corresponding methods in the OvrLibrary interface.
Finally, in order to make the API more completely accessible through the Hmd class alone, I created static methods in the Hmd type to wrap the corresponding methods in the OvrLibrary interface.
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.