Skip to content
Alexander Evans edited this page Mar 1, 2021 · 4 revisions

A Complete Explanation of This Example Open VR Driver

This page is a complete technical explanation of this sample driver and Open VR drivers in general. If you are a complete beginner to Open VR you came to the right place. I also made a video that goes over everything in this page and walks you through making the driver step-by-step.

What Does The Sample Driver Do?

This sample driver simply causes the game character to continuously walk forward. After learning how this driver works you should be able to easily modify it to take output from an actual device and transform that output to joystick/trackpad commands to control the movement of the game character. If your device will have other functions besides joystick/trackpad input, such as buttons, you should have a good understanding of how to add those functions.

Project Setup

If you're familiar with setting up C++ projects, you can probably skip this section. Just grab the Open VR library and header files.

To create an Open VR driver, you'll need to first create a C++ dll. I used Visual Studio 2019 and created a dll project (dynamic library).

When the project is created it may auto-generate some files for you, like main.cpp and pch.h. I just got rid of those. Once you have your project created, you need to add the Open VR header files and library to the project.

  1. Download the latest Open VR release and extract it.
  2. Copy everything in the headers folder to your project. I just put them in my include folder.
  3. Go to the lib folder and copy the appropriate .lib file. For me it was the lib file under win64. Paste that into your project. In my project I just made a lib folder and put it there.

Now we're going to modify our project settings.

  1. Right-click the project in Visual Studio.
  2. Change the configuration to release and platform to x64 or whatever is appropriate for you.
  3. Select properties.
  4. Go to Configuration Properties -> C/C++ -> Precompiled Headers.
  5. If you got rid of the pch.h file like I did, set Precompiled Headers to Not Using Precompiled Headers.
  6. Go to Configuration Properties -> VC++ Directories.
  7. Update Include Directories and Library Directories appropriately so that the the header and library files you copied previously can be found.
  8. Click okay.

When building your project, remember to build in release mode with x64. I accidently built in debug mode and tried using the generated dll, but found that it would not be loaded. Checking the logs, it would say that it can't find the dll, even though it was in the correct spot. The log message doesn't say that there is a dll there but it's not built correctly.

Steam Logs

Logs where your driver will be mentioned are in C:\Program Files (x86)\Steam\logs. I found the vrserver.txt log to contain the most information.

Driver Implementation Overview

If you look at this page in the Open VR documentation you'll see that at minimum you need to create the following.

  1. A class that implements ITrackedDeviceServerDriver. This class represents your device. If you have more than one kind of device your driver is supporting, you'll have an implementation of ITrackedDeviceServerDriver for each kind of device. In this example we just have one simulated device, so only one implementation of ITrackedDeviceServerDriver is made. In this class is where you should check the state of your device (buttons pressed, joystick values, etc.) and update Open VR with that state information. For example, in this class you tell Open VR your device has an X button. When the button on your device is pushed, you tell Open VR.
  2. A class that implements IServerTrackedDeviceProvider. This class manages all of your ITrackedDeviceServerDriver classes. It will create all the instances of ITrackedDeviceServerDriver and add all of those to Open VR. It will call each ITrackedDeviceServerDriver every frame to update device state.
  3. A provider factory function. Open VR will call this function to get an instance of IServerTrackedDeviceProvider, your instance, that it will use to initialize your driver and call every frame to get updates.

So in summary you need a class that represents your device (ITrackedDeviceServerDriver), or multiple of them if you have multiple device types, a class that manages all your devices (IServerTrackedDeviceProvider), and a method Open VR calls to get your provider class.

VR Namespace

If you don't want to have to type vr:: everytime you reference something defined by Open VR, follow this section.

ITrackedDeviceServerDriver Implementation

I'm going to go over the implementation I wrote for ITrackedDeviceServerDriver for this example. You can find the declaration of ITrackedDeviceServerDriver in the openvr_driver.h file. First, take a look at that declaration and all the methods. We'll go through them one-by-one.

Activate

You should configure your device here, meaning give Open VR information about your device and set up handles so that you can update Open VR whenever your device state changes.

Passed into this method is an ID for the device. You need that ID to retrieve a container that stores all the properties for your device.

PropertyContainerHandle_t props = VRProperties()->TrackedDeviceToPropertyContainer(driverId);

You can set properties for your device like the following.

