1st June 2025

What's new in bpat? (2024 edition)

bpat is a cross-platform framework for multimedia applications that I'm developing. It has been extremely long since my last update, so I figured it is a good time to do an update on new developments since then :-).

Robust interval timers

bpat's event loop (bpatloop) now has support for interval timers. Interval timers are callbacks which are registered to be called at a certain interval with the event loop.

Code example showing bpatloop interval timer usage
# create and start new interval timer
let timerId = loop.onInterval(loopIntervalMsOfFps(60), proc (ntimes: Int128) =
  echo "I am called 60 times a second!"
)

# let it run...

# stop and destroy interval timer
loop.clearInterval(timerId)

Timers in bpatloop have been designed for a level of robustness that is needed for smooth multimedia applications and robust gameplay logic:

  • Based on the monotonic clock -- Interval timers use the platform's monotonic clock (such as CLOCK_MONOTONIC on Linux). This ensures that timers are not affected by time jumps caused by system clock changes.
  • Fractional intervals -- Users use fractions to specify the timer interval, as opposed to a single integer or float as used in most common event loop systems. This allows the exact representation of all rational numbers (within range), including recurring decimal timer intervals. Such timer intervals occur quite freqeuently in practice. For example, the standard 60FPS game logic loop requires a timer interval of , which can be represented precisely as the tuple .
  • Deadline tracking -- As bpatloop is a single-threaded event loop, long-running callbacks will block the event loop and may cause other timers to miss their deadlines. On platforms such as the web, the web browser may decide to put the web page to sleep, preventing timers from firing and causing them to miss their deadlines.

    bpatloop robustly tracks how many times an interval timer's deadline was missed, and provides this information in the ntimes callback argument, allowing timer callbacks to take appropriate action. For example, an animation callback may use ntimes to scale how far it interpolates its animation, or a fixed-timestep game logic callback may use ntimes to choose how many times it run its game logic step.

Unified pointer events

bpat supports both mice and touch input -- this is essential for supporting devices which may not have a mouse (such as smartphones) and for devices which may not have a touchscreen (such as desktops).

Previously, bpat treated mouse and touch events separately -- mice would emit the events {ekMouseMove, ekMouseDown, ekMouseUp} whereas touch would emit the events {ekTouchStart, ekTouchMove, ekTouchEnd}. Their input model was also slightly different: only one mouse existed under bpat's model, and the mouse had multiple buttons. On the other hand, up to eight touch fingers could exist under bpat's model, but those fingers did not have any buttons. These small differences led to a poorer programming experience as it meant that all code needing cross-device positional user input had to handle both mouse and touch events, and their different input models, even if there wasn't a need to distinguish between them. For example, a simple touch UI application just needs the mouse and touch event's x and y coordinates.

Drawing inspiration from Android's MotionEvent pointers, Windows pointer events, and the web pointer events spec, bpat now has a unified pointer input model. Both mice and touch now generate the generic pointer events {ekPointerMove, ekPointerDown, ekPointerUp, ekPointerCancel} with generic properties such as pointer x and y. The pointer's underlying type, such as whether it comes from a mouse, touch or stylus, is also made known to applications so that they can access their type-specific properties and handle each pointer differently if needed.

One nice side effect of this change is that since many of bpat's supported platforms (Android, Windows, web) support this style of generic pointer events, bpat's platform code can just use them directly instead of needing to perform conversion into mouse/touch events, thus simplifying bpat's code.

Unified input events can be seen in the "Input Events" bpat demo (Recorded on Android)

Origin-preserved pointer event timestamps

Origin-preserving pointer event timestamps have also been implemented for generic pointer events. These timestamps come directly from the underlying platform and maintain their precision up to nanoseconds. Such timestamps are essential for implementing natural-feeling touch gestures, such as kinetic scrolling, as they require accurate computation of the touch fling velocity.

Kinetic scrolling implemented in "Flinging" bpat demo (Recorded on Android)

HTTP/S client support

HTTP/1.1 and HTTPS client support has been implemented for all platforms and can be accessed in a platform-agnostic manner through bpatfetch. TLS support is implemented using BearSSL -- I obviously do not have the chops to be implementing cryptographic algorithms myself.

The "GitHub API" demo uses bpatfetch to perform a GET request to the GitHub API (Recorded on Android)

Gamepad support

Gamepad support has been implemented for all platforms and is enabled automatically, with gamepads producing {ekGamepadConnected, ekGamepadDisconnected, ekButtonDown, ekButtonUp, ekAxisMove} events. bpat currently uses evdev on Linux, Windows.Gaming.Input on Windows (with likely migration to GameInput in the future as Windows.Gaming.Input has been getting buggier with every Windows update), the generic key and motion events on Android, and the Gamepad API on the web.

One key design consideration I had to make was what kind of platform-agnostic input model to present to applications. The ideal would have been to have a way to tell, for each gamepad, which are the "main" buttons or dpads/sticks such that games can have a sensible default gamepad control mapping. However, looking around the various platform APIs it soon become apparent that such an ideal would be hard to achieve. As an example:

  • For evdev, a gamepad is simply an input device with abstract keys and axes. There is no strong standard on how a gamepad's buttons, d-pads and sticks map to keys and axes, and sometimes it can vary quite greatly. Personally I have seen d-pads being mapped to buttons on some controllers, and axes on others. There is also no consistency in the real-world directions of the axes -- up can be positive on some controllers and negative on others.
  • Similarly to evdev, the Gamepad API for the web has buttons and axes. It does provide support for the "Standard Gamepad" mapping which recommends the user agent to order the gamepad buttons and axes such that they match consistently with the standard gamepad layout. However, as the wording implies, this is not required, and so can't be relied upon in all cases.

As such, I've simply passed on solving this issue for now and just expose the raw buttons and axes of gamepads (and their values). If gamepads expose other input types, such as switches (D-pads) in the case of Windows, they are simply mapped to buttons/axes. Gamepad apps would need to allow users to set their own gamepad mappings (perhaps via a user-friendly gamepad mapping wizard).

Internal code refactorings, stability improvements

Finally, it would be remiss to not mention the extensive internal code refactorings and bug fixes that bpat has gone through, improving the flexibility and stability of the engine. For example, the event loop bpatloop has been extracted from the core windowing module bpatcore, allowing the event loop to be used in command-line applications. Various other modules such as bpatpath for path-manipulation functionality has been made standalone such that it can be used without bpat, e.g. with nim-fww.

After five years of development, these small incremental improvements have accumulated to make bpat very usable in practice -- I now regularly use bpat for writing small cross-platform apps and computer graphics experiments, and I rarely find myself reaching for other frameworks nowadays.