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

kit_app.py, example_kit_app_ui.py, kit-console.py

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.

sensors/tools/cam/calibration/calibration.py, CalibrationSimulation.run()

Contains while not task.done(): app.update(). The checked-in calibration CLI runners call the experiment synchronously. Calling this API from a Kit asyncio task would be unsafe; downstream callers must use an asynchronous interface rather than a nested pump.

sensors/tools/cam/capture.py, _run_async()

Pumps app.update() until a scheduled capture coroutine finishes. The synchronous capture() API is used by capture_depth_sensor.py; it must not be called inside an asyncio task. Async callers can use ViewportCapture’s async methods.

Script Editor _execute_script_async()

Runs user code inside a task. User-supplied synchronous waits are affected and must migrate as above. The existing asyncio.run() dialog is not a general reentry guard.

Material lookup (material_utils.py), viewport material drag/drop, checkpoint-combobox destruction

Use explicit asyncio.run() or run_until_complete(), not just Kit ticks. Their entry-point behavior is deliberately unchanged in this patch.

Material-library task cancellation

Uses a bounded blocking future wait or run_until_complete(); not made reentrant by this patch.

Profiler trace export

Calls asyncio.run() in an executor worker. A blanket prohibition based on the main loop’s state would also affect this distinct path.

Golden-image population and sensor validation commands

Use synchronous asyncio.run() entry points; unchanged by this patch.

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.