Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
cefd225
feat: add indexed fabric transform kernels for local/world matrix pro…
pv-nvidia May 18, 2026
6341d1d
test: convert fabric kernel tests to plain functions (no classes)
pv-nvidia May 21, 2026
59bdf95
test: rewrite fabric kernel tests with wp.array (no Fabric runtime deps)
pv-nvidia May 21, 2026
d6a65af
refactor: extract world↔local math into @wp.func for testability
pv-nvidia May 21, 2026
0c52abc
refactor: extract world↔local math into @wp.func for testability
pv-nvidia May 21, 2026
936ae76
refactor: inline @wp.func helpers, add __all__, sync with full-stack
pv-nvidia May 22, 2026
d127525
Revert "refactor: inline @wp.func helpers, add __all__, sync with ful…
pv-nvidia May 22, 2026
e795df9
docs: update changelog wording ('Will be used by' → 'Used by')
pv-nvidia May 22, 2026
0af88a6
docs: fix changelog wording (Used by → Will be used by)
pv-nvidia May 23, 2026
b467252
revert: remove wp.where refactor (unrelated to this PR)
pv-nvidia May 23, 2026
db06981
test: replace scipy with warp builtins in fabric kernel test helper
pv-nvidia May 23, 2026
a3176fa
docs: remove dead ThreeDWorld link (domain squatted)
pv-nvidia May 23, 2026
4dc3b5c
feat: Fabric-accelerated get/set_local_poses via indexedfabricarray
pv-nvidia May 25, 2026
77fbb0e
refactor: remove kernel unit tests, consolidate changelogs for combin…
pv-nvidia May 25, 2026
fee3e75
refactor: inline @wp.func helpers into production kernels
pv-nvidia May 26, 2026
3765299
feat: deprecate set/get_scales, add explicit local/world scale API
pv-nvidia May 26, 2026
cf89d7f
fix: UsdFrameView.set/get_world_scales Gf.Vec3d type errors
pv-nvidia May 26, 2026
8e00c5a
cleanup: remove redundant get/set_scales overrides from subclasses
pv-nvidia May 26, 2026
a304d88
fix: review improvements to scale API refactoring
May 30, 2026
769e4df
feat: return ProxyArray from scale getters, add scale contract tests,…
May 30, 2026
9c06038
cleanup: remove derived-class details from BaseFrameView docstrings
Jun 1, 2026
4c1e020
cleanup: rename _get/set_scales_default to _impl, warn only once
Jun 1, 2026
05e0ed6
revert: restore docs/source/setup/ecosystem.rst to upstream state
Jun 1, 2026
5d73a3e
cleanup: update stale set_scales/get_scales references in FabricFrame…
Jun 1, 2026
3f1820f
fix: reset newton_site_frame_view.py to upstream + minimal scale addi…
Jun 2, 2026
3c6de78
feat: add tests for world and local scale conversions under scaled pa…
pv-nvidia Jun 2, 2026
025eea7
test: fix FrameView scale contracts
Jun 3, 2026
7e82bd1
style: format FrameView scale contract test
Jun 4, 2026
fc9f12c
bench: include FrameView scale operations
pv-nvidia Jun 4, 2026
1566c63
fix: preserve ProxyArray get_scales contract
pv-nvidia Jun 4, 2026
ead581b
docs: remove ProxyArray scale return attribution
pv-nvidia Jun 4, 2026
57fd126
fix: preserve Newton legacy scale semantics
pv-nvidia Jun 4, 2026
96195d7
style: format Newton scale type annotation
pv-nvidia Jun 4, 2026
c109f6a
fix: flush Fabric world matrices after local writes
pv-nvidia Jun 4, 2026
2bc2c64
style: apply pre-commit formatting
pv-nvidia Jun 4, 2026
2966561
feat: configure Fabric change-block matrix flushes
pv-nvidia Jun 5, 2026
ce9ab26
Restore three-selection RO/RW design, drop eager world<->local flushes
pv-nvidia Jun 5, 2026
264baf5
docs: clarify Fabric getter synchronization
pv-nvidia Jun 6, 2026
1713bfc
Introduce FrameViewSpaceWriter context API; drop dirty tracking
pv-nvidia Jun 9, 2026
0c47acb
refactor: split xform_space_writer(space) into xform_world_space_writ…
pv-nvidia Jun 22, 2026
eecb4c2
refactor: collapse Fabric selections from three to two
pv-nvidia Jun 23, 2026
7ce0f0f
docs: describe Fabric selection layout as end state in changelog
pv-nvidia Jun 23, 2026
c2f7a7f
fix: best-effort opposite-space sync on writer-scope exception
pv-nvidia Jun 23, 2026
54e26c1
docs: clarify writer scope is synchronous; no step/render inside
pv-nvidia Jun 23, 2026
d961744
docs: clarify what the writer-scope tracking pause actually prevents
pv-nvidia Jun 23, 2026
c3fc506
docs: correct mental model of the tracking pause (pull-based, not push)
pv-nvidia Jun 23, 2026
45fd8ab
docs: use precise Omniverse terminology (Fabric Hierarchy vs Fabric)
pv-nvidia Jun 23, 2026
714bc7b
docs: drop intra-branch history from changelog fragments
pv-nvidia Jun 23, 2026
a12cce9
fix(bench): exclude one-time init from Total / Overall speedup
pv-nvidia Jun 24, 2026
4e92976
test: drop tests that assert against past code, rename stale survivors
pv-nvidia Jun 24, 2026
d6104a1
fix: sync after every getter, not only on the cached path
pv-nvidia Jun 24, 2026
15646c6
refactor: dedupe _compute_fabric_indices via the existing primitive
pv-nvidia Jun 24, 2026
70f1400
refactor: comprehensions over imperative loops in fabric index helpers
pv-nvidia Jun 24, 2026
7824217
fix: restore clone-template suffix stripping for Newton site ref prim
pv-nvidia Jun 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/source/api/lab/isaaclab.utils.rst
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,16 @@ Warp operations
:members:
:imported-members:
:show-inheritance:

Warp Fabric kernels
^^^^^^^^^^^^^^^^^^^

Warp kernels for reading and writing Fabric ``Matrix4d`` attributes
(``omni:fabric:worldMatrix`` / ``omni:fabric:localMatrix``) via
:class:`wp.fabricarray` and :class:`wp.indexedfabricarray`. Used by
:class:`~isaaclab_physx.sim.views.FabricFrameView` to keep child world and
local matrices consistent without round-tripping through USD.

.. automodule:: isaaclab.utils.warp.fabric
:members:
:show-inheritance:
3 changes: 2 additions & 1 deletion scripts/benchmarks/benchmark_view_comparison.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,8 @@ def _run_pose_benchmarks(

start_time = time.perf_counter()
for _ in range(num_iterations):
view.set_world_poses(new_positions, orientations)
with view.xform_world_space_writer() as w:
w.set_poses(new_positions, orientations)
timing_results["set_world_poses"] = (time.perf_counter() - start_time) / num_iterations

ret_pos, ret_quat = view.get_world_poses()
Expand Down
121 changes: 106 additions & 15 deletions scripts/benchmarks/benchmark_xform_prim_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,16 @@ def benchmark_frame_view( # noqa: C901
is_newton = api == "isaaclab-newton-site"

def to_torch(a):
return wp.to_torch(a) if isinstance(a, wp.array) else a
if isinstance(a, wp.array):
return wp.to_torch(a)
if hasattr(a, "torch"):
return a.torch
return a

try:
# -- Warmup --------------------------------------------------------
xform_view.get_world_poses()
xform_view.get_world_scales()

# -- get_world_poses -----------------------------------------------
if is_newton:
Expand All @@ -161,8 +166,13 @@ def to_torch(a):
computed_results["initial_world_orientations"] = orientations_t.clone()

# -- set_world_poses -----------------------------------------------
# ``.warp`` unwraps the ProxyArray returned by ``get_*_poses`` /
# ``get_*_scales`` to the underlying ``wp.array`` that ``wp.clone``
# requires. ProxyArray was introduced in PR #5304 ("ProxyArray and
# Asset/Sensor level property caching") which changed the FrameView
# getter return type. Applies to every ``wp.clone`` call below.
if is_newton:
new_positions = wp.clone(positions)
new_positions = wp.clone(positions.warp)
wp.to_torch(new_positions)[:, 2] += 0.1
else:
new_positions = positions_t.clone()
Expand All @@ -172,7 +182,8 @@ def to_torch(a):
torch.cuda.synchronize()
start_time = time.perf_counter()
for _ in range(num_iterations):
xform_view.set_world_poses(new_positions, orientations)
with xform_view.xform_world_space_writer() as w:
w.set_poses(new_positions, orientations)
if is_newton:
torch.cuda.synchronize()
timing_results["set_world_poses"] = (time.perf_counter() - start_time) / num_iterations
Expand All @@ -198,7 +209,7 @@ def to_torch(a):

# -- set_local_poses -----------------------------------------------
if is_newton:
new_translations = wp.clone(translations)
new_translations = wp.clone(translations.warp)
wp.to_torch(new_translations)[:, 2] += 0.1
else:
new_translations = translations_t.clone()
Expand All @@ -208,7 +219,8 @@ def to_torch(a):
torch.cuda.synchronize()
start_time = time.perf_counter()
for _ in range(num_iterations):
xform_view.set_local_poses(new_translations, orientations_local)
with xform_view.xform_local_space_writer() as w:
w.set_poses(new_translations, orientations_local)
if is_newton:
torch.cuda.synchronize()
timing_results["set_local_poses"] = (time.perf_counter() - start_time) / num_iterations
Expand All @@ -217,6 +229,72 @@ def to_torch(a):
computed_results["local_translations_after_set"] = to_torch(ta).clone()
computed_results["local_orientations_after_set"] = to_torch(ola).clone()

# -- get_world_scales ----------------------------------------------
if is_newton:
torch.cuda.synchronize()
start_time = time.perf_counter()
for _ in range(num_iterations):
world_scales = xform_view.get_world_scales()
if is_newton:
torch.cuda.synchronize()
timing_results["get_world_scales"] = (time.perf_counter() - start_time) / num_iterations

world_scales_t = to_torch(world_scales)
computed_results["initial_world_scales"] = world_scales_t.clone()

# -- set_world_scales ----------------------------------------------
if is_newton:
new_world_scales = wp.clone(world_scales.warp)
wp.to_torch(new_world_scales)[:] = 1.1
else:
new_world_scales = world_scales_t.clone()
new_world_scales[:] = 1.1

if is_newton:
torch.cuda.synchronize()
start_time = time.perf_counter()
for _ in range(num_iterations):
with xform_view.xform_world_space_writer() as w:
w.set_scales(new_world_scales)
if is_newton:
torch.cuda.synchronize()
timing_results["set_world_scales"] = (time.perf_counter() - start_time) / num_iterations

computed_results["world_scales_after_set"] = to_torch(xform_view.get_world_scales()).clone()

# -- get_local_scales ----------------------------------------------
if is_newton:
torch.cuda.synchronize()
start_time = time.perf_counter()
for _ in range(num_iterations):
local_scales = xform_view.get_local_scales()
if is_newton:
torch.cuda.synchronize()
timing_results["get_local_scales"] = (time.perf_counter() - start_time) / num_iterations

local_scales_t = to_torch(local_scales)
computed_results["initial_local_scales"] = local_scales_t.clone()

# -- set_local_scales ----------------------------------------------
if is_newton:
new_local_scales = wp.clone(local_scales.warp)
wp.to_torch(new_local_scales)[:] = 0.9
else:
new_local_scales = local_scales_t.clone()
new_local_scales[:] = 0.9

if is_newton:
torch.cuda.synchronize()
start_time = time.perf_counter()
for _ in range(num_iterations):
with xform_view.xform_local_space_writer() as w:
w.set_scales(new_local_scales)
if is_newton:
torch.cuda.synchronize()
timing_results["set_local_scales"] = (time.perf_counter() - start_time) / num_iterations

computed_results["local_scales_after_set"] = to_torch(xform_view.get_local_scales()).clone()

# -- get_both (world + local) --------------------------------------
if is_newton:
torch.cuda.synchronize()
Expand All @@ -233,7 +311,8 @@ def to_torch(a):
torch.cuda.synchronize()
start_time = time.perf_counter()
for _ in range(num_iterations):
xform_view.set_world_poses(new_positions, orientations)
with xform_view.xform_world_space_writer() as w:
w.set_poses(new_positions, orientations)
xform_view.get_world_poses()
if is_newton:
torch.cuda.synchronize()
Expand Down Expand Up @@ -267,15 +346,26 @@ def print_results(results_dict: dict[str, dict[str, float]], num_prims: int, num
print(header)
print("-" * 120)

operations = [
("Initialization", "init"),
# ``init`` is the one-time view-construction cost. We display it in the
# per-operation table but EXCLUDE it from the steady-state totals and the
# overall speedup -- otherwise a backend whose construction is dominated
# by stage population (e.g. Newton, where the first call materializes the
# site cache) shows a misleading "0.00x" overall and crushes the rest of
# the table. The overall row is intended to compare per-iteration cost.
init_op = ("Initialization (one-time)", "init")
per_iter_operations = [
("Get World Poses", "get_world_poses"),
("Set World Poses", "set_world_poses"),
("Get Local Poses", "get_local_poses"),
("Set Local Poses", "set_local_poses"),
("Get World Scales", "get_world_scales"),
("Set World Scales", "set_world_scales"),
("Get Local Scales", "get_local_scales"),
("Set Local Scales", "set_local_scales"),
("Get Both (World+Local)", "get_both"),
("Interleaved World Set->Get", "interleaved_world_set_get"),
]
operations = [init_op, *per_iter_operations]

for op_name, op_key in operations:
row = f"{op_name:<28}"
Expand All @@ -286,15 +376,16 @@ def print_results(results_dict: dict[str, dict[str, float]], num_prims: int, num

print("=" * 120)

total_row = f"{'Total':<28}"
total_row = f"{'Total (per-iter ops)':<28}"
for name in api_names:
total_row += f" {sum(results_dict[name].values()) * 1000:>{col_width}.4f}"
per_iter_total = sum(results_dict[name].get(k, 0) for _, k in per_iter_operations)
total_row += f" {per_iter_total * 1000:>{col_width}.4f}"
print(f"\n{total_row}")

baseline = "isaaclab-usd"
if baseline in results_dict and len(api_names) > 1:
print("\n" + "=" * 120)
print(f"SPEEDUP vs {baseline.replace('-', ' ').title()}")
print(f"SPEEDUP vs {baseline.replace('-', ' ').title()} (per-iter ops; one-time init excluded)")
print("=" * 120)
header = f"{'Operation':<28}"
for name in api_names:
Expand All @@ -304,7 +395,7 @@ def print_results(results_dict: dict[str, dict[str, float]], num_prims: int, num
print("-" * 120)

base = results_dict[baseline]
for op_name, op_key in operations:
for op_name, op_key in per_iter_operations:
row = f"{op_name:<28}"
base_t = base.get(op_key, 0)
for name in api_names:
Expand All @@ -317,11 +408,11 @@ def print_results(results_dict: dict[str, dict[str, float]], num_prims: int, num
print(row)

print("=" * 120)
print(f"{'Overall':>28}", end="")
total_base = sum(base.values())
print(f"{'Overall (per-iter ops)':>28}", end="")
total_base = sum(base.get(k, 0) for _, k in per_iter_operations)
for name in api_names:
if name != baseline:
total_impl = sum(results_dict[name].values())
total_impl = sum(results_dict[name].get(k, 0) for _, k in per_iter_operations)
if total_base > 0 and total_impl > 0:
print(f" {total_base / total_impl:>{col_width}.2f}x", end="")
else:
Expand Down
30 changes: 30 additions & 0 deletions source/isaaclab/changelog.d/fabric-local-poses.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
Added
^^^^^

* Added explicit local/world scale getters
:meth:`~isaaclab.sim.views.BaseFrameView.get_local_scales` and
:meth:`~isaaclab.sim.views.BaseFrameView.get_world_scales` to the FrameView
API, implemented for :class:`~isaaclab.sim.views.UsdFrameView`. Scale
writes go through the writer scope (see the ``xform-space-writer``
fragment).

* Added :func:`~isaaclab.utils.warp.fabric.decompose_indexed_fabric_transforms`,
:func:`~isaaclab.utils.warp.fabric.compose_indexed_fabric_transforms`,
:func:`~isaaclab.utils.warp.fabric.update_indexed_local_matrix_from_world`, and
:func:`~isaaclab.utils.warp.fabric.update_indexed_world_matrix_from_local`
Warp kernels operating on :class:`wp.indexedfabricarray` for reading and
writing Fabric ``Matrix4d`` attributes (``omni:fabric:worldMatrix`` /
``omni:fabric:localMatrix``).

Deprecated
^^^^^^^^^^

* Deprecated :meth:`~isaaclab.sim.views.BaseFrameView.get_scales` and
:meth:`~isaaclab.sim.views.BaseFrameView.set_scales`. For reads, use
the explicit ``get_local_scales`` (operates on ``xformOp:scale``) or
``get_world_scales`` (composed world-space scale). For writes, use
``with view.xform_world_space_writer() as w: w.set_scales(...)`` (or
``xform_local_space_writer``). The deprecated methods still work but
emit a ``DeprecationWarning``;
:class:`~isaaclab.sim.views.UsdFrameView` preserves prior behavior by
defaulting to local scales.
30 changes: 30 additions & 0 deletions source/isaaclab/changelog.d/xform-space-writer.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
Added
^^^^^

* Added :class:`~isaaclab.sim.views.FrameViewSpaceWriterBase`, the new context-managed
write API for ``FrameView``-managed prim transforms. Open with
``view.xform_world_space_writer()`` or ``view.xform_local_space_writer()`` and call
:meth:`~isaaclab.sim.views.FrameViewSpaceWriterBase.set_poses` /
:meth:`~isaaclab.sim.views.FrameViewSpaceWriterBase.set_scales` inside the scope;
the writer's ``__exit__`` derives the opposite-space matrices once and
synchronizes once. Only one writer scope may be active per view at a
time. View-level getters
(:meth:`~isaaclab.sim.views.BaseFrameView.get_world_poses` etc.) raise
:class:`RuntimeError` while a writer scope is active.

* Added the two concrete tag classes
:class:`~isaaclab.sim.views.FrameViewWorldSpaceWriter` and
:class:`~isaaclab.sim.views.FrameViewLocalSpaceWriter` returned by
:meth:`~isaaclab.sim.views.BaseFrameView.xform_world_space_writer` /
:meth:`~isaaclab.sim.views.BaseFrameView.xform_local_space_writer`.

Deprecated
^^^^^^^^^^

* Deprecated :meth:`~isaaclab.sim.views.BaseFrameView.set_world_poses` and
:meth:`~isaaclab.sim.views.BaseFrameView.set_local_poses`. Use
``with view.xform_world_space_writer() as w: w.set_poses(...)`` (or
:meth:`~isaaclab.sim.views.BaseFrameView.xform_local_space_writer`)
instead. The deprecated methods still work but emit a one-time
``DeprecationWarning`` per class and open a single-statement writer scope
internally.
14 changes: 8 additions & 6 deletions source/isaaclab/isaaclab/sensors/camera/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,8 @@ def set_world_poses(
orientations = convert_camera_frame_orientation_convention(orientations, origin=convention, target="opengl")
ori_wp = wp.from_torch(orientations.contiguous(), dtype=wp.vec4f)
idx_wp = self._resolve_env_ids_wp(env_ids)
self._view.set_world_poses(pos_wp, ori_wp, idx_wp)
with self._view.xform_world_space_writer() as writer:
writer.set_poses(pos_wp, ori_wp, idx_wp)

def set_world_poses_from_view(
self, eyes: torch.Tensor, targets: torch.Tensor, env_ids: Sequence[int] | None = None
Expand Down Expand Up @@ -434,11 +435,12 @@ def set_world_poses_from_view(
env_ids_torch = env_ids_torch.index_select(0, valid_indices)
orientations = quat_from_matrix(rotation_matrix)
idx_wp = wp.from_torch(env_ids_torch.contiguous(), dtype=wp.int32)
self._view.set_world_poses(
wp.from_torch(eyes.contiguous(), dtype=wp.vec3f),
wp.from_torch(orientations.contiguous(), dtype=wp.vec4f),
idx_wp,
)
with self._view.xform_world_space_writer() as writer:
writer.set_poses(
wp.from_torch(eyes.contiguous(), dtype=wp.vec3f),
wp.from_torch(orientations.contiguous(), dtype=wp.vec4f),
idx_wp,
)

"""
Operations
Expand Down
4 changes: 4 additions & 0 deletions source/isaaclab/isaaclab/sim/views/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@ __all__ = [
"BaseFrameView",
"UsdFrameView",
"FrameView",
"FrameViewSpaceWriterBase",
"FrameViewWorldSpaceWriter",
"FrameViewLocalSpaceWriter",
# Deprecated alias
"XformPrimView",
]

from .base_frame_view import BaseFrameView
from .usd_frame_view import UsdFrameView
from .frame_view import FrameView
from .xform_space_writer import FrameViewSpaceWriterBase, FrameViewWorldSpaceWriter, FrameViewLocalSpaceWriter
# Deprecated alias
from .xform_prim_view import XformPrimView
Loading
Loading