Skip to content

[Backport release/3.0.0-beta2] FrameView local poses (#5677) + view-scoped Fabric selections (#6805) - #7342

Merged
kellyguo11 merged 4 commits into
isaac-sim:release/3.0.0-beta2from
pv-nvidia:pv/backport-6805-beta2
Aug 29, 2026
Merged

kellyguo11 merged 4 commits into
isaac-sim:release/3.0.0-beta2from
pv-nvidia:pv/backport-6805-beta2

Conversation

@pv-nvidia

@pv-nvidia pv-nvidia commented Aug 25, 2026 •

Copy link
Copy Markdown
Contributor

Description

Backports two already-merged FrameView PRs to release/3.0.0-beta2, cherry-picked in merge order:

Two support commits, because this branch predates some prerequisites:

Conflict resolutions converge each touched file on its post-#6805 state from develop, so the backported code is byte-identical to what has been running and tested there. Two consequences of that choice:

  • get_scales() now returns ProxyArray in all backends, as on develop. On this branch it previously returned wp.array, so callers that pass the result straight to Warp APIs need .warp (Torch users: .torch). This is the one API change the backport carries.
  • The Fabric-hierarchy import stays unconditional (the optional import is Removes Isaac Sim core extensions from app file to avoid warp conflict #6499, not backported).

The only deviation from develop's state is test-infra: device parametrization uses literal ["cpu", "cuda:0"] because test_devices() does not exist on this branch.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality) — FrameView.close(), writer-scope API
  • Breaking change (relative to this release branch only): get_scales() returns ProxyArray instead of wp.array, matching develop

Tests

  • test_views_xform_prim_ovphysx.py — 49 passed (26 cpu + 24 cuda:0; needs one process per device, ovphysx<=0.3.7 binds device mode in C++ on first PhysX(...))
  • test_views_xform_prim_newton.py — 58 passed

@github-actions github-actions Bot added documentation Improvements or additions to documentation isaac-mimic Related to Isaac Mimic team labels Aug 25, 2026
@pv-nvidia pv-nvidia changed the title Pv/backport 6805 beta2 [Backport release/3.0.0-beta2] FrameView local poses (#5677) + view-scoped Fabric selections (#6805) Aug 25, 2026
alexmillane and others added 4 commits August 27, 2026 08:55
…r API (isaac-sim#5677)

Two things:

1. **Local-space pose and scale ops are now GPU-fast on the Fabric
backend.**
They go through the same Warp-kernel path that world-space ops already
used.
The old USD fallback for local-space is ~100× slower (see the table
below).

2. **All transform writes go through a small context manager.** You open
a
   scope, do your writes inside it, and the scope handles the cleanup:

   ```python
   with view.xform_world_space_writer() as writer:
       writer.set_poses(positions=p, orientations=o)
       writer.set_scales(scales=s)
   ```

   Use `view.xform_local_space_writer()` for local-space writes.

Builds on Piotr's prototype at

[`bareya/pbarejko/camera-update`](https://github.com/bareya/IsaacLab/tree/pbarejko/camera-update).

A FrameView keeps two copies of each prim's transform in Fabric storage:
the `omni:fabric:worldMatrix` and `omni:fabric:localMatrix` attributes.
When you write one, the other has to be recomputed so they stay
consistent.

Without a scope, that recompute has to run after every single write.
With
a scope, it runs **once**, when the scope closes. You can call
`set_poses`
and `set_scales` as many times as you want inside the scope; on exit,
one
Warp kernel derives the other space and one `wp.synchronize()` runs.
Empty scopes cost nothing.

The scope also pauses **Fabric Hierarchy**'s transform-change tracking
while you're writing, then restores it on exit. (Fabric itself is just
the data store; Fabric Hierarchy, exposed through USDRT's
`IFabricHierarchy`, is the plugin that watches writes to
`omni:fabric:localMatrix` / `omni:fabric:worldMatrix` and keeps them
mutually consistent across the hierarchy.) Its tracking is pull-based:
a per-attribute listener records writes into a changelog, and the
plugin drains and processes that changelog on the next call to
`IFabricHierarchy::update_world_xforms()`. Pausing the listener does
not block the writes themselves — they still land in Fabric storage —
it just keeps them out of the changelog. With the changelog empty, the
next tick has nothing to "catch up on" for these prims, and can't
decide one of the two spaces is canonical and derive the other from
it. See the next section for why that matters.

Rules:
- Only one writer can be open on a view at a time. Opening a second one
  raises `RuntimeError`.
- While a writer is open, the view's own getters (`get_world_poses`,
etc.)
raise. Read through the writer (`writer.get_poses()`) inside the scope,
  or close the scope first.
- **Do not advance the simulation or render from inside the scope.**
  No `sim.step()`, no `world.render()`, no `SimulationApp.update()`.
The matrices are mid-write until exit, and rendering against that state
would read torn data. Keep scopes short and close them before stepping.
- If something raises inside the scope (a user-code bug, a notebook
cell interrupt, etc.), the scope still runs the opposite-space recompute
  on exit so the world and local matrices stay consistent prim-by-prim.
  The partial write itself is not rolled back — if you need
  all-or-nothing, snapshot the matrices yourself before entering.

correctness)

A writer scope is synchronous Python code, so the renderer can't see
anything mid-scope — no tick runs until we exit. The risk is on the
**next** render/sim tick after the scope closes.

On that next tick, Fabric Hierarchy runs its
`IFabricHierarchy::update_world_xforms()` step, which can recompute
`omni:fabric:worldMatrix` from `omni:fabric:localMatrix` (or vice
versa). If it thinks one of the two was the user's most recent edit,
it derives the other from it; the derived value — not what we wrote —
is then what the **Fabric Scene Delegate (FSD)** hands to RTX on the
render path.

Three things in this PR keep that next-tick recompute from clobbering
our
work:

1. **Opposite-space derive at scope exit.** By the time the scope
closes,
   `omni:fabric:worldMatrix` and `omni:fabric:localMatrix` are mutually
   consistent prim-by-prim, so any recompute Fabric Hierarchy does is a
   no-op.

2. **Fabric-Hierarchy change tracking is paused for the whole scope.**
   The writer calls `track_local_xform_changes(False)` and
   `track_world_xform_changes(False)` on enter (saving the prior state)
   and restores them on exit. While tracking is paused, the listeners
   don't record our writes in their changelog, so when the next tick
   runs there's nothing for `update_world_xforms()` to "catch up on".

3. **Two persistent selections with explicit read-only / read-write
   flags.** Outside the scope, the view's selection is fully read-only —
   both `omni:fabric:worldMatrix` and `omni:fabric:localMatrix` are
   flagged RO. Inside the scope, the writer flips to a fully read-write
   selection.

   ```
_sel_ro : worldMatrix=RO, localMatrix=RO (steady state, between writes)
_sel_rw : worldMatrix=RW, localMatrix=RW (active during a writer scope)
   ```

   Both selections are built once when the view is initialized and kept
alive for its lifetime. The writer scope flips a single flag (`_is_rw`)
   on enter/exit; nothing is rebuilt. The RO steady state tells Fabric
   Hierarchy's next-tick `update_world_xforms()` that no attribute is
   user-authored, so it leaves both alone.

- **Fabric (PhysX):** the fast GPU path described above.
- **USD, Newton, OVPhysX:** the writer is a thin wrapper. Writes go
straight
to the backend's existing storage. No batching, no extra recompute
(there
is no second matrix to keep in sync). Same API across all four backends.

Newton has two different "scale" ideas, and the API keeps them apart:

- `NewtonSiteFrameView.set_scales(...)` (deprecated) writes the **Newton
  collision-shape geometry** size.
- The writer's `set_scales(...)` writes per-site **transform scale**,
  matching what USD FrameView does.

They operate on different state and are not merged.

**New (recommended):**
- `view.xform_world_space_writer()` / `view.xform_local_space_writer()`
—
  a context manager with `set_poses`, `set_scales`, `get_poses`,
  `get_scales`.

**Deprecated (still works, warns once per class on first use):**
- `set_world_poses` / `set_local_poses` — use the writer scope.
- `get_scales` / `set_scales` — use `get_local_scales` /
  `get_world_scales`, or the writer's `set_scales`.

Per-iteration timings (lower is better):

| Operation | USD (ms) | Fabric (ms) | Newton Site (ms) |

|----------------------------|---------:|------------:|-----------------:|
| Get World Poses | 8.6148 | 0.0375 | 0.0307 |
| Set World Poses | 19.1357 | 0.1090 | 0.0368 |
| Get Local Poses | 5.9556 | 0.0374 | 0.0497 |
| Set Local Poses | 7.9794 | 0.0979 | 0.0790 |
| Get World Scales | 13.1193 | 0.0371 | 0.0015 |
| Set World Scales | 21.4571 | 0.1014 | 0.0612 |
| Get Local Scales | 3.2544 | 0.0375 | 0.0022 |
| Set Local Scales | 3.8350 | 0.0937 | 0.0587 |
| Get Both (World+Local) | 14.7506 | 0.0738 | 0.1302 |
| Interleaved World Set→Get | 28.1046 | 0.1432 | 0.1588 |
| **Per-iter total** | **126.2** | **0.77** | **0.61** |

Speedup vs USD (per-iter ops; one-time view construction excluded):

| Operation                  | Fabric × | Newton Site × |
|----------------------------|---------:|--------------:|
| Get World Poses            |   229.6× |        280.2× |
| Set World Poses            |   175.6× |        519.8× |
| Get Local Poses            |   159.0× |        119.9× |
| Set Local Poses            |    81.5× |        101.0× |
| Get World Scales           |   353.9× |       8911.5× |
| Set World Scales           |   211.6× |        350.4× |
| Get Local Scales           |    86.8× |       1489.9× |
| Set Local Scales           |    40.9× |         65.4× |
| Get Both (World+Local)     |   200.0× |        113.3× |
| Interleaved World Set→Get  |   196.2× |        177.0× |
| **Overall (per-iter)**     | **164×** |       **207×** |

One-time view construction (reported separately, not part of the
per-iter total): USD 4.6 ms, Fabric 4.4 ms, Newton Site 1013 ms — the
Newton number is dominated by stage population on the first call.
Steady-state per-iteration cost is what the speedup row reflects.

- Getters call `wp.synchronize()` before returning, so the returned
  `ProxyArray` is always immediately readable from both GPU and host
  code — no caller-side sync needed. (Both the cached and the
  per-indices paths sync; this used to be asymmetric.)

---------

Co-authored-by: pv-nvidia <peter.verswyvelen@gmail.com>
Co-authored-by: pv-nvidia <{"message":"Not Found","documentation_url":"https://docs.github.com/rest/users/emails#list-email-addresses-for-the-authenticated-user","status":"404"}>
…aac-sim#6805)

Camera world-pose resolution stalled at high environment counts on the
PhysX backend, badly enough to time out benchmarks. There are two
independent bottlenecks; the PR also adds a lifecycle hook that came out
of review.

whole stage

Cameras read their poses from Fabric. To do that the frame view needs to
know where each camera's data sits in Fabric memory, so it builds a
lookup table from "camera number" to "Fabric slot".

Building that table was very slow. The view selected prims by requiring
the Fabric world and local matrix attributes — but every xformable in
the stage carries those, so the selection matched the **entire stage**
(~1.1M prims at 8192 environments), not the view's handful of cameras.
Finding its own prims in that list then meant building a Python
dictionary over all of it, on the host:

```python
path_to_idx = {str(p): i for i, p in enumerate(selection.GetPaths())}
```

That ran twice per rebuild (once for the children, once for their
parents), and rebuilt whenever `PrepareForReuse()` reported a bucket
change — in practice on every environment reset.

The cost of that dictionary scales with the size of the whole scene, not
with the number of cameras: it converts every path in the stage to a
Python string and inserts it. Measured directly, one full rebuild takes
10 ms on a bare stage but **556 ms once 100k unrelated prims are
present** (table below), and it grows linearly from there — seconds of
host work per reset at the ~1.1M prims of an 8192-environment scene.

**Fix:** author a private per-view `uint` index attribute on each
managed prim (and on each unique parent), holding that prim's view
index. Every selection requires the matching index attribute, so
**selections resolve to exactly the view's prims**. The view-to-Fabric
slot mapping is then rebuilt by inverting that attribute in a single
Warp kernel launch:

```python
fabric_slots[view_indices[fabric_slot]] = fabric_slot
```

That is `O(view)` device work with no host-side path resolution and no
Python objects created per frame.

There is also **no cache**, so there is nothing to invalidate. The table
is rebuilt from live Fabric data on every access, which means a bucket
reorder can never leave a stale mapping behind. Selections are still
checked with `GetCount()`, which is exact here: the index attribute
belongs to one view, so the count can only change if one of that view's
prims actually disappeared — and then the view raises a clear error
instead of silently reading the wrong prim.

Attribute names embed a process-wide monotonic uid, so a dead view's
leftover attributes can never satisfy a live view's selection — and
since review, they are removed outright when the view is released
(section 3). Parent reads get their own read-only selection, keeping the
child RO/RW flip semantics from isaac-sim#5677 intact.

```python
for i, path in enumerate(input_paths):
    mapping[i] = paths.index(path)   # O(N) scan, per path
```

**Fix:** build the reverse dict once (first occurrence wins, matching
`list.index` semantics), then resolve each path in `O(1)`. Measured over
200k paths, preserving first-occurrence semantics costs nothing versus
an unguarded dict comprehension, so the fix carries no behaviour change.

review)

The per-view index attributes from fix 1 were initially never removed,
so views recreated over the same prims on a long-lived stage accumulated
attribute pairs, widening those prims' Fabric buckets. Measured over 8
successive views on one stage (512 prims), attributes on a single prim
went 1 → 8 and first-access rose monotonically from 120 ms to 173 ms.

**Fix:** `FabricFrameView.close()` removes the view's tags (measured at
4.7 µs per prim, ~77 ms for an 8192-env view) and is safe to call more
than once. `BaseFrameView` gains a no-op `close()` so callers can close
any backend uniformly. Every in-repo callsite now closes its view:
`Camera` when invalidated **and when dropped** (`__del__`),
`prim_world_positions` after each per-env view it creates in a loop, and
`SceneAsset` before replacing its cached view. Views dropped without
`close()` are cleaned up best-effort from `__del__`, following the
shutdown-safe idiom the env classes already use (`sys` bound as a
default argument; nothing runs during interpreter finalization, where
Fabric dies with the process anyway), and a warning names the view so
the missing `close()` call can be fixed.

