Extension Development with FSD#
This guide covers best practices and patterns for developing Kit extensions that work effectively with the Fabric Scene Delegate (FSD). While FSD is enabled by default in Kit 109+, extension developers need to understand its architecture and behavior to write efficient, correct code.
Understanding the Architecture Around FSD#
FSD introduces a fundamental shift in how scene data flows through Kit applications. Understanding this architecture is essential for writing correct extension code.
USD serves as the authoring layer where scene changes are made and persisted. This is where extensions create prims, set attributes, author materials, and build the scene graph. All changes made through USD’s authoring APIs are saved to USD files.
Fabric serves as the runtime layer where simulation, rendering, and performance-critical operations occur. Fabric is optimized for high-throughput data access and modification, making it ideal for per-frame updates, simulations, and queries.
USDRT Population synchronizes data from USD to Fabric in optimized batches. Unlike OmniHydra’s immediate synchronization, USDRT Population uses explicit (delayed) synchronization, meaning USD and Fabric may contain different data until synchronization is triggered.
This separation provides significant performance benefits — batch updates are orders of magnitude faster than one-by-one synchronization — but it requires developers to be aware of when data is synchronized and which API to use for their use case.
Who Should Read This#
This guide is intended for:
Extension developers creating new Kit extensions
Developers updating existing extensions for FSD compatibility
Anyone writing code that works with both USD and Fabric APIs
Developers experiencing issues with data consistency or stale data when FSD is enabled
Technical leads architecting new Kit-based applications
Note: If you’re just using existing Kit extensions and not developing your own, you typically don’t need to read this guide. FSD works transparently for end users.
Choosing Your Workflow#
The right workflow depends on your requirements for data persistence and performance:
Need data to persist in USD files? → Use the USD-only workflow
Need high performance with transient data? → Use the Fabric-only workflow
Need both persistence and high-performance runtime? → Separate into authoring (USD) and runtime (Fabric) components
USD-only Workflow#
When to Use#
This workflow is appropriate when:
Changes must be saved to USD files (scene authoring, asset creation)
Data needs to be version controlled or shared
Compatibility with USD-based tools and pipelines is required
Performance is acceptable (not performance-critical)
Working with traditional authoring workflows (importers, converters, offline tools)
How It Works#
When using the USD-only workflow, you work exclusively with USD’s authoring APIs. FSD handles synchronization automatically — your USD changes are batched and synchronized to Fabric, then FSD reads from Fabric for rendering.
import omni.usd
import omni.timeline
from pxr import UsdGeom, Gf
# Get the current USD stage
usd_context = omni.usd.get_context()
stage = usd_context.get_stage()
# Get timeline for time code conversion
timeline = omni.timeline.get_timeline_interface()
# Create and modify USD prims as usual
cube_prim = stage.DefinePrim("/World/Cube", "Cube")
cube = UsdGeom.Cube(cube_prim)
# Set the cube size
cube.GetSizeAttr().Set(100.0)
# Set the display color (red)
cube.GetDisplayColorAttr().Set([Gf.Vec3f(1.0, 0.0, 0.0)])
# Add transform operations with time-sampled animation
xformable = UsdGeom.Xformable(cube_prim)
xform_op = xformable.AddTranslateOp()
# Animate the cube from (100, 0, 0) to (100, 200, 50) over 10 seconds
xform_op.Set(Gf.Vec3d(100, 0, 0), time=timeline.time_to_time_code(0.0))
xform_op.Set(Gf.Vec3d(100, 200, 50), time=timeline.time_to_time_code(10.0))
# Changes are automatically synchronized to Fabric before rendering
# Time-sampled data is evaluated automatically at each frame
Important Considerations#
Synchronization Timing: USD changes are synchronized to Fabric in batches, typically right before rendering. For most authoring workflows, this is transparent and requires no special handling.
Extension Ordering: If your USD-only extension needs to run before other extensions that read from Fabric, ensure proper extension ordering is used.
Performance: Batch synchronization is highly efficient. Even thousands of USD changes are synchronized quickly because USDRT Population batches them together.
Best Practices#
Use standard USD APIs — no special FSD-specific code needed
Let FSD handle synchronization automatically
Specify extension update order if ordering matters
For more details on synchronization strategy, see FSD vs OmniHydra
Fabric-only Workflow#
When to Use#
This workflow is appropriate when:
High performance is critical (simulations, procedural generation, heavy per-frame updates)
Data is transient and doesn’t need to persist in USD files
Making large numbers of changes every frame
Implementing runtime-only features (physics simulation results, procedural effects, temporary visualizations)
Need direct access to Fabric’s vectorized, high-throughput data structures
Transform Representation in Fabric#
FSD uses a different transform representation than OmniHydra’s legacy format. Understanding this is crucial for Fabric-only code.
Legacy Format (OmniHydra):
_localMatrix,_worldMatrixattributesSee Working with OmniHydra Transforms for details
FSD Format (Recommended):
omni:fabric:localMatrix,omni:fabric:worldMatrixattributesManaged by the IFabricHierarchy interface
See Fabric Transforms with IFabricHierarchy for complete details
Migration: If you have code using the legacy format, update to the new format. FSD’s Fabric Hierarchy automatically converts legacy transforms for compatibility, but direct usage of the new format is preferred.
The Essential Pattern: Sync Before Read#
Always synchronize USD to Fabric before reading Fabric data to ensure you’re working with up-to-date information:
import omni.usd
from usdrt import Usd
# Get the USDRT stage (Fabric view of the USD stage)
usd_stage_id = omni.usd.get_context().get_stage_id()
usdrt_stage = Usd.Stage.Attach(usd_stage_id)
# CRITICAL: Synchronize USD changes to Fabric first
# This is cheap when there are no pending changes
usdrt_stage.SynchronizeToFabric()
# Now it's safe to read from Fabric
prim = usdrt_stage.SomeAPI(...)
# ... read attributes, transforms, etc.
A Note on Implicit Synchronization#
Some USDRT APIs (like GetPrimAtPath() and GetAttributeAtPath()) automatically trigger synchronization when called. This means you may not always need to explicitly call SynchronizeToFabric(). However, for clarity and predictability, we recommend explicitly calling SynchronizeToFabric() at the start of your read operations. This makes your synchronization points obvious and ensures your code works consistently.
Important: If you’re mixing USD authoring with Fabric reads in the same code path, implicit synchronization can lead to subtle bugs. See the Mixed USD/Fabric Workflow section for details on these pitfalls.
Working with IFabricHierarchy#
IFabricHierarchy is the recommended interface for working with transforms in Fabric. Key concept: After setting local transforms, the world transforms are in an inconsistent state until you call update_world_xforms().
import omni.usd
from usdrt import Gf, Usd, Sdf, Rt, hierarchy
# Get the hierarchy interface
usd_stage_id = omni.usd.get_context().get_stage_id()
usdrt_stage = Usd.Stage.Attach(usd_stage_id)
fabric_id = usdrt_stage.GetFabricId()
stage_id = usdrt_stage.GetStageIdAsStageId()
hier = hierarchy.IFabricHierarchy().get_fabric_hierarchy(fabric_id, stage_id)
# Set a local transform using Rt.Xformable API
cube_path = Sdf.Path("/World/Cube")
cube_prim = usdrt_stage.GetPrimAtPath(cube_path)
xformable = Rt.Xformable(cube_prim)
new_transform = Gf.Matrix4d(1) # Identity matrix
new_transform.SetTranslateOnly(Gf.Vec3d(50, 25, 10))
# Set the local transform
local_matrix_attr = xformable.GetFabricHierarchyLocalMatrixAttr()
local_matrix_attr.Set(new_transform)
# Alternative: using set_local_xform of IFabricHierarchy
# This also updates all descendant transforms immediately
# However, when you use it for a large number of prims, setting the matrices
# first and then updating the hierarchy once is much more efficient
# hier.set_local_xform(Sdf.Path("/World/Cube"), new_transform)
# Query child transform
child_path = Sdf.Path("/World/Cube/Child")
child_xformable = Rt.Xformable(cube_prim)
child_world_matrix_attr = child_xformable.GetFabricHierarchyWorldMatrixAttr()
# WRONG: Don't query world transforms yet - they are stale!
# world_xform = child_world_matrix_attr.Get() # Would be incorrect!
# CORRECT: Update world transforms first
hier.update_world_xforms()
# Now world transforms are consistent and correct
world_xform = child_world_matrix_attr.Get()
print(f"Correct world transform: {world_xform}")
Complete Example: Simulation Loop#
import omni.usd
from usdrt import Usd, Sdf, Gf, Rt, hierarchy
class MySimulationExtension:
def __init__(self):
# Set up USDRT stage and hierarchy once
usd_stage_id = omni.usd.get_context().get_stage_id()
self.usdrt_stage = Usd.Stage.Attach(usd_stage_id)
fabric_id = self.usdrt_stage.GetFabricId()
stage_id = self.usdrt_stage.GetStageIdAsStageId()
self.hier = hierarchy.IFabricHierarchy().get_fabric_hierarchy(
fabric_id, stage_id
)
# Cache the path and xformable
self.physics_path = Sdf.Path("/World/PhysicsObject")
self.physics_prim = self.usdrt_stage.GetPrimAtPath(self.physics_path)
self.xformable = Rt.Xformable(self.physics_prim)
def on_simulation_step(self, dt):
# 1. Sync any pending USD changes to Fabric
self.usdrt_stage.SynchronizeToFabric()
# 2. Read current local transform from Fabric
local_matrix_attr = self.xformable.GetFabricHierarchyLocalMatrixAttr()
current_transform = local_matrix_attr.Get()
# 3. Compute new transforms (your simulation logic here)
translation = Gf.Vec3d(
current_transform.GetRow(3)[0] + dt * 10, # Move 10 units/sec
current_transform.GetRow(3)[1],
current_transform.GetRow(3)[2]
)
new_transform = Gf.Matrix4d(1)
new_transform.SetTranslateOnly(translation)
# 4. Write new transform to Fabric
local_matrix_attr.Set(new_transform)
# 5. Update world transforms for rendering
self.hier.update_world_xforms()
# Rendering will now see the updated transforms
Best Practices#
Always sync before reading: Call
SynchronizeToFabric()before querying Fabric dataBatch your reads: Synchronization has overhead — sync once, then read multiple prims
Update world transforms: Call
hier.update_world_xforms()after modifying local transformsUse IFabricHierarchy: It’s the recommended, maintained API for transform operations
Set local, read local/world: Local transforms are the source of truth; world transforms are derived
Mixed USD/Fabric Workflow (Not Recommended)#
Why This Is Discouraged#
Mixing USD authoring APIs with Fabric/USDRT reading APIs in the same code path is strongly discouraged because:
USD and Fabric may contain different data until synchronization occurs
Easy to accidentally read stale Fabric data
Hard to debug — no error occurs, just incorrect results
Poor performance if you sync frequently to avoid staleness
Violates the authoring/runtime separation principle
Recommended approach: Separate your extension into distinct authoring and runtime components.
The Implicit Synchronization Trap#
A common source of bugs when mixing USD and Fabric APIs is implicit synchronization.
What is implicit synchronization?
Certain USDRT API calls trigger synchronization automatically to ensure you get up-to-date data. These include:
usdrt_stage.GetPrimAtPath()- Getting prims from the USDRT stageusdrt_prim.GetAttributeAtPath()- Getting an attribute of a primOther USDRT query operations such as
GetPrimsWithTypeName
When you call these APIs, they automatically synchronize any pending USD changes to Fabric. This is convenient, but it creates a dangerous trap: this is a one-time synchronization. Subsequent USD changes after the API call are not automatically synchronized.
Why this is dangerous:
You might think Fabric is always up-to-date because the first query worked
No error or warning occurs—your code just reads stale data
The bug is timing-dependent and hard to reproduce
It violates the principle that synchronization should be explicit and predictable
Here’s a concrete example of this trap:
import omni.usd
from pxr import UsdGeom
from usdrt import Usd, Sdf, Rt, hierarchy
# Get both USD and USDRT stages
usd_stage = omni.usd.get_context().get_stage()
usd_stage_id = omni.usd.get_context().get_stage_id()
usdrt_stage = Usd.Stage.Attach(usd_stage_id)
# 1. Create a prim in USD
cube_prim = usd_stage.DefinePrim("/World/NewCube", "Cube")
xformable = UsdGeom.Xformable(cube_prim)
translate_op = xformable.AddTranslateOp()
# 2. This USDRT API call triggers IMPLICIT synchronization
# Fabric now has the prim with its current state
usdrt_prim = usdrt_stage.GetPrimAtPath("/World/NewCube")
usdrt_xformable = Rt.Xformable(usdrt_prim)
# 3. Further USD changes are NOT automatically synchronized!
translate_op.Set((10, 0, 0))
# 4. WRONG: Reading from Fabric now gives stale data
# The translate operation from step 3 is NOT yet in Fabric
# local_matrix_attr = usdrt_xformable.GetFabricHierarchyLocalMatrixAttr()
# matrix = local_matrix_attr.Get() # Missing the translation!
# 5. CORRECT: Explicitly synchronize to see recent USD changes
usdrt_stage.SynchronizeToFabric()
# 6. Now Fabric has the updated transform
fabric_id = usdrt_stage.GetFabricId()
hier = hierarchy.IFabricHierarchy().get_fabric_hierarchy(fabric_id, usd_stage_id)
local_matrix = hier.get_local_xform(Sdf.Path("/World/NewCube"))
print(f"Transform with translation: {local_matrix}")
Key Takeaway: Implicit synchronization is a one-time event that happens when you call certain USDRT APIs. It does not keep Fabric automatically synchronized with subsequent USD changes. This makes mixed USD/Fabric code fragile and error-prone. Always explicitly call SynchronizeToFabric() when you need to see USD changes in Fabric.
If You Must Mix APIs: The Right Pattern#
If you absolutely must mix USD and Fabric APIs in the same code path, follow this explicit synchronization pattern:
import omni.usd
from pxr import UsdGeom, Gf
from usdrt import Usd, Rt
# Get both stages
usd_stage = omni.usd.get_context().get_stage()
usd_stage_id = omni.usd.get_context().get_stage_id()
usdrt_stage = Usd.Stage.Attach(usd_stage_id)
# 1. Make all your USD changes first
cube = usd_stage.DefinePrim("/World/MixedCube", "Cube")
xformable = UsdGeom.Xformable(cube)
xformable.AddTranslateOp().Set(Gf.Vec3d(10, 20, 30))
# 2. Explicitly synchronize
usdrt_stage.SynchronizeToFabric()
# 3. Now it's safe to read from Fabric
usdrt_prim = usdrt_stage.GetPrimAtPath("/World/MixedCube")
xformable_rt = Rt.Xformable(usdrt_prim)
world_matrix = xformable_rt.GetFabricHierarchyWorldMatrixAttr().Get()
# world_matrix now contains the correct data
print(f"Transform in Fabric: {world_matrix}")
Note:
GetPrimAtPath()actually triggers implicit synchronization, making the explicitSynchronizeToFabric()call technically redundant in this example. However, explicitly callingSynchronizeToFabric()is still the recommended pattern because:
It makes synchronization points obvious and intentional in your code
It works consistently with any USDRT API, not just ones that implicitly sync
Your code remains correct even if implicit sync behavior changes in future Kit versions
It’s much easier to understand, debug, and maintain
Caveats of this approach:
Performance penalty from frequent synchronization
Code is harder to maintain and understand
Still violates separation of concerns
Future Kit versions may optimize differently
The Recommended Approach: Separate Components#
Instead of mixing USD and Fabric APIs in the same code, separate your extension into distinct components:
# authoring_component.py - Works only with USD
def create_and_configure_prim(stage, path, translation):
"""Pure authoring function - only USD APIs"""
cube = stage.DefinePrim(path, "Cube")
xformable = UsdGeom.Xformable(cube)
xformable.AddTranslateOp().Set(translation)
return cube
# runtime_component.py - Works only with Fabric
def query_transform(usdrt_stage, path):
"""Pure runtime function - only Fabric APIs"""
# Assumes synchronization has already happened
prim = usdrt_stage.GetPrimAtPath(path)
xformable = Rt.Xformable(prim)
return xformable.GetFabricHierarchyWorldMatrixAttr().Get()
# main_extension.py - Orchestrates both
class MyExtension:
def on_author_mode(self):
stage = omni.usd.get_context().get_stage()
create_and_configure_prim(stage, "/World/Cube", Gf.Vec3d(10, 0, 0))
# USD changes will be synced automatically before rendering
def on_simulation_step(self):
usdrt_stage = Usd.Stage.Attach(omni.usd.get_context().get_stage_id())
usdrt_stage.SynchronizeToFabric() # Sync once at start of frame
transform = query_transform(usdrt_stage, "/World/Cube")
# ... use transform for simulation
Additional Resources#
FSD vs OmniHydra - Detailed comparison of synchronization strategies
Fabric Transforms with IFabricHierarchy - Complete transform API reference
Configuration - FSD settings and optimization options
Known Issues - Current limitations and workarounds