VRProperties()->SetStringProperty(props, Prop_InputProfilePath_String, "{example}/input/controller_profile.json");
	VRProperties()->SetInt32Property(props, Prop_ControllerRoleHint_Int32, ETrackedControllerRole::TrackedControllerRole_Treadmill);

The first property set is the path to the input profile Open VR should use for your device. I will cover input profiles later. The second property set tells Open VR what kind of device your device is. In my case, I'm making a treadmill type device.

There are many, many properties you can set. You can take a look at the openvr_driver.h file to see them all. I looked at several other driver implementations and here are a few I saw others using. I found they were not needed to get my driver to work, but some of them might be good to have (like serial number).

VRProperties()->SetUint64Property(props, Prop_CurrentUniverseId_Uint64, 2);
VRProperties()->SetBoolProperty(props, Prop_HasControllerComponent_Bool, true);
VRProperties()->SetBoolProperty(props, Prop_NeverTracked_Bool, true);
VRProperties()->SetInt32Property(props, Prop_Axis0Type_Int32, k_eControllerAxis_TrackPad);
VRProperties()->SetInt32Property(props, Prop_Axis2Type_Int32, k_eControllerAxis_Joystick);
VRProperties()->SetStringProperty(props, Prop_SerialNumber_String, "example_controler_serial");
VRProperties()->SetStringProperty(props, Prop_RenderModelName_String, "vr_controller_vive_1_5");
uint64_t availableButtons = ButtonMaskFromId(k_EButton_SteamVR_Touchpad) |
	ButtonMaskFromId(k_EButton_IndexController_JoyStick);
VRProperties()->SetUint64Property(props, Prop_SupportedButtons_Uint64, availableButtons);

You need to tell Open VR what kind of input your device will be providing. For this example, we'll provide joystick and trackpad input.

VRDriverInput()->CreateScalarComponent(props, "/input/joystick/y", &joystickYHandle, EVRScalarType::VRScalarType_Absolute,
		EVRScalarUnits::VRScalarUnits_NormalizedTwoSided);
VRDriverInput()->CreateScalarComponent(props, "/input/trackpad/y", &trackpadYHandle, EVRScalarType::VRScalarType_Absolute,
	EVRScalarUnits::VRScalarUnits_NormalizedTwoSided);
VRDriverInput()->CreateScalarComponent(props, "/input/joystick/x", &joystickXHandle, EVRScalarType::VRScalarType_Absolute,
	EVRScalarUnits::VRScalarUnits_NormalizedTwoSided);
VRDriverInput()->CreateScalarComponent(props, "/input/trackpad/x", &trackpadXHandle, EVRScalarType::VRScalarType_Absolute,
	EVRScalarUnits::VRScalarUnits_NormalizedTwoSided);

Joystick and trackpad are scalars, meaning they have numeric values, so we use CreateScalarComponent. Buttons have boolean values, so you'd use CreateBooleanComponent for buttons. The "/input/..." string was retrieved from the driver input page. The handles act as pointers that you'll use to update the state of the joystick/trackpad each frame. Take a look at the EVRScalarType and EVRScalarUnits classes. There are comments there that tell you what values to use for these and, importantly, the range for the scalar values.

GetPose

This sample is not using pose. Take a look at this documentation if you don't know what pose is. It's basically an object that contains position, orientation, and velocity data about your device. To simply move the game character forward pose is not needed, so that is why in my sample this method is just returning a default pose.

RunFrame

This method will be called every frame and you should check the states of your device and if they changed update Open VR with the new values. For this example we just created scalar components for joystick and trackpad, so we use the UpdateScalarComponent method to update those. Use the handles you initialized in the Activate method. The value can be from -1 to 1.

I ran into an issue that tripped me up for a while that I'll point out here. Originally I just created a scalar for joystick Y because I just wanted to move the character forward. In the game I was testing with, Baam Squad, nothing happens if I only create and update that one scalar. I added joystick X and trackpad X and Y and found it started working. I took away joystick X and trackpad X and found it stopped working, even though I was only using the Y direction. So I recommend creating and updating scalars for both joystick and trackpad and both directions, even if you're only using one (any real driver would update both directions). The issue might be only for the game I was trying, but it's safe to just update all of those inputs.

Deactivate

This method is called when your driver is unloaded. Do whatever you need to do with your devices when this happens.

GetComponent

This method is stil a bit of a mystery to me. Most of the samples, including the open vr sample, just return null all the time in this method. I found one driver implementation that wasn't just returning null, so I did the same as that one. Later after I got this sample driver to work I changed this method back to just returning nulls all the time and didn't notice anything break. So I'm not really sure what this method does.

