Skip to content

Commit 26f1360

Browse files
committed
Merge branch 'develop' of https://github.com/isaac-sim/IsaacLab into zhengyuz/kitless-visual-color-randomization
# Conflicts: # source/isaaclab/test/envs/test_color_randomization.py
2 parents d76828e + 1acbea6 commit 26f1360

342 files changed

Lines changed: 11787 additions & 2762 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/actions/upload-omni-github-test-results/junit_to_omni_github_results.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,28 @@ def _test_id(testcase: ET.Element) -> str:
5454
return "::".join(part for part in parts if part) or "unknown-testcase"
5555

5656

57+
def _testcase_markers(testcase: ET.Element) -> list[str]:
58+
"""Return the intent markers recorded on a testcase, in stable order.
59+
60+
pytest serializes ``item.user_properties`` as ``<property>`` elements under a
61+
``<properties>`` child of the testcase. The repo-root ``conftest.py`` records the
62+
auto-applied intent markers there under the ``markers`` property as a comma-separated
63+
string. Missing or empty values yield an empty list.
64+
"""
65+
properties = _first_child(testcase, {"properties"})
66+
if properties is None:
67+
return []
68+
markers: list[str] = []
69+
for prop in properties:
70+
if _local_name(prop.tag) != "property" or prop.attrib.get("name") != "markers":
71+
continue
72+
for value in prop.attrib.get("value", "").split(","):
73+
value = value.strip()
74+
if value:
75+
markers.append(value)
76+
return markers
77+
78+
5779
def _short_message(element: ET.Element | None) -> str | None:
5880
"""Return a compact message from a failure, error, or skip element."""
5981
if element is None:
@@ -83,7 +105,6 @@ def _convert_testcase(
83105
"test_id": _test_id(testcase),
84106
"passed": failure_or_error is None and skipped is None,
85107
"duration": _duration_seconds(testcase),
86-
"test_type": test_type,
87108
"group_id": group_id,
88109
"retries": retries,
89110
"log_paths": log_paths,
@@ -106,6 +127,12 @@ def _convert_testcase(
106127
if any(pattern in detail_text for pattern in patterns):
107128
row[flag] = True
108129

130+
markers = _testcase_markers(testcase)
131+
if markers:
132+
row["test_type"] = ",".join([test_type, *markers])
133+
else:
134+
row["test_type"] = test_type
135+
109136
return row
110137

111138

.github/actions/upload-omni-github-test-results/test_junit_to_omni_github_results.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,42 @@ def test_convert_junit_adds_log_paths_for_junit_and_comparison_artifacts(tmp_pat
167167
junit_log_url,
168168
comparison_images_url,
169169
]
170+
171+
172+
def test_convert_junit_appends_markers_to_test_type_with_separator(tmp_path: Path) -> None:
173+
"""Recorded intent markers should append to the base test_type with comma separators."""
174+
converter = _load_converter_module()
175+
junit_file = tmp_path / "report.xml"
176+
output_dir = tmp_path / "out"
177+
junit_file.write_text(
178+
"""<?xml version="1.0" encoding="utf-8"?>
179+
<testsuite name="pytest" tests="2" failures="0" errors="0" skipped="0" time="2">
180+
<testcase classname="test_camera" name="test_rgb" time="1">
181+
<properties>
182+
<property name="markers" value="integration,rendering"/>
183+
</properties>
184+
</testcase>
185+
<testcase classname="test_math" name="test_quat" time="1"/>
186+
</testsuite>
187+
""",
188+
encoding="utf-8",
189+
)
190+
191+
converter.convert_junit(
192+
junit_file=junit_file,
193+
output_dir=output_dir,
194+
test_tool_id="pytest",
195+
test_type="pytest",
196+
app_platform="linux-x86_64",
197+
app_config="test-job",
198+
group_name="Docker + Tests / isaaclab",
199+
junit_log_url="",
200+
comparison_images_url="",
201+
retries=0,
202+
)
203+
204+
marked, unmarked = _load_rows(output_dir)
205+
# The base type must be separated from the first marker, not fused into "pytestintegration".
206+
assert marked["test_type"] == "pytest,integration,rendering"
207+
# Testcases without markers keep the bare base type.
208+
assert unmarked["test_type"] == "pytest"

.github/workflows/daily-compatibility.yml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,19 @@ jobs:
9595
fetch-depth: 1
9696
lfs: true
9797

98+
# Single source of truth: read the OV runtime pins from [tool.isaaclab.versions].
99+
- name: Resolve OV runtime pins from pyproject
100+
id: ov_pins
101+
run: |
102+
PINS=$(python3 <<'PY'
103+
import re
104+
block = re.search(r"^\[tool\.isaaclab\.versions\]\n(.*?)(?:\n\[|\Z)", open("pyproject.toml").read(), re.S | re.M).group(1)
105+
vals = dict(re.findall(r'(\w+)\s*=\s*"([^"]+)"', block))
106+
print(f"ovphysx=={vals['ovphysx']} ovrtx{vals['ovrtx']}")
107+
PY
108+
)
109+
echo "pins=$PINS" >> "$GITHUB_OUTPUT"
110+
98111
- name: Build Docker Image
99112
uses: ./.github/actions/docker-build
100113
with:
@@ -111,7 +124,7 @@ jobs:
111124
image-tag: ${{ env.DOCKER_IMAGE_TAG }}
112125
pytest-options: ""
113126
filter-pattern: "isaaclab_tasks"
114-
extra-pip-packages: "ovrtx ovphysx==0.4.13"
127+
extra-pip-packages: ${{ steps.ov_pins.outputs.pins }}
115128

116129
- name: Copy All Test Results from IsaacLab Tasks Container
117130
run: |

.github/workflows/license-check.yaml

Lines changed: 142 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,145 @@ concurrency:
1515

1616
jobs:
1717
license-check:
18+
# Fork PRs cannot access NGC_API_KEY, so they cannot resolve the immutable
19+
# OVPhysX wheel required by the development extras.
20+
if: github.event.pull_request.head.repo.full_name == github.repository
1821
runs-on: ubuntu-24.04
22+
env:
23+
NGC_API_KEY: ${{ secrets.NGC_API_KEY }}
1924

2025
steps:
2126
- name: Checkout code
2227
uses: actions/checkout@v6
2328
with:
2429
filter: tree:0
2530

31+
- name: Load OVPhysX wheelhouse configuration
32+
id: config
33+
shell: bash
34+
run: |
35+
set -euo pipefail
36+
resource=$(yq -r .ovphysx_wheelhouse_resource .github/workflows/config.yaml)
37+
echo "wheelhouse_resource=${resource}" >> "$GITHUB_OUTPUT"
38+
39+
- name: Restore NGC CLI cache
40+
uses: actions/cache@v4
41+
with:
42+
path: ${{ runner.temp }}/ngc-cli-cache/ngccli_linux.zip
43+
key: ngc-cli-${{ runner.os }}-${{ runner.arch }}-4.20.0-5cf084c88998c58ad8abf7849d2d1b41d578423886eb03018df10194e341d35b
44+
45+
- name: Extract OVPhysX wheelhouse
46+
id: extract-wheelhouse
47+
shell: bash
48+
env:
49+
WHEELHOUSE_RESOURCE: ${{ steps.config.outputs.wheelhouse_resource }}
50+
run: |
51+
set -euo pipefail
52+
53+
if [ -z "${NGC_API_KEY:-}" ]; then
54+
echo "::error::NGC_API_KEY is unavailable; cannot download the configured OVPhysX wheelhouse"
55+
exit 1
56+
fi
57+
58+
NGC_CLI_VERSION="4.20.0"
59+
NGC_CLI_SHA256="5cf084c88998c58ad8abf7849d2d1b41d578423886eb03018df10194e341d35b"
60+
NGC_CLI_URL="https://api.ngc.nvidia.com/v2/resources/nvidia/ngc-apps/ngc_cli/versions/${NGC_CLI_VERSION}/files/ngccli_linux.zip"
61+
62+
wheelhouse_root="$(mktemp -d "${RUNNER_TEMP:-/tmp}/ovphysx-wheelhouse.XXXXXX")"
63+
download_root="$(mktemp -d "${RUNNER_TEMP:-/tmp}/ovphysx-wheelhouse-download.XXXXXX")"
64+
ngc_home="$(mktemp -d "${RUNNER_TEMP:-/tmp}/ovphysx-ngc-home.XXXXXX")"
65+
ngc_unpack_dir=""
66+
preserve_wheelhouse_root=false
67+
cleanup() {
68+
if [ "$preserve_wheelhouse_root" != "true" ]; then
69+
rm -rf "$wheelhouse_root"
70+
fi
71+
rm -rf "$download_root" "$ngc_home"
72+
if [ -n "$ngc_unpack_dir" ]; then
73+
rm -rf "$ngc_unpack_dir"
74+
fi
75+
}
76+
trap cleanup EXIT
77+
78+
cache_dir="${RUNNER_TEMP:-/tmp}/ngc-cli-cache"
79+
ngc_zip="${cache_dir}/ngccli_linux.zip"
80+
mkdir -p "$cache_dir"
81+
if [ ! -f "$ngc_zip" ]; then
82+
echo "Downloading NGC CLI ${NGC_CLI_VERSION}"
83+
curl -fsSL -o "$ngc_zip" "$NGC_CLI_URL"
84+
fi
85+
echo "${NGC_CLI_SHA256} ${ngc_zip}" | sha256sum -c -
86+
ngc_unpack_dir="$(mktemp -d "${RUNNER_TEMP:-/tmp}/ngc-cli.XXXXXX")"
87+
unzip -q "$ngc_zip" -d "$ngc_unpack_dir"
88+
export PATH="${ngc_unpack_dir}/ngc-cli:${PATH}"
89+
90+
export NGC_CLI_API_KEY="$NGC_API_KEY"
91+
export NGC_CLI_ORG=nvidian
92+
export NGC_CLI_TEAM=no-team
93+
export NGC_CLI_HOME="$ngc_home"
94+
95+
ngc --version
96+
echo "Downloading wheelhouse resource: $WHEELHOUSE_RESOURCE"
97+
ngc registry resource download-version "$WHEELHOUSE_RESOURCE" --dest "$download_root"
98+
99+
mapfile -t manifests < <(find "$download_root" -type f -name manifest.json)
100+
if [ "${#manifests[@]}" -ne 1 ]; then
101+
echo "::error::expected exactly one manifest.json in downloaded wheelhouse resource, found ${#manifests[@]}"
102+
printf '%s\n' "${manifests[@]}"
103+
exit 1
104+
fi
105+
106+
payload_dir="$(dirname "${manifests[0]}")"
107+
if [ ! -d "${payload_dir}/wheelhouse" ]; then
108+
echo "::error::downloaded wheelhouse resource is missing wheelhouse/ next to manifest.json"
109+
exit 1
110+
fi
111+
112+
mkdir -p "$wheelhouse_root/wheelhouse"
113+
cp "${payload_dir}/manifest.json" "$wheelhouse_root/manifest.json"
114+
cp "${payload_dir}/wheelhouse/"*.whl "$wheelhouse_root/wheelhouse/"
115+
116+
python3 - <<'PY' "$wheelhouse_root/manifest.json" "$wheelhouse_root/wheelhouse"
117+
import hashlib
118+
import json
119+
import pathlib
120+
import sys
121+
122+
manifest_path = pathlib.Path(sys.argv[1])
123+
wheelhouse_dir = pathlib.Path(sys.argv[2])
124+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
125+
126+
expected = {
127+
"artifact": "ovphysx-wheelhouse",
128+
"platform": "manylinux_2_35_x86_64",
129+
}
130+
for key, value in expected.items():
131+
actual = manifest.get(key)
132+
if actual != value:
133+
raise SystemExit(f"manifest {key!r} mismatch: expected {value!r}, got {actual!r}")
134+
135+
wheels = manifest.get("wheels")
136+
if not isinstance(wheels, list) or not wheels:
137+
raise SystemExit("manifest has no wheels list")
138+
139+
for wheel in wheels:
140+
filename = wheel.get("file")
141+
expected_sha = wheel.get("sha256")
142+
if not filename or not expected_sha:
143+
raise SystemExit(f"invalid wheel manifest entry: {wheel!r}")
144+
wheel_path = wheelhouse_dir / filename
145+
if not wheel_path.is_file():
146+
raise SystemExit(f"manifest wheel is missing from wheelhouse: {filename}")
147+
actual_sha = hashlib.sha256(wheel_path.read_bytes()).hexdigest()
148+
if actual_sha != expected_sha:
149+
raise SystemExit(f"sha256 mismatch for {filename}: expected {expected_sha}, got {actual_sha}")
150+
151+
print(f"Validated {len(wheels)} wheelhouse wheels from {manifest_path}")
152+
PY
153+
154+
echo "wheelhouse_root=$wheelhouse_root" >> "$GITHUB_OUTPUT"
155+
echo "wheelhouse_dir=$wheelhouse_root/wheelhouse" >> "$GITHUB_OUTPUT"
156+
26157
# - name: Install jq
27158
# run: sudo apt-get update && sudo apt-get install -y jq
28159

@@ -60,10 +191,13 @@ jobs:
60191
OMNI_KIT_ACCEPT_EULA: yes
61192
ACCEPT_EULA: Y
62193
ISAACSIM_ACCEPT_EULA: YES
194+
UV_FIND_LINKS: ${{ steps.extract-wheelhouse.outputs.wheelhouse_dir }}
63195
run: |
64196
uv sync --extra all
65-
# Isaac Sim isn't in the dev pyproject; sync first so it isn't pruned.
66-
uv pip install 'isaacsim[all,extscache]==${{ vars.ISAACSIM_BASE_VERSION || '6.0.0' }}'
197+
# Isaac Sim conflicts with --extra all under uv, so install it imperatively
198+
# after the sync. Read the pinned spec from pyproject (single source of truth).
199+
ISAACSIM_SPEC=$(.venv/bin/python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['optional-dependencies']['isaacsim'][0])")
200+
uv pip install "$ISAACSIM_SPEC"
67201
uv pip install pip-licenses pipdeptree \
68202
-r tools/template/requirements.txt \
69203
-r docs/requirements.txt
@@ -153,3 +287,9 @@ jobs:
153287
else
154288
echo "All packages were checked."
155289
fi
290+
291+
- name: Clean up OVPhysX wheelhouse
292+
if: always()
293+
shell: bash
294+
run: |
295+
rm -rf "${{ steps.extract-wheelhouse.outputs.wheelhouse_root }}"

.github/workflows/license-exceptions.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,11 @@
488488
"license": "LicenseRef-NVIDIA-SOFTWARE-LICENSE",
489489
"comment": "NVIDIA"
490490
},
491+
{
492+
"package": "cuda-toolkit",
493+
"license": null,
494+
"comment": "NVIDIA CUDA Toolkit EULA (LicenseRef-NVIDIA-SOFTWARE-LICENSE); wheel ships no license metadata"
495+
},
491496
{
492497
"package": "omniverseclient",
493498
"license": "NVIDIA Proprietary Software, https://www.nvidia.com/en-us/agreements/enterprise-software/nvidia-software-license-agreement/",
@@ -507,5 +512,15 @@
507512
"package": "pytetwild",
508513
"license": "Mozilla Public License 2.0 (MPL 2.0)",
509514
"comment": "MPL-2.0 / OSRB"
515+
},
516+
{
517+
"package": "distlib",
518+
"license": "Python Software Foundation License",
519+
"comment": "PSFL / OSRB"
520+
},
521+
{
522+
"package": "ovrtx",
523+
"license": "NVIDIA Proprietary Software",
524+
"comment": "NVIDIA"
510525
}
511526
]

