Skip to content

Fix first-frame RTX synchronization after tensor pose writes - #7138

Open
StriverAlex wants to merge 5 commits into
isaac-sim:developfrom
StriverAlex:striveralex/fix-physx-forward-sync
Open

Fix first-frame RTX synchronization after tensor pose writes#7138
StriverAlex wants to merge 5 commits into
isaac-sim:developfrom
StriverAlex:striveralex/fix-physx-forward-sync

Conversation

@StriverAlex

@StriverAlex StriverAlex commented Aug 17, 2026

Copy link
Copy Markdown

Description

This is the restored replacement for #6661. The original pull request became unreopenable after the author fork was deleted and recreated; its reviewed implementation history was preserved, rebased onto current develop, and pushed from the new fork identity.

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

  • Native tensor pose writes bypass the propagation needed for the next RTX frame.
  • PhysicsManager.before_kit_app_update() -> bool provides a backend-neutral frame-boundary hook. The default implementation is a no-op.
  • PhysicsManager.has_pending_kit_app_update() -> bool lets a frame owner bypass an existing dedup/visualizer early return without consuming backend state. The default is False.
  • PhysxManager records a private pending token only after a tensor pose mutation succeeds.
  • At the next Kit update, the PhysX backend drops a stale token if a real physics step already consumed the change; otherwise it runs one native update_simulation() and consumes the token.
  • Multiple writes in the same public physics step coalesce into one synchronization.
  • A failed native synchronization is requeued so a transient failure does not silently lose the pending write.
  • The boolean result tells the frame owner whether one final Fabric forward() is required before app.update().
  • Kit simulation playback state is disabled only around this synchronization and restored with try/finally.

The resulting invariants are:

  • SimulationContext.forward() does not call native update_simulation().
  • A clean Kit update performs no additional Fabric forward.
  • A dirty Kit update performs one native synchronization followed by one final Fabric forward.
  • The renderer utility retains its existing single unconditional Fabric forward on actual clean and dirty frames.
  • Normal physics steps and new clean frames do not inspect or clear generic dirty state.
  • Only calls already eligible for renderer dedup or visualizer skip query the pending-work predicate; a same-step pose write bypasses both early returns.

Tensor writer coverage

The barrier is notified by all six native tensor pose implementations:

  • rigid object root-link pose
  • rigid object root center-of-mass pose
  • rigid object collection body-link pose
  • rigid object collection body center-of-mass pose
  • articulation root-link pose
  • articulation root center-of-mass pose

Root/body-pose, indexed, and mask-based public APIs are covered through their existing delegation to these implementations.

Review concerns addressed

The performance regression reviewed on the original PR 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.

The Greptile review on this replacement PR identified two same-step stale-frame paths. Commit 06113203b adds a backend-neutral pending-work predicate and makes RTX renderer dedup conditional on there being no pending synchronization. Commit 79be514e0 applies the same invariant to Kit visualizer capture reuse: a clean post-step capture still reuses the frame, while a pose write after that pump invalidates the reuse and performs the existing prepare/forward/update sequence. The predicate is read-only; before_kit_app_update() remains coupled to an actual 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.

Current-develop validation

Rebased onto develop at cd7ea42ae27a95e955a2d317dac72228e09692d7. Current head: 79be514e056a91770ce149e790e4067cab311355.

  • The restored three-commit aggregate patch ID before and after the rebase is identical: 469b81480a74655032c03d1deb45569ff21ebd18.
  • Both same-step stale-frame regressions failed before their respective review fixes and passed after them.
  • 27 writer, renderer, and Kit visualizer regression tests passed.
  • 2 native PhysX callback, coalescing, and pending-lifecycle integration tests passed.
  • 4 real CUDA articulation and rigid-object-collection writer cases passed.
  • The real Kit RTX first-frame regression passed on the candidate.
  • The same test file mounted over clean current develop failed as expected: first-frame red fraction 0.000000, stable red fraction 0.010468.
  • Ruff 0.14.10 lint and formatting checks passed for all changed Python files.
  • git diff --check passed.
  • Full repository suite was not run.

