Code Architecture#
Complete architecture reference for the NVIDIA RTX Remix Toolkit.
For extension naming conventions and dependency direction, see Extension Guide.
USD Contexts#
The app runs three distinct USD contexts simultaneously, each isolating its own stage:
Context enum |
String value |
Layout name |
Code name |
|---|---|---|---|
|
|
Modding Layout |
StageCraft |
|
|
Ingestion Layout |
IngestCraft |
|
|
AI Tools Layout |
TextureCraft |
Always pass context_name to USD operations. Never assume the default context — code that only works with "" is
broken in IngestCraft and TextureCraft.
Extension Lifecycle#
Standard pattern — single instance, simple lifecycle#
_INSTANCE = None
def get_instance():
return _INSTANCE
class MyExtension(omni.ext.IExt):
def on_startup(self, ext_id: str):
global _INSTANCE
_INSTANCE = MyCore()
def on_shutdown(self):
global _INSTANCE
_INSTANCE.destroy()
_INSTANCE = None
Context-aware pattern — one instance per USD context#
Preferred for any extension that operates on a stage. Instances are created lazily on first access:
_instances: dict[str, "MyCore"] = {}
def get_instance(context_name: str = "") -> "MyCore":
if context_name not in _instances:
_instances[context_name] = MyCore(context_name=context_name)
return _instances[context_name]
class MyExtension(omni.ext.IExt):
def on_startup(self, ext_id: str):
pass # instances are created on first access
def on_shutdown(self):
_instances.clear()
default_attr cleanup pattern — multiple attributes to tear down#
reset_default_attrs calls destroy() on each attribute (if it exists), then resets it to the default value. Use this
instead of manually nulling attributes in on_shutdown:
from omni.flux.utils.common import reset_default_attrs
class MyExtension(omni.ext.IExt):
def __init__(self):
super().__init__()
self.default_attr = {"_core": None, "_sub": None}
for attr, value in self.default_attr.items():
setattr(self, attr, value)
def on_startup(self, ext_id: str):
self._core = MyCore()
self._sub = EventSubscription(...)
def on_shutdown(self):
reset_default_attrs(self)
Pure library pattern — no lifecycle management#
Extensions that are just Python packages omit extension.py entirely. Register only via [[python.module]] in
extension.toml. omni.flux.utils.common is an example.
Design Principles#
Separate concerns so that code can be reused across any UI, application, or context.
.coredoes the work. It takes inputs and produces outputs. No UI, no menus..widgetshows the UI. It fires subscriptions when the user interacts. It does no processing..controllerconnects them. It is the only extension a user needs to enable.
This means you can swap the UI without changing the logic, and reuse the logic from a completely different UI. A
.widget built on omni.ui.Frame can be embedded in a window, a sidebar, or a panel in any application — not just the
one it was designed for.
Widget rule: Always build widgets on omni.ui.Frame or omni.ui.Stack, never on omni.ui.Window. A widget must be
embeddable anywhere without creating a standalone window.
Style rule: Never define a stylesheet inside a widget or window extension. Styles belong in the app-level .style
extension. Use identifier names on UI elements so the stylesheet can target them.
Event System#
Use omni.flux.utils.common.Event and EventSubscription for intra-extension events (not carb events):
from omni.flux.utils.common import Event, EventSubscription
self._on_change = Event()
self._sub = EventSubscription(self._on_change, self._handle_change)
Critical lifetime rule: An EventSubscription is only active while it is referenced. If you do not assign the
return value to self, it is garbage-collected immediately and the callback never fires. Always store subscriptions as
instance attributes.
Events Manager#
lightspeed.events_manager provides a global event bus for Remix-wide lifecycle events (game capture loaded, stage
replaced, etc.). Event extensions register themselves on startup and unregister on shutdown:
from lightspeed.events_manager import get_instance as _get_event_manager_instance
class MyEventExtension(omni.ext.IExt):
def on_startup(self, ext_id: str):
self._core = MyEventCore()
_get_event_manager_instance().register_event(self._core)
def on_shutdown(self):
_get_event_manager_instance().unregister_event(self._core)
Use lightspeed.events_manager for cross-extension Remix lifecycle events. Use omni.flux.utils.common.Event for
events scoped to a single extension’s public API.
Commands (Undo Support)#
All user-facing actions that modify data must be omni.kit.commands.Command subclasses with do() and undo(). Direct
mutations without commands will be rejected in review.
See Implementing Commands for the full implementation reference.
Factory / Plugin Pattern#
Many systems (validators, stage manager, AI tools) use omni.flux.factory.base. Plugins register themselves and are
discovered at runtime. See omni.flux.validator.factory for the canonical example.
Service extensions register with omni.flux.service.factory:
from omni.flux.service.factory import get_instance as _get_service_factory_instance
def on_startup(self, _ext_id):
_get_service_factory_instance().register_plugins([MyService])
def on_shutdown(self):
_get_service_factory_instance().unregister_plugins([MyService])
See Implementing REST Service Endpoints for the full implementation reference.
Settings#
Declared in extension.toml under [settings] (transient) or [settings.persistent] (persisted across sessions).
Access at runtime via carb.settings.get_settings().
[settings]
exts."my.ext.name".some_key = "default_value"
[settings.persistent]
exts."my.ext.name".user_pref = "default_value"
Pip Archive#
Extensions that need third-party Python packages must declare "omni.flux.pip_archive" = {} as a dependency — do not
add pip packages directly to requirements.txt or import them without this. omni.flux.pip_archive bundles packages (
including Pillow, numpy, PyGit2, etc.) and makes them importable. If a package you need is not in the archive, raise it
with the team rather than trying to install it another way.
See Adding Pip Package Dependencies for the full reference.
Job Queue#
omni.flux.job_queue.core provides a generic priority-based job queue with dependency tracking. Use JobGraph for
multi-step jobs with dependencies. AI tool integrations (ComfyUI, etc.) use this for background processing.
App Files vs. Extension Dependencies#
Most extensions are loaded transitively through [dependencies] in another extension’s extension.toml. Only add an
extension directly to a .kit app file if it is a standalone top-level UI entry point with no parent extension that
would load it.
App files live in source/apps/:
App file |
When to add |
|---|---|
|
Core feature needed across all Remix contexts |
|
StageCraft (main modding view) only |
|
TextureCraft (texture remastering) only |
|
IngestCraft (asset ingestion) only |
|
Developer-only / debug features |