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):

FSD Format (Recommended):

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 data

  • Batch your reads: Synchronization has overhead — sync once, then read multiple prims

  • Update world transforms: Call hier.update_world_xforms() after modifying local transforms

  • Use 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



Additional Resources#