Put a log in this method that outputs the value of the string that's passed in if you want to see the possible return values.

VRDriverLog()->Log("This is a log message");

EnterStandby

In this method, tell your device to go into stand-by mode if applicable.

DebugRequest

See the comment block for this method in ITrackedDeviceServerDriver.

IServerTrackedDeviceProvider Implementation

Init

Initialize your provider here. All the examples started with this bit of code that initializes your driver context.

EVRInitError initError = InitServerDriverContext(pDriverContext);
if (initError != EVRInitError::VRInitError_None)
{
    return initError;
}

Next, create instances of your device drivers, your ITrackedDeviceServerDriver implementations, and add then to the list of tracked devices.

controllerDriver = new ControllerDriver();
VRServerDriverHost()->TrackedDeviceAdded("example_controller", TrackedDeviceClass_Controller, controllerDriver);

Cleanup

Do whatever you need to do when the driver is unloaded. In my case I just delete the pointer to the device controller.

GetInterfaceVersion

Returns the version of ITrackedDeviceServerDriver you're using.

RunFrame

This method is called every frame. You should update all of your device controllers here.

ShouldBlockStandbyMode

Return true if stand-by mode should be blocked for some reason. False otherwise.

EnterStandby

Called when your driver should enter stand-by mode.

LeaveStandby

Called when your driver should leave stand-by mode and go back to full operation.

Provider Factory Implementation

This page shows how to implement the factory. Just create an instance of your provider and return it when the corresponding version is passed in.

#define HMD_DLL_EXPORT extern "C" __declspec( dllexport )

DeviceProvider deviceProvider; //global, single instance, of the class that provides OpenVR with all of your devices.

/**
This method returns an instance of your provider that OpenVR uses.
**/
HMD_DLL_EXPORT
void* HmdDriverFactory(const char* interfaceName, int* returnCode)
{
	if (strcmp(interfaceName, IServerTrackedDeviceProvider_Version) == 0) 
	{
		return &deviceProvider;
	}

	if (returnCode)
	{
		*returnCode = vr::VRInitError_Init_InterfaceNotFound;
	}

	return NULL;
}

Input Profile

Read through this page first. That page should give you a good idea of how input profiles work.

One thing to watch out for is that if you want the Steam binding UI to work, you need to implement input_bindingui_right. I wrongly assumed that I wouldn't need to define any images.

"input_bindingui_right": {
	"image": "{example}/icons/game_controller.svg"
}

When I didn't define the image, this is what I saw when opening up the Steam binding UI. I didn't see anything in the logs telling me what the problem was so I had to just try things for a while to figure it out.

Empty Steam Binding UI

Legacy Bindings

The largest issue I ran into was with legacy bindings. I did not see anything in the Open VR documentation on them other then a short paragraph on this page. It seems you definitely need to define at least one. I'll explain below, but it seems that most VR games still use legacy bindings. Also the Steam binding UI seems to only use legacy bindings too. Here is the one I eventually got working.

{
   "bindings" : {
      "/actions/legacy" : {
	  "haptics" : [
         ],
         "poses" : [
         ],
         "sources" : [
			{
               "inputs" : {
                  "position" : {
                     "output" : "/actions/legacy/in/left_axis0_value"
                  }
               },
               "mode" : "trackpad",
               "path" : "/user/treadmill/input/trackpad"
            },
			{
               "inputs" : {
                  "position" : {
                     "output" : "/actions/legacy/in/left_axis0_value"
                  }
               },
               "mode" : "joystick",
               "path" : "/user/treadmill/input/joystick"
            }
         ]
      }
   },
   "category" : "legacy",
   "controller_type" : "example_controller",
   "description" : "Legacy mapping for example",
   "name" : "Legacy_Binding_Example",
   "options" : {
      "mirror_actions" : false,
      "returnBindingsWithLeftHand" : true,
      "simulated_controller_type" : "none"
    }
}

Maybe the values that output and path can be are listed somewhere, but I could not find any list or documentation telling me what I should put in those values. I had to look at legacy files that other's have implemented.

In addition to getting the bindings section correct, I found for the games I tested (Baam Squad and Skyrim) you need to define the options section and make sure the bindings are returned with the left hand. I believe this will cause the trackpad/joystick input from your driver to be compared to the trackpad/joystick input from the regular controls and then Open VR will take the larger of the two values.

For drivers that you implement I recommend you try to find a similar driver and look at the legacy bindings they are using. If your driver is for a controller similar to touch controllers, look at the binding files at C:\Program Files (x86)\Steam\steamapps\common\SteamVR\drivers\oculus\resources\input. If you're making a treadmill kind of device, install Kat Loco or Cybershoes and find the binding files they are using. Take a look at the Driver Directory Structure section below to learn where those files might be located.

Driver.VrDriverManifest

Add a vrDriverManifest file. I just copied it from another driver and changed the name.

Driver Directory Structure

Build your project in release mode, x64. This will produce your driver dll. You need to rename your dll to be driver_{name}, where name is the name you set in the VrDriverManifest file. For example, for this sample I set the name to be example so the dll needs to be named driver_example.dll.

You need to create a directory with the following structure. Change the names appropriately for your driver.

  • example
    • bin
      • win64
        • driver_example.dll
    • resources
      • icons
        • game_controller.svg
      • input
        • controller_profile.json
        • legacy_binding_example.json
    • driver.vrdrivermanifest

Once you have that, you can either paste the folder here: C:\Program Files (x86)\Steam\steamapps\common\SteamVR\drivers, or update C:\Users\YourUser\AppData\Local\openvr\openvrpaths.vrpath. If you update the vrpath file just add the path to your driver folder in the external_drivers array.

Testing The Driver

Note: I did all my testing with my Oculus Quest via Virtual Desktop.

  1. Go to settings. settings button
  2. Go to controllers and click test controllers. test controllers button
  3. Click the drop-down and select your controller. test controllers selection
  4. You should see the following. There will be a little blue dot at the top of the circles, indicating Steam thinks the joystick/trackpad is being pushed forward. Note that for me this only worked when I did all this through the headset. I noticed that if I put the headset down and tried all this from my computer the test controller UI would not update when I pressed buttons and used the joysticks. This might be becaue Steam put all the controllers into stand-by mode when I put the headset down. example controller UI
  5. Go back to Controllers and click manage controller bindings. manage controller bindings
  6. Select the game from the drop down that you want to use the driver with. For me I selected Baam Squad. controller binding app selection controller binding app dropdown
  7. Switch the active controller binding to custom. controller binding switch
  8. Click choose another. controller bindings choose another
  9. Select the example controller. controller binding controller selection controller binding select example controller
  10. Edit the binding. edit controller binding
  11. Ensure the joystick and trackpad bindings look like below. example legacy binding
  12. Click more options. legacy bindings more options
  13. Ensure return bindings with left hand is checked. legacy bindings options selection
  14. If everything looks right so far, go back to your game library and start the game.
  15. Make sure your game character moves forward constantly when free locomotion is available.

More Advice

  • At the time of this writing, this issue is still open with Open VR. The issue describes a problem where the 2 hand controllers and treadmill controller aren't assigned in the correct order. Towards the end the k_pch_Driver_LoadPriority_Int32 property is mentioned as a possible solution. I haven't run into this issue yet but will try messing around with this property if I do.
  • This issue describes some issues that may be present with treadmill devices.
  • More information on legacy bindings can be found here.
  • I've looked at the binding files that Cybershoes and a few other locomotion drivers use and it appears that most games still use legacy bindings. I also don't see a way in the Steam UI to make a binding that isn't legacy.
  • Instead of just copying and pasting the header files from open vr, you could create a git sub-repository that gets auto updated. I've seen a few other drivers do this.
  • Instead of manually setting up your driver directory, you could create a script that automatically creates it.
  • You can add an input profile per game. In your controller's main input profile, you can specify that for a particular game it should use the input profile you defined.
"default_bindings": [
{
	"controller_type": "example",
	"app_key": "the steam app key",
	"binding_url": "your-game-binding-file.json"
}
]
  • I just ran into a new issue as of 2/28/2021 in my vr shoes software repository. In that repository, in the manifest file, the name of my driver was "ffVrShoes." For some reason, open VR seems to now not like the capital letters in the name. If I leave the capital letters in, then any controller bindings I have for my driver will not activate. I'd go to the binding settings for a particular game, and the current binding would be empty. I'd try to activate one of my existing bindings, but nothing would happen. Once I replaced the capital letters in the name with lower-case ones, the bindings would activate again. Make sure to also update the driver name in your other resource files to not use capital letters (specifically, I updated the manifest, input profile, and legacy input profile).

Resources