Skip to content
Haiku OSDeep Dive Published Updated 8 min readViews unavailable

Haiku BJoystick: Controller Discovery, Polling, and Input Decoding

Build reliable Haiku BJoystick integrations with device discovery, enhanced-mode polling, axis and button decoding, rescans, and latency-aware cleanup.

Haiku exposes joysticks and game controllers through BJoystick, a Device Kit class that discovers game-port devices and reads their current controls. It is deliberately a different path from keyboard and pointer events delivered through the Input Server. A game can poll a controller on its own update cadence without turning every axis movement into a window-system message.

That separation is useful, but it also means a controller is not a keyboard with a different icon. Applications must enumerate device names, open one controller, call Update() before reading a sample, interpret a variable control layout, and handle unplug or open failures. The API also retains a historical return-value quirk: Open() is declared as status_t, but Haiku’s implementation returns the open file descriptor on success. Treating every nonzero result as failure silently rejects a successfully opened controller.

Choose enhanced mode and discover deliberately

The public header provides Open(portName) and Open(portName, enhanced). The one-argument overload forwards to enhanced mode. The Device Kit documentation says standard mode exists for BeBox compatibility and is not implemented in Haiku; new code should use enhanced mode. Enhanced access is count-based, so code can allocate for the number of axes, hats, and buttons the selected device reports instead of assuming a fixed gamepad layout.

CountDevices() and GetDeviceName() operate on the current enumeration. The BJoystick constructor performs a scan, and Haiku adds RescanDevices() to refresh the names after a device change. Device enumeration is not a per-frame input query. Scan when presenting a device chooser or when the application has a reason to reconcile the device list, then keep the chosen port name associated with a stable application-level controller slot.

#include <Joystick.h>
#include <OS.h>
#include <stdio.h>

static status_t PrintAvailableControllers()
{
    BJoystick probe;
    status_t status = probe.RescanDevices();
    if (status != B_OK)
        return status;

    for (int32 i = 0; i < probe.CountDevices(); ++i) {
        char name[B_OS_NAME_LENGTH];
        status = probe.GetDeviceName(i, name, sizeof(name));
        if (status != B_OK)
            continue;

        printf("controller[%ld]: %s\n", (long)i, name);
    }
    return B_OK;
}

This discovery example does not open each port. Enumeration tells the UI what can be selected; Open() is the point at which the application claims a concrete device handle and can query its layout. A device can disappear between those two operations, so every open attempt must be checked and the chooser should retain a retry path rather than assuming a stale name is still usable.

Handle Open() as a file-descriptor result

The class reference documents that a successful Open() returns the device file descriptor, while errors are negative status values. The implementation stores the descriptor and returns it directly. Therefore, use a negative-value test for Open(), not status != B_OK. For other methods such as Update() and GetAxisValues(), use their documented B_OK status result.

BJoystick joystick;
char portName[B_OS_NAME_LENGTH];

if (joystick.GetDeviceName(0, portName, sizeof(portName)) != B_OK)
    return B_ERROR;

status_t openResult = joystick.Open(portName);
if (openResult < 0) {
    // B_BAD_VALUE, B_NO_INIT, or B_ERROR are failures; a nonnegative
    // result is the open file descriptor, not a B_OK status code.
    return openResult;
}

The check intentionally accepts descriptor zero as success. The operating system can assign file descriptor 0 if the process has closed standard input; testing openResult <= 0 would incorrectly reject that valid case. Once open, close the device explicitly when the controller is deselected or the owning object shuts down. BJoystick also closes its descriptor in its destructor, but explicit lifecycle boundaries make ownership easier to audit.

The interface permits a full device path or a device filename. Keep the exact name returned by GetDeviceName() and pass it back to Open() rather than synthesizing a path from a model name. Controller model labels and device-port identifiers are different values; a display label can change or be duplicated while the selected port name is what the API opens.

Read a coherent sample on the application cadence

Call Update() before reading values. It asks the driver for the current data for each stick, updates the values in the BJoystick object, and returns an error if the object is not initialized, the device is closed, or a read fails. Do not consume old axis values after an unsuccessful update and present them as a fresh sample. Track the last successful sample time and expose stale or disconnected state separately from a neutral axis position.

The count methods describe the opened device’s layout. Query them after opening, then size buffers from those counts. GetAxisValues() writes int16 values, GetHatValues() writes uint8 values, and GetButtonValues() writes a boolean per button. In enhanced mode, ButtonValues() is convenient for the first 32 buttons only; the boolean-array method is the correct choice when a device exposes more. Never use a fixed two-axis or four-button array without checking the device’s reported counts.

#include <Joystick.h>
#include <memory>
#include <vector>

static status_t ReadController(BJoystick& joystick)
{
    status_t status = joystick.Update();
    if (status != B_OK)
        return status;

    const int32 axisCount = joystick.CountAxes();
    std::vector<int16> axes(axisCount);
    if (axisCount > 0) {
        status = joystick.GetAxisValues(axes.data());
        if (status != B_OK)
            return status;
    }

    const int32 hatCount = joystick.CountHats();
    std::vector<uint8> hats(hatCount);
    if (hatCount > 0) {
        status = joystick.GetHatValues(hats.data());
        if (status != B_OK)
            return status;
    }

    const int32 buttonCount = joystick.CountButtons();
    std::unique_ptr<bool[]> buttons(new bool[buttonCount]);
    if (buttonCount > 0) {
        status = joystick.GetButtonValues(buttons.get());
        if (status != B_OK)
            return status;
    }

    // Map this sample into application state here. Keep raw values
    // separate from game-specific dead zones and action bindings.
    return B_OK;
}

The buffers in this example are valid for the duration of the call and have at least the capacity required by the corresponding count. The code samples stick zero, which is the default argument of the getters. If a device reports multiple sticks, call the getters for each valid stick index and store the samples under the matching controller/stick identity. Do not assume that all sticks share the same semantic layout just because the API exposes a common axis count.

The driver reports axis values, not application intent. Convert them in a separate mapping layer: preserve the raw signed value for diagnostics, normalize only after you have measured the device’s usable range, then apply a configurable dead zone and invert or remap axes according to user settings. A digital hat is also distinct from two analog axes. Avoid converting both into one synthetic vector before the application has decided how diagonals and simultaneous directional input should behave.

Keep polling bounded and make hot-plug recovery explicit

Poll from a controlled game or input update loop, not from an unbounded while (true) loop that consumes a CPU core. The API does not promise one universal controller report rate; hardware, driver, and scheduling all matter. If a game uses a fixed frame loop, sample once per input tick and timestamp the last successful read. If the application is a desktop utility, use a bounded timer or worker that can stop cleanly when its window closes.

Do not call view methods from a background polling thread. Publish a compact controller snapshot to the UI looper using a message or another synchronized handoff. This keeps drawing and widget state in the owning window thread and gives the poller a clear shutdown sequence. Keep error handling at the boundary: on an update failure, mark the sample stale, report the controller as unavailable if appropriate, and offer a rescan/reopen flow.

When a user reports that a controller vanished, call RescanDevices() and rebuild the selectable device list from the returned names. Reopen only after validating that the selected device still exists. A successful rescan does not prove that the same physical controller kept the same port name or that every control is calibrated. Re-run layout discovery after a successful open and keep user bindings attached to an application-level profile or verified controller identity, not merely to enumeration index zero.

Avoid using EnterEnhancedMode() as a substitute for opening the desired device. The class reference describes the enhanced mode option on Open(), and the one-argument overload already requests it. The older horizontal, vertical, button1, and button2 members represent compatibility-oriented state and are not a schema for every modern controller. Use the count and getter APIs for general devices.

Verify behavior with a small acceptance matrix

Test at least one controller with two axes and one with an unusual layout if hardware is available. Confirm that discovery returns a usable name, Open() succeeds using the documented nonnegative test, counts match the chosen controller, and Update() followed by getters returns values that change with physical input. Test no controller present, a device removed after enumeration, a failed open, an update error, more than 32 buttons if available, and application shutdown while polling is active.

Log device name, controller module/name where supported, axis/hat/button counts, sample timestamp, and operation status. Avoid recording every raw sample in production logs; a bounded diagnostic window around a failure is more useful and avoids overwhelming the log. Keep the last successful values distinct from the last attempted poll so support staff can tell “neutral” from “no recent input.”

The core contract is small: enumerate, open, update, read the reported layout, and close. Most controller bugs happen when an application skips one of those boundaries, assumes every gamepad is identical, or confuses the legacy open return value with a conventional status code. Put the device contract behind one input adapter and expose normalized actions to the rest of the game or tool.

Related:

Sources:

Comments