Legacy Events#

See the primary documentation on Events.

For Kit Kernel 107.0, transition has started to the Events 2.0 model for Kernel and RunLoop events. Other event streams may transition in future versions.

History (Events 1.0)#

Historically, the carb.events plugin was used to create IEventStream objects, which were then owned by the systems that created them. The IEventStream could function either as an instant event delivery system (using the dispatch() function), or as a message queue where events could be pushed into the event stream which would be pumped at a later time. Subscribers could be added to either the push- or pop-side of the queue.

IEventStream can be challenging to use effectively. Furthermore, its reliance on heap memory internally and carb.dictionary as a payload system can make it performance constrained. Due to ABI compatibility, some of these issues are not easily solved. It was proposed to re-evaluate our event system as a whole to see if performance and usability can be improved, while also maintaining backwards-compatibility for existing extensions and applications.

Events 2.0#

Events 2.0 is the realization of that work. Since most of the events currently in use were instant events, a new system called IEventDispatcher was created solely to deliver instant events. For situations where events must be queued, IMessageQueue was created to store those messages. And in order to maintain backwards compatibility, IEventsAdapter was created which implements the IEventStream interface but functions as an adapter layer to and from Events 1.0 to Events 2.0.

To depart from using carb.dictionary as a payload mechanism, an extensible system was created in a new carb.variant plugin. This allows Variant objects that are ABI-safe and can be extended with custom types.

There are two primary interfaces to manage Events 2.0: IEventDispatcher and IMessageQueue, both provided by the carb.eventdispatcher plugin.

An adapter is also provided by carb.events and can be created with the new IEventsAdapter interface.

IEventDispatcher#

Unlike IEventStream, IEventDispatcher is an instant-only event system. Like IEventStream’s dispatch() and pump() functions, events are dispatched from the calling thread. But since IEventDispatcher’s events are always instant, they can be created on the stack. In most cases, no heap memory is required for dispatching an instant event.

Two major differences from IEventStream is that IEventDispatcher events are name-based rather than integral hashes; and the event dispatcher is global. To subscribe to an event with the older IEventStream you would first have to get access to the IEventStream instance through the system that created it; there is no global registry of event streams. In contrast, with IEventDispatcher you observe an event by fully-qualified name. This does mean that event names typically must be longer and more descriptive to avoid collisions. For instance, many Kit Kernel events have the prefix omni.kit.app: or omni.ext: to indicate that they are events sent by those respective modules. See the recommendations for event names.

To provide a better payload interface than carb.dictionary, a new carb.variant plugin was created with better type conversion support to C++ (and Python) types. The carb.variant system can also be extended to support user types.

Both carb.eventdispatcher and carb.variant make heavy use of Carbonite’s RString registered string system. This allows strings to be compared for [in]equality very quickly and passed as 32-bit values (instead of C-style strings or heap-allocated strings). The event names mentioned above also use the RString system.

IMessageQueue#

The new component to perform queue-like functionality in Events 2.0 is IMessageQueue. This is a thread-safe queue to which events may be pushed, and later popped and processed. This can be combined with IEventDispatcher to dispatch all of the queued messages through the popAllAndDispatch() helper function. The push-side of the IMessageQueue can be either asynchronous, or synchronous (the pushing thread blocks until the message has been popped and processed).

All IMessageQueue instances are named and can be found through IMessageQueueFactory without having to track down the instance through the system that created it.

Bridging the gap#

It’s in our best interest to provide backwards compatibility for something as foundational as our event system. In order to do this, we have created an adapter layer. This allows transitioning to the more optimal Events 2.0 components as time allows, rather than forcing extensions to convert instantly.

Most of the effort here is handled within Kit Kernel–existing event streams, while deprecated, have been replaced with adapters that translate the old Events 1.0 model to the newer Events 2.0 model, both for subscribing to events as well as dispatching events. This is done by having the adapter use the same interface as IEventStream.

This adapter layer allows Events 2.0 observers to receive dispatches from Events 1.0, and allows Events 1.0 subscribers to receive dispatches from Events 2.0. More information below.

Converting Events 1.0 subscriptions to 2.0 observers (C++)#

An existing C++ use of an event stream might look like this:

auto app = carb::getFramework()->acquireInterface<omni::kit::IApp>();
m_updateEvtSub = carb::events::createSubscriptionToPop(
    app->getUpdateEventStream(),
    [this](carb::events::IEvents* e) {
        _update(carb::dictionary::getCachedDictionaryInterface()->get<float>(e->payload, "dt"));
    },
    carb::events::kDefaultOrder, "Menu update");

Converted to Events 2.0, this subscriber looks like this:

auto ed = carb::getCachedInterface<carb::eventdispatcher::IEventDispatcher>();
m_updateEvtSub = ed->observeEvent(
    carb::RStringKey("omni.kit.ui/Menu"),
    carb::eventdispatcher::kDefaultOrder,
    omni::kit::kGlobalEventUpdate,
    [this](const carb::eventdispatcher::Event& e) {
        const static carb::RStringKey kDt("dt");
        _update(e.getValueOr<float>(kDt, 0.f));
    });

Let’s break this down a bit. First, here’s the documentation for IEventStream for reference. Our Events 1.0 subscription used a helper function: carb::events::createSubscriptionToPop(). Here is the function prototype:

inline carb::ObjectPtr<carb::events::ISubscription> carb::events::createSubscriptionToPop(
    carb::events::IEventStream *stream,         // The event stream to use
    std::function<void(IEvent*)> onEventFn,     // The handler that will be invoked when the event occurs
    carb::events::Order order = kDefaultOrder,  // An optional order value to determine priority
    const char *subscriptionName = nullptr,     // The optional name for debugging and profiling
)

Now let’s look at using Events 2.0 with IEventDispatcher::observeEvent():

template<class Invocable, class ...Args>
carb::eventdispatcher::ObserverGuard carb::eventdispatcher::IEventDispatcher::observeEvent(
    carb::RStringKey observerName,      // The name of the observer for debugging and profiling
    carb::eventdispatcher::Order order, // The order value to determine priority
    carb::RString eventName,            // The name of the event to observe
    Invocable&& invocable,              // A Invocable object (lambda or function) that will be called
    Args&&... filterArgs,               // Optional filter arguments
)

Several of the items are similar:

  • order has the same meaning in both Events 1.0 and 2.0. It’s a (signed) priority order for calling events. The lowest order number is called first.

  • subscriptionName is now observerName and, due to its usefulness, now required. Note this is a RStringKey–a registered string so that the string is stored once and copied by value after that. RStringKey supports an index field too, which can be used to differentiate different instances with the same name. For example, carb::RStringKey(1, "Menu") has an index value of 1 and will be rendered "Menu_1" for debugging.

  • onEventFn is now invocable but essentially have the same meaning. However, the object passed to the event callback function is different. In Events 1.0 this was a non-const pointer to a ref-counted IEvent instance. This potentially allowed the event to be dangerously modified by an event handler. And the reference counting demanded that the object always be created on the heap. For Events 2.0, the parameter is now a const reference to Event. Modification is no longer possible, and the payload is easier to access. This Event is not reference counted so it can be ephemeral; it only exists while the event is dispatching.

And some differences:

  • Instead of passing the instance of the IEventStream, now an eventName is given as a registered string (RString). Best practice is to use a constant value, such as omni::kit::app::kGlobalEventUpdate from our example above.

  • Events 2.0 has a powerful new feature: filterArgs. This optional argument allows specifying zero or more key/value arguments that the event must exactly match in order for the event to be passed to our observer callback. An example might be an event that is dispatched when a Window minimizes. The handler for that Window might observe the event filtering on the Window ID.

Converting Events 1.0 subscriptions to 2.0 observers (Python)#

An existing Python use of an event stream might look like this:

def _on_event(self, e: carb.events.IEvent):
    dt = e.payload['dt']
    _update(dt)

def __init__(self):
    self._event_sub = omni.kit.app.get_app().get_update_event_stream().create_subscription_to_pop(
        self._on_event,
        name="Menu update"
    )

Converted to Events 2.0, this subscriber looks like this:

def _on_event(self, e: carb.eventdispatcher.Event):
    _update(e['dt'])

def __init__(self):
    self._event_sub = carb.eventdispatcher.get_eventdispatcher().observe_event(
        observer_name="omni.kit.ui/Menu",
        event_name=omni.kit.app.GLOBAL_EVENT_UPDATE,
        on_event=_on_event
    )

Let’s break this down a bit. First, here’s the documentation for IEventStream for reference. Our Events 1.0 subscription used the method: create_subscription_to_pop(). Here is the function definition:

create_subscription_to_pop(
    self: carb.events.IEventStream,
    fn: Callable[[carb.events.IEvent], None],
    order: int = 0,
    name: str = None,
) -> carb.events.ISubscription

Now let’s look at using Events 2.0 with observe_event():

observe_event(
    self: carb.eventdispatcher.IEventDispatcher,
    order: int = 0,
    event_name: str,
    on_event: Callable[[carb.eventdispatcher._eventdispatcher.Event], None],
    filter: handle = None,
    observer_name: str = '<python>'
) -> carb.eventdispatcher.ObserverGuard

Several of the items are similar:

  • order has the same meaning in both Events 1.0 and 2.0. It’s a (signed) priority order for calling events. The lowest order number is called first.

  • name is now observer_name. Providing this is highly recommended as it is useful in debugging and profiling.

  • fn is now on_event but essentially have the same meaning. However, the object passed to the event function is different. In Events 1.0 this was a IEvent and the payload was accessed through the payload attribute. For Events 2.0, the parameter is now a Event. The payload can be accessed directly through __getitem__ (i.e. event['key']).

And some differences:

  • Events 2.0 requires the event_name to observe. It is highly recommended that this is a named value, such as omni.kit.app.GLOBAL_EVENT_UPDATE from the example.

  • Events 2.0 has a powerful new feature: filter. This optional argument allows specifying key/value arguments in a dict that the event must exactly match in order for the event to be passed to our observer callback. An example might be an event that is dispatched when a Window minimizes. The handler for that Window might observe the event filtering on the Window ID.

Since two strings must be passed to observe_event(), it is highly recommended to use keyword arguments.

Using IEventsAdapter to convert event streams#

IEventsAdapter is used to generate implementations of IEventStream that function in an Events 2.0 manner.

Several different types of adapters can be created based on how IEventStream is used:

Constructing an IEventsAdapter requires passing a descriptor and usually requires a mapping of Events 1.0 integral events to Events 2.0 event names. See the documentation for more information.