AsyncioDAT is a C++ TouchDesigner operator that provides a managed asyncio event loop for Python scripting within TouchDesigner. It enables truly asynchronous programming without blocking the main TouchDesigner thread.
- Automatic Event Loop Management: Initializes, manages, and tears down an asyncio event loop automatically
- Frame-Synchronized Processing: Processes asyncio events once per TouchDesigner frame for smooth integration
- Non-Blocking Operations: Enables async I/O, network requests, and other long-running operations without freezing TouchDesigner
- Easy Python Integration: Simple Python API for adding coroutines and managing async tasks
- Comprehensive Error Handling: Robust error handling and recovery mechanisms
- TouchDesigner 2025.32820 or newer. The operator is built against Custom Operator SDK v4 and TouchDesigner's embedded CPython 3.11.
- Windows 10/11, or macOS on Apple Silicon. The macOS build is arm64 only, matching TouchDesigner.
-
Download the asset for your platform from the latest release and extract it:
Platform Asset Contents Windows AsyncioDAT-windows.zipAsyncioDAT.dllmacOS AsyncioDAT-macos.zipAsyncioDAT.pluginbundle -
Put the operator in a
Plugins/folder beside your.toe. TouchDesigner loads Custom Operators from aPlugins/directory next to the project file; there is no way to point it at an arbitrary directory. (APlugins/folder in your TouchDesigner user directory —Documents/Derivative/Plugins/— works too, and makes the operator available to every project.)MyProject/ ├── MyProject.toe └── Plugins/ └── AsyncioDAT.dll # or AsyncioDAT.plugin on macOS -
macOS only: the downloaded bundle carries a quarantine attribute and is not signed or notarized, so TouchDesigner will refuse to load it until you clear the attribute:
xattr -dr com.apple.quarantine Plugins/AsyncioDAT.plugin
-
Open your project. On first load, TouchDesigner shows a modal asking you to approve/trust the Custom Operator — accept it. Then add an Asyncio DAT from the operator palette.
See Building from Source below. build.ps1 /
build.sh copy the freshly built operator into tests/td/Plugins/, so the
bundled test project (tests/td/test.toe) picks it up.
AsyncioDAT supports loading configuration from a config.toml file in the working directory. This allows you to specify custom Python paths and callback locations without modifying code.
Create a config.toml file with the following structure:
[main]
paths = [
"path/to/your/modules",
"another/path/with spaces",
".venv/Lib/site-packages"
]
[asyncio]
callback_module_path = "path/to/your/callback.py"Configuration sections:
-
[main]section:paths: Array of directory paths to prepend tosys.pathfor Python module discovery
-
[asyncio]section:callback_module_path: Path to the Python file containing theon_startcallback function (and other lifecycle callbacks)
Default behavior (no config.toml):
If no config.toml is found beside the .toe file, AsyncioDAT:
- Does not prepend any extra paths to
sys.path - Loads lifecycle callbacks from
asyncio_dat_callbacks.pyin the working directory (optional — it's fine if the file doesn't exist)
- Add an AsyncioDAT operator to your TouchDesigner network
- Ensure the "Auto Poll" parameter is enabled (default)
- The operator will automatically initialize an asyncio event loop
The AsyncioDAT operator exposes the following methods:
# Get reference to the operator
asyncio_op = op('Asyncio1')
# Initialize asyncio (called automatically)
asyncio_op.initialize_asyncio()
# Shutdown asyncio
asyncio_op.shutdown_asyncio()
# Process events manually (if auto-polling is disabled)
asyncio_op.poll_event_loop()
# Check if asyncio is running
is_running = asyncio_op.is_running()# Add a coroutine as a task to the event loop
async def my_async_function():
await asyncio.sleep(1)
print("Async task completed!")
coro = my_async_function()
success = asyncio_op.add_task(coro)
# Create a task object (returns the task for manipulation)
task = asyncio_op.create_task(coro)
# Run a coroutine until completion (blocking)
result = asyncio_op.run_coroutine(coro)# Get the current event loop object
loop = asyncio_op.get_event_loop()
# Use the loop directly for advanced operations
if loop:
loop.call_later(5.0, lambda: print("Called after 5 seconds"))
# Number of callbacks currently ready to run on the loop
pending = asyncio_op.get_callback_count()The operator holds a dictionary of named Python objects that outlives any single
script — useful for keeping a client, a connection pool, or a service object
alive across cooks. Register them from on_start (see
Lifecycle Callbacks) and reach them from anywhere.
asyncio_op = op('Asyncio1')
# Register / replace. Returns True on success; re-registering a name replaces
# the previous object and releases the reference to it.
asyncio_op.set_plugin('my_service', MyService())
# Look up. Returns None if the name is not registered.
service = asyncio_op.get_plugin('my_service')
asyncio_op.has_plugin('my_service') # -> True
asyncio_op.del_plugin('my_service') # -> True, or False if it wasn't there
asyncio_op.clear_plugins() # remove all of themThe plugins property is an attribute-style view over the same registry, and
plugin_names lists the registered names:
asyncio_op.plugins.my_service = MyService() # same as set_plugin
service = asyncio_op.plugins.my_service # AttributeError if not registered
del asyncio_op.plugins.my_service # same as del_plugin
'my_service' in asyncio_op.plugins # -> bool
asyncio_op.plugin_names # -> list of namesThese methods act on whichever AsyncioDAT instance owns the event loop, so
calling them before one is active raises RuntimeError.
import asyncio
async def simple_task():
print("Starting async task...")
await asyncio.sleep(2.0)
print("Async task completed!")
return "Task result"
# Add to the managed event loop
op('Asyncio1').add_task(simple_task())async def periodic_task():
count = 0
while count < 10:
print(f"Periodic task tick: {count}")
count += 1
await asyncio.sleep(1.0)
op('Asyncio1').add_task(periodic_task())import aiohttp
import asyncio
async def fetch_data(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
data = await response.text()
print(f"Received {len(data)} characters from {url}")
return data
# Fetch data asynchronously without blocking TouchDesigner
url = "https://httpbin.org/delay/2"
op('Asyncio1').add_task(fetch_data(url))async def worker(worker_id, duration):
print(f"Worker {worker_id} starting...")
await asyncio.sleep(duration)
print(f"Worker {worker_id} finished!")
return f"Result from worker {worker_id}"
# Start multiple workers concurrently
for i in range(5):
op('Asyncio1').add_task(worker(i, random.uniform(1, 3)))All parameters live on the Asyncio page:
- Active: Toggle that initializes (on) or shuts down (off) the managed event loop (default: enabled)
- Reset Event Loop: Pulse to shut down and reinitialize the asyncio event loop
- Auto Poll: Toggle automatic event polling every frame (default: enabled)
- Add Stop Task: Toggle that schedules
loop.stop()each poll so the loop drains ready tasks viarun_foreverbefore yielding (default: off) - Max Status Rows: Maximum number of status messages shown in the output table (default: 10)
- Clear Status: Pulse to clear the status message table
- on_poll_begin callback active: Toggle that enables the
on_poll_begincallback each frame (default: off) - on_poll_end callback active: Toggle that enables the
on_poll_endcallback each frame (default: off)
loop_running: Read-only boolean indicating if the event loop is activeasyncio_initialized: Read-only boolean indicating if asyncio is properly initializedplugin_names: Read-only list of registered plugin namesplugins: Accessor for registered plugins (e.g.op('Asyncio1').plugins.my_plugin)
AsyncioDAT exposes a Python callbacks DAT with these hooks: on_initialized,
on_poll_begin, on_poll_end, on_shutdown_begin, on_shutdown_complete. In
addition, an on_start(asyncio_dat) function in the configured callback module
(asyncio_dat_callbacks.py by default) is called once when the first AsyncioDAT
instance is created — useful for registering plugins or starting background
services. See tests/td/asyncio_dat_callbacks.py for a worked example.
The AsyncioDAT operator uses the loop.run_until_complete(asyncio.sleep(0)) pattern to process pending events without blocking. This approach:
- Allows all ready tasks to execute
- Processes I/O completions
- Runs scheduled callbacks
- Yields control back to TouchDesigner quickly
- The asyncio event loop runs on TouchDesigner's main thread
- Event processing is synchronized with TouchDesigner's frame rate
- Async operations don't block the UI or rendering
- Python Global Interpreter Lock (GIL) is properly managed
- Task exceptions are caught and logged to the console
- Event loop errors trigger automatic reinitialization
- Robust cleanup on operator destruction
- Graceful handling of Python environment changes
AsyncioDAT is ideal for:
- Network requests (HTTP, WebSocket, etc.)
- File I/O operations
- Database queries
- Inter-process communication
- Waiting for external events
For CPU-intensive work, consider:
- Using
asyncio.to_thread()for thread execution - Breaking work into smaller chunks with
await asyncio.sleep(0) - Using TouchDesigner's built-in multithreading where appropriate
# Good: Use context managers for resources
async def good_example():
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.text()
# Good: Clean up resources properly
async def cleanup_example():
try:
# Some async operation
await some_operation()
finally:
# Cleanup code
await cleanup_resources()async def robust_task():
try:
result = await risky_operation()
return result
except Exception as e:
print(f"Task failed: {e}")
return None-
"Asyncio not initialized" Error
- Ensure the AsyncioDAT operator is properly created
- Check that initialization completed successfully
- Try using the Reset pulse parameter
-
Tasks Not Executing
- Verify "Auto Poll" is enabled
- Check the operator's status output for error messages
- Ensure the operator is cooking every frame
-
Performance Issues
- Avoid blocking operations in async functions
- Use appropriate await points with
asyncio.sleep(0) - Monitor the number of concurrent tasks
-
Memory Leaks
- Ensure proper cleanup of resources
- Use context managers for file/network operations
- Check for circular references in task closures
The operator's output table carries the most recent status messages (how many is set by Max Status Rows), and the same messages go to the textport. Attach an Info CHOP to the operator for live counters:
| Channel | Meaning |
|---|---|
event_loop_active |
1 while asyncio is initialized |
event_loop_auto_poll |
1 while Auto Poll is on |
event_loop_poll_count |
Frames polled since initialization |
event_loop_poll_duration |
Time spent in the last poll, in milliseconds |
- CMake 3.25 or later
- Visual Studio 2019/2022 (Windows) or Xcode Command Line Tools (macOS)
- Python 3.11, provided via uv:
uv python install 3.11
TouchDesigner embeds CPython 3.11, so the operator must be built against 3.11.
Python is not vendored in this repository — a uv-managed CPython ships the
headers and import library CMake needs. The TouchDesigner Custom Operator SDK
headers are included under ext/td/include/ (see NOTICE).
- Clone the repository and install Python 3.11:
uv python install 3.11 - Build the operator (CMake auto-detects the uv Python 3.11):
.\build.ps1 # WindowsOr with CMake directly (see./build.sh # macOS/LinuxCMakePresets.json):cmake --preset dev cmake --build --preset dev --target asyncio_dat
- The plugin is built and copied to
tests/td/Plugins/automatically.
The project includes GitHub Actions workflows for:
- CI: builds and runs the unit suites on Windows and macOS for every push and
pull request to
main - Release: triggered by pushing a
v*.*.*tag. It builds, tests, and packages on both platforms before publishing anything, and takes the release notes from the matching CHANGELOG.md entry. See CONTRIBUTING.md.
Release artifacts (AsyncioDAT-windows.zip containing AsyncioDAT.dll, and
AsyncioDAT-macos.zip containing the AsyncioDAT.plugin bundle) are attached
to GitHub releases for easy download.
The project structure:
src/asyncio_dat.cpp/h: Main operator implementationsrc/py_bindings.cpp/h: Python C API bindingssrc/config.cpp/h:config.tomlparsingext/td/include/: TouchDesigner Custom Operator SDK headers (see NOTICE)tests/cpp/: Catch2 unit tests (no TouchDesigner required)tests/python/: pytest suite against a compiled test extension (no TouchDesigner required)tests/td/: TouchDesigner project, scripts, and the local integration harnessCMakeLists.txt: CMake build configuration
This project is licensed under the MIT License — see LICENSE. Bundled and third-party components are attributed in NOTICE.
Contributions are welcome. See CONTRIBUTING.md for the build, test, and pull request expectations. Security issues go through private reporting, not a public issue.
Release-by-release changes are recorded in CHANGELOG.md.
For questions and support:
- Check the example files under
tests/td/(e.g.examples/quickstart.py) - Review the TouchDesigner integration suite in
tests/td/(asyncio_test.py, run bytd_test_runner.py) - See TESTING.md for the unit tests and the local TouchDesigner integration harness
- Open an issue on the project repository