apps/isaaclab.python.headless.kit

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,11 @@ interceptSysStdOutput = false
6262
logSysStdOutput = false
6363

6464
[settings]
65-
# MGPU is always on, you can turn it from the settings, and force this off to save even more resource if you
66-
# only want to use a single GPU on your MGPU system
67-
# False for Isaac Sim
68-
renderer.multiGpu.enabled = true
69-
renderer.multiGpu.autoEnable = true
65+
# Use one renderer GPU by default. Applications that need single-process multi-GPU rendering can override these
66+
# settings through AppLauncher's ``kit_args``.
67+
renderer.multiGpu.enabled = false
68+
renderer.multiGpu.autoEnable = false
69+
renderer.multiGpu.maxGpuCount = 1
7070
'rtx-transient'.resourcemanager.enableTextureStreaming = true
7171
app.asyncRendering = false
7272
app.asyncRenderingLowLatency = false

apps/isaaclab.python.headless.rendering.kit

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,6 @@ rtx.ambientOcclusion.enabled = false
7676
# Set the DLSS model
7777
rtx.post.dlss.execMode = 0 # can be 0 (Performance), 1 (Balanced), 2 (Quality), or 3 (Auto)
7878

79-
# Avoids unnecessary GPU context initialization
80-
renderer.multiGpu.maxGpuCount=1
81-
8279
# Force synchronous rendering to improve training results
8380
omni.replicator.asyncRendering = false
8481

apps/isaaclab.python.kit

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,12 @@ omni.replicator.asyncRendering = false
249249
app.asyncRendering = false
250250
app.asyncRenderingLowLatency = false
251251

252+
# Use one renderer GPU by default. Applications that need single-process multi-GPU rendering can override these
253+
# settings through AppLauncher's ``kit_args``.
254+
renderer.multiGpu.enabled = false
255+
renderer.multiGpu.autoEnable = false
256+
renderer.multiGpu.maxGpuCount = 1
257+
252258
### FSD
253259
app.useFabricSceneDelegate = true
254260
rtx.hydra.readTransformsFromFabricInRenderDelegate = true

apps/isaaclab.python.rendering.kit

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,6 @@ rtx.ambientOcclusion.enabled = false
7171
# Set the DLSS model
7272
rtx.post.dlss.execMode = 0 # can be 0 (Performance), 1 (Balanced), 2 (Quality), or 3 (Auto)
7373

74-
# Avoids unnecessary GPU context initialization
75-
renderer.multiGpu.maxGpuCount=1
76-
7774
# Force synchronous rendering to improve training results
7875
omni.replicator.asyncRendering = false
7976

apps/isaaclab.python.xr.openxr.kit

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ app.asyncRendering = true
2424
app.asyncRenderingLowLatency = true
2525

2626
# For XR, set this back to default "#define OMNI_MAX_DEVICE_GROUP_DEVICE_COUNT 16"
27+
renderer.multiGpu.enabled = true
28+
renderer.multiGpu.autoEnable = true
2729
renderer.multiGpu.maxGpuCount = 16
2830
renderer.gpuEnumeration.glInterop.enabled = true # Allow Kit XR OpenXR to render headless
2931

0 commit comments

Comments
 (0)