Fix first-frame RTX synchronization after tensor pose writes - #6661
Fix first-frame RTX synchronization after tensor pose writes#6661StriverAlex wants to merge 3 commits into
Conversation
Greptile SummaryThis PR fixes a rendering stale-pose bug where rigid-body positions written through the PhysX tensor API were not visible to RTX cameras until after the next public physics step. The fix adds an
Confidence Score: 4/5Safe to merge for the common case; the suppression flag uses a correct save/restore pattern and the fix was validated against both the current develop branch and v3.0.0-beta2. The new source/isaaclab_physx/isaaclab_physx/physics/physx_manager.py — the unconditional Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant SimCtx as SimulationContext
participant Mgr as PhysxManager
participant PhysX as IPhysX
participant Fabric
User->>SimCtx: reset()
SimCtx->>Mgr: warmup + start_simulation()
Mgr-->>SimCtx: "_view_created=True"
User->>User: write_root_pose_to_sim_index()
note over User: tensor pose written
User->>SimCtx: forward()
SimCtx->>Mgr: forward()
Mgr->>Mgr: "_suppress_step_callbacks=True"
Mgr->>PhysX: update_simulation(dt, 0.0)
note over PhysX: PRE/POST suppressed
PhysX-->>Mgr: poses flushed
Mgr->>Mgr: "_suppress_step_callbacks=False"
Mgr->>Fabric: _update_fabric(0.0, 0.0)
Fabric-->>SimCtx: RTX sees updated pose
User->>SimCtx: step()
SimCtx->>Mgr: simulate() + fetch_results()
note over Mgr: callbacks fire, step count incremented
|
|
|
||
| def guarded(cb: Callable) -> Callable: | ||
| def wrapper(dt: float) -> Any: | ||
| return cb(dt) if cls._view_created else None | ||
| return cb(dt) if cls._view_created and not cls._suppress_step_callbacks else None |
There was a problem hiding this comment.
Suppression scope is limited to PhysxManager-registered callbacks
The _suppress_step_callbacks flag only silences callbacks that went through PhysxManager._subscribe_isaac(). Any code that registers step-event listeners directly via omni.physx.get_physx_interface().subscribe_physics_on_step_events() will still fire during forward(). Worth documenting as a known limitation — third-party or user code that bypasses the manager will observe a step event during each forward() call.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
AntoineRichard
left a comment
There was a problem hiding this comment.
AI review — Request changes\n\nI am requesting changes because this moves a scene-wide PhysX update into the general path, including calls that have no pending rigid-body tensor pose writes.\n\n calls before visualizer updates, and returns . Consequently, Kit-rendered workloads execute on every rendered frame, not only for the reset/first-camera-frame case addressed by this PR.\n\nI reproduced the proposed behavior locally on an RTX 5090 with 4,096 environments, including the PR's callback suppression:\n\n| Measurement | Current path | Proposed path | Difference |\n|---|---:|---:|---:|\n| | 0.00247 ms | 2.2366 ms | +2.234 ms/call |\n| Rendered environment step | 21.93 ms | 24.35 ms | +11.0% |\n\nThere is also a callback-contract gap. only suppresses callbacks wrapped by . A callback registered directly with still fires. In a local probe, PRE and POST counts remained zero after the current , but each incremented once with the proposed path. This can introduce observable behavior and additional extension callback work on every render.\n\nCould you provide more information about the workflow that needs this synchronization?\n\n- Is the stale RTX pose limited to reset-time writes, or can tensor pose writes occur before arbitrary camera frames?\n- Which tensor write APIs need to become visible immediately?\n- How often does the affected workflow call , and at what typical environment counts and render intervals?\n- Does the workflow use Kit visualization, camera-only rendering, or both?\n- Were any narrower PhysX flush/update APIs evaluated, and what specifically remained stale with them?\n\nIf the full call is necessary, I think it should be behind a dirty flag or a dedicated render synchronization path that runs only after an affected tensor pose write and immediately before an RTX frame consumes it. The existing lightweight articulation/Fabric path should remain cheap when no such synchronization is required.\n\nPlease also add coverage for direct native PhysX step subscribers if is intended to preserve callback-free semantics.
AntoineRichard
left a comment
There was a problem hiding this comment.
AI review — Request changes
I am requesting changes because this moves a scene-wide PhysX update into the general PhysxManager.forward() path, including calls that have no pending rigid-body tensor pose writes.
SimulationContext.update_visualizers() calls forward() before visualizer updates, and KitVisualizer.requires_forward_before_step() returns True. Consequently, Kit-rendered workloads execute update_simulation() on every rendered frame, not only for the reset/first-camera-frame case addressed by this PR.
I reproduced the proposed behavior locally on an RTX 5090 with 4,096 Isaac-Cartpole-Direct environments, including the PR's callback suppression:
| Measurement | Current path | Proposed path | Difference |
|---|---|---|---|
forward() |
0.00247 ms | 2.2366 ms | +2.234 ms/call |
| Rendered environment step | 21.93 ms | 24.35 ms | +11.0% |
There is also a callback-contract gap. _suppress_step_callbacks only suppresses callbacks wrapped by PhysxManager._subscribe_isaac(). A callback registered directly with omni.physx.get_physx_interface().subscribe_physics_on_step_events() still fires. In a local probe, PRE and POST counts remained zero after the current forward(), but each incremented once with the proposed update_simulation() path. This can introduce observable behavior and additional extension callback work on every render.
Could you provide more information about the workflow that needs this synchronization?
- Is the stale RTX pose limited to reset-time writes, or can tensor pose writes occur before arbitrary camera frames?
- Which tensor write APIs need to become visible immediately?
- How often does the affected workflow call
sim.forward(), and at what typical environment counts and render intervals? - Does the workflow use Kit visualization, camera-only rendering, or both?
- Were any narrower PhysX flush/update APIs evaluated, and what specifically remained stale with them?
If the full update_simulation() call is necessary, I think it should be behind a dirty flag or a dedicated render synchronization path that runs only after an affected tensor pose write and immediately before an RTX frame consumes it. The existing lightweight articulation/Fabric forward() path should remain cheap when no such synchronization is required.
Please also add coverage for direct native PhysX step subscribers if SimulationContext.forward() is intended to preserve callback-free semantics.
Superseded by the correctly formatted AI review submitted immediately afterward.
|
Thank you @AntoineRichard for the detailed review, and especially for measuring the rendering-path regression at 4096 environments. Your feedback made the performance and callback constraints much clearer. I reworked the fix in commit
I also evaluated the narrower APIs before retaining Thanks again for the concrete guidance and performance data. They helped narrow the change to the render boundary and avoid adding cost to the normal forward path. |
94c93f5 to
33533ff
Compare
Run the PhysX update path from forward() while suppressing IsaacLab step callbacks so rendering receives tensor-written rigid-body poses without exposing a public physics step. Constraint: Preserve SimulationContext.forward() zero-step semantics. Rejected: Move reset warmup after play | It does not sync later tensor pose writes. Rejected: Use tensor or Fabric flush APIs | Isaac Sim 6.0 did not update RTX output. Confidence: high Scope-risk: moderate Directive: Keep internal forward updates invisible to physics-step callbacks. Tested: RTX first-frame regression, SimulationContext tests, renderer utility tests, and full pre-commit. Not-tested: Full repository pytest suite.
Record the public physics-step epoch only when a rigid-object pose is written. Render performs the internal PhysX sync only while that epoch is still current, so an intervening public step consumes the pending work without adding a second update. Constraint: Preserve latest develop normal-step and clean-render performance. Rejected: Clear a boolean on every PhysX step | It adds work to the dominant develop hot path. Confidence: high Scope-risk: moderate Directive: Keep update_simulation gated by a pending rigid-pose write and skip it after a public step. Tested: pre-commit; develop-negative RTX regression; RTX first-frame regression; native callback regression; clean-render and dirty-mark microbenchmarks. Not-tested: Full SimulationContext suite is blocked by the container Newton API mismatch and a later Kit process segfault.
Confine native PhysX synchronization to a step-stamped write barrier consumed at actual Kit frame boundaries, and cover every tensor pose writer plus real first-frame rendering. Constraint: Clean forward and normal physics-step throughput must remain unchanged. Rejected: Unconditional native update in forward | It taxes every forward and violates the clean-path callback invariant. Rejected: Generic dirty state or manager-owned forward | It leaks backend policy or duplicates forwarding work. Confidence: high Scope-risk: moderate Directive: Keep update_simulation gated to pending tensor pose writes at a frame-producing Kit update. Tested: 25 writer/renderer/visualizer tests; 2 native callback tests; 4 CUDA writer tests; RTX red/green regression; Ruff; develop-candidate-develop ABA benchmark. Not-tested: Full repository suite.
33533ff to
7012c18
Compare
|
Restored as #7138 after the original StriverAlex fork was deleted and recreated, which left this PR without a recoverable head repository. The replacement preserves the reviewed implementation, rebases it onto current develop, and includes fresh RTX, CUDA, callback, Ruff, and PhysX ABA performance validation. |
Description
PhysX tensor pose writers can change simulation state without advancing the public physics step. RTX/Fabric may therefore render the previous transform in the first frame after a write.
This change keeps the normal physics and clean rendering paths lightweight. It introduces a step-stamped tensor-pose write barrier that is consumed only at an actual Kit/RTX frame boundary.
Fixes #6394.
Root cause and design
PhysicsManager.before_kit_app_update() -> boolprovides a backend-neutral frame-boundary hook. The default implementation is a no-op.PhysxManagerrecords a private pending token only after a tensor pose mutation succeeds.update_simulation()and consumes the token.forward()is required beforeapp.update().try/finally.The resulting invariants are:
SimulationContext.forward()does not call nativeupdate_simulation().Tensor writer coverage
The barrier is notified by all six native tensor pose implementations:
Root-pose, indexed, and mask-based public APIs are covered through their existing delegation to these implementations.
Review concerns addressed
The previously reviewed performance regression came from placing the full native PhysX update in
forward(), which taxed every call. This revision removes that behavior entirely. Synchronization is local to the PhysX backend, causally tied to a successful tensor pose write, keyed to the public physics-step epoch, and consumed only by a frame-producing Kit update.This is deliberately not a general-purpose manager dirty flag: the state models one backend-specific synchronization obligation and remains private to
PhysxManager.Validation
Rebased onto current
developat3b736feb04a98f708bb69ed1f69eb0f8e81b2adf.developfailed as expected: first-frame red fraction0.000000, stable red fraction0.010468.The local Git LFS pre-push hook was skipped because its SSH authentication helper hung in the validation environment. The 16 PR files are ordinary text files and contain no LFS pointers.
Performance regression check
The same Isaac Sim container command was run in develop-candidate-develop order with
Isaac-Cartpole-Direct, 4096 environments, 500 measured steps, 100 warmup steps, and the Kit visualizer.The two develop endpoints differed by 11.59%, showing monotonic host drift during the run. The candidate landed essentially at the midpoint:
-0.18%FPS and-0.16%iteration time versus the arithmetic mean of the bracketing develop runs. This does not reproduce a candidate-specific+11%regression. An earlier, more stable ABA run likewise placed the candidate at+1.13%FPS versus the develop mean with only1.30%baseline spread.Type of change
Checklist
forward()and normal physics-step behavior.