Current-develop performance ABA

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, the Kit visualizer, and explicit presets=physx because the task defaults have recently changed toward Newton.

Run FPS Iteration time
develop before 186,311.13 21.9847 ms
candidate 186,025.57 22.0185 ms
develop after 185,917.70 22.0313 ms

The two develop endpoints differed by 0.2114%. Relative to their arithmetic mean, the candidate measured -0.0477% FPS and +0.0476% iteration time, both well inside baseline drift and far from the previously reviewed +11% regression. The host CPU governor reported powersave, so the bracketed comparison is used instead of interpreting a single absolute run.

One develop-post launch aborted in NVIDIA RTCore/Vulkan before sampling and was rerun after confirming there was no stale benchmark GPU process; no metric from the failed launch is included.

Type of change

  • Bug fix with targeted regression coverage

Checklist

  • Restored the original reviewed change after the fork was recreated.
  • Rebased onto current develop.
  • Preserved clean forward() and normal physics-step behavior.
  • Covered all PhysX tensor pose writer implementations in scope.
  • Added changelog fragments.
  • Added real first-frame RTX regression coverage.
  • Reproduced the failure on clean current develop.
  • Addressed the replacement PR's same-step RTX dedup and post-pump Kit capture review findings.
  • Ran targeted functional, callback, CUDA, lint, formatting, and performance checks.
  • Added the contributor entry.
  • Full repository test suite run.

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.
@StriverAlex
StriverAlex requested a review from a team August 17, 2026 10:42
@github-actions github-actions Bot added bug Something isn't working isaac-lab Related to Isaac Lab team labels Aug 17, 2026
@greptile-apps

greptile-apps Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a step-stamped PhysX tensor-pose synchronization barrier so same-step pose writes are propagated before the next Kit/RTX frame without adding work to normal physics or clean rendering paths.

  • Adds backend-neutral pending-work and frame-boundary hooks to PhysicsManager.
  • Tracks successful PhysX tensor pose writes across rigid objects, collections, and articulations.
  • Makes RTX renderer deduplication and Kit visualizer frame reuse conditional on the absence of pending synchronization.
  • Preserves and restores Kit playback state around frame updates.
  • Adds unit, integration, CUDA, and first-frame RTX regression coverage.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; both previously reported stale-frame paths now check pending backend work and perform synchronization before producing or sampling the next Kit frame.

Important Files Changed

Filename Overview
source/isaaclab/isaaclab/physics/physics_manager.py Adds backend-neutral, no-op pending-work and pre-Kit-update hooks.
source/isaaclab_physx/isaaclab_physx/physics/physx_manager.py Implements the step-stamped tensor-pose write barrier, stale-token handling, retry preservation, and lifecycle cleanup.
source/isaaclab_physx/isaaclab_physx/renderers/isaac_rtx_renderer_utils.py Correctly bypasses same-frame deduplication and visualizer ownership skips when backend synchronization is pending.
source/isaaclab_visualizers/isaaclab_visualizers/kit/kit_visualizer.py Invalidates captured-frame reuse after a post-pump pose write and performs synchronization immediately before the Kit update.
source/isaaclab_physx/isaaclab_physx/assets/articulation/articulation.py Notifies the barrier after successful native root-link and center-of-mass pose writes.
source/isaaclab_physx/isaaclab_physx/assets/rigid_object/rigid_object.py Notifies the barrier after successful native rigid-object pose writes.
source/isaaclab_physx/isaaclab_physx/assets/rigid_object_collection/rigid_object_collection.py Notifies the barrier after successful native collection body-pose writes.
source/isaaclab_physx/test/renderers/test_isaac_rtx_renderer_utils.py Covers same-step pending-write dedup bypass and playback-state restoration.
source/isaaclab_visualizers/test/test_kit_visualizer.py Covers clean capture reuse, post-pump invalidation, synchronization ordering, and failure cleanup.

