Extension Guide#
Complete reference for creating and documenting extensions in the RTX Remix Toolkit. This file covers naming conventions, directory layout, configuration, and documentation structure.
Extension Naming Conventions#
Extension names encode their role:
.core— Business logic only, no UI. Reusable across any UI or application..widget— Reusable UI component built onomni.ui.Frame/omni.ui.Stack(never a window). ExposesEventsubscriptions for interactions; contains no business logic..window— Wraps one or more widgets in anomni.ui.Window. Often skipped — a simple window can live directly in.controller..menu— Menu items only, exposes subscriptions. Often skipped — a simple menu can live directly in.controller..model— Data models for tree/list views (omni.ui.AbstractItemModel).controller— The top-level entry point for a feature. Wires.core,.widget/.window, and.menutogether. This is the extension users enable to activate a feature..service— REST API endpoints (FastAPI via Omniverse microservices).plugin.*— Plugin implementations loaded by a factory.app.resources— Shared assets (icons, images, fonts) for an application. No code..style— Global application stylesheet. No per-widget inline styles; all styles come from here..bundle— Meta-extension that aggregates others for convenience loading
Dependency Direction#
.controller → .widget + .core. .widget and .core must never depend on each other.
Flux / Lightspeed Namespaces#
Generic UI and utilities go in omni.flux.*. RTX Remix-specific behavior goes in lightspeed.trex.*. A Lightspeed
extension typically imports a Flux widget and adds USD/game-specific behavior on top. Reusable logic should always be
extracted to Flux first.
Directory Layout#
source/extensions/<ext-name>/
├── config/
│ └── extension.toml # Required: package metadata, dependencies, settings, test config
├── data/
│ ├── icon.png # 256×256 icon shown in the Extensions window
│ └── preview.png # Screenshot shown in the Extensions Overview
├── docs/
│ ├── CHANGELOG.md # Keep a Changelog format (required for versioning)
│ ├── README.md # Extension documentation
│ └── index.rst # Sphinx autodoc entry (copy pattern from existing exts)
├── <namespace>/<path>/<name>/ # Python package — mirrors the extension name exactly
│ ├── __init__.py # Declares __all__ and re-exports public API
│ ├── extension.py # omni.ext.IExt subclass + module-level get_instance()
│ ├── *.py # Implementation files
│ └── tests/
│ ├── __init__.py # Must export test classes for runner discovery
│ ├── unit/
│ │ ├── __init__.py
│ │ └── test_*.py
│ └── e2e/
│ ├── __init__.py
│ └── test_*.py
└── premake5.lua # Build symlink script (boilerplate — copy from any existing ext)
The Python package path mirrors the extension name: omni.flux.job_queue.core → omni/flux/job_queue/core/. For
lightspeed.* extensions, replace the root with lightspeed/.
extension.toml Reference#
[package]
version = "1.0.0" # Semantic versioning — bump on every change
authors = ["Name <email@nvidia.com>"]
title = "Human Readable Title"
description = "One-line description"
readme = "docs/README.md"
changelog = "docs/CHANGELOG.md"
category = "internal" # or "Service", "Rendering", etc.
keywords = ["keyword1", "keyword2"]
icon = "data/icon.png"
preview_image = "data/preview.png"
repository = "https://gitlab-master.nvidia.com/lightspeedrtx/lightspeed-kit/"
[dependencies]
# Direct dependencies only — never rely on transitive deps
"omni.usd" = {}
"omni.flux.utils.common" = {}
"other.ext" = { order = -100 } # negative order = load earlier
# Platform-specific dependencies
[dependencies."filter:platform"."windows-x86_64"]
"windows.only.ext" = { version = "1.2.3", exact = true }
# Transient settings (reset on restart)
[settings]
exts."my.ext.name".some_key = "default_value"
# Persistent settings (saved across sessions)
[settings.persistent]
exts."my.ext.name".user_pref = "default_value"
# Python module registration
[[python.module]]
name = "my.ext.name"
# Test config — use lightspeed.trex.tests.dependencies for trex exts,
# omni.flux.tests.dependencies for flux exts
[[test]]
dependencies = [
"lightspeed.trex.tests.dependencies",
]
# Suppress known-harmless log lines
stdoutFailPatterns.exclude = [
"*[omni.kit.registry.nucleus.utils.common] Skipping deletion of:*",
]
# Optional named test suite (e.g. a fast "startup" suite)
[[test]]
name = "startup"
dependencies = [
"lightspeed.trex.tests.dependencies",
]
Notes:
Only declare dependencies that are actually imported by this extension.
Add
"omni.flux.pip_archive" = {}if the extension uses any third-party pip packages.Never rely on transitive dependencies — if you import it, declare it.
premake5.lua Boilerplate#
local ext = get_current_extension_info()
project_ext(ext)
repo_build.prebuild_link {
{ "lightspeed/", ext.target_dir.."/lightspeed" }, -- use "omni/" for flux exts
{ "data", ext.target_dir.."/data" },
{ "docs", ext.target_dir.."/docs" },
}
Python Module Stubs#
<namespace>/<path>/<name>/__init__.py#
"""
* SPDX-FileCopyrightText: Copyright (c) <YEAR> NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
"""
__all__ = ["get_instance"]
from .extension import get_instance
<namespace>/<path>/<name>/extension.py#
"""
* SPDX-FileCopyrightText: Copyright (c) <YEAR> NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
"""
__all__ = ["get_instance"]
import carb
import omni.ext
_INSTANCE = None
def get_instance():
return _INSTANCE
class MyExtension(omni.ext.IExt):
def on_startup(self, ext_id: str):
carb.log_info("[<ext-name>] Startup")
global _INSTANCE
_INSTANCE = self
def on_shutdown(self):
carb.log_info("[<ext-name>] Shutdown")
global _INSTANCE
_INSTANCE = None
docs/index.rst#
Copy the pattern from a nearby extension of the same type:
<ext-name>
==========
.. automodule:: <ext-name>
:members:
:undoc-members:
:show-inheritance:
docs/CHANGELOG.md Starter#
# Changelog
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
## [1.0.0]
### Added
- Created
Tests Export Pattern#
Every tests/__init__.py must export its test classes so the test runner can discover them. Update this as you add
tests:
"""
* SPDX-FileCopyrightText: Copyright (c) <YEAR> NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
"""
from .unit.test_my_module import TestMyModule
from .e2e.test_my_workflow import TestMyWorkflow
An empty tests/__init__.py causes the test runner to find nothing, even if test files exist — always export your test
classes.
tests/unit/__init__.py and tests/e2e/__init__.py are empty files (no license header required for empty
__init__.py).
docs/README.md Structure#
Every extension’s docs/README.md must describe the extension clearly enough that a developer can understand its role,
scope, and architecture without reading the code. The Non-Responsibilities section is as important as
Responsibilities — it prevents scope creep and misuse.
Required sections#
Title and summary — the extension name and one sentence describing what it is.
Responsibilities — a concrete list of what this extension owns. Be specific.
Non-Responsibilities — what this extension deliberately does not do, and which extension owns that instead. Examples:
A
.widgetextension: “Does not validate or process data. Fires subscriptions and lets the caller decide what to do.”A
.coreextension: “Does not create any UI. Has no dependency onomni.ui.”
Architecture — how the extension is structured:
Key classes and their individual roles
Non-obvious design decisions and the reason for them
How it connects to dependencies (what it calls, what events it listens to)
Optional sections (include when relevant)#
Usage — a code snippet showing how another extension imports and uses this one.
Settings — any carb settings this extension exposes, with key paths, defaults, and purpose.
Known limitations — intentional gaps, unhandled edge cases, or things deferred to future work.
README template#
# <Extension Name>
One-line description.
## Responsibilities
- Specific thing this extension is responsible for
- Another specific responsibility
## Non-Responsibilities
- What this extension does NOT do (and why, or what extension handles it instead)
- Another explicit non-responsibility
## Architecture
Description of how the extension is structured. Name the key classes and explain
the role of each. Call out any non-obvious design choices.
### Key Classes
- `MyCore` — owns the main processing logic; stateless between calls
- `MyEventHandler` — subscribes to `lightspeed.events_manager` and drives the core
## Usage
```python
from my.extension import get_instance
core = get_instance(context_name="")
result = core.do_something(input_data)
```
## Settings
| Setting path | Default | Description |
|---|---|---|
| `exts.my.ext.name.some_key` | `"default"` | What this controls |
What NOT to write in a README#
Don’t document every class and method — that belongs in docstrings.
Don’t describe implementation details that are obvious from reading the code.
Don’t leave the auto-generated one-line stub as the final documentation.
Don’t write “This extension provides functionality for X” — say specifically what functionality.