Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions docs/source/api/lab/isaaclab.sim.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ isaaclab.sim

SimulationContext
SimulationCfg
BackendCfg

.. rubric:: Functions

Expand All @@ -40,6 +41,10 @@ Simulation Configuration
:show-inheritance:
:exclude-members: __init__

.. autoclass:: BackendCfg
:members:
:exclude-members: __init__

Simulation Context Builder
--------------------------

Expand Down
6 changes: 6 additions & 0 deletions docs/source/api/lab_newton/isaaclab_newton.physics.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

NewtonManager
NewtonCfg
NewtonBackendCfg
NewtonSoftContactCfg
NewtonCollisionPipelineCfg
NewtonFeatherstoneManager
Expand Down Expand Up @@ -53,6 +54,11 @@ Physics Configuration
:show-inheritance:
:exclude-members: __init__

.. autoclass:: NewtonBackendCfg
:members:
:show-inheritance:
:exclude-members: __init__

.. autoclass:: NewtonSoftContactCfg
:members:
:show-inheritance:
Expand Down
6 changes: 6 additions & 0 deletions docs/source/api/lab_ov/isaaclab_ov.physics.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

OvPhysxManager
OvPhysxCfg
OvPhysxBackendCfg

.. currentmodule:: isaaclab_ov.physics

Expand All @@ -27,3 +28,8 @@ Physics Configuration
:members:
:show-inheritance:
:exclude-members: __init__

.. autoclass:: OvPhysxBackendCfg
:members:
:show-inheritance:
:exclude-members: __init__
4 changes: 4 additions & 0 deletions docs/source/api/lab_ov/isaaclab_ov.renderers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ The following classes are part of the public :mod:`isaaclab_ov.renderers` API.

OVRTXRenderer
OVRTXRendererCfg
OVRTXBackendCfg
map_attribute_for_warp_writes

.. autoclass:: OVRTXRenderer
Expand All @@ -23,4 +24,7 @@ The following classes are part of the public :mod:`isaaclab_ov.renderers` API.
.. autoclass:: OVRTXRendererCfg
:show-inheritance:

.. autoclass:: OVRTXBackendCfg
:show-inheritance:

.. autofunction:: map_attribute_for_warp_writes
6 changes: 6 additions & 0 deletions docs/source/api/lab_physx/isaaclab_physx.physics.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

PhysxManager
PhysxCfg
PhysxBackendCfg

.. currentmodule:: isaaclab_physx.physics

Expand All @@ -27,6 +28,11 @@ Physics Configuration
:show-inheritance:
:exclude-members: __init__

.. autoclass:: PhysxBackendCfg
:members:
:show-inheritance:
:exclude-members: __init__

Additional Public Classes
-------------------------

Expand Down
20 changes: 20 additions & 0 deletions docs/source/concepts/backend_architecture.rst
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,26 @@ cross-backend lifecycle work. ``MODEL_INIT`` occurs during scene construction,
``PHYSICS_READY`` after physics initialization, and ``STOP`` during shutdown.
The concrete ``close()`` implementation dispatches the ``STOP`` event.

``SimulationContext`` owns native resources. ``get_or_create_backend(backend_cfg)``
reuses one resource for equal configurations of the same concrete type; a cache miss
constructs ``backend_cfg.class_type(backend_cfg)``.
:class:`~isaaclab.sim.BackendCfg` describes native construction inputs, while
``PhysicsCfg`` selects a physics manager. Finalize configurations before
registration and treat them, including nested values, as read-only afterward.
Use a new configuration for different settings. ``close_backend(backend)`` closes
the exact registered object after all consumers have released their bindings;
it does not compare or hash configurations. Resources implement ``close()``;
failed release retains the entry for retry. Simulation teardown closes all
remaining resources after renderers and visualizers have released their bindings.

Managers and native renderers expose their borrowed resource through ``backend``.
For example, ``NewtonManager.backend.model`` accesses the finalized native model.
Closing a renderer releases its bindings, not the shared native resource.
Exposing native handles does not replace SDP transport.

Clone contexts are registered separately as ``sim.clone_contexts[Context] = Context(...)``
before plan dispatch. They apply the plan but do not own native runtime resources.

Portable asset and sensor interfaces
------------------------------------

Expand Down
14 changes: 14 additions & 0 deletions source/isaaclab/changelog.d/native-resource-identity.major.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Changed
^^^^^^^