The stall was invisible in Tracy. Comparing captures before and after
the commit that introduced it, Kit's own per-frame work is unchanged —
`App Update` totals 65.1 s before and 62.3 s after, over ~120 frames.
All the lost time sits in the gaps *between* frames, where the main
thread is blocked in Python and Kit records nothing.

A `py-spy` capture found the cause: `_compute_fabric_indices_for`
accounted for **11.86% of samples (23.2 s of a 195 s run)**, reached
through `camera.reset()` → `get_world_poses()` → the selection rebuild.
Every frame on that chain carries essentially the same share, so the
whole cost is that one function.

Fixes nvbug 6535498.

- Bug fix (non-breaking change which fixes an issue)
- New feature (non-breaking change which adds functionality) —
`FrameView.close()`

Task `Isaac-Lift-KukaAllegro-Camera`, 8192 environments,
`presets=physx,isaacsim_rtx_renderer,duo_camera`. Commit `69888c34e471`
(which introduced the problem) against its parent `e5f99d320338`:

| | Parent | With the problem |
| --- | ---: | ---: |
| Mean environment step | 699 ms | 7193 ms |
| Slow steps (over 3 s) | 1 of 100 | 28 of 100, averaging 18.9 s |
| GPU utilization | 23.4% | 4.7% |