Sequence Diagram

sequenceDiagram
    participant Writer as Tensor pose writer
    participant PhysX as PhysxManager
    participant Owner as Renderer / KitVisualizer
    participant Fabric as Fabric
    participant Kit as Kit app
    Writer->>PhysX: notify_tensor_pose_write()
    PhysX->>PhysX: Record current public-step token
    Owner->>PhysX: has_pending_kit_app_update()
    PhysX-->>Owner: true
    Owner->>PhysX: before_kit_app_update()
    PhysX->>PhysX: update_simulation()
    PhysX-->>Owner: requires re-forward
    Owner->>Fabric: forward()
    Owner->>Kit: app.update()
    Kit-->>Owner: RTX frame with updated pose
Loading

Reviews (3): Last reviewed commit: "Keep post-pump pose writes visible to Ki..." | Re-trigger Greptile

@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

Reviewed the step-stamped PhysX tensor-pose write barrier, including all six writer notification paths, token coalescing and lifecycle, and consumption at Kit/RTX frame boundaries. The proposed concern that update_simulation(dt, 0.0) advances physics is not established by the patch: the public step count remains unchanged, and native callback emission alone does not demonstrate simulation advancement or rendered/headless divergence.

  • Design and architecture: The backend-specific synchronization obligation remains private to PhysxManager, while the additive backend-neutral hook lets frame owners request synchronization without burdening normal physics steps or clean rendering paths. The renderer utility and Kit visualizer both consume the hook at an actual app-update boundary.
  • API: PhysicsManager.before_kit_app_update() -> bool is an additive, documented default-no-op API. Both consumers honor its boolean re-forward contract, existing tensor writer signatures remain unchanged, and package changelog coverage is present.
  • Implementation: The six PhysX tensor pose implementations notify only after their native transform write, writes coalesce by public physics-step epoch, stale tokens are dropped after a real step, failed synchronization is requeued, and lifecycle resets clear pending state. Kit playback state is restored with try/finally. Native pre/post callback emission during the synchronization is an observable tradeoff covered by tests, but the supplied finding does not establish that it advances simulation state or identify a broken callback consumer; using zero elapsed time is likewise not shown to preserve the required propagation.

No blocking issues. No inline issue met the actionable-evidence threshold; the assessment above records the review feedback.

Automated review; human maintainers own approval decisions.

Expose a backend-neutral pending-work predicate so frame owners bypass existing no-op paths only while a tensor-pose synchronization obligation remains.

Constraint: Preserve the clean new-frame path and existing camera deduplication behavior

Rejected: Consume before_kit_app_update to probe pending work | that would mutate backend state before deciding to produce a frame

Confidence: high

Scope-risk: narrow

Directive: Keep before_kit_app_update consumption coupled to an actual frame-producing Kit update

Tested: Ruff 0.14.10; 26 writer/renderer/visualizer tests; 2 native lifecycle tests; 1 real CUDA/RTX test; PhysX ABA benchmark

Not-tested: Full repository suite
@StriverAlex

Copy link
Copy Markdown
Author

@greptileai

Comment thread source/isaaclab_visualizers/isaaclab_visualizers/kit/kit_visualizer.py Outdated
Invalidate an already-pumped Kit frame only when the active physics backend reports pending synchronization work, preserving clean recorder frame reuse.

Constraint: Preserve clean recorder frame reuse and normal visualization cadence
Rejected: Always pump render_rgb_array | adds unnecessary Kit work to every capture
Confidence: high
Scope-risk: narrow
Directive: Treat backend pending work as invalidating app-pumped deduplication state
Tested: Kit visualizer 6 passed; combined writer/RTX/visualizer 27 passed; native lifecycle 2 passed; real CUDA/RTX first-frame 1 passed; Ruff 0.14.10 and diff-check passed
Not-tested: Full repository suite
@StriverAlex

Copy link
Copy Markdown
Author

@greptileai

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working isaac-lab Related to Isaac Lab team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant