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 nowobserverName
and, due to its usefulness, now required. Note this is aRStringKey
–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 nowinvocable
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-countedIEvent
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 toEvent
. Modification is no longer possible, and the payload is easier to access. ThisEvent
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 aneventName
is given as a registered string (RString
). Best practice is to use a constant value, such asomni::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 nowobserver_name
. Providing this is highly recommended as it is useful in debugging and profiling.fn
is nowon_event
but essentially have the same meaning. However, the object passed to the event function is different. In Events 1.0 this was aIEvent
and the payload was accessed through thepayload
attribute. For Events 2.0, the parameter is now aEvent
. 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 asomni.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 adict
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:
eDispatch
- Simple dispatch-only adapter, to replaceIEventStream
that only diddispatch()
and supported only pop-side subscribers.ePushPump
- The most common adapter, replacesIEventStream
that didpush()
immediately followed bypump()
, supporting both push- and pop-side subscribers.eFull
- To replaceIEventStream
instances that used the queue functionality.
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.