GPU and CPU utilization both drop during the slow steps, which is what a
stall looks like — the pipeline is waiting, not doing extra work.

With this PR, the same task at 8192 environments steps at **mean 1.554
s, peak 1.663 s** — peak within 7% of mean, i.e. the stalls are gone.
(Measured on an RTX A6000, so the absolute step time is not comparable
to the table above, which was captured on different hardware; the stall
signature is the comparison that matters.)

L40, `cuda:0`, 1024-prim view (1024 children + 1024 parents). "Filler"
is xformable prims **not** in the view but carrying Fabric matrices —
i.e. the rest of a real scene.

| filler prims | `develop` selection | this PR |
| ---: | ---: | ---: |
| 0 | 2,048 | 1,024 |
| 10,000 | 12,048 | 1,024 |
| 100,000 | 102,048 | **1,024** |

`develop` scales with the stage; this PR is pinned to the view.

| filler prims | `develop` child | `develop` parent | `develop` full RO
rebuild | PR child | PR parent |
| ---: | ---: | ---: | ---: | ---: | ---: |
| 0 | 4.67 | 5.40 | 10.24 | 0.137 | 0.191 |
| 10,000 | 24.05 | 24.69 | 62.47 | 0.139 | 0.193 |
| 100,000 | 274.21 | 270.47 | **556.41** | **0.137** | **0.190** |

`develop` grows linearly with stage size — **556 ms per rebuild** at
100k filler prims. This PR is flat at **~0.14 ms**, a **~2000×**
reduction, and unchanged from 0 to 100k filler prims. First access at
100k filler: 1824 ms → 383 ms.

| N | `develop` | this PR | speedup |
| ---: | ---: | ---: | ---: |
| 1,000 | 0.0038 s | 0.0001 s | 27× |
| 5,000 | 0.0862 s | 0.0007 s | 120× |
| 20,000 | 1.3241 s | 0.0030 s | 434× |
| 50,000 | 8.5507 s | 0.0092 s | **929×** |
| 200,000 | (not run) | 0.0467 s | — |

Outputs verified identical at every N. At the ~200k rigid bodies of an
8192-environment scene the old path takes minutes.

| Suite | Result |
| --- | --- |
| `isaaclab_physx/test/sim/test_views_xform_prim_fabric.py` | 79 passed,
4 skipped (`cpu` + `cuda:0` on an L40) |
| `isaaclab/test/sim/test_views_xform_prim.py` (USD contract) | 63
passed |
| `isaaclab_newton/test/sim/test_views_xform_prim_newton.py` | 58 passed
|
| `isaaclab/test/sensors/test_camera.py` (main consumer, incl. the new
`close()` call) | 35 passed, 0 missing-`close()` warnings |
| `isaaclab/test/utils/warp/test_proxy_array.py` | 81 passed |

New tests:

- `test_selections_match_only_the_view_prims` asserts each selection
matches exactly the prims the view manages. Verified to fail without the
fix — with the selections unscoped it reports `matched 8 prims, expected
4`, because the child selections pick up the parents too.
- `test_close_removes_index_attributes` asserts the tag-scoped selection
drops to zero after `close()` and that a second `close()` is a no-op.
- `test_garbage_collection_removes_index_attributes_and_warns` asserts a
view dropped without `close()` still removes its tags on collection and
logs the warning.

`test_fabric_rebuild_after_topology_change` was updated to drive the new
refresh paths instead of the removed `_rebuild_{ro,rw}_arrays`.

The 4 skips are environmental: empty device parameter sets, and Fabric
hierarchy bindings that are unavailable in a headless experience.

**Coverage gap:** `test_physx_scene_data_backend.py` and
`test_ovphysx_scene_data_backend.py` both skip at collection on the
machine used here, so the `create_mapping` change is exercised only by
the standalone benchmark above, not by the test suite. Worth a look in
CI, where those backends are available.

Not applicable — no visual change. The benchmarks above cover the
behaviour change.

**Review feedback addressed so far:** the index-attribute accumulation
is fixed by `close()` (section 3); the opposite-space recompute no
longer refreshes the same selection twice on writer-scope exit;
`create_mapping` keeps `list.index`'s first-occurrence semantics for
duplicate paths (measured free at 200k paths); and docstrings/changelogs
were trimmed to the observable contract.

**The `isaaclab`/`isaaclab_physx` changelog fragments are still named
`fix-physx-newton-camera-pose-scaling.rst`**, inherited from isaac-sim#6554 —
nothing here touches Newton or scaling. The new `isaaclab_mimic`
fragment uses the branch slug. Happy to rename if reviewers prefer.

