Usage Examples#
Run a Coroutine Using Async Engine#
import asyncio
from omni.kit.async_engine import run_coroutine
async def async_multiply(x, y):
await asyncio.sleep(1) # Simulate an async operation
return x * y
# Run the coroutine using the async engine
task = run_coroutine(async_multiply(6, 7))
Nested updates and synchronous waits#
Starting with 0.0.5, when an asyncio task or callback calls app.update() while a Kit asyncio tick is active,
the nested update does not dispatch asyncio tasks or plain callbacks. Pending work stays queued until the
caller returns or yields. This includes callbacks submitted with call_soon_threadsafe, executor completions,
and future done-callbacks. Other parts of the application update may still execute; this is not a general
application-reentrancy guarantee.
This is a compatibility change. A synchronous loop inside a task or callback such as
while not future.done(): app.update() can wait forever, even if a worker has already queued its completion.
A warning is emitted on the first suppressed reentry per outer tick. The warning does not terminate the wait.
For keep_loop_running=false, this warning replaces the previous nested-tick event-loop-running error.
Use cooperative waits instead:
import omni.kit.app
from omni.kit.async_engine import run_coroutine
async def update_scene():
app = omni.kit.app.get_app()
for _ in range(60):
await app.next_update_async()
# Await asyncio futures directly; wrap concurrent.futures.Future with
# asyncio.wrap_future() when waiting for a worker's result.
# From a synchronous UI callback or Script Editor, schedule and return.
# Do not spin-wait for this task using app.update().
task = run_coroutine(update_scene())
Top-level host loops that call app.update() outside an active asyncio tick are unchanged. Merely finding
loop.is_running() true is not evidence of reentry: Kit normally keeps its loop running between frames.
Scope and remaining reentry paths#
The guard protects only recursive calls to Kit’s run_once tick, normally reached through app.update().
It does not guard explicit loop.run_until_complete() / loop.run_forever() or Kit’s asyncio.run() wrapper.
Driving the same loop explicitly from one of its callbacks can still drain the ready queue underneath its
outer iteration. asyncio.run() normally creates a separate loop, but Kit’s wrapper also changes the main
loop’s running state; its nested behavior is not made safe by the tick guard.
Do not use these blocking entry points to wait from inside Kit asyncio tasks or callbacks. They require separate compatibility work; this change deliberately leaves their existing behavior unchanged. The rendering transition tests are synchronous-rendering smoke coverage, not evidence that all reentry paths or the native renderer crash reported in NVBug 6783259 are fixed.
Source audit for this change#
The feature/110.4 checkout was searched for Python application-update loops and blocking asyncio entry points
under kit/source and rendering/source. Findings relevant to compatibility:
Caller |
Assessment |
|---|---|
|
Host application/frame pumps; the guard does not suppress top-level ticks. Embedding their synchronous entry points inside a task is not covered by that guarantee. |
|
Contains |
|
Pumps |
Script Editor |
Runs user code inside a task. User-supplied synchronous waits are affected and must migrate as above. The existing |
Material lookup ( |
Use explicit |
Material-library task cancellation |
Uses a bounded blocking future wait or |
Profiler trace export |
Calls |
Golden-image population and sensor validation commands |
Use synchronous |
This is a source audit, not an exhaustive dynamic proof. Downloaded extensions, downstream applications and arbitrary Script Editor code are outside this checkout and need owner validation before release. No confirmed in-tree nested spin-wait caller was identified by this audit; the calibration and capture APIs remain downstream-use risks.