* **Breaking:** Simplified native resource registration to ``sim.get_or_create_backend(backend_cfg)``.
Move constructor inputs into a ``BackendCfg`` whose ``class_type`` constructs the resource from cfg;
equal configurations of the same concrete type shared one resource. Registered cfgs were retained
without copying; finalize them before registration and treat them as read-only. Release a resource
with ``sim.close_backend(backend)`` using its object identity, not its configuration.
Replace resource ``clear()`` methods with ``close()``.
* **Breaking:** Separated clone contexts from native ownership. Replace clone-context registration
through ``get_or_create_backend(Context, ...)`` with ``sim.clone_contexts[Context] = Context(...)``.
* Added ``field(metadata={"copy": False})`` support to ``configclass`` for borrowed native inputs.
Construction, ``copy()``, and ``replace()`` preserved these references without changing the
independent copying of ordinary configuration fields.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Added
^^^^^

* Added ``RendererCfg.supported_output_types()`` and recursive configclass validation so every ``CameraCfg`` detects
renderer/data-type incompatibilities without task-specific guards, starting the simulator, or importing renderer
implementations.
2 changes: 1 addition & 1 deletion source/isaaclab/isaaclab/cloner/clone_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ def _context_rows(
rows_by_context.setdefault(context_type, set()).update(rows)

if UsdReplicateContext in rows_by_context:
sim.get_or_create_backend(UsdReplicateContext, sim.stage)
sim.clone_contexts[UsdReplicateContext] = UsdReplicateContext(sim.stage)
return {
context_type: tuple(sorted(rows & populated_rows))
for context_type, rows in rows_by_context.items()
Expand Down
4 changes: 2 additions & 2 deletions source/isaaclab/isaaclab/cloner/replicate_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ def replicate(plan: ClonePlan, *, replicate_physics: bool = True) -> None:
context_types = tuple(
context_type for context_type in plan.context_rows if replicate_physics or context_type is UsdReplicateContext
)
missing = [context_type for context_type in context_types if context_type not in sim._backend_registry]
missing = [context_type for context_type in context_types if context_type not in sim.clone_contexts]
if missing:
names = ", ".join(f"{context_type.__module__}.{context_type.__qualname__}" for context_type in missing)
raise RuntimeError(f"Clone contexts must be registered before plan dispatch: {names}.")

contexts = [sim._backend_registry[context_type] for context_type in context_types]
contexts = [sim.clone_contexts[context_type] for context_type in context_types]
for context in sorted(contexts, key=lambda item: item.replicate_priority):
context.replicate(plan)

Expand Down
15 changes: 15 additions & 0 deletions source/isaaclab/isaaclab/renderers/renderer_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

from isaaclab.utils import configclass

from .output_contract import RenderBufferKind, RenderBufferSpec

if TYPE_CHECKING:
from .base_renderer import BaseRenderer

Expand All @@ -23,3 +25,16 @@ class RendererCfg:
"""Renderer implementation class. Concrete configs must set this field."""

renderer_type: str = "default"

def supported_output_types(self) -> dict[RenderBufferKind, RenderBufferSpec] | None:
"""Return the camera output layouts supported by this renderer configuration.

Concrete renderer configurations override this method when their output contract can be
determined without importing or instantiating the renderer implementation. Returning
``None`` defers compatibility validation until the renderer is created.

Returns:
Mapping from supported output types to their buffer layouts, or ``None`` when the
renderer is selected dynamically.
"""
return None
16 changes: 16 additions & 0 deletions source/isaaclab/isaaclab/sensors/camera/camera_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,3 +251,19 @@ def __post_init__(self):
# Reset to default so re-runs of ``__post_init__`` (via ``SensorBase.__init__``'s
# ``cfg.copy()``) don't re-forward and clobber a user-set ``renderer_cfg`` field.
setattr(self, field_name, default)

def validate_config(self) -> None:
"""Validate the requested data types against the selected renderer contract."""
if self.renderer_cfg is None:
return
supported_specs = self.renderer_cfg.supported_output_types()
if supported_specs is None:
return
supported = {str(kind) for kind in supported_specs}
unsupported = sorted(set(self.data_types) - supported)
Comment on lines +261 to +263

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 OVRTX combinations pass validation

This checks each requested output independently, so an OVRTX camera requesting both rgb and simple_shading_full_mdl passes validate() even though those outputs cannot share one render product. The same gap allows multiple simple-shading modes. These configurations then raise during renderer setup, defeating the new early compatibility check.

Knowledge Base Used:

if unsupported:
raise ValueError(
f"Renderer {type(self.renderer_cfg).__name__} only supports data types {sorted(supported)}, "
f"but the camera is configured with unsupported types: {unsupported}. "
"Choose supported data types or select a different renderer."
)
2 changes: 1 addition & 1 deletion source/isaaclab/isaaclab/sim/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ from .schemas import (
modify_rigid_body_properties,
modify_spatial_tendon_properties,
)
from .simulation_cfg import SimulationCfg
from .simulation_cfg import BackendCfg, SimulationCfg
from .simulation_context import SimulationContext, build_simulation_context

# Forwarded to isaaclab_newton.sim.schemas via __getattr__ shim
Expand Down
12 changes: 12 additions & 0 deletions source/isaaclab/isaaclab/sim/simulation_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from __future__ import annotations

from dataclasses import MISSING
from typing import Literal

from isaaclab.physics import PhysicsCfg
Expand All @@ -19,6 +20,17 @@
from isaaclab.visualizers import VisualizerCfg


@configclass
class BackendCfg:
"""Construction inputs and value identity for a simulation-owned native resource.

Finalize all fields before registration and treat them as read-only afterward.
"""

class_type: type = MISSING
"""Resource class constructed as ``class_type(cfg)``; must implement ``close()``."""


@configclass
class SimulationCfg:
"""Configuration for simulation physics.
Expand Down
55 changes: 37 additions & 18 deletions source/isaaclab/isaaclab/sim/simulation_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from collections.abc import Callable, Iterator
from contextlib import contextmanager
from dataclasses import fields
from typing import TYPE_CHECKING, Any, TypeVar, cast
from typing import TYPE_CHECKING, Any

import torch
import warp as wp
Expand All @@ -35,14 +35,12 @@

from isaaclab.cloner.clone_plan import ClonePlan

from .simulation_cfg import SimulationCfg
from .simulation_cfg import BackendCfg, SimulationCfg
from .spawners import DomeLightCfg, GroundPlaneCfg

logger = logging.getLogger(__name__)


_BackendT = TypeVar("_BackendT")

# Visualizer type names (CLI and config). App launcher parses CSV and stores as a space-separated setting.
_VISUALIZER_TYPES = ("newton_gl", "newton_rtx", "rerun", "viser", "kit")
# Deprecated aliases mapped to their canonical names.
Expand Down Expand Up @@ -127,7 +125,9 @@ def __init__(self, cfg: SimulationCfg | None = None):

# Store config
self.cfg = SimulationCfg() if cfg is None else cfg
self._backend_registry: dict[type[object], object] = {}
self._backend_registry: list[tuple[BackendCfg, Any]] = []
self.clone_contexts: dict[type, Any] = {}
"""Clone-context instances registered by type before plan dispatch; not native resource owners."""

use_isaac_sim = has_kit()
self._physics = _resolve_physics_cfg(self.cfg.physics, use_isaac_sim=use_isaac_sim)
Expand Down Expand Up @@ -981,23 +981,42 @@ def get_setting(self, name: str) -> Any:
"""Get a setting value."""
return self._settings_helper.get(name)

def get_or_create_backend(self, backend_type: type[_BackendT], *args: Any, **kwargs: Any) -> _BackendT:
"""Return the simulation-scoped native backend for a type.
def get_or_create_backend(self, cfg: BackendCfg) -> Any:
"""Return the simulation-scoped native backend for a configuration.

Consumers that register the same backend type resolve one shared native resource
instead of constructing state to synchronize.
Equal configurations of the same concrete type share a resource. Finalize configurations
before registration and treat them as read-only afterward; use a new cfg for new settings.

Args:
backend_type: Backend class to construct when the resource does not exist.
*args: Positional arguments used only when constructing the resource.
**kwargs: Keyword arguments used only when constructing the resource.
cfg: Native resource configuration. A cache miss constructs ``cfg.class_type(cfg)``.

Returns:
The existing or newly constructed native backend.
"""
if backend_type not in self._backend_registry:
self._backend_registry[backend_type] = backend_type(*args, **kwargs)
return cast(_BackendT, self._backend_registry[backend_type])
for registered_cfg, resource in self._backend_registry:
if type(registered_cfg) is type(cfg) and registered_cfg == cfg:
return resource
resource = cfg.class_type(cfg)
self._backend_registry.append((cfg, resource))
return resource

def close_backend(self, backend: Any) -> None:
"""Close one registered resource by object identity after all consumers release their bindings.

A failed release retains the registry entry so teardown can be retried.

Args:
backend: The exact registered resource to close, not its configuration.

Raises:
KeyError: The backend is not registered with this context.
"""
for index, (_, resource) in enumerate(self._backend_registry):
if resource is backend:
resource.close()
self._backend_registry.pop(index)
return
raise KeyError(backend)

@classmethod
def clear_instance(cls) -> None:
Expand Down Expand Up @@ -1025,9 +1044,9 @@ def run_cleanup(callback: Callable[[], Any]) -> None:
run_cleanup(viz.close)
instance._visualizers.clear()

for resource in instance._backend_registry.values():
if (clear := getattr(resource, "clear", None)) is not None:
run_cleanup(clear)
instance.clone_contexts.clear()
for _, resource in instance._backend_registry:
run_cleanup(resource.close)
instance._backend_registry.clear()

# Tear down the stage. We skip clear_stage() (prim-by-prim deletion) since
Expand Down
Loading
Loading