**A handful of tests still drop views without `close()`** and now log
the new warning during runs; harmless, cleanup to follow.

EDIT: fixed — the only remaining warning is the one
`test_garbage_collection_removes_index_attributes_and_warns` asserts on.
`Camera` previously closed its view only on invalidation, so a dropped
camera still went through `__del__`; it now closes in `Camera.__del__`
too. `test_camera.py` went from 3 warnings to 0.

`SceneDataProvider` fix unchanged (credited with a `Co-authored-by`
line) and replaces its Fabric frame view fix.

cached the result, reusing it while the number of selected prims stayed
the same. Two problems with that. The cache key is unsafe: an equal prim
count does not mean the prims or their order stayed the same, so after a
bucket reorder or a same-count membership change the cached indices
point at the wrong prims and cameras silently read another prim's
transform. And it treats the symptom — the underlying operation is still
proportional to the size of the whole scene, just performed less often.

Making the selection small removes the need for a cache at all, so both
problems go away.

- [x] I have read and understood the [contribution
guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html)
- [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with
`./isaaclab.sh --format`
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] I have added a changelog fragment under
`source/<pkg>/changelog.d/` for every touched package (do **not** edit
`CHANGELOG.rst` or bump `extension.toml` — CI handles that)
- [x] I have added my name to the `CONTRIBUTORS.md` or my name already
exists there

---------

Co-authored-by: pv-nvidia <197907000+pv-nvidia@users.noreply.github.com>
Co-authored-by: yts-nv <yts-nv@users.noreply.github.com>
Co-authored-by: Kelly Guo <kellyg@nvidia.com>
(cherry picked from commit 480370c)
The isaac-sim#5677 cherry-pick brought in OvPhysxFrameView code calling OvPhysxView
(try_binding_for / read_into). That class arrives with isaac-sim#6224, which is not on
this release branch, so the module import raised ModuleNotFoundError as soon as
an OVPhysX frame view was initialized.

Restore the direct physx.create_tensor_binding / binding.read calls this branch
provides.
@pv-nvidia
pv-nvidia force-pushed the pv/backport-6805-beta2 branch from 689325b to 2f6b179 Compare August 27, 2026 08:59
@pv-nvidia
pv-nvidia marked this pull request as ready for review August 27, 2026 12:11
@pv-nvidia
pv-nvidia requested a review from a team August 27, 2026 12:11
@greptile-apps

greptile-apps Bot commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This backport adds writer-scoped FrameView transform updates, local/world pose and scale support across USD, Fabric, Newton, and OVPhysX, and view-scoped Fabric selections with deterministic cleanup.

  • Introduces context-managed world/local transform writers and guarded view getters.
  • Adds Fabric matrix kernels, per-view selections, topology refresh, and lifecycle cleanup.
  • Extends backend implementations and migrates camera, terrain, Mimic, tests, and benchmarks to the writer API.
  • Optimizes scene-data transform mapping and adds the clone-template compatibility shim.

Confidence Score: 3/5

The PR should not merge until signed-scale handling is corrected because reflected transforms can be changed silently by the new world/local writer paths.

Fabric partial writes reconstruct existing transforms with unsigned scales, while USD world-scale writes divide by unsigned parent scales; both produce incorrect transforms for valid negatively scaled hierarchies.

Files Needing Attention: source/isaaclab/isaaclab/utils/warp/fabric.py; source/isaaclab/isaaclab/sim/views/usd_frame_view.py

Important Files Changed

