I'd gotten feature parity in engine code through to the mid-twenties on HMH, when I decided to branch and rewrite all my code sans C++. My development environment is Gentoo, so I'd been using the built-in winegcc compiler to do all my debug win32 builds. This hasn't been a problem until implementing XInput, which currently has poor to no support in WINE, and is awaiting some specific HID framework implementation whose design is rumoured to be fantastic.

So in the interim, I decided to start with DirectInput for my Pure C win32 rewrite, and add XInput later when I polish the win32 platform code. I knew next to nothing about COM and DInput when I started, but fortunately WINE provides a working implementation with source code and headers. So now that I have it working, a few CAVEATS:

Quake-III-Arena's source has some clues, but its DIRECTINPUT_VERSION == 0x0300 which isn't documented on MSDN anymore AFAICT, ie. the types, parameters, and functions in DIRECTINPUT_VERSION 0x0800 are neither the same as, nor compatible with 0x0300.

A lot of the DirectInput functionality is not required to get to a working gamepad. I can see coming back to some of it and using it to test and ensure support for as many devices as possible. Though For the debug version, you can skip Enumeration functions, their callbacks, GetCapabilities, and pretty much anything outside of Creating the DInput context, the Device context, SetCooperativeLevel, SetDataFormat, Aquire, and then the Poll and GetDeviceState loop.

Trying to follow MSDN through to a win is difficult, but not impossible. When their Doc's say you have to "acquire" a device, there is an actual DirectInputDevice8::Acquire method, even though they don't mention it or link to its documentation, and I didn't find it until I searched specifically for the term, acquire.

Writing in Pure C requires some fun manual Library Loading, and macros, to map vtable methods to callable functions, prefixed with IDirectInput8_<method> and IDirectInputDevice8_<method>. In addition to the usual parameters, the pointer to the calling object is inserted as the first argument as the THIS_ pointer. So functions in MSDN that list four parameters actually get five. Its almost identical to what Casey did to rig XInput functions to load pointers or stubs based on what Libraries (or no Libraries) are available.

On GNU+Linux, I tested an XBox360 controller using xboxdrv, and a Steam Controller using sc-xbox.py using the DirectInput built-in DIJOYSTATE2 data structure. Both pipe their input to the kernel through the uinput userspace driver, and passed to the executable through WINE to DirectInput.

XBox460 had slightly poorer support, with:
11 buttons, A, B, X, Y, LSh, RSh, Back, Start, Guide, LTh, RTh
2 relative joysticks, L&R
Unfortunately neither trigger returns any data anywhere in a DIJOYSTATE2 struct (driver, device, or dinput? not sure yet)
Rumble has not yet been tested.

The Steam Controller has Excellent support, with:
̶1̶2̶ 11† Buttons; A, B, X, Y, LSh, RSh, Back, Start, Guide, and LTh RTh and CenterTh
One relational joystick, one absolute touchpad
Six way (distinctly tested) directional touch-Dpad
And both shoulder triggers
The battery cover base left and right work, but just mirror A&B. (driver, device, or dinput? not sure yet)
Rumble has not you been tested.

†Left Thumb[Touch DPad button] and Center Thumb[Analog Joy Button] are mapped to the same output ;)