Filename Overview
source/isaaclab/isaaclab/utils/warp/fabric.py Adds indexed Fabric transform composition and propagation, but decomposition cannot preserve signed scales during partial writes.
source/isaaclab/isaaclab/sim/views/usd_frame_view.py Implements writer-backed USD local/world operations, but world-scale conversion loses reflected parent-scale signs.
source/isaaclab_physx/isaaclab_physx/sim/views/fabric_frame_view.py Adds view-scoped tagged selections, writer synchronization, topology refresh, and deterministic cleanup with substantial focused coverage.
source/isaaclab/isaaclab/sim/views/base_frame_view.py Defines the new common writer, getter, scale, and close contracts consistently.
source/isaaclab_newton/isaaclab_newton/sim/views/newton_site_frame_view.py Adapts Newton sites to the writer API while separating transform scales from legacy collision-shape scales.
source/isaaclab_ovphysx/isaaclab_ovphysx/sim/views/ovphysx_frame_view.py Adapts OVPhysX pose operations and delegates scale operations to the internal USD view.
source/isaaclab/isaaclab/sensors/camera/camera.py Migrates camera pose writes and lifecycle invalidation to writer scopes and explicit FrameView cleanup.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Caller[FrameView caller] --> Scope{Writer space}
  Scope -->|World| World[Write world pose or scale]
  Scope -->|Local| Local[Write local pose or scale]
  World --> DeriveLocal[Derive local matrix]
  Local --> DeriveWorld[Derive world matrix]
  DeriveLocal --> Sync[Synchronize and close scope]
  DeriveWorld --> Sync
  Sync --> Read[Guarded world/local getters]
Loading

Comments Outside Diff (1)

  1. source/isaaclab/isaaclab/utils/warp/fabric.py, line 405 (link)

    P1 Signed scales are lost

    When a Fabric-managed prim has a negative scale and a caller performs a partial writer update, decomposition reconstructs scale from non-negative vector lengths before recomposing the matrix, causing the prim's reflection to be silently changed or removed.

    Knowledge Base Used: Physics backend extensions

Reviews (1): Last reviewed commit: "fix: use the OVPhysX tensor binding API ..." | Re-trigger Greptile

Comment on lines +290 to +294
parent_scale = Gf.Vec3d(
Gf.Vec3d(parent_world[0][0], parent_world[0][1], parent_world[0][2]).GetLength(),
Gf.Vec3d(parent_world[1][0], parent_world[1][1], parent_world[1][2]).GetLength(),
Gf.Vec3d(parent_world[2][0], parent_world[2][1], parent_world[2][2]).GetLength(),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Parent reflection sign lost

When a prim has a negatively scaled parent, the world-space scale writer derives parent scale with unsigned basis-vector lengths, producing a local scale with the wrong sign and a composed world transform that does not match the requested scale.

Knowledge Base Used: Physics backend extensions

@isaaclab-review-bot isaaclab-review-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isaac Lab Review Bot

The backport consistently introduces the FrameView writer-scope architecture and view-scoped Fabric selections, but it also introduces compatibility regressions in the legacy scale API, a writer-lock failure path, shared world/local scale storage, and public documentation that does not match the implemented API or warning behavior.

  • Design and architecture: The _get_*_impl, _apply_*, and _make_*_writer split is applied consistently across USD, Fabric, Newton, and OvPhysX. However, FrameViewSpaceWriterBase.__enter__ claims the per-view writer lock before backend initialization without releasing it if initialization raises, leaving the view unusable after an entry failure.
  • API: The legacy get_scales() contract changes from wp.array to ProxyArray without prior deprecation, and FabricFrameView's Fabric-disabled fallback changes legacy scale semantics from local to world space. The local-writer example incorrectly uses translations= although writer set_poses() accepts positions=, and several changelog fragments promise deprecation warnings that the implementation never emits.
  • Implementation: The Fabric selection scoping and world/local matrix derivation paths are well covered, including rotated and scaled parents. The remaining implementation defect is that full world-scale and local-scale reads reuse the same cached buffer and ProxyArray, so a later read silently mutates an earlier result. Writer entry also needs exception-safe lock and backend-state restoration.

Significant concerns. Posted 6 actionable findings inline.

Automated review; human maintainers own approval decisions.

# Scale getter/setter convenience helpers.
# ------------------------------------------------------------------

def get_scales(self, indices: wp.array | None = None) -> ProxyArray:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Warning · Api — get_scales return type changed without deprecation

get_scales() was declared and implemented as returning wp.array; it now returns ProxyArray, and every backend _get_scales_impl wraps its result. Callers doing wp.clone(view.get_scales()) or wp.to_torch(...) break, while the changelog states this helper "remains supported ... (not deprecated)". Keep the legacy getter returning wp.array and reserve ProxyArray for the new explicit scale getters, or deprecate the change first.

f"({type(self._view._active_writer).__name__}). Exit the existing scope before "
"opening a new one."
)
self._view._active_writer = self

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Warning · Implementation — Writer lock leaks when entry fails

__enter__ sets _view._active_writer = self before calling _enter_impl(). Python does not invoke __exit__ when __enter__ raises, so a failure in backend setup (e.g. _initialize_fabric() raising on a missing prim) leaves the view permanently locked: every getter and every future writer then raises. Clear _active_writer (and undo any partial backend state) if _enter_impl() raises.

scales_wp = _to_float32_2d(scales)
use_cached = indices is None or indices == slice(None)
if use_cached:
scales_wp = self._fabric_scales_buf

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Warning · Implementation — World and local scale getters share one buffer

_decompose_scales writes into the single cached _fabric_scales_buf and returns _fabric_scales_ta for both _get_world_scales_impl and _get_local_scales_impl on the indices=None path. Calling get_world_scales() then get_local_scales() silently overwrites the first result, which then reports local values. The pose getters correctly use separate world/local buffers; add a distinct local-scale buffer and ProxyArray here.

return self._usd_view.get_scales(indices)
def _get_scales_impl(self, indices=None) -> ProxyArray:
"""Fabric: get_scales returns world-space scales (legacy behavior)."""
return self._get_world_scales_impl(indices)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Warning · Api — Fallback get_scales/set_scales switch to world space

Previously, when /physics/fabricEnabled was false, FabricFrameView.get_scales/set_scales delegated to UsdFrameView and operated on local xformOp:scale. _get_scales_impl/_set_scales_impl now always route through the world-space hooks, which compose with (and divide by) the parent world scale. Under scaled parents this silently changes both reads and writes on the USD-fallback path; route the fallback legacy path to the local hooks.

.. code-block:: python

with view.xform_local_space_writer() as w:
w.set_poses(translations=t, orientations=o)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Suggestion · Api — Docstring example uses nonexistent set_poses keyword

The xform_local_space_writer example calls w.set_poses(translations=t, orientations=o), but FrameViewSpaceWriterBase.set_poses and every backend implementation declare the parameter as positions. Copying this documented snippet raises TypeError. Fix the example, and note the rename for callers migrating from set_local_poses(translations=...).

implementations so writes follow the new
:meth:`~isaaclab.sim.views.BaseFrameView.xform_world_space_writer` /
:meth:`~isaaclab.sim.views.BaseFrameView.xform_local_space_writer` context API.
``set_world_poses`` / ``set_local_poses`` shims still work (one-time

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Suggestion · Api — Changelog promises unimplemented deprecation warnings

This fragment (and the newton/ovphysx/physx fabric-local-poses fragments) states that the pose shims and get_scales/set_scales emit a DeprecationWarning. No warnings.warn exists in any added backend or in BaseFrameView, and source/isaaclab/changelog.d/xform-space-writer.rst says these helpers are explicitly not deprecated. Either emit the warnings or drop the deprecation claims so release notes match shipped behavior.

@pv-nvidia
pv-nvidia force-pushed the pv/backport-6805-beta2 branch from 2f6b179 to 89adc46 Compare August 27, 2026 12:58

@AntoineRichard AntoineRichard left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should be doing this, but if they need it.

@pv-nvidia
pv-nvidia enabled auto-merge (squash) August 27, 2026 14:21
@kellyguo11

Copy link
Copy Markdown
Contributor

run-ci

@isaaclab-bot isaaclab-bot Bot added ci:run-docker Trigger the on-demand Docker and GPU CI workflow and removed ci:run-docker Trigger the on-demand Docker and GPU CI workflow labels Aug 27, 2026
@pv-nvidia
pv-nvidia disabled auto-merge August 28, 2026 10:07
@kellyguo11
kellyguo11 merged commit bffdce9 into isaac-sim:release/3.0.0-beta2 Aug 29, 2026
75 of 133 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation infrastructure isaac-mimic Related to Isaac Mimic team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants