From 573c36c1c33d2ee9318c4c61c23b04f38c358fae Mon Sep 17 00:00:00 2001 From: CPrutean Date: Mon, 3 Aug 2026 16:23:32 -0700 Subject: [PATCH 01/38] fix: only normalize DISPLAY for bare-number values The DISPLAY normalization prepended ':' to anything not already starting with one. That was meant for environments exporting a bare number ("1"), but it also mangled hostname-qualified specs such as "host.docker.internal:0" into ":host.docker.internal:0", breaking X11 forwarding to a remote X server. Narrow the match to bare numbers so remote specs are left alone. --- docker/bashrc.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docker/bashrc.sh b/docker/bashrc.sh index 7db664d..e1b2db5 100644 --- a/docker/bashrc.sh +++ b/docker/bashrc.sh @@ -77,7 +77,11 @@ if [ -n "${FORCE_VNC:-}" ]; then # not the host's real DISPLAY (which is still reachable via the bind-mounted # X11 socket and would otherwise render GUI apps on the host instead of VNC). export DISPLAY="${FORCE_VNC_DISPLAY:-:77}" -elif [[ -n ${DISPLAY:-} && ${DISPLAY} != :* ]]; then +elif [[ -n ${DISPLAY:-} && ${DISPLAY} =~ ^[0-9]+(\.[0-9]+)?$ ]]; then + # Some environments export DISPLAY as a bare number (e.g. "1") instead of ":1" - normalize + # that case only. Don't touch host:display specs like "host.docker.internal:0" or + # "172.20.32.1:0" (WSL2/macOS/Windows passthrough) - those are already valid as-is, and + # prepending ":" to them would turn a valid remote display into a broken local one. export DISPLAY=":${DISPLAY}" fi From 14095f8cf9190bf13aa1f2f367556decc067e46f Mon Sep 17 00:00:00 2001 From: CPrutean Date: Mon, 3 Aug 2026 16:23:44 -0700 Subject: [PATCH 02/38] feat: add macOS and Windows devcontainer variants for GUI forwarding Docker Desktop cannot bind-mount the host's X11/Wayland socket into the container on macOS or on Windows without WSL2/WSLg, so GUI windows had no way to reach the host and fell back to the internal VNC stack. Forward them over the X11 protocol's TCP transport instead, reachable at host.docker.internal, with new devcontainer variants mirroring the existing nvidia one. To support this, generalize x_server.sh's Wayland-only passthrough check into try_display_passthrough(), which also tries a generic X11 display before falling back to Xvfb/VNC. As a side effect this also makes native Linux X11-without-Wayland hosts work. Also start VirtualGL's 3D X server when passthrough lands on a remote display: XQuartz and VcXsrv can show windows but cannot provide a usable OpenGL context, so GL has to be rendered container-side instead. It degrades gracefully when VirtualGL isn't installed. Two bugs this surfaced: - Xvfb cannot bind a remote spec like "host.docker.internal:0", so normalize DISPLAY back to ":0" before starting the internal server. - The "display already in use" guard fired on the host's own display and aborted startup; passthrough already proves nothing local is listening, so drop it. --- .devcontainer/macos/devcontainer.json | 53 +++++++++++++++++ .devcontainer/windows/devcontainer.json | 53 +++++++++++++++++ .devcontainer/x_server.sh | 79 ++++++++++++++++++++++--- docker/docker-compose-macos.yml | 36 +++++++++++ docker/docker-compose-windows.yml | 39 ++++++++++++ 5 files changed, 253 insertions(+), 7 deletions(-) create mode 100644 .devcontainer/macos/devcontainer.json create mode 100644 .devcontainer/windows/devcontainer.json create mode 100644 docker/docker-compose-macos.yml create mode 100644 docker/docker-compose-windows.yml diff --git a/.devcontainer/macos/devcontainer.json b/.devcontainer/macos/devcontainer.json new file mode 100644 index 0000000..497faad --- /dev/null +++ b/.devcontainer/macos/devcontainer.json @@ -0,0 +1,53 @@ +{ + "dockerComposeFile": [ + "../../docker/docker-compose.yml", + "../../docker/docker-compose-dev.yml", + "../../docker/docker-compose-macos.yml" + ], + "service": "sim", + "runServices": ["sim"], + "workspaceFolder": "/home/trickfire/simulations", + "remoteEnv": { + "HOST_WORKSPACE": "${localWorkspaceFolder}" + }, + "postCreateCommand": "pip3 install --break-system-packages -e .", // install the sim cli + "postStartCommand": "bash ./.devcontainer/x_server.sh || true", + + "customizations": { + "vscode": { + "settings": { + "shfmt.executablePath": "/usr/local/bin/shfmt" + }, + "extensions": [ + // --- Python --- + "ms-python.python", + "ms-python.vscode-pylance", + "charliermarsh.ruff", + "ms-python.mypy-type-checker", + + // --- C and C++ --- + "ms-vscode.cmake-tools", + "josetr.cmake-language-support-vscode", + "llvm-vs-code-extensions.vscode-clangd", + "ms-vscode.makefile-tools", + + // --- Robotics / ROS / URDF / meshes --- + "Ranch-Hand-Robotics.rde-ros-2", + "smilerobotics.urdf", + "morningfrog.urdf-visualizer", + "misiekhardcore.stl-previewer", + + // --- Container --- + "ms-azuretools.vscode-docker", + + // --- Formatters --- + "esbenp.prettier-vscode", + "yzhang.markdown-all-in-one", + "DotJoshJohnson.xml", + "tamasfe.even-better-toml", + "github.vscode-github-actions", + "mkhl.shfmt" + ] + } + } +} diff --git a/.devcontainer/windows/devcontainer.json b/.devcontainer/windows/devcontainer.json new file mode 100644 index 0000000..fdbc1ed --- /dev/null +++ b/.devcontainer/windows/devcontainer.json @@ -0,0 +1,53 @@ +{ + "dockerComposeFile": [ + "../../docker/docker-compose.yml", + "../../docker/docker-compose-dev.yml", + "../../docker/docker-compose-windows.yml" + ], + "service": "sim", + "runServices": ["sim"], + "workspaceFolder": "/home/trickfire/simulations", + "remoteEnv": { + "HOST_WORKSPACE": "${localWorkspaceFolder}" + }, + "postCreateCommand": "pip3 install --break-system-packages -e .", // install the sim cli + "postStartCommand": "bash ./.devcontainer/x_server.sh || true", + + "customizations": { + "vscode": { + "settings": { + "shfmt.executablePath": "/usr/local/bin/shfmt" + }, + "extensions": [ + // --- Python --- + "ms-python.python", + "ms-python.vscode-pylance", + "charliermarsh.ruff", + "ms-python.mypy-type-checker", + + // --- C and C++ --- + "ms-vscode.cmake-tools", + "josetr.cmake-language-support-vscode", + "llvm-vs-code-extensions.vscode-clangd", + "ms-vscode.makefile-tools", + + // --- Robotics / ROS / URDF / meshes --- + "Ranch-Hand-Robotics.rde-ros-2", + "smilerobotics.urdf", + "morningfrog.urdf-visualizer", + "misiekhardcore.stl-previewer", + + // --- Container --- + "ms-azuretools.vscode-docker", + + // --- Formatters --- + "esbenp.prettier-vscode", + "yzhang.markdown-all-in-one", + "DotJoshJohnson.xml", + "tamasfe.even-better-toml", + "github.vscode-github-actions", + "mkhl.shfmt" + ] + } + } +} diff --git a/.devcontainer/x_server.sh b/.devcontainer/x_server.sh index 012a501..d6dbf4b 100755 --- a/.devcontainer/x_server.sh +++ b/.devcontainer/x_server.sh @@ -57,7 +57,51 @@ parse_args() { done } -try_wayland_passthrough() { +# VirtualGL's "3D X server": a container-local headless X server that GL rendering actually +# happens against. Only needed when $DISPLAY is a remote X server (macOS/XQuartz, +# Windows/VcXsrv), which can display windows but can't provide a usable OpenGL context - its +# indirect GLX is deprecated and broken, so OGRE2 apps (Gazebo, RViz) fail to create a +# renderer. VirtualGL renders here instead (Mesa llvmpipe, OpenGL 4.5) and sends only the +# finished frames to the host's X server as plain X11 images. +# +# Skipped for local passthrough (native Linux, WSLg): there the app's GL already works +# directly against the host's GPU, and routing through VirtualGL would only cost performance. +start_vgl_3d_server() { + if [[ $DISPLAY == :* ]]; then + return 0 + fi + + if ! command -v vglrun >/dev/null 2>&1; then + log "[VGL] vglrun not found - skipping (Gazebo/RViz will not be able to render)" + return 0 + fi + + local vgl_display="${VGL_DISPLAY:-:88}" + if xdpyinfo -display "$vgl_display" &>/dev/null; then + log "[VGL] 3D X server already running on $vgl_display" + return 0 + fi + + log "[VGL] Starting VirtualGL 3D X server (Xvfb) on $vgl_display" + Xvfb "$vgl_display" -screen 0 2560x1440x24 >>"$LOG_FILE" 2>&1 & + local vgl_pid=$! + disown "$vgl_pid" 2>/dev/null || true + + # Xvfb takes a moment to start listening; without this the first `sim gazebo` after a + # container start could race it and silently fall back to unaccelerated rendering. + local i + for i in $(seq 1 20); do + if xdpyinfo -display "$vgl_display" &>/dev/null; then + log "[VGL] 3D X server ready on $vgl_display" + return 0 + fi + sleep 0.25 + done + + log "[VGL] WARNING: 3D X server on $vgl_display did not come up - see $LOG_FILE" +} + +try_display_passthrough() { if [ -n "$FORCE_VNC" ]; then DISPLAY="${FORCE_VNC_DISPLAY:-:77}" unset WAYLAND_DISPLAY @@ -65,11 +109,26 @@ try_wayland_passthrough() { return fi + # Case 1: Linux host with a Wayland compositor, or WSL2 with WSLg. The host socket is + # bind-mounted into /run/host-runtime by docker-compose-dev.yml. local wayland_sock="/run/host-runtime/${WAYLAND_DISPLAY:-wayland-0}" if [ -S "$wayland_sock" ]; then log "[X11] Using Wayland socket at $wayland_sock" exit 0 fi + + # Case 2: a real X11 display is already reachable at $DISPLAY. This covers: + # - native Linux X11 (host's /tmp/.X11-unix bind-mounted, DISPLAY inherited from the host) + # - WSL2 with WSLg's X11 socket + # - macOS + XQuartz reachable over TCP at host.docker.internal:0 + # (see docker/docker-compose-macos.yml / .devcontainer/macos) + # - Windows + VcXsrv/X410 reachable over TCP at host.docker.internal:0 + # (see docker/docker-compose-windows.yml / .devcontainer/windows) + if [ -n "$DISPLAY" ] && xdpyinfo -display "$DISPLAY" &>/dev/null; then + log "[X11] Using host X11 display at $DISPLAY" + start_vgl_3d_server + exit 0 + fi } detect_backend() { @@ -158,7 +217,18 @@ start_services() { main() { parse_args "$@" - try_wayland_passthrough + try_display_passthrough + + # try_display_passthrough only returns (rather than exiting) when no host display was + # usable, so it's safe to claim $DISPLAY for our own Xvfb/Xorg below - we just confirmed + # nothing answers on it. However $DISPLAY may hold a remote/TCP spec like + # "host.docker.internal:0" (macOS/Windows configs, when XQuartz/VcXsrv wasn't reachable) - + # that's not a valid local display for Xvfb/Xorg to bind to, so normalize it to a plain + # local display number first. Valid local specs always start with ':'. + if [[ $DISPLAY != :* ]]; then + log "[X11] $DISPLAY unreachable; falling back to internal Xvfb/Xorg on :0" + DISPLAY=":0" + fi detect_backend parse_screen_resolution @@ -167,11 +237,6 @@ main() { : "${VNC_PORT:?VNC_PORT is not set}" : "${NOVNC_PORT:?NOVNC_PORT is not set}" - if xdpyinfo -display "$DISPLAY" &>/dev/null; then - log "[ERROR] Display $DISPLAY is already in use!" - exit 1 - fi - start_services } diff --git a/docker/docker-compose-macos.yml b/docker/docker-compose-macos.yml new file mode 100644 index 0000000..9058ba4 --- /dev/null +++ b/docker/docker-compose-macos.yml @@ -0,0 +1,36 @@ +# macOS + XQuartz addon compose for base compose. +# +# Docker Desktop for Mac runs containers inside a Linux VM and cannot bind-mount the host's +# X11/Wayland unix sockets into the container (Docker Desktop only special-cases a handful of +# sockets like docker.sock and the ssh-agent socket - arbitrary sockets, including XQuartz's, +# aren't shared through its virtiofs/gRPC-FUSE file sharing). So instead of a socket bind mount, +# GUI windows are forwarded over the X11 protocol's native TCP transport to XQuartz, which Docker +# Desktop's "host.docker.internal" DNS name makes reachable from inside the container. +# +# Setup required on the Mac host (see docs/setup/docker.mdx for details): +# 1. Install XQuartz: https://www.xquartz.org +# 2. XQuartz > Settings > Security > check "Allow connections from network clients", then +# log out/restart XQuartz for it to take effect. +# 3. In a Terminal: `xhost + 127.0.0.1` +# +# Use this file instead of docker-compose-gpu.yml (there is no GPU passthrough on macOS). +# +# XQuartz can display windows but cannot provide a usable OpenGL context - its indirect GLX +# is deprecated and broken (XQuartz/XQuartz#144), so Gazebo/RViz (OGRE2) can't render through +# it directly. GL rendering therefore goes through VirtualGL against a local headless X server +# inside the container, with only finished frames sent to XQuartz as plain X11 images. +# x_server.sh starts that 3D X server; `sim gazebo` wraps the launch in `vglrun`. + +services: + sim: + environment: + # XQuartz's default display is :0. QT_X11_NO_MITSHM is already set to 1 in the base + # compose file, which is required here too: MIT-SHM assumes a local shared-memory + # segment, which doesn't exist across this TCP connection. + DISPLAY: "host.docker.internal:0" + # Display number of the container-local X server VirtualGL renders into (started by + # .devcontainer/x_server.sh). Kept clear of :0 and of FORCE_VNC's :77. + VGL_DISPLAY: ":88" + # VirtualGL's X11 Transport: deliver rendered frames as ordinary X11 images over the + # existing connection. Avoids needing a vglclient process on the Mac or an extra port. + VGL_COMPRESS: "proxy" diff --git a/docker/docker-compose-windows.yml b/docker/docker-compose-windows.yml new file mode 100644 index 0000000..d8022c9 --- /dev/null +++ b/docker/docker-compose-windows.yml @@ -0,0 +1,39 @@ +# Windows (native, no WSL2/WSLg) addon compose for base compose. +# +# If Docker Desktop is using the WSL2 backend and you open this repo from inside a WSL2 distro, +# you don't need this file - WSLg already forwards Wayland/X11 through the same +# docker-compose-dev.yml socket bind mounts used on native Linux, so use the default +# .devcontainer/devcontainer.json instead. +# +# This file is for the remaining case: Docker Desktop's Hyper-V backend, or VS Code connecting +# from a native (non-WSL) Windows filesystem path. There, the container can't reach any host +# Wayland/X11 socket directly, so GUI windows are forwarded over the X11 protocol's native TCP +# transport to a Windows-hosted X server, reachable via Docker Desktop's "host.docker.internal" +# DNS name. +# +# Setup required on the Windows host (see docs/setup/docker.mdx for details): +# 1. Install an X server that supports TCP, e.g. VcXsrv (https://sourceforge.net/projects/vcxsrv) +# or X410 (Microsoft Store). +# 2. Launch it with network access enabled and access control disabled, e.g. for VcXsrv's +# XLaunch: "Multiple windows", display number 0, check "Disable access control". +# 3. Allow the X server through Windows Defender Firewall when prompted (Private networks). +# +# Use this file instead of docker-compose-gpu.yml (there is no GPU passthrough here). +# +# VcXsrv/X410 can display windows but their indirect GLX (IGLX) can't give Gazebo/RViz (OGRE2) +# a usable OpenGL context - the same architectural dead end confirmed by hand on macOS/XQuartz. +# GL rendering therefore goes through VirtualGL against a local headless X server inside the +# container, with only finished frames sent to the Windows X server as plain X11 images. +# x_server.sh starts that 3D X server; `sim gazebo` wraps the launch in `vglrun`. + +services: + sim: + environment: + # Matches the ":0" / display-number-0 default used by VcXsrv's XLaunch and X410. + DISPLAY: "host.docker.internal:0" + # Display number of the container-local X server VirtualGL renders into (started by + # .devcontainer/x_server.sh). Kept clear of :0 and of FORCE_VNC's :77. + VGL_DISPLAY: ":88" + # VirtualGL's X11 Transport: deliver rendered frames as ordinary X11 images over the + # existing connection. Avoids needing a vglclient process on Windows or an extra port. + VGL_COMPRESS: "proxy" From d2bc186864db3658bdc3fd87c2a78c0bae388a36 Mon Sep 17 00:00:00 2001 From: CPrutean Date: Mon, 3 Aug 2026 16:23:56 -0700 Subject: [PATCH 03/38] feat: render Gazebo and RViz through VirtualGL on remote X servers Forwarding X11 to XQuartz/VcXsrv gets windows onto the host, but not rendering: those servers cannot hand back a usable OpenGL context. Their indirect GLX is deprecated and unmaintained upstream, so OGRE2 - used by both Gazebo and RViz - failed at glXMakeCurrent with GLXBadContext and never created a renderer. Gazebo showed a blank window and rviz2 died with "Unable to create the rendering window". Neither Mesa fallback helps: forcing indirect GLX hits the same dead end, and the software/direct path cannot match XQuartz's GLX fbconfigs at all ("failed to create drisw screen"). Install VirtualGL and route rendering through it instead. GL runs against a container-local headless X server (Mesa llvmpipe, OpenGL 4.5) and only finished frames go to the host as ordinary X11 images, which XQuartz and VcXsrv handle reliably. Using VirtualGL's X11 transport keeps this free of any host-side install beyond the X server itself, and needs no extra ports. `sim gazebo` applies the vglrun prefix automatically; LD_PRELOAD propagates it to the Gazebo server, Gazebo GUI and RViz processes. Rendering stays software-only - there is no GPU passthrough on these hosts - so this buys correctness and native windows, not speed. Verified on Apple Silicon with XQuartz: Gazebo and RViz both render, with IGLX left disabled. --- cli/gazebo/launch.py | 203 ++++++++++++++++++++++++++++++++++++++++--- docker/Dockerfile | 27 ++++++ 2 files changed, 216 insertions(+), 14 deletions(-) diff --git a/cli/gazebo/launch.py b/cli/gazebo/launch.py index 2af21fc..de74ee6 100644 --- a/cli/gazebo/launch.py +++ b/cli/gazebo/launch.py @@ -5,6 +5,7 @@ import os import re import shutil +import socket import subprocess from datetime import datetime from pathlib import Path @@ -107,28 +108,202 @@ def _run_logged_command( raise subprocess.CalledProcessError(return_code, command) +def _x11_port_for(display: str) -> int: + """The X11 protocol's TCP port for a display spec is 6000 + display number.""" + try: + num_part = display.rsplit(":", 1)[-1].split(".")[0] + return 6000 + int(num_part) + except (ValueError, IndexError): + return 6000 + + +def _diagnose_display(display: str, xdpyinfo_stderr: str) -> str: + """Build a specific, actionable explanation for why `display` couldn't be opened. + + Runs its own DNS/TCP checks (independent of xdpyinfo) so the error points at the + layer that's actually broken, instead of a bare "cannot connect". + """ + lines = [f"Cannot connect to display {display}", ""] + is_local = display.startswith(":") + + if is_local: + lines += [ + "This is a local display spec - the container expected a Wayland/X11 socket to", + "already be forwarded in (native Linux host, or WSL2/WSLg).", + "", + "Checks to run inside the container:", + " 1. grep X11 /tmp/start_x_server.log", + " Look for '[X11] Using host X11 display' or '[X11] Using Wayland socket'.", + " If you see Xvfb/vkms/dummy-driver/noVNC lines instead, passthrough failed", + " at container startup and it fell back to the internal VNC stack - connect", + " a VNC viewer to localhost:5900 (or http://localhost:6080/vnc.html), or fix", + " passthrough on the host and restart the container to retry it.", + " 2. ls -la /tmp/.X11-unix/", + " Empty means the host's X11 socket wasn't bind-mounted in, or nothing is", + " listening on it on the host.", + ] + return "\n".join(lines) + + host = display.split(":", 1)[0] + lines += [ + f"This is a remote display spec (host '{host}') - used by the macOS/Windows", + "devcontainer configs to forward GUI windows to XQuartz/VcXsrv over TCP.", + "", + ] + + try: + ip = socket.gethostbyname(host) + lines.append(f" [OK] DNS: '{host}' resolves to {ip}") + except OSError as e: + lines += [ + f" [FAIL] DNS: '{host}' did not resolve ({e})", + "", + " Docker Desktop provides this name automatically to containers. If it's", + " missing, Docker Desktop may not be running, or this isn't actually", + " running inside the container (check your shell prompt).", + ] + return "\n".join(lines) + + port = _x11_port_for(display) + try: + with socket.create_connection((host, port), timeout=3): + lines.append(f" [OK] TCP: port {port} on {host} is reachable") + except OSError as e: + lines += [ + f" [FAIL] TCP: could not connect to {host}:{port} ({e})", + "", + " macOS + XQuartz:", + " - Is XQuartz actually running? (`ps aux | grep -i xquartz` on the Mac)", + " - XQuartz > Settings > Security > 'Allow connections from network", + " clients' must be checked, then XQuartz fully restarted for it to", + " take effect.", + " Windows + VcXsrv/X410:", + " - Is the X server running? For VcXsrv, XLaunch must have 'Disable", + " access control' checked.", + " - Windows Defender Firewall may be silently blocking it - check for a", + " blocked-app prompt, or allow it manually for Private networks.", + ] + return "\n".join(lines) + + lines += [ + " [FAIL] X11: connected over TCP, but the X server rejected the session:", + f" {xdpyinfo_stderr.strip() or '(no error output captured)'}", + "", + " DNS and TCP are both fine, so this is an X11 access-control problem, not a", + " network problem:", + "", + " macOS:", + " Run on the Mac (not in the container): `DISPLAY=:0 xhost + 127.0.0.1`", + " Do NOT use `xhost -display :0 + ...` - this is a documented xhost bug:", + " '-display' is parsed as 'remove a host named display', not a real flag.", + " xhost always connects using your shell's $DISPLAY env var instead, so set", + " it as a one-off prefix like above. This resets every time XQuartz", + " restarts, so you'll need to re-run it after any XQuartz restart.", + " Windows:", + " Relaunch VcXsrv/X410 with 'Disable access control' checked - there's no", + " separate allow-list step needed once that's set.", + ] + return "\n".join(lines) + + +def _display_reachable(display: str) -> bool: + """Whether an X server is answering on `display`.""" + return ( + subprocess.run( + ["xdpyinfo", "-display", display], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ).returncode + == 0 + ) + + def _check_display() -> None: info("Checking for display...") display = os.environ.get("DISPLAY") if not display: - die("DISPLAY environment variable not set") - if ( - subprocess.call( - ["xdpyinfo", "-display", display], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL + die( + "DISPLAY environment variable not set\n\n" + " This is normally set by the devcontainer's compose config. If you're\n" + " seeing this, something stripped it from your shell - try a fresh\n" + " terminal/container restart, or run `env | grep DISPLAY` to confirm." + ) + + result = subprocess.run( + ["xdpyinfo", "-display", display], + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + text=True, + check=False, + ) + if result.returncode != 0: + die(_diagnose_display(display, result.stderr)) + + +def _configure_virtualgl_rendering(env: dict[str, str]) -> list[str]: + """Route GL rendering through VirtualGL when displaying on a remote X server. + + macOS (XQuartz) and Windows (VcXsrv/X410) can display X11 windows over TCP, but can't + hand back a usable OpenGL context: their indirect GLX is deprecated and broken, so OGRE2 + - which both Gazebo and RViz use - fails at glXMakeCurrent and never creates a renderer. + + VirtualGL splits the two concerns. GL rendering runs against a container-local headless X + server (Mesa llvmpipe, OpenGL 4.5) started by .devcontainer/x_server.sh, and only the + finished frames go to the host's X server as ordinary X11 images - which it handles fine. + + Returns the command prefix to launch under, or an empty prefix if VirtualGL isn't needed + or isn't usable (in which case the launch still proceeds, just without GL acceleration). + """ + display = os.environ.get("DISPLAY", "") + if display.startswith(":"): + return [] # local passthrough (Linux/WSLg) - the app's GL already works directly + + if not shutil.which("vglrun"): + warn( + "VirtualGL (vglrun) is not installed, so Gazebo/RViz have no way to get a\n" + " working GL context on this host - expect a blank Gazebo window and\n" + " rviz2 dying with 'Unable to create the rendering window'.\n" + " \n" + " Rebuild the container to pick it up, or set FORCE_VNC=1 in docker/.env\n" + " and recreate the container to render over VNC instead." ) - != 0 - ): - die("Cannot connect to display " + display) + return [] + + vgl_display = os.environ.get("VGL_DISPLAY", ":88") + if not _display_reachable(vgl_display): + warn( + f"VirtualGL's 3D X server on {vgl_display} isn't running, so Gazebo/RViz can't\n" + " get a working GL context - expect rendering to fail.\n" + " \n" + " Start it with: bash .devcontainer/x_server.sh\n" + " (it normally starts automatically when the container starts)" + ) + return [] + + # These force Mesa onto the host X server's indirect-GLX path - the very thing VirtualGL + # exists to avoid. Left set, they also break the local GL context VirtualGL renders into, + # so drop them for the launched processes. + for stale in ("LIBGL_ALWAYS_INDIRECT", "MESA_LOADER_DRIVER_OVERRIDE"): + env.pop(stale, None) + + env["VGL_DISPLAY"] = vgl_display + # X11 Transport: hand rendered frames over as ordinary X11 images on the connection we + # already have. No vglclient process on the host and no extra port needed. + env.setdefault("VGL_COMPRESS", "proxy") + + info(f"Rendering through VirtualGL ({vgl_display} -> {display})") + return ["vglrun"] -def _configure_force_vnc_rendering(env: dict[str, str]) -> list[str]: - """Force software GL rendering for Gazebo/OGRE2 when running under FORCE_VNC""" - if not os.environ.get("FORCE_VNC"): +def _configure_rendering(env: dict[str, str]) -> list[str]: + """Pick how Gazebo/OGRE2 should get its GL context, based on where it's being displayed.""" + if os.environ.get("FORCE_VNC"): + info("FORCE_VNC: forcing software rendering (llvmpipe) - no direct GPU access") + env["LIBGL_ALWAYS_SOFTWARE"] = "1" return [] - info("FORCE_VNC: forcing software rendering (llvmpipe) - no direct GPU access") - env["LIBGL_ALWAYS_SOFTWARE"] = "1" - return [] + return _configure_virtualgl_rendering(env) def _setup_pixi_env() -> None: @@ -179,7 +354,7 @@ def build_and_launch(robot_name: str, *, build_only: bool = False, no_build: boo build = not no_build launch = not build_only env = os.environ.copy() - render_prefix = _configure_force_vnc_rendering(env) + render_prefix = _configure_rendering(env) bringup_pkg, description_pkg, launch_file_name = _validate_robot_layout(robot_name) if build: diff --git a/docker/Dockerfile b/docker/Dockerfile index ff52414..1bb0377 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -241,6 +241,33 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ mesa-utils libglx-mesa0 libgl1-mesa-dri x11-apps \ xserver-xorg-core xserver-xorg-video-dummy kmod +# VirtualGL - makes Gazebo/RViz render on macOS (XQuartz) and Windows (VcXsrv/X410). +# +# Those hosts can display X11 windows over TCP just fine, but they cannot hand back a usable +# OpenGL context: their indirect GLX (IGLX) is deprecated and broken, so OGRE2 - which both +# Gazebo and RViz use - fails at glXMakeCurrent with GLXBadContext and never creates a +# renderer. Forcing Mesa down the indirect path doesn't help, and the direct/software path +# can't match XQuartz's GLX fbconfigs at all ("failed to create drisw screen"). +# Upstream considers IGLX a dead end: https://github.com/XQuartz/XQuartz/issues/144 +# +# VirtualGL splits the two concerns instead. GL rendering runs against a local headless X +# server inside the container (Mesa llvmpipe, OpenGL 4.5), and only the finished frames are +# handed to the host's X server as ordinary X11 images - which XQuartz/VcXsrv do reliably. +# Not used on native Linux/WSLg, where the app's GL already works directly against the host. +# +# Not packaged in Ubuntu, so install the upstream .deb (checksummed per architecture). +ARG VIRTUALGL_VERSION=3.1.4 +RUN ARCH="$(dpkg --print-architecture)" && \ + case "${ARCH}" in \ + arm64) VGL_SHA256="5583e08ee5694d14c6ea0f25d2cfd73fa6d5077869c6645c334a948265a777bb" ;; \ + amd64) VGL_SHA256="02edc6b599571c385389af1a006f07a70c298e1d97c580a9bfd4b39d835c51e6" ;; \ + *) echo "No VirtualGL build for architecture '${ARCH}'" >&2; exit 1 ;; \ + esac && \ + curl -L -s -f -o /tmp/virtualgl.deb \ + "https://github.com/VirtualGL/virtualgl/releases/download/${VIRTUALGL_VERSION}/virtualgl_${VIRTUALGL_VERSION}_${ARCH}.deb" && \ + echo "${VGL_SHA256} */tmp/virtualgl.deb" | sha256sum --strict --check && \ + dpkg -i /tmp/virtualgl.deb && \ + rm -f /tmp/virtualgl.deb # ---------------------------------------------------------------------------- # # DEV TOOLING # From c9cfc106b44fbb84a5e1faa5f42ccc7f307f159c Mon Sep 17 00:00:00 2001 From: CPrutean Date: Mon, 3 Aug 2026 16:24:03 -0700 Subject: [PATCH 04/38] docs: document macOS and Windows GUI forwarding setup Cover the XQuartz and VcXsrv/X410 setup steps, how GL rendering is routed through VirtualGL, and how to verify it. Note two gotchas found while setting this up: `xhost -display :0 + ...` does not do what it looks like (its own man page documents this as a bug), and GL apps launched by hand on these hosts need the `vglrun` prefix that `sim gazebo` adds for you. --- docs/gazebo/gazebo.mdx | 6 +++ docs/setup/docker.mdx | 103 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 104 insertions(+), 5 deletions(-) diff --git a/docs/gazebo/gazebo.mdx b/docs/gazebo/gazebo.mdx index 9496e2e..3f505ce 100644 --- a/docs/gazebo/gazebo.mdx +++ b/docs/gazebo/gazebo.mdx @@ -48,5 +48,11 @@ Run `sim gazebo clean` to delete stale build artifacts, then try again. Stale ar **Gazebo window doesn't appear (Dev Container):** Connect via your VNC viewer at `localhost:5900` and verify it works with `xeyes`. If the display isn't running, restart it with `.devcontainer/x_server.sh`. +**RViz crashes with "Unable to create the rendering window", or Gazebo shows a blank window (macOS/Windows):** +OGRE2 (used by both Gazebo and RViz) needs a real GL context, which XQuartz/VcXsrv can't provide over their indirect GLX - the X11 connection works, but rendering doesn't. The container routes GL through VirtualGL to work around this, and `sim gazebo` launches under `vglrun` automatically. Seeing this error means that path isn't active. Check for a warning from `sim gazebo` at startup, then verify GL directly with `vglrun /opt/VirtualGL/bin/glxspheres64` - if `vglrun` is missing, rebuild the container; if the 3D X server isn't running, start it with `bash .devcontainer/x_server.sh`. `FORCE_VNC=1` in `docker/.env` plus a container recreate is the fallback - see [Docker setup](../../setup/docker/) for details. + **Package not found errors after launch:** Try running `sim gazebo clean` and building again. + +**"Package '``_bringup' not found" right after launch:** +`sim gazebo ` takes a robot name (`arm`, `chassis`, etc. - see `robots.json`), not a subcommand. `sim gazebo launch` gets parsed as robot name `"launch"`, which doesn't exist. Use `sim gazebo arm` or `sim gazebo chassis` instead. diff --git a/docs/setup/docker.mdx b/docs/setup/docker.mdx index 6889bc4..39659ea 100644 --- a/docs/setup/docker.mdx +++ b/docs/setup/docker.mdx @@ -7,9 +7,27 @@ With this approcach everything runs inside a Docker container with everything de ## 1. Build the devcontainer -If you are using VSCode, open the cloned folder in VSCode. You should see a prompt to **Reopen in Container** in your bottom right. If the prompt doesn't appear, make sure you have the [extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed. +There's a base devcontainer config plus a couple of variants for GUI forwarding on hosts that can't share a Linux display socket directly (see [Check if display works](#2-check-if-display-works) below to figure out which one you need): -If you are using the CLI, navigate to the folder in your terminal and then run `devcontainer up`. +- **Linux (Wayland or X11) and WSL2** → default config, no changes needed. +- **macOS with XQuartz** → `macos` config. +- **Windows without WSL2/WSLg** (Hyper-V backend, or opening the repo from a native Windows path) → `windows` config. + +### VSCode + +Open the cloned folder in VSCode. You should see a prompt to **Reopen in Container** in your bottom right. To pick a variant, open the Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`) and run **Dev Containers: Reopen in Container**, then select **macos** or **windows** from the configuration picker instead of the default. If the prompt doesn't appear, make sure you have the [extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed. + +### CLI + +```bash title="Terminal" +devcontainer up +``` + +Or, for a variant: + +```bash title="Terminal" +devcontainer up --config .devcontainer/macos/devcontainer.json +``` This will take a long time if it is your first time (30 mins)! Docker builds Chrono and its dependencies (many lines of C++) from source, and then the same for Gazebo and ROS! Subsequent launches will be very fast though. @@ -29,13 +47,76 @@ If your host runs a Wayland compositor (GNOME, KDE, Hyprland, etc.), the log pri The container forwards Wayland and X11 directly to your host compositor. GUI windows appear on your desktop natively — you can skip the VNC steps below. +### Linux host with X11 (no VNC needed) + +If your host runs Xorg instead of Wayland, the container forwards X11 directly through the host's `/tmp/.X11-unix` socket, which is bind-mounted in automatically. The log prints: + +``` +[X11] Using host X11 display at :0 +``` + +GUI windows appear on your desktop natively — you can skip the VNC steps below. + ### WSL2 with WSLg (no VNC needed) WSL2 includes WSLg, a built-in Wayland compositor with X11 forwarding. Gazebo and RViz windows appear via WSLg automatically, the same as on native Linux with Wayland above. -### All other cases (macOS, headless servers, Windows without WSLg) +### macOS with XQuartz (no VNC needed) + +Make sure you built the `macos` devcontainer variant (see [step 1](#1-build-the-devcontainer)). GUI windows are forwarded over the X11 protocol to [XQuartz](https://www.xquartz.org) instead of VNC: + +1. Install XQuartz if you don't already have it: `brew install --cask xquartz`, or download it from [xquartz.org](https://www.xquartz.org). +2. Open XQuartz, go to **XQuartz > Settings > Security**, and check **"Allow connections from network clients"**. +3. Fully quit and reopen XQuartz (log out and back in, or restart) for that setting to take effect. +4. In a Mac Terminal, run `DISPLAY=:0 xhost + 127.0.0.1` to authorize the connection from Docker Desktop's VM. You'll need to run this again every time XQuartz restarts, since it isn't a persistent setting. + + + It's tempting to write `xhost -display :0 + 127.0.0.1`, but that doesn't do what it looks like. `xhost`'s own man page documents this as a bug: `-display` is parsed as "remove a host named `display`", not as a flag - `xhost` always connects using your shell's `$DISPLAY` env var instead, which may be stale (pointing at a dead XQuartz session) if you launched XQuartz manually rather than by opening the app normally. Set `DISPLAY` as an env var prefix on the command instead, as in step 4. + + +Once the container starts, the log should print: + +``` +[X11] Using host X11 display at host.docker.internal:0 +``` + +If you instead see the container falling back to starting Xvfb + noVNC, double check steps 2-4 above, then check `/tmp/start_x_server.log` inside the container. -On hosts without a Wayland socket, the script starts a VNC stack. Depending on what GPU or virtual display driver is available, you will see one of the following: +If the display *is* reachable but `sim gazebo launch` (or similar) still fails with "Cannot connect to display", it now prints a diagnostic breakdown - DNS resolution, TCP reachability, and X11 authorization are checked separately, with specific fix suggestions for whichever layer is actually broken. + + + A working X11 connection isn't enough on its own to render Gazebo/RViz. XQuartz can display windows, but it can't give them a usable OpenGL context - its indirect GLX (IGLX) is deprecated and [unsupported upstream](https://github.com/XQuartz/XQuartz/issues/144), so OGRE2 (used by both Gazebo and RViz) fails to create a renderer at all. + + The container works around this with [VirtualGL](https://virtualgl.org), which is set up automatically - there is nothing to install on your Mac beyond XQuartz itself, and no extra ports to open. GL rendering happens against a headless X server *inside* the container (Mesa llvmpipe, OpenGL 4.5), and only the finished frames are sent to XQuartz as ordinary X11 images, which it handles reliably. `sim gazebo` wraps the launch in `vglrun` for you. + + You do **not** need to enable XQuartz's "Enable IGLX" / `enable_iglx` preference - VirtualGL deliberately avoids that path, and leaving it off is fine. + + Rendering is done in software (llvmpipe), so expect modest frame rates rather than GPU performance - macOS has no GPU passthrough into Docker. If rendering still fails, `sim gazebo` prints a warning explaining which piece is missing; setting `FORCE_VNC=1` in `docker/.env` and recreating the container is the fallback. + + +### Windows without WSL2/WSLg (no VNC needed) + +Make sure you built the `windows` devcontainer variant (see [step 1](#1-build-the-devcontainer)). If you're using WSL2 with Docker Desktop's WSL2 backend, you don't need this - use the default config, which gets WSLg support automatically (see above). + +1. Install an X server that supports TCP connections: [VcXsrv](https://sourceforge.net/projects/vcxsrv) (free) or X410 (Microsoft Store, paid). +2. Launch it with network access enabled. For VcXsrv's XLaunch: choose "Multiple windows", display number `0`, then on the "Extra settings" page check **"Disable access control"**. +3. If Windows Defender Firewall prompts you, allow the X server on Private networks. + +Once the container starts, the log should print: + +``` +[X11] Using host X11 display at host.docker.internal:0 +``` + +If it falls back to Xvfb + noVNC instead, confirm the X server is running with access control disabled and that the firewall isn't blocking it. + + + Same as the macOS/XQuartz note above: VcXsrv/X410 can display windows but can't provide Gazebo/RViz (OGRE2) with a usable OpenGL context over indirect GLX. The container routes GL through [VirtualGL](https://virtualgl.org) automatically - rendering happens inside the container with Mesa llvmpipe and only finished frames are sent to your X server, so there's nothing extra to install on Windows and no extra ports to open. Rendering is in software, so expect modest frame rates. If it still doesn't render, `sim gazebo` warns upfront - `FORCE_VNC=1` in `docker/.env` plus a container recreate is the fallback. + + +### All other cases (headless servers, or the above not configured) + +On hosts without a reachable Wayland or X11 display, the script starts a VNC stack. Depending on what GPU or virtual display driver is available, you will see one of the following: ``` [X11] Desktop NVIDIA GPU detected, using Xorg nvidia driver @@ -67,9 +148,21 @@ If the script fails, it automatically dumps `/tmp/start_x_server.log` to the ter Before launching the sim, confirm both display paths are functioning: -- **X11 (Gazebo / RViz):** Run `xeyes` inside the container. If a pair of animated eyes appears (in your VNC viewer or on your local desktop), X11 is working and Gazebo will render. +- **X11 (Gazebo / RViz):** Run `xeyes` inside the container. If a pair of animated eyes appears (in your VNC viewer or on your local desktop), X11 is working. - **Vulkan (Chrono):** Run `vkcube` inside the container. If a spinning textured cube renders without errors, Vulkan is working and Chrono's VSG visualizer will function. +On macOS/Windows, `xeyes` only proves the X11 connection works - it doesn't use OpenGL, so it can succeed while Gazebo/RViz still fail to render. To check the GL path as well, run this inside the container: + +```bash title="Container" +vglrun /opt/VirtualGL/bin/glxspheres64 +``` + +A window of spinning spheres should appear on your desktop, and the terminal should report a frame rate and `OpenGL Renderer: llvmpipe`. If instead you get `vglrun: command not found`, rebuild the container; if it reports that it can't open the 3D X server, run `bash .devcontainer/x_server.sh` to start it. + + + `sim gazebo` adds the `vglrun` prefix for you, but anything you launch yourself that renders with OpenGL needs it too - `vglrun rviz2`, `vglrun gz sim`, and so on. Without it, those apps go straight to XQuartz/VcXsrv's broken indirect GLX and fail to create a renderer. Plain X11 tools like `xeyes` don't need it. This only applies on macOS/Windows; on Linux and WSLg, run them normally. + + ## 3. Launch the sim Once the display is running, head to [Running Gazebo](../../guides/gazebo/) to launch your first sim. From eb57b73278823bc5414413f18839b972a26dda01 Mon Sep 17 00:00:00 2001 From: CPrutean Date: Wed, 5 Aug 2026 13:50:37 -0700 Subject: [PATCH 05/38] feat: moved from a docker compose centric dev container to a devcontainer cli centric dev container --- .devcontainer/devcontainer.json | 4 ++ .devcontainer/macos/devcontainer.json | 3 ++ .devcontainer/nvidia/devcontainer.json | 3 ++ .devcontainer/windows/devcontainer.json | 3 ++ chrono/data/vsg/imgui.ini | 3 +- docker/docker-compose.yml | 72 ++++++++++++------------- 6 files changed, 51 insertions(+), 37 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ff6a03b..25f977e 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,4 +1,5 @@ { + "name": "simulations", "dockerComposeFile": ["../docker/docker-compose.yml", "../docker/docker-compose-dev.yml"], "service": "sim", "runServices": ["sim"], @@ -13,6 +14,9 @@ "customizations": { "vscode": { "settings": { + // Keep the Activity Bar (and its Explorer icon) visible in the + // container window so the file explorer can't disappear on launch. + "workbench.activityBar.location": "default", "shfmt.executablePath": "/usr/local/bin/shfmt" }, "extensions": [ diff --git a/.devcontainer/macos/devcontainer.json b/.devcontainer/macos/devcontainer.json index 497faad..332a55e 100644 --- a/.devcontainer/macos/devcontainer.json +++ b/.devcontainer/macos/devcontainer.json @@ -16,6 +16,9 @@ "customizations": { "vscode": { "settings": { + // Keep the Activity Bar (and its Explorer icon) visible in the + // container window so the file explorer can't disappear on launch. + "workbench.activityBar.location": "default", "shfmt.executablePath": "/usr/local/bin/shfmt" }, "extensions": [ diff --git a/.devcontainer/nvidia/devcontainer.json b/.devcontainer/nvidia/devcontainer.json index 7ed8ec6..9497a36 100644 --- a/.devcontainer/nvidia/devcontainer.json +++ b/.devcontainer/nvidia/devcontainer.json @@ -16,6 +16,9 @@ "customizations": { "vscode": { "settings": { + // Keep the Activity Bar (and its Explorer icon) visible in the + // container window so the file explorer can't disappear on launch. + "workbench.activityBar.location": "default", "shfmt.executablePath": "/usr/local/bin/shfmt" }, "extensions": [ diff --git a/.devcontainer/windows/devcontainer.json b/.devcontainer/windows/devcontainer.json index fdbc1ed..2daeb98 100644 --- a/.devcontainer/windows/devcontainer.json +++ b/.devcontainer/windows/devcontainer.json @@ -16,6 +16,9 @@ "customizations": { "vscode": { "settings": { + // Keep the Activity Bar (and its Explorer icon) visible in the + // container window so the file explorer can't disappear on launch. + "workbench.activityBar.location": "default", "shfmt.executablePath": "/usr/local/bin/shfmt" }, "extensions": [ diff --git a/chrono/data/vsg/imgui.ini b/chrono/data/vsg/imgui.ini index 1319453..2f804f6 100644 --- a/chrono/data/vsg/imgui.ini +++ b/chrono/data/vsg/imgui.ini @@ -8,5 +8,6 @@ Size=531,121 [Window][Simulation] Pos=5,5 -Size=238,362 +Size=349,458 +Collapsed=1 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 36004ac..3d6a5e9 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,43 +1,43 @@ services: - sim: - build: - context: .. - dockerfile: docker/Dockerfile - target: sim - network: host - args: - VNC_PORT: "${VNC_PORT:?VNC_PORT must be set, see docker/.env}" - NOVNC_PORT: "${NOVNC_PORT:?NOVNC_PORT must be set, see docker/.env}" - ROSBRIDGE_PORT: "${ROSBRIDGE_PORT:?ROSBRIDGE_PORT must be set, see docker/.env}" - image: simulations:latest - container_name: simulations - user: trickfire + sim: + build: + context: .. + dockerfile: docker/Dockerfile + target: sim + network: host + args: + VNC_PORT: "${VNC_PORT:?VNC_PORT must be set, see docker/.env}" + NOVNC_PORT: "${NOVNC_PORT:?NOVNC_PORT must be set, see docker/.env}" + ROSBRIDGE_PORT: "${ROSBRIDGE_PORT:?ROSBRIDGE_PORT must be set, see docker/.env}" + image: simulations:latest + container_name: simulations + user: trickfire - environment: - DISPLAY: "${DISPLAY:-:0}" - VNC_PORT: "${VNC_PORT:?VNC_PORT must be set, see docker/.env}" - NOVNC_PORT: "${NOVNC_PORT:?NOVNC_PORT must be set, see docker/.env}" - ROSBRIDGE_PORT: "${ROSBRIDGE_PORT:?ROSBRIDGE_PORT must be set, see docker/.env}" - QT_X11_NO_MITSHM: "1" - TZ: ${TZ:-UTC} + environment: + DISPLAY: "${DISPLAY:-:0}" + VNC_PORT: "${VNC_PORT:?VNC_PORT must be set, see docker/.env}" + NOVNC_PORT: "${NOVNC_PORT:?NOVNC_PORT must be set, see docker/.env}" + ROSBRIDGE_PORT: "${ROSBRIDGE_PORT:?ROSBRIDGE_PORT must be set, see docker/.env}" + QT_X11_NO_MITSHM: "1" + TZ: ${TZ:-UTC} - volumes: - - ..:/home/trickfire/simulations - - /lib/modules:/lib/modules:ro - - type: bind - source: ${XDG_RUNTIME_DIR:-/run/user/1000} - target: /run/user/1000 - bind: - create_host_path: true + volumes: + - ..:/home/trickfire/simulations + - /lib/modules:/lib/modules:ro + - type: bind + source: ${XDG_RUNTIME_DIR:-/run/user/1000} + target: /run/user/1000 + bind: + create_host_path: true - working_dir: /home/trickfire/simulations + working_dir: /home/trickfire/simulations - ports: - - "${VNC_PORT}:${VNC_PORT}" - - "${NOVNC_PORT}:${NOVNC_PORT}" - - "${ROSBRIDGE_PORT}:${ROSBRIDGE_PORT}" + ports: + - "${VNC_PORT}:${VNC_PORT}" + - "${NOVNC_PORT}:${NOVNC_PORT}" + - "${ROSBRIDGE_PORT}:${ROSBRIDGE_PORT}" - privileged: true + privileged: true - stdin_open: true - tty: true + stdin_open: true + tty: true From 286409bbf5c956693a1aeaf8d28c009ed7af9e71 Mon Sep 17 00:00:00 2001 From: CPrutean Date: Wed, 5 Aug 2026 14:04:01 -0700 Subject: [PATCH 06/38] feat: moved from a docker compose centric dev container to a devcontainer cli centric dev container --- .devcontainer/detect-host-gui-env.sh | 60 +++++++++++++++++++++++++ .devcontainer/devcontainer.json | 5 +++ .devcontainer/macos/devcontainer.json | 3 ++ .devcontainer/nvidia/devcontainer.json | 3 ++ .devcontainer/windows/devcontainer.json | 3 ++ .gitignore | 2 + docker/docker-compose.yml | 10 ++++- docs/setup/docker.mdx | 14 +++--- 8 files changed, 93 insertions(+), 7 deletions(-) create mode 100755 .devcontainer/detect-host-gui-env.sh diff --git a/.devcontainer/detect-host-gui-env.sh b/.devcontainer/detect-host-gui-env.sh new file mode 100755 index 0000000..e497e9b --- /dev/null +++ b/.devcontainer/detect-host-gui-env.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Host-side GUI environment detection. +# +# Run by the BASE devcontainer's `initializeCommand` on the host, before the +# container is (re)created — on both `devcontainer up` and VS Code's "Reopen in +# Container". It detects the host OS and writes docker/.env.host, which the base +# compose loads via `env_file`. That injects the platform-appropriate DISPLAY +# (and VirtualGL settings) into the container, so GUI forwarding works out of the +# box without hand-picking a macos/windows compose variant. +# +# This file is generated and host-specific — it is gitignored, never committed. +set -eu + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" +repo_dir="$(cd "$script_dir/.." && pwd)" +out="$repo_dir/docker/.env.host" + +uname_s="$(uname -s 2>/dev/null || echo unknown)" + +# Emit the file: a provenance header line describing the host, followed by env +# assignments (KEY=VALUE, one per line — docker compose env_file format). +emit() { + { + echo "# generated by .devcontainer/detect-host-gui-env.sh — do not edit or commit" + echo "# host: $1" + shift + printf '%s\n' "$@" + } >"$out" +} + +case "$uname_s" in +Darwin) + # macOS: Docker Desktop's Linux VM can't bind-mount the host's X11/Wayland + # sockets, so forward over the X11 protocol's TCP transport to XQuartz via + # host.docker.internal, and render through VirtualGL (XQuartz's indirect + # GLX can't give OGRE2 a usable GL context). See docker-compose-macos.yml. + emit "macOS (XQuartz over TCP + VirtualGL)" \ + "DISPLAY=host.docker.internal:0" \ + "VGL_DISPLAY=:88" \ + "VGL_COMPRESS=proxy" + ;; +MINGW* | MSYS* | CYGWIN*) + # Native Windows (Git Bash / MSYS): same TCP forwarding to a Windows X + # server (VcXsrv/X410). WSL2/WSLg reports as Linux below and uses sockets. + emit "Windows (VcXsrv/X410 over TCP + VirtualGL)" \ + "DISPLAY=host.docker.internal:0" \ + "VGL_DISPLAY=:88" \ + "VGL_COMPRESS=proxy" + ;; +Linux) + # Native Linux or WSL2/WSLg: docker-compose-dev.yml bind-mounts the host's + # X11/Wayland sockets, so pass the host DISPLAY straight through (:0 default). + emit "Linux/WSLg (direct socket passthrough)" \ + "DISPLAY=${DISPLAY:-:0}" + ;; +*) + emit "unknown ($uname_s) — falling back to local display :0" \ + "DISPLAY=${DISPLAY:-:0}" + ;; +esac diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 25f977e..3f503d5 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -8,6 +8,11 @@ "remoteEnv": { "HOST_WORKSPACE": "${localWorkspaceFolder}" }, + // Runs on the host before the container starts: detects the OS and writes + // docker/.env.host so GUI forwarding (XQuartz/VcXsrv/Wayland) is auto-configured + // without hand-picking a macos/windows variant. `|| true` so a hiccup here never + // blocks container creation. + "initializeCommand": "bash .devcontainer/detect-host-gui-env.sh || true", "postCreateCommand": "pip3 install --break-system-packages -e .", // install the sim cli "postStartCommand": "bash ./.devcontainer/x_server.sh || true", diff --git a/.devcontainer/macos/devcontainer.json b/.devcontainer/macos/devcontainer.json index 332a55e..315bd7c 100644 --- a/.devcontainer/macos/devcontainer.json +++ b/.devcontainer/macos/devcontainer.json @@ -10,6 +10,9 @@ "remoteEnv": { "HOST_WORKSPACE": "${localWorkspaceFolder}" }, + // Generate docker/.env.host on the host so DISPLAY/VirtualGL are set (base compose + // reads it via env_file). `|| true` so a hiccup never blocks container creation. + "initializeCommand": "bash .devcontainer/detect-host-gui-env.sh || true", "postCreateCommand": "pip3 install --break-system-packages -e .", // install the sim cli "postStartCommand": "bash ./.devcontainer/x_server.sh || true", diff --git a/.devcontainer/nvidia/devcontainer.json b/.devcontainer/nvidia/devcontainer.json index 9497a36..70ac472 100644 --- a/.devcontainer/nvidia/devcontainer.json +++ b/.devcontainer/nvidia/devcontainer.json @@ -10,6 +10,9 @@ "remoteEnv": { "HOST_WORKSPACE": "${localWorkspaceFolder}" }, + // Generate docker/.env.host on the host so DISPLAY/VirtualGL are set (base compose + // reads it via env_file). `|| true` so a hiccup never blocks container creation. + "initializeCommand": "bash .devcontainer/detect-host-gui-env.sh || true", "postCreateCommand": "pip3 install --break-system-packages -e .", // install the sim cli "postStartCommand": "bash ./.devcontainer/x_server.sh || true", diff --git a/.devcontainer/windows/devcontainer.json b/.devcontainer/windows/devcontainer.json index 2daeb98..ea5723b 100644 --- a/.devcontainer/windows/devcontainer.json +++ b/.devcontainer/windows/devcontainer.json @@ -10,6 +10,9 @@ "remoteEnv": { "HOST_WORKSPACE": "${localWorkspaceFolder}" }, + // Generate docker/.env.host on the host so DISPLAY/VirtualGL are set (base compose + // reads it via env_file). `|| true` so a hiccup never blocks container creation. + "initializeCommand": "bash .devcontainer/detect-host-gui-env.sh || true", "postCreateCommand": "pip3 install --break-system-packages -e .", // install the sim cli "postStartCommand": "bash ./.devcontainer/x_server.sh || true", diff --git a/.gitignore b/.gitignore index aa2688e..c703927 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,8 @@ chrono/results/* # env **/*.env !docker/.env +# host-detected GUI env, generated by .devcontainer/detect-host-gui-env.sh +docker/.env.host # trickfire-docs .trickfire-docs/ diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 3d6a5e9..ab90a61 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -13,8 +13,16 @@ services: container_name: simulations user: trickfire + # Host-detected GUI env (DISPLAY, VirtualGL). Generated on the host by + # .devcontainer/detect-host-gui-env.sh (base devcontainer initializeCommand), + # so `devcontainer up` forwards the right display per-platform without + # picking a macos/windows variant. required:false: fine if absent (e.g. the + # explicit macos/windows overlays set DISPLAY themselves and override this). + env_file: + - path: .env.host + required: false + environment: - DISPLAY: "${DISPLAY:-:0}" VNC_PORT: "${VNC_PORT:?VNC_PORT must be set, see docker/.env}" NOVNC_PORT: "${NOVNC_PORT:?NOVNC_PORT must be set, see docker/.env}" ROSBRIDGE_PORT: "${ROSBRIDGE_PORT:?ROSBRIDGE_PORT must be set, see docker/.env}" diff --git a/docs/setup/docker.mdx b/docs/setup/docker.mdx index 39659ea..2a7c806 100644 --- a/docs/setup/docker.mdx +++ b/docs/setup/docker.mdx @@ -7,15 +7,17 @@ With this approcach everything runs inside a Docker container with everything de ## 1. Build the devcontainer -There's a base devcontainer config plus a couple of variants for GUI forwarding on hosts that can't share a Linux display socket directly (see [Check if display works](#2-check-if-display-works) below to figure out which one you need): +The **default config auto-detects your host** and configures GUI forwarding for you — no variant picking needed. Before the container starts, the base devcontainer's `initializeCommand` runs `.devcontainer/detect-host-gui-env.sh`, which detects your OS and writes `docker/.env.host` (loaded by the compose `env_file`) so the right display is forwarded: -- **Linux (Wayland or X11) and WSL2** → default config, no changes needed. -- **macOS with XQuartz** → `macos` config. -- **Windows without WSL2/WSLg** (Hyper-V backend, or opening the repo from a native Windows path) → `windows` config. +- **Linux (Wayland or X11) and WSL2/WSLg** → host display socket, passed straight through. +- **macOS with XQuartz** → forwarded over TCP to XQuartz via `host.docker.internal`, rendered with VirtualGL. +- **Windows without WSL2/WSLg** (Hyper-V backend, or a native Windows path) → forwarded over TCP to VcXsrv/X410. + +So on every platform you just use the default config below. The `macos` and `windows` config variants still exist as explicit overrides if auto-detection ever gets it wrong; use the `nvidia` variant to opt into GPU acceleration (see [nvidia setup](/setup/nvidia)). ### VSCode -Open the cloned folder in VSCode. You should see a prompt to **Reopen in Container** in your bottom right. To pick a variant, open the Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`) and run **Dev Containers: Reopen in Container**, then select **macos** or **windows** from the configuration picker instead of the default. If the prompt doesn't appear, make sure you have the [extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed. +Open the cloned folder in VSCode. You should see a prompt to **Reopen in Container** in your bottom right — accept it, or run **Dev Containers: Reopen in Container** from the Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`) and pick the default config. If the prompt doesn't appear, make sure you have the [extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed. ### CLI @@ -23,7 +25,7 @@ Open the cloned folder in VSCode. You should see a prompt to **Reopen in Contain devcontainer up ``` -Or, for a variant: +To force a specific variant (rarely needed — only if auto-detection is wrong, or for GPU): ```bash title="Terminal" devcontainer up --config .devcontainer/macos/devcontainer.json From 93875d139a719ffed3761236a94d38fa7dd2557e Mon Sep 17 00:00:00 2001 From: CPrutean Date: Wed, 5 Aug 2026 14:25:17 -0700 Subject: [PATCH 07/38] fix: fixed some issues in chrono workflow --- chrono/Makefile | 184 ++++++++++++++++++++++++++++++++++++++++--- cli/chrono/chrono.py | 4 +- 2 files changed, 178 insertions(+), 10 deletions(-) diff --git a/chrono/Makefile b/chrono/Makefile index 4547346..3e02ffe 100644 --- a/chrono/Makefile +++ b/chrono/Makefile @@ -1,15 +1,181 @@ -BUILD_DIR := ./build +# CMAKE generated file: DO NOT EDIT! +# Generated by "Unix Makefiles" Generator, CMake Version 3.28 -.PHONY: all run clean +# Default target executed when no arguments are given to make. +default_target: all +.PHONY : default_target -all: $(BUILD_DIR)/sim +# Allow only one "make -f Makefile2" at a time, but pass parallelism. +.NOTPARALLEL: -$(BUILD_DIR)/sim: ./CMakeLists.txt ./main.cpp - cmake -GNinja -B $(BUILD_DIR) -S . -DCMAKE_BUILD_TYPE=Release -DCMAKE_EXPORT_COMPILE_COMMANDS=ON - cmake --build $(BUILD_DIR) +#============================================================================= +# Special targets provided by cmake. -run: $(BUILD_DIR)/sim - cd $(BUILD_DIR) && ./sim +# Disable implicit rules so canonical targets will work. +.SUFFIXES: +# Disable VCS-based implicit rules. +% : %,v + +# Disable VCS-based implicit rules. +% : RCS/% + +# Disable VCS-based implicit rules. +% : RCS/%,v + +# Disable VCS-based implicit rules. +% : SCCS/s.% + +# Disable VCS-based implicit rules. +% : s.% + +.SUFFIXES: .hpux_make_needs_suffix_list + +# Command-line flag to silence nested $(MAKE). +$(VERBOSE)MAKESILENT = -s + +#Suppress display of executed commands. +$(VERBOSE).SILENT: + +# A target that is always out of date. +cmake_force: +.PHONY : cmake_force + +#============================================================================= +# Set environment variables for the build. + +# The shell in which to execute make rules. +SHELL = /bin/sh + +# The CMake executable. +CMAKE_COMMAND = /usr/bin/cmake + +# The command to remove a file. +RM = /usr/bin/cmake -E rm -f + +# Escaping for special characters. +EQUALS = = + +# The top-level source directory on which CMake was run. +CMAKE_SOURCE_DIR = /home/trickfire/simulations/chrono + +# The top-level build directory on which CMake was run. +CMAKE_BINARY_DIR = /home/trickfire/simulations/chrono + +#============================================================================= +# Targets provided globally by CMake. + +# Special rule for the target edit_cache +edit_cache: + @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "No interactive CMake dialog available..." + /usr/bin/cmake -E echo No\ interactive\ CMake\ dialog\ available. +.PHONY : edit_cache + +# Special rule for the target edit_cache +edit_cache/fast: edit_cache +.PHONY : edit_cache/fast + +# Special rule for the target rebuild_cache +rebuild_cache: + @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Running CMake to regenerate build system..." + /usr/bin/cmake --regenerate-during-build -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) +.PHONY : rebuild_cache + +# Special rule for the target rebuild_cache +rebuild_cache/fast: rebuild_cache +.PHONY : rebuild_cache/fast + +# The main all target +all: cmake_check_build_system + $(CMAKE_COMMAND) -E cmake_progress_start /home/trickfire/simulations/chrono/CMakeFiles /home/trickfire/simulations/chrono//CMakeFiles/progress.marks + $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 all + $(CMAKE_COMMAND) -E cmake_progress_start /home/trickfire/simulations/chrono/CMakeFiles 0 +.PHONY : all + +# The main clean target clean: - rm -rf $(BUILD_DIR) + $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 clean +.PHONY : clean + +# The main clean target +clean/fast: clean +.PHONY : clean/fast + +# Prepare targets for installation. +preinstall: all + $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 preinstall +.PHONY : preinstall + +# Prepare targets for installation. +preinstall/fast: + $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 preinstall +.PHONY : preinstall/fast + +# clear depends +depend: + $(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-build-system CMakeFiles/Makefile.cmake 1 +.PHONY : depend + +#============================================================================= +# Target rules for targets named sim + +# Build rule for target. +sim: cmake_check_build_system + $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 sim +.PHONY : sim + +# fast build rule for target. +sim/fast: + $(MAKE) $(MAKESILENT) -f CMakeFiles/sim.dir/build.make CMakeFiles/sim.dir/build +.PHONY : sim/fast + +main.o: main.cpp.o +.PHONY : main.o + +# target to build an object file +main.cpp.o: + $(MAKE) $(MAKESILENT) -f CMakeFiles/sim.dir/build.make CMakeFiles/sim.dir/main.cpp.o +.PHONY : main.cpp.o + +main.i: main.cpp.i +.PHONY : main.i + +# target to preprocess a source file +main.cpp.i: + $(MAKE) $(MAKESILENT) -f CMakeFiles/sim.dir/build.make CMakeFiles/sim.dir/main.cpp.i +.PHONY : main.cpp.i + +main.s: main.cpp.s +.PHONY : main.s + +# target to generate assembly for a file +main.cpp.s: + $(MAKE) $(MAKESILENT) -f CMakeFiles/sim.dir/build.make CMakeFiles/sim.dir/main.cpp.s +.PHONY : main.cpp.s + +# Help Target +help: + @echo "The following are some of the valid targets for this Makefile:" + @echo "... all (the default if no target is provided)" + @echo "... clean" + @echo "... depend" + @echo "... edit_cache" + @echo "... rebuild_cache" + @echo "... sim" + @echo "... main.o" + @echo "... main.i" + @echo "... main.s" +.PHONY : help + + + +#============================================================================= +# Special targets to cleanup operation of make. + +# Special rule to run CMake to check the build system integrity. +# No rule that depends on this can have commands that come from listfiles +# because they might be regenerated. +cmake_check_build_system: + $(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-build-system CMakeFiles/Makefile.cmake 0 +.PHONY : cmake_check_build_system + diff --git a/cli/chrono/chrono.py b/cli/chrono/chrono.py index 5e8bc37..ff7c4a7 100644 --- a/cli/chrono/chrono.py +++ b/cli/chrono/chrono.py @@ -7,7 +7,9 @@ def run(): - result = subprocess.run(["make", "run"], cwd=CHRONO_TERRAIN_DIR, check=False) + result = subprocess.run(["make"], cwd=CHRONO_TERRAIN_DIR, check=False, stdout=open("/dev/null", 'w')) + result = subprocess.run(["./sim"], cwd=CHRONO_TERRAIN_DIR, check=False) + if result.returncode != 0: sys.exit(result.returncode) From 5eeaf6ebfc0f3ddd0d0038dd4f7125e24bb3d3fc Mon Sep 17 00:00:00 2001 From: CPrutean Date: Wed, 5 Aug 2026 16:27:34 -0700 Subject: [PATCH 08/38] feat: consolidated devcontainers into one file with overrides --- .devcontainer/detect-host-env.sh | 106 ++++++++++++++++++++++++ .devcontainer/detect-host-gui-env.sh | 60 -------------- .devcontainer/devcontainer.json | 10 +-- .devcontainer/macos/devcontainer.json | 59 ------------- .devcontainer/nvidia/devcontainer.json | 59 ------------- .devcontainer/windows/devcontainer.json | 59 ------------- .devcontainer/x_server.sh | 5 +- .gitignore | 7 +- docker/.env | 10 --- docker/.env.defaults | 14 ++++ docker/docker-compose-gpu.yml | 18 ---- docker/docker-compose-macos.yml | 36 -------- docker/docker-compose-windows.yml | 39 --------- docker/docker-compose.yml | 17 ++-- docs/setup/docker.mdx | 19 ++--- docs/setup/nvidia.mdx | 14 ++-- 16 files changed, 158 insertions(+), 374 deletions(-) create mode 100755 .devcontainer/detect-host-env.sh delete mode 100755 .devcontainer/detect-host-gui-env.sh delete mode 100644 .devcontainer/macos/devcontainer.json delete mode 100644 .devcontainer/nvidia/devcontainer.json delete mode 100644 .devcontainer/windows/devcontainer.json delete mode 100644 docker/.env create mode 100644 docker/.env.defaults delete mode 100644 docker/docker-compose-gpu.yml delete mode 100644 docker/docker-compose-macos.yml delete mode 100644 docker/docker-compose-windows.yml diff --git a/.devcontainer/detect-host-env.sh b/.devcontainer/detect-host-env.sh new file mode 100755 index 0000000..3456ab3 --- /dev/null +++ b/.devcontainer/detect-host-env.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# Host-side environment detection for the (single) devcontainer. +# +# Run by the devcontainer's `initializeCommand` on the host, before the container +# is (re)created — on both `devcontainer up` and VS Code's "Reopen in Container". +# It detects the host OS and GPU and generates two gitignored files the compose +# stack reads, so one devcontainer config works everywhere with no variant to pick: +# +# docker/.env interpolation source (auto-loaded by compose): ports from +# .env.defaults + optional .env.local overrides, plus +# SIM_GPU_RUNTIME which flips `runtime: nvidia` on/off. +# docker/.env.host container env (loaded via compose `env_file`): DISPLAY + +# VirtualGL for GUI forwarding, and the NVIDIA_* vars when a +# usable GPU is present. Conditional vars are simply omitted +# when not applicable (an absent line means unset, not empty — +# which matters for __EGL_VENDOR_LIBRARY_FILENAMES). +# +# Both generated files are host-specific and gitignored — never committed. +set -eu + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" +docker_dir="$(cd "$script_dir/../docker" && pwd)" +env_out="$docker_dir/.env" +host_out="$docker_dir/.env.host" +defaults="$docker_dir/.env.defaults" +overrides="$docker_dir/.env.local" + +uname_s="$(uname -s 2>/dev/null || echo unknown)" + +# Enable the nvidia runtime only when it will actually work: a driver that +# answers (nvidia-smi) AND docker with the nvidia runtime registered. Anything +# short of that stays on the default runtime, so non-GPU hosts never break. +gpu_available() { + [ "$uname_s" = "Linux" ] || return 1 + command -v nvidia-smi >/dev/null 2>&1 || return 1 + nvidia-smi -L >/dev/null 2>&1 || return 1 + docker info --format '{{json .Runtimes}}' 2>/dev/null | grep -q '"nvidia"' || return 1 +} + +gpu_runtime="" +if gpu_available; then + gpu_runtime="nvidia" +fi + +# --- docker/.env: compose interpolation source (ports + GPU runtime) --- +{ + cat "$defaults" + if [ -f "$overrides" ]; then + echo "" + echo "# --- user overrides (docker/.env.local) ---" + cat "$overrides" + fi + echo "" + echo "# --- host-detected (generated, do not edit) ---" + echo "SIM_GPU_RUNTIME=$gpu_runtime" +} >"$env_out" + +# --- docker/.env.host: container env (DISPLAY / VirtualGL / NVIDIA) --- +{ + echo "# generated by .devcontainer/detect-host-env.sh — do not edit or commit" + case "$uname_s" in + Darwin) + # macOS: Docker Desktop's Linux VM can't bind-mount the host's X11/ + # Wayland sockets, so forward over the X11 protocol's TCP transport to + # XQuartz via host.docker.internal. XQuartz's indirect GLX can't give + # OGRE2 (Gazebo/RViz) a usable GL context, so render through VirtualGL + # against a container-local headless X server (VGL_DISPLAY) and ship + # finished frames as plain X11 images (VGL_COMPRESS=proxy). `sim gazebo` + # wraps launches in vglrun. + echo "# host: macOS (XQuartz over TCP + VirtualGL)" + echo "DISPLAY=host.docker.internal:0" + echo "VGL_DISPLAY=:88" + echo "VGL_COMPRESS=proxy" + ;; + MINGW* | MSYS* | CYGWIN*) + # Native Windows (Git Bash / MSYS): same TCP forwarding to a Windows X + # server (VcXsrv/X410), same VirtualGL path as macOS. WSL2/WSLg reports + # as Linux below and uses sockets. Needs bash on PATH (Git for Windows). + echo "# host: Windows (VcXsrv/X410 over TCP + VirtualGL)" + echo "DISPLAY=host.docker.internal:0" + echo "VGL_DISPLAY=:88" + echo "VGL_COMPRESS=proxy" + ;; + Linux) + # Native Linux or WSL2/WSLg: docker-compose-dev.yml bind-mounts the + # host's X11/Wayland sockets, so pass the host DISPLAY through (:0). + echo "# host: Linux/WSLg (direct socket passthrough)" + echo "DISPLAY=${DISPLAY:-:0}" + ;; + *) + echo "# host: unknown ($uname_s) — falling back to local display :0" + echo "DISPLAY=${DISPLAY:-:0}" + ;; + esac + if [ -n "$gpu_runtime" ]; then + # A working nvidia stack was detected; hand the GPU to the container. + # (Only written when present — __EGL_VENDOR_LIBRARY_FILENAMES must be + # absent, not empty, on non-GPU hosts or it breaks software EGL.) + echo "# nvidia GPU detected (runtime: nvidia)" + echo "NVIDIA_VISIBLE_DEVICES=all" + echo "NVIDIA_DRIVER_CAPABILITIES=graphics,display,compute,utility" + echo "__EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/10_nvidia.json" + fi +} >"$host_out" + +echo "[detect-host-env] host=$uname_s gpu=${gpu_runtime:-none} -> wrote docker/.env, docker/.env.host" diff --git a/.devcontainer/detect-host-gui-env.sh b/.devcontainer/detect-host-gui-env.sh deleted file mode 100755 index e497e9b..0000000 --- a/.devcontainer/detect-host-gui-env.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env bash -# Host-side GUI environment detection. -# -# Run by the BASE devcontainer's `initializeCommand` on the host, before the -# container is (re)created — on both `devcontainer up` and VS Code's "Reopen in -# Container". It detects the host OS and writes docker/.env.host, which the base -# compose loads via `env_file`. That injects the platform-appropriate DISPLAY -# (and VirtualGL settings) into the container, so GUI forwarding works out of the -# box without hand-picking a macos/windows compose variant. -# -# This file is generated and host-specific — it is gitignored, never committed. -set -eu - -script_dir="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" -repo_dir="$(cd "$script_dir/.." && pwd)" -out="$repo_dir/docker/.env.host" - -uname_s="$(uname -s 2>/dev/null || echo unknown)" - -# Emit the file: a provenance header line describing the host, followed by env -# assignments (KEY=VALUE, one per line — docker compose env_file format). -emit() { - { - echo "# generated by .devcontainer/detect-host-gui-env.sh — do not edit or commit" - echo "# host: $1" - shift - printf '%s\n' "$@" - } >"$out" -} - -case "$uname_s" in -Darwin) - # macOS: Docker Desktop's Linux VM can't bind-mount the host's X11/Wayland - # sockets, so forward over the X11 protocol's TCP transport to XQuartz via - # host.docker.internal, and render through VirtualGL (XQuartz's indirect - # GLX can't give OGRE2 a usable GL context). See docker-compose-macos.yml. - emit "macOS (XQuartz over TCP + VirtualGL)" \ - "DISPLAY=host.docker.internal:0" \ - "VGL_DISPLAY=:88" \ - "VGL_COMPRESS=proxy" - ;; -MINGW* | MSYS* | CYGWIN*) - # Native Windows (Git Bash / MSYS): same TCP forwarding to a Windows X - # server (VcXsrv/X410). WSL2/WSLg reports as Linux below and uses sockets. - emit "Windows (VcXsrv/X410 over TCP + VirtualGL)" \ - "DISPLAY=host.docker.internal:0" \ - "VGL_DISPLAY=:88" \ - "VGL_COMPRESS=proxy" - ;; -Linux) - # Native Linux or WSL2/WSLg: docker-compose-dev.yml bind-mounts the host's - # X11/Wayland sockets, so pass the host DISPLAY straight through (:0 default). - emit "Linux/WSLg (direct socket passthrough)" \ - "DISPLAY=${DISPLAY:-:0}" - ;; -*) - emit "unknown ($uname_s) — falling back to local display :0" \ - "DISPLAY=${DISPLAY:-:0}" - ;; -esac diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 3f503d5..38cadba 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -8,11 +8,11 @@ "remoteEnv": { "HOST_WORKSPACE": "${localWorkspaceFolder}" }, - // Runs on the host before the container starts: detects the OS and writes - // docker/.env.host so GUI forwarding (XQuartz/VcXsrv/Wayland) is auto-configured - // without hand-picking a macos/windows variant. `|| true` so a hiccup here never - // blocks container creation. - "initializeCommand": "bash .devcontainer/detect-host-gui-env.sh || true", + // Runs on the host before the container starts: detects the OS and GPU and + // generates docker/.env + docker/.env.host, so GUI forwarding (XQuartz/VcXsrv/ + // Wayland) and nvidia GPU are auto-configured for every platform — no per-OS + // config to pick. `|| true` so a hiccup here never blocks container creation. + "initializeCommand": "bash .devcontainer/detect-host-env.sh || true", "postCreateCommand": "pip3 install --break-system-packages -e .", // install the sim cli "postStartCommand": "bash ./.devcontainer/x_server.sh || true", diff --git a/.devcontainer/macos/devcontainer.json b/.devcontainer/macos/devcontainer.json deleted file mode 100644 index 315bd7c..0000000 --- a/.devcontainer/macos/devcontainer.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "dockerComposeFile": [ - "../../docker/docker-compose.yml", - "../../docker/docker-compose-dev.yml", - "../../docker/docker-compose-macos.yml" - ], - "service": "sim", - "runServices": ["sim"], - "workspaceFolder": "/home/trickfire/simulations", - "remoteEnv": { - "HOST_WORKSPACE": "${localWorkspaceFolder}" - }, - // Generate docker/.env.host on the host so DISPLAY/VirtualGL are set (base compose - // reads it via env_file). `|| true` so a hiccup never blocks container creation. - "initializeCommand": "bash .devcontainer/detect-host-gui-env.sh || true", - "postCreateCommand": "pip3 install --break-system-packages -e .", // install the sim cli - "postStartCommand": "bash ./.devcontainer/x_server.sh || true", - - "customizations": { - "vscode": { - "settings": { - // Keep the Activity Bar (and its Explorer icon) visible in the - // container window so the file explorer can't disappear on launch. - "workbench.activityBar.location": "default", - "shfmt.executablePath": "/usr/local/bin/shfmt" - }, - "extensions": [ - // --- Python --- - "ms-python.python", - "ms-python.vscode-pylance", - "charliermarsh.ruff", - "ms-python.mypy-type-checker", - - // --- C and C++ --- - "ms-vscode.cmake-tools", - "josetr.cmake-language-support-vscode", - "llvm-vs-code-extensions.vscode-clangd", - "ms-vscode.makefile-tools", - - // --- Robotics / ROS / URDF / meshes --- - "Ranch-Hand-Robotics.rde-ros-2", - "smilerobotics.urdf", - "morningfrog.urdf-visualizer", - "misiekhardcore.stl-previewer", - - // --- Container --- - "ms-azuretools.vscode-docker", - - // --- Formatters --- - "esbenp.prettier-vscode", - "yzhang.markdown-all-in-one", - "DotJoshJohnson.xml", - "tamasfe.even-better-toml", - "github.vscode-github-actions", - "mkhl.shfmt" - ] - } - } -} diff --git a/.devcontainer/nvidia/devcontainer.json b/.devcontainer/nvidia/devcontainer.json deleted file mode 100644 index 70ac472..0000000 --- a/.devcontainer/nvidia/devcontainer.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "dockerComposeFile": [ - "../../docker/docker-compose.yml", - "../../docker/docker-compose-dev.yml", - "../../docker/docker-compose-gpu.yml" - ], - "service": "sim", - "runServices": ["sim"], - "workspaceFolder": "/home/trickfire/simulations", - "remoteEnv": { - "HOST_WORKSPACE": "${localWorkspaceFolder}" - }, - // Generate docker/.env.host on the host so DISPLAY/VirtualGL are set (base compose - // reads it via env_file). `|| true` so a hiccup never blocks container creation. - "initializeCommand": "bash .devcontainer/detect-host-gui-env.sh || true", - "postCreateCommand": "pip3 install --break-system-packages -e .", // install the sim cli - "postStartCommand": "bash ./.devcontainer/x_server.sh || true", - - "customizations": { - "vscode": { - "settings": { - // Keep the Activity Bar (and its Explorer icon) visible in the - // container window so the file explorer can't disappear on launch. - "workbench.activityBar.location": "default", - "shfmt.executablePath": "/usr/local/bin/shfmt" - }, - "extensions": [ - // --- Python --- - "ms-python.python", - "ms-python.vscode-pylance", - "charliermarsh.ruff", - "ms-python.mypy-type-checker", - - // --- C and C++ --- - "ms-vscode.cmake-tools", - "josetr.cmake-language-support-vscode", - "llvm-vs-code-extensions.vscode-clangd", - "ms-vscode.makefile-tools", - - // --- Robotics / ROS / URDF / meshes --- - "Ranch-Hand-Robotics.rde-ros-2", - "smilerobotics.urdf", - "morningfrog.urdf-visualizer", - "misiekhardcore.stl-previewer", - - // --- Container --- - "ms-azuretools.vscode-docker", - - // --- Formatters --- - "esbenp.prettier-vscode", - "yzhang.markdown-all-in-one", - "DotJoshJohnson.xml", - "tamasfe.even-better-toml", - "github.vscode-github-actions", - "mkhl.shfmt" - ] - } - } -} diff --git a/.devcontainer/windows/devcontainer.json b/.devcontainer/windows/devcontainer.json deleted file mode 100644 index ea5723b..0000000 --- a/.devcontainer/windows/devcontainer.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "dockerComposeFile": [ - "../../docker/docker-compose.yml", - "../../docker/docker-compose-dev.yml", - "../../docker/docker-compose-windows.yml" - ], - "service": "sim", - "runServices": ["sim"], - "workspaceFolder": "/home/trickfire/simulations", - "remoteEnv": { - "HOST_WORKSPACE": "${localWorkspaceFolder}" - }, - // Generate docker/.env.host on the host so DISPLAY/VirtualGL are set (base compose - // reads it via env_file). `|| true` so a hiccup never blocks container creation. - "initializeCommand": "bash .devcontainer/detect-host-gui-env.sh || true", - "postCreateCommand": "pip3 install --break-system-packages -e .", // install the sim cli - "postStartCommand": "bash ./.devcontainer/x_server.sh || true", - - "customizations": { - "vscode": { - "settings": { - // Keep the Activity Bar (and its Explorer icon) visible in the - // container window so the file explorer can't disappear on launch. - "workbench.activityBar.location": "default", - "shfmt.executablePath": "/usr/local/bin/shfmt" - }, - "extensions": [ - // --- Python --- - "ms-python.python", - "ms-python.vscode-pylance", - "charliermarsh.ruff", - "ms-python.mypy-type-checker", - - // --- C and C++ --- - "ms-vscode.cmake-tools", - "josetr.cmake-language-support-vscode", - "llvm-vs-code-extensions.vscode-clangd", - "ms-vscode.makefile-tools", - - // --- Robotics / ROS / URDF / meshes --- - "Ranch-Hand-Robotics.rde-ros-2", - "smilerobotics.urdf", - "morningfrog.urdf-visualizer", - "misiekhardcore.stl-previewer", - - // --- Container --- - "ms-azuretools.vscode-docker", - - // --- Formatters --- - "esbenp.prettier-vscode", - "yzhang.markdown-all-in-one", - "DotJoshJohnson.xml", - "tamasfe.even-better-toml", - "github.vscode-github-actions", - "mkhl.shfmt" - ] - } - } -} diff --git a/.devcontainer/x_server.sh b/.devcontainer/x_server.sh index d6dbf4b..8942ac0 100755 --- a/.devcontainer/x_server.sh +++ b/.devcontainer/x_server.sh @@ -121,9 +121,8 @@ try_display_passthrough() { # - native Linux X11 (host's /tmp/.X11-unix bind-mounted, DISPLAY inherited from the host) # - WSL2 with WSLg's X11 socket # - macOS + XQuartz reachable over TCP at host.docker.internal:0 - # (see docker/docker-compose-macos.yml / .devcontainer/macos) # - Windows + VcXsrv/X410 reachable over TCP at host.docker.internal:0 - # (see docker/docker-compose-windows.yml / .devcontainer/windows) + # (both set by .devcontainer/detect-host-env.sh -> docker/.env.host) if [ -n "$DISPLAY" ] && xdpyinfo -display "$DISPLAY" &>/dev/null; then log "[X11] Using host X11 display at $DISPLAY" start_vgl_3d_server @@ -222,7 +221,7 @@ main() { # try_display_passthrough only returns (rather than exiting) when no host display was # usable, so it's safe to claim $DISPLAY for our own Xvfb/Xorg below - we just confirmed # nothing answers on it. However $DISPLAY may hold a remote/TCP spec like - # "host.docker.internal:0" (macOS/Windows configs, when XQuartz/VcXsrv wasn't reachable) - + # "host.docker.internal:0" (macOS/Windows hosts, when XQuartz/VcXsrv wasn't reachable) - # that's not a valid local display for Xvfb/Xorg to bind to, so normalize it to a plain # local display number first. Valid local specs always start with ':'. if [[ $DISPLAY != :* ]]; then diff --git a/.gitignore b/.gitignore index c703927..813f0e2 100644 --- a/.gitignore +++ b/.gitignore @@ -31,9 +31,12 @@ chrono/results/* # env **/*.env -!docker/.env -# host-detected GUI env, generated by .devcontainer/detect-host-gui-env.sh +# docker/.env is generated each `devcontainer up` by .devcontainer/detect-host-env.sh +# (from docker/.env.defaults + docker/.env.local + host detection). docker/.env.defaults +# is the committed source of truth; docker/.env.local holds per-machine overrides. +docker/.env docker/.env.host +docker/.env.local # trickfire-docs .trickfire-docs/ diff --git a/docker/.env b/docker/.env deleted file mode 100644 index 4b31831..0000000 --- a/docker/.env +++ /dev/null @@ -1,10 +0,0 @@ -# main source of truth -# for docker exposed ports - -VNC_PORT=5900 -NOVNC_PORT=6080 -ROSBRIDGE_PORT=9090 - -# set to one if you want -# to force vnc startup -# FORCE_VNC=1 diff --git a/docker/.env.defaults b/docker/.env.defaults new file mode 100644 index 0000000..c056411 --- /dev/null +++ b/docker/.env.defaults @@ -0,0 +1,14 @@ +# Committed defaults for docker exposed ports (and optional flags). +# +# This file is the source of truth. On `devcontainer up`, .devcontainer/detect-host-env.sh +# generates the real (gitignored) docker/.env from: these defaults + your optional +# docker/.env.local overrides + host-detected values (DISPLAY/GPU). To change ports or +# force VNC on YOUR machine, create docker/.env.local with the keys you want to override +# — do not edit this file (it's shared) or docker/.env (it's regenerated each up). + +VNC_PORT=5900 +NOVNC_PORT=6080 +ROSBRIDGE_PORT=9090 + +# set to 1 in docker/.env.local if you want to force vnc startup +# FORCE_VNC=1 diff --git a/docker/docker-compose-gpu.yml b/docker/docker-compose-gpu.yml deleted file mode 100644 index 5876f48..0000000 --- a/docker/docker-compose-gpu.yml +++ /dev/null @@ -1,18 +0,0 @@ -# nvidia gpu acceleration, addon compose for base compose - -services: - sim: - runtime: nvidia - - environment: - NVIDIA_VISIBLE_DEVICES: all - NVIDIA_DRIVER_CAPABILITIES: graphics,display,compute,utility - __EGL_VENDOR_LIBRARY_FILENAMES: /usr/share/glvnd/egl_vendor.d/10_nvidia.json - - volumes: - - /tmp/.X11-unix:/tmp/.X11-unix - - command: >- - bash -lc 'set -e; - python3 -m venv .venv && source .venv/bin/activate && pip install -e . && \ - exec sleep infinity' diff --git a/docker/docker-compose-macos.yml b/docker/docker-compose-macos.yml deleted file mode 100644 index 9058ba4..0000000 --- a/docker/docker-compose-macos.yml +++ /dev/null @@ -1,36 +0,0 @@ -# macOS + XQuartz addon compose for base compose. -# -# Docker Desktop for Mac runs containers inside a Linux VM and cannot bind-mount the host's -# X11/Wayland unix sockets into the container (Docker Desktop only special-cases a handful of -# sockets like docker.sock and the ssh-agent socket - arbitrary sockets, including XQuartz's, -# aren't shared through its virtiofs/gRPC-FUSE file sharing). So instead of a socket bind mount, -# GUI windows are forwarded over the X11 protocol's native TCP transport to XQuartz, which Docker -# Desktop's "host.docker.internal" DNS name makes reachable from inside the container. -# -# Setup required on the Mac host (see docs/setup/docker.mdx for details): -# 1. Install XQuartz: https://www.xquartz.org -# 2. XQuartz > Settings > Security > check "Allow connections from network clients", then -# log out/restart XQuartz for it to take effect. -# 3. In a Terminal: `xhost + 127.0.0.1` -# -# Use this file instead of docker-compose-gpu.yml (there is no GPU passthrough on macOS). -# -# XQuartz can display windows but cannot provide a usable OpenGL context - its indirect GLX -# is deprecated and broken (XQuartz/XQuartz#144), so Gazebo/RViz (OGRE2) can't render through -# it directly. GL rendering therefore goes through VirtualGL against a local headless X server -# inside the container, with only finished frames sent to XQuartz as plain X11 images. -# x_server.sh starts that 3D X server; `sim gazebo` wraps the launch in `vglrun`. - -services: - sim: - environment: - # XQuartz's default display is :0. QT_X11_NO_MITSHM is already set to 1 in the base - # compose file, which is required here too: MIT-SHM assumes a local shared-memory - # segment, which doesn't exist across this TCP connection. - DISPLAY: "host.docker.internal:0" - # Display number of the container-local X server VirtualGL renders into (started by - # .devcontainer/x_server.sh). Kept clear of :0 and of FORCE_VNC's :77. - VGL_DISPLAY: ":88" - # VirtualGL's X11 Transport: deliver rendered frames as ordinary X11 images over the - # existing connection. Avoids needing a vglclient process on the Mac or an extra port. - VGL_COMPRESS: "proxy" diff --git a/docker/docker-compose-windows.yml b/docker/docker-compose-windows.yml deleted file mode 100644 index d8022c9..0000000 --- a/docker/docker-compose-windows.yml +++ /dev/null @@ -1,39 +0,0 @@ -# Windows (native, no WSL2/WSLg) addon compose for base compose. -# -# If Docker Desktop is using the WSL2 backend and you open this repo from inside a WSL2 distro, -# you don't need this file - WSLg already forwards Wayland/X11 through the same -# docker-compose-dev.yml socket bind mounts used on native Linux, so use the default -# .devcontainer/devcontainer.json instead. -# -# This file is for the remaining case: Docker Desktop's Hyper-V backend, or VS Code connecting -# from a native (non-WSL) Windows filesystem path. There, the container can't reach any host -# Wayland/X11 socket directly, so GUI windows are forwarded over the X11 protocol's native TCP -# transport to a Windows-hosted X server, reachable via Docker Desktop's "host.docker.internal" -# DNS name. -# -# Setup required on the Windows host (see docs/setup/docker.mdx for details): -# 1. Install an X server that supports TCP, e.g. VcXsrv (https://sourceforge.net/projects/vcxsrv) -# or X410 (Microsoft Store). -# 2. Launch it with network access enabled and access control disabled, e.g. for VcXsrv's -# XLaunch: "Multiple windows", display number 0, check "Disable access control". -# 3. Allow the X server through Windows Defender Firewall when prompted (Private networks). -# -# Use this file instead of docker-compose-gpu.yml (there is no GPU passthrough here). -# -# VcXsrv/X410 can display windows but their indirect GLX (IGLX) can't give Gazebo/RViz (OGRE2) -# a usable OpenGL context - the same architectural dead end confirmed by hand on macOS/XQuartz. -# GL rendering therefore goes through VirtualGL against a local headless X server inside the -# container, with only finished frames sent to the Windows X server as plain X11 images. -# x_server.sh starts that 3D X server; `sim gazebo` wraps the launch in `vglrun`. - -services: - sim: - environment: - # Matches the ":0" / display-number-0 default used by VcXsrv's XLaunch and X410. - DISPLAY: "host.docker.internal:0" - # Display number of the container-local X server VirtualGL renders into (started by - # .devcontainer/x_server.sh). Kept clear of :0 and of FORCE_VNC's :77. - VGL_DISPLAY: ":88" - # VirtualGL's X11 Transport: deliver rendered frames as ordinary X11 images over the - # existing connection. Avoids needing a vglclient process on Windows or an extra port. - VGL_COMPRESS: "proxy" diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index ab90a61..85d0db0 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -13,11 +13,18 @@ services: container_name: simulations user: trickfire - # Host-detected GUI env (DISPLAY, VirtualGL). Generated on the host by - # .devcontainer/detect-host-gui-env.sh (base devcontainer initializeCommand), - # so `devcontainer up` forwards the right display per-platform without - # picking a macos/windows variant. required:false: fine if absent (e.g. the - # explicit macos/windows overlays set DISPLAY themselves and override this). + # nvidia runtime, enabled only when the host has a working GPU. Detected on + # the host by .devcontainer/detect-host-env.sh, which writes SIM_GPU_RUNTIME + # into docker/.env (empty on non-GPU hosts, where compose omits `runtime` + # entirely and falls back to the default runtime). + runtime: "${SIM_GPU_RUNTIME:-}" + + # Host-detected container env (DISPLAY + VirtualGL, and NVIDIA_* when a GPU + # is present). Generated on the host into docker/.env.host by + # .devcontainer/detect-host-env.sh (initializeCommand), so one config + # forwards the right display per-platform with no variant to pick. + # required:false: harmless if absent (e.g. raw `docker compose` outside the + # devcontainer, which doesn't run initializeCommand). env_file: - path: .env.host required: false diff --git a/docs/setup/docker.mdx b/docs/setup/docker.mdx index 2a7c806..a836aeb 100644 --- a/docs/setup/docker.mdx +++ b/docs/setup/docker.mdx @@ -7,17 +7,18 @@ With this approcach everything runs inside a Docker container with everything de ## 1. Build the devcontainer -The **default config auto-detects your host** and configures GUI forwarding for you — no variant picking needed. Before the container starts, the base devcontainer's `initializeCommand` runs `.devcontainer/detect-host-gui-env.sh`, which detects your OS and writes `docker/.env.host` (loaded by the compose `env_file`) so the right display is forwarded: +There is **one devcontainer config, and it auto-detects your host** — no variant to pick. Before the container starts, its `initializeCommand` runs `.devcontainer/detect-host-env.sh`, which detects your OS and GPU and generates `docker/.env` + `docker/.env.host` (loaded by compose) so GUI forwarding *and* GPU acceleration are configured for you: - **Linux (Wayland or X11) and WSL2/WSLg** → host display socket, passed straight through. - **macOS with XQuartz** → forwarded over TCP to XQuartz via `host.docker.internal`, rendered with VirtualGL. - **Windows without WSL2/WSLg** (Hyper-V backend, or a native Windows path) → forwarded over TCP to VcXsrv/X410. +- **NVIDIA GPU** (Linux, with the [nvidia container toolkit](/setup/nvidia) installed) → detected automatically and handed to the container; otherwise the default runtime is used. -So on every platform you just use the default config below. The `macos` and `windows` config variants still exist as explicit overrides if auto-detection ever gets it wrong; use the `nvidia` variant to opt into GPU acceleration (see [nvidia setup](/setup/nvidia)). +So on every platform you just use the single config below. ### VSCode -Open the cloned folder in VSCode. You should see a prompt to **Reopen in Container** in your bottom right — accept it, or run **Dev Containers: Reopen in Container** from the Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`) and pick the default config. If the prompt doesn't appear, make sure you have the [extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed. +Open the cloned folder in VSCode. You should see a prompt to **Reopen in Container** in your bottom right — accept it, or run **Dev Containers: Reopen in Container** from the Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`). If the prompt doesn't appear, make sure you have the [extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed. ### CLI @@ -25,11 +26,9 @@ Open the cloned folder in VSCode. You should see a prompt to **Reopen in Contain devcontainer up ``` -To force a specific variant (rarely needed — only if auto-detection is wrong, or for GPU): - -```bash title="Terminal" -devcontainer up --config .devcontainer/macos/devcontainer.json -``` + + Ports and flags are defined in `docker/.env.defaults` (committed). To change them on your machine, put the keys you want in `docker/.env.local` (gitignored) — e.g. `FORCE_VNC=1`. The real `docker/.env` is regenerated from the defaults, your `.env.local`, and host detection on every `devcontainer up`, so don't edit it directly. + This will take a long time if it is your first time (30 mins)! Docker builds Chrono and its dependencies (many lines of C++) from source, and then the same for Gazebo and ROS! Subsequent launches will be very fast though. @@ -65,7 +64,7 @@ WSL2 includes WSLg, a built-in Wayland compositor with X11 forwarding. Gazebo an ### macOS with XQuartz (no VNC needed) -Make sure you built the `macos` devcontainer variant (see [step 1](#1-build-the-devcontainer)). GUI windows are forwarded over the X11 protocol to [XQuartz](https://www.xquartz.org) instead of VNC: +On macOS the container is auto-configured for XQuartz (see [step 1](#1-build-the-devcontainer)). GUI windows are forwarded over the X11 protocol to [XQuartz](https://www.xquartz.org) instead of VNC: 1. Install XQuartz if you don't already have it: `brew install --cask xquartz`, or download it from [xquartz.org](https://www.xquartz.org). 2. Open XQuartz, go to **XQuartz > Settings > Security**, and check **"Allow connections from network clients"**. @@ -98,7 +97,7 @@ If the display *is* reachable but `sim gazebo launch` (or similar) still fails w ### Windows without WSL2/WSLg (no VNC needed) -Make sure you built the `windows` devcontainer variant (see [step 1](#1-build-the-devcontainer)). If you're using WSL2 with Docker Desktop's WSL2 backend, you don't need this - use the default config, which gets WSLg support automatically (see above). +On native Windows the container is auto-configured for a TCP X server (see [step 1](#1-build-the-devcontainer)). If you're using WSL2 with Docker Desktop's WSL2 backend, none of this is needed - detection uses WSLg's socket automatically (see above). Native Windows detection requires bash on PATH (Git for Windows) so the host script can run. 1. Install an X server that supports TCP connections: [VcXsrv](https://sourceforge.net/projects/vcxsrv) (free) or X410 (Microsoft Store, paid). 2. Launch it with network access enabled. For VcXsrv's XLaunch: choose "Multiple windows", display number `0`, then on the "Extra settings" page check **"Disable access control"**. diff --git a/docs/setup/nvidia.mdx b/docs/setup/nvidia.mdx index d683386..388c6fb 100644 --- a/docs/setup/nvidia.mdx +++ b/docs/setup/nvidia.mdx @@ -35,20 +35,16 @@ docker run --rm --runtime=nvidia --gpus all nvidia/cuda:12.0-base-ubuntu22.04 nv You should see your GPU listed in the output. -## 2. Open the NVIDIA devcontainer +## 2. Open the devcontainer -There is a dedicated devcontainer configuration at `.devcontainer/nvidia/` that layers GPU support on top of the base setup. Use it instead of the default one. - -### VSCode - -Open the repository folder in VSCode. When prompted to **Reopen in Container**, click the prompt and select **NVIDIA** from the configuration picker. If the prompt doesn't appear, open the Command Palette (`Ctrl+Shift+P`) and run **Dev Containers: Reopen in Container**, then select **NVIDIA**. - -### CLI +There is no separate GPU config to pick. Once the toolkit above is installed, just open the **default** devcontainer the [normal way](../docker/#1-build-the-devcontainer) — on `devcontainer up` (or VS Code "Reopen in Container"), `.devcontainer/detect-host-env.sh` probes the host and, when it finds a working NVIDIA setup (`nvidia-smi` plus the `nvidia` runtime registered with Docker), enables `runtime: nvidia` and hands the GPU to the container. If the toolkit isn't installed, it silently stays on the default runtime — so this same config works with or without a GPU. ```bash title="Terminal" -devcontainer up --config .devcontainer/nvidia/devcontainer.json +devcontainer up ``` +You can confirm detection fired in the `devcontainer up` output — look for `[detect-host-env] host=Linux gpu=nvidia`. + ## 3. Launch the simulation Once inside the container, follow the normal [Docker guide](../docker/) from the display check onward. The GPU is available automatically — no extra flags needed when running `sim`. From ef2f695917f207ba2d3b23cc4f297e4a4572e48c Mon Sep 17 00:00:00 2001 From: CPrutean Date: Wed, 5 Aug 2026 17:20:50 -0700 Subject: [PATCH 09/38] feat: removed unnecessary comments --- .devcontainer/detect-host-env.sh | 37 -------------------------------- .devcontainer/devcontainer.json | 6 ------ .devcontainer/x_server.sh | 21 ------------------ cli/gazebo/launch.py | 5 ----- docker/Dockerfile | 20 +++++------------ docker/bashrc.sh | 11 +--------- docker/docker-compose.yml | 10 --------- 7 files changed, 6 insertions(+), 104 deletions(-) diff --git a/.devcontainer/detect-host-env.sh b/.devcontainer/detect-host-env.sh index 3456ab3..bbd1726 100755 --- a/.devcontainer/detect-host-env.sh +++ b/.devcontainer/detect-host-env.sh @@ -1,21 +1,4 @@ #!/usr/bin/env bash -# Host-side environment detection for the (single) devcontainer. -# -# Run by the devcontainer's `initializeCommand` on the host, before the container -# is (re)created — on both `devcontainer up` and VS Code's "Reopen in Container". -# It detects the host OS and GPU and generates two gitignored files the compose -# stack reads, so one devcontainer config works everywhere with no variant to pick: -# -# docker/.env interpolation source (auto-loaded by compose): ports from -# .env.defaults + optional .env.local overrides, plus -# SIM_GPU_RUNTIME which flips `runtime: nvidia` on/off. -# docker/.env.host container env (loaded via compose `env_file`): DISPLAY + -# VirtualGL for GUI forwarding, and the NVIDIA_* vars when a -# usable GPU is present. Conditional vars are simply omitted -# when not applicable (an absent line means unset, not empty — -# which matters for __EGL_VENDOR_LIBRARY_FILENAMES). -# -# Both generated files are host-specific and gitignored — never committed. set -eu script_dir="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" @@ -27,9 +10,6 @@ overrides="$docker_dir/.env.local" uname_s="$(uname -s 2>/dev/null || echo unknown)" -# Enable the nvidia runtime only when it will actually work: a driver that -# answers (nvidia-smi) AND docker with the nvidia runtime registered. Anything -# short of that stays on the default runtime, so non-GPU hosts never break. gpu_available() { [ "$uname_s" = "Linux" ] || return 1 command -v nvidia-smi >/dev/null 2>&1 || return 1 @@ -42,7 +22,6 @@ if gpu_available; then gpu_runtime="nvidia" fi -# --- docker/.env: compose interpolation source (ports + GPU runtime) --- { cat "$defaults" if [ -f "$overrides" ]; then @@ -55,35 +34,22 @@ fi echo "SIM_GPU_RUNTIME=$gpu_runtime" } >"$env_out" -# --- docker/.env.host: container env (DISPLAY / VirtualGL / NVIDIA) --- { echo "# generated by .devcontainer/detect-host-env.sh — do not edit or commit" case "$uname_s" in Darwin) - # macOS: Docker Desktop's Linux VM can't bind-mount the host's X11/ - # Wayland sockets, so forward over the X11 protocol's TCP transport to - # XQuartz via host.docker.internal. XQuartz's indirect GLX can't give - # OGRE2 (Gazebo/RViz) a usable GL context, so render through VirtualGL - # against a container-local headless X server (VGL_DISPLAY) and ship - # finished frames as plain X11 images (VGL_COMPRESS=proxy). `sim gazebo` - # wraps launches in vglrun. echo "# host: macOS (XQuartz over TCP + VirtualGL)" echo "DISPLAY=host.docker.internal:0" echo "VGL_DISPLAY=:88" echo "VGL_COMPRESS=proxy" ;; MINGW* | MSYS* | CYGWIN*) - # Native Windows (Git Bash / MSYS): same TCP forwarding to a Windows X - # server (VcXsrv/X410), same VirtualGL path as macOS. WSL2/WSLg reports - # as Linux below and uses sockets. Needs bash on PATH (Git for Windows). echo "# host: Windows (VcXsrv/X410 over TCP + VirtualGL)" echo "DISPLAY=host.docker.internal:0" echo "VGL_DISPLAY=:88" echo "VGL_COMPRESS=proxy" ;; Linux) - # Native Linux or WSL2/WSLg: docker-compose-dev.yml bind-mounts the - # host's X11/Wayland sockets, so pass the host DISPLAY through (:0). echo "# host: Linux/WSLg (direct socket passthrough)" echo "DISPLAY=${DISPLAY:-:0}" ;; @@ -93,9 +59,6 @@ fi ;; esac if [ -n "$gpu_runtime" ]; then - # A working nvidia stack was detected; hand the GPU to the container. - # (Only written when present — __EGL_VENDOR_LIBRARY_FILENAMES must be - # absent, not empty, on non-GPU hosts or it breaks software EGL.) echo "# nvidia GPU detected (runtime: nvidia)" echo "NVIDIA_VISIBLE_DEVICES=all" echo "NVIDIA_DRIVER_CAPABILITIES=graphics,display,compute,utility" diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 38cadba..826ee19 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -8,10 +8,6 @@ "remoteEnv": { "HOST_WORKSPACE": "${localWorkspaceFolder}" }, - // Runs on the host before the container starts: detects the OS and GPU and - // generates docker/.env + docker/.env.host, so GUI forwarding (XQuartz/VcXsrv/ - // Wayland) and nvidia GPU are auto-configured for every platform — no per-OS - // config to pick. `|| true` so a hiccup here never blocks container creation. "initializeCommand": "bash .devcontainer/detect-host-env.sh || true", "postCreateCommand": "pip3 install --break-system-packages -e .", // install the sim cli "postStartCommand": "bash ./.devcontainer/x_server.sh || true", @@ -19,8 +15,6 @@ "customizations": { "vscode": { "settings": { - // Keep the Activity Bar (and its Explorer icon) visible in the - // container window so the file explorer can't disappear on launch. "workbench.activityBar.location": "default", "shfmt.executablePath": "/usr/local/bin/shfmt" }, diff --git a/.devcontainer/x_server.sh b/.devcontainer/x_server.sh index 8942ac0..6eab0e2 100755 --- a/.devcontainer/x_server.sh +++ b/.devcontainer/x_server.sh @@ -57,15 +57,6 @@ parse_args() { done } -# VirtualGL's "3D X server": a container-local headless X server that GL rendering actually -# happens against. Only needed when $DISPLAY is a remote X server (macOS/XQuartz, -# Windows/VcXsrv), which can display windows but can't provide a usable OpenGL context - its -# indirect GLX is deprecated and broken, so OGRE2 apps (Gazebo, RViz) fail to create a -# renderer. VirtualGL renders here instead (Mesa llvmpipe, OpenGL 4.5) and sends only the -# finished frames to the host's X server as plain X11 images. -# -# Skipped for local passthrough (native Linux, WSLg): there the app's GL already works -# directly against the host's GPU, and routing through VirtualGL would only cost performance. start_vgl_3d_server() { if [[ $DISPLAY == :* ]]; then return 0 @@ -117,12 +108,6 @@ try_display_passthrough() { exit 0 fi - # Case 2: a real X11 display is already reachable at $DISPLAY. This covers: - # - native Linux X11 (host's /tmp/.X11-unix bind-mounted, DISPLAY inherited from the host) - # - WSL2 with WSLg's X11 socket - # - macOS + XQuartz reachable over TCP at host.docker.internal:0 - # - Windows + VcXsrv/X410 reachable over TCP at host.docker.internal:0 - # (both set by .devcontainer/detect-host-env.sh -> docker/.env.host) if [ -n "$DISPLAY" ] && xdpyinfo -display "$DISPLAY" &>/dev/null; then log "[X11] Using host X11 display at $DISPLAY" start_vgl_3d_server @@ -218,12 +203,6 @@ main() { parse_args "$@" try_display_passthrough - # try_display_passthrough only returns (rather than exiting) when no host display was - # usable, so it's safe to claim $DISPLAY for our own Xvfb/Xorg below - we just confirmed - # nothing answers on it. However $DISPLAY may hold a remote/TCP spec like - # "host.docker.internal:0" (macOS/Windows hosts, when XQuartz/VcXsrv wasn't reachable) - - # that's not a valid local display for Xvfb/Xorg to bind to, so normalize it to a plain - # local display number first. Valid local specs always start with ':'. if [[ $DISPLAY != :* ]]; then log "[X11] $DISPLAY unreachable; falling back to internal Xvfb/Xorg on :0" DISPLAY=":0" diff --git a/cli/gazebo/launch.py b/cli/gazebo/launch.py index de74ee6..64e4336 100644 --- a/cli/gazebo/launch.py +++ b/cli/gazebo/launch.py @@ -281,15 +281,10 @@ def _configure_virtualgl_rendering(env: dict[str, str]) -> list[str]: ) return [] - # These force Mesa onto the host X server's indirect-GLX path - the very thing VirtualGL - # exists to avoid. Left set, they also break the local GL context VirtualGL renders into, - # so drop them for the launched processes. for stale in ("LIBGL_ALWAYS_INDIRECT", "MESA_LOADER_DRIVER_OVERRIDE"): env.pop(stale, None) env["VGL_DISPLAY"] = vgl_display - # X11 Transport: hand rendered frames over as ordinary X11 images on the connection we - # already have. No vglclient process on the host and no extra port needed. env.setdefault("VGL_COMPRESS", "proxy") info(f"Rendering through VirtualGL ({vgl_display} -> {display})") diff --git a/docker/Dockerfile b/docker/Dockerfile index 1bb0377..71c9678 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -234,6 +234,7 @@ RUN rosdep init || true # VNC / DISPLAY # # ---------------------------------------------------------------------------- # + RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ apt-get update && apt-get install -y --no-install-recommends \ @@ -241,21 +242,10 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ mesa-utils libglx-mesa0 libgl1-mesa-dri x11-apps \ xserver-xorg-core xserver-xorg-video-dummy kmod -# VirtualGL - makes Gazebo/RViz render on macOS (XQuartz) and Windows (VcXsrv/X410). -# -# Those hosts can display X11 windows over TCP just fine, but they cannot hand back a usable -# OpenGL context: their indirect GLX (IGLX) is deprecated and broken, so OGRE2 - which both -# Gazebo and RViz use - fails at glXMakeCurrent with GLXBadContext and never creates a -# renderer. Forcing Mesa down the indirect path doesn't help, and the direct/software path -# can't match XQuartz's GLX fbconfigs at all ("failed to create drisw screen"). -# Upstream considers IGLX a dead end: https://github.com/XQuartz/XQuartz/issues/144 -# -# VirtualGL splits the two concerns instead. GL rendering runs against a local headless X -# server inside the container (Mesa llvmpipe, OpenGL 4.5), and only the finished frames are -# handed to the host's X server as ordinary X11 images - which XQuartz/VcXsrv do reliably. -# Not used on native Linux/WSLg, where the app's GL already works directly against the host. -# -# Not packaged in Ubuntu, so install the upstream .deb (checksummed per architecture). +# ---------------------------------------------------------------------------- # +# VirtualGL # +# ---------------------------------------------------------------------------- # + ARG VIRTUALGL_VERSION=3.1.4 RUN ARCH="$(dpkg --print-architecture)" && \ case "${ARCH}" in \ diff --git a/docker/bashrc.sh b/docker/bashrc.sh index e1b2db5..49886d6 100644 --- a/docker/bashrc.sh +++ b/docker/bashrc.sh @@ -24,9 +24,7 @@ _tf_find_repo() { "/workspaces/simulations" \ "/workspace/simulations"; do if [ -d "$d/gazebo" ] && [ -f "$d/pyproject.toml" ]; then - printf "%s\n" "$d" - return 0 - fi + printf "%s\n" "$d" return 0 fi done d="$(git rev-parse --show-toplevel 2>/dev/null || true)" @@ -73,15 +71,8 @@ fi # ---------- display ---------- if [ -n "${FORCE_VNC:-}" ]; then - # Point new shells at the virtual display x_server.sh set up for FORCE_VNC, - # not the host's real DISPLAY (which is still reachable via the bind-mounted - # X11 socket and would otherwise render GUI apps on the host instead of VNC). export DISPLAY="${FORCE_VNC_DISPLAY:-:77}" elif [[ -n ${DISPLAY:-} && ${DISPLAY} =~ ^[0-9]+(\.[0-9]+)?$ ]]; then - # Some environments export DISPLAY as a bare number (e.g. "1") instead of ":1" - normalize - # that case only. Don't touch host:display specs like "host.docker.internal:0" or - # "172.20.32.1:0" (WSL2/macOS/Windows passthrough) - those are already valid as-is, and - # prepending ":" to them would turn a valid remote display into a broken local one. export DISPLAY=":${DISPLAY}" fi diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 85d0db0..7159f67 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -13,18 +13,8 @@ services: container_name: simulations user: trickfire - # nvidia runtime, enabled only when the host has a working GPU. Detected on - # the host by .devcontainer/detect-host-env.sh, which writes SIM_GPU_RUNTIME - # into docker/.env (empty on non-GPU hosts, where compose omits `runtime` - # entirely and falls back to the default runtime). runtime: "${SIM_GPU_RUNTIME:-}" - # Host-detected container env (DISPLAY + VirtualGL, and NVIDIA_* when a GPU - # is present). Generated on the host into docker/.env.host by - # .devcontainer/detect-host-env.sh (initializeCommand), so one config - # forwards the right display per-platform with no variant to pick. - # required:false: harmless if absent (e.g. raw `docker compose` outside the - # devcontainer, which doesn't run initializeCommand). env_file: - path: .env.host required: false From ccf02ace1731535de422f7cbb21b3546da9bbe55 Mon Sep 17 00:00:00 2001 From: CPrutean Date: Wed, 5 Aug 2026 18:03:19 -0700 Subject: [PATCH 10/38] fix: fixed formatting --- docker/bashrc.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/bashrc.sh b/docker/bashrc.sh index 49886d6..742ad58 100644 --- a/docker/bashrc.sh +++ b/docker/bashrc.sh @@ -24,7 +24,8 @@ _tf_find_repo() { "/workspaces/simulations" \ "/workspace/simulations"; do if [ -d "$d/gazebo" ] && [ -f "$d/pyproject.toml" ]; then - printf "%s\n" "$d" return 0 fi + printf "%s\n" "$d" return 0 + fi done d="$(git rev-parse --show-toplevel 2>/dev/null || true)" From 5d31df8644276dbe0d275efb20330cd259294548 Mon Sep 17 00:00:00 2001 From: Matej Stastny Date: Wed, 5 Aug 2026 19:23:25 -0700 Subject: [PATCH 11/38] refactor: prebuilt vsg & chrono --- .github/workflows/vsg-chrono.yml | 96 +++++++++++++++++++++++ docker/Dockerfile | 126 ++++--------------------------- docker/vsg-chrono.Dockerfile | 121 +++++++++++++++++++++++++++++ 3 files changed, 230 insertions(+), 113 deletions(-) create mode 100644 .github/workflows/vsg-chrono.yml create mode 100644 docker/vsg-chrono.Dockerfile diff --git a/.github/workflows/vsg-chrono.yml b/.github/workflows/vsg-chrono.yml new file mode 100644 index 0000000..b7faf0b --- /dev/null +++ b/.github/workflows/vsg-chrono.yml @@ -0,0 +1,96 @@ +name: Build VSG + Chrono Base Image + +on: + push: + branches: [main] + paths: + - "docker/vsg-chrono.Dockerfile" + workflow_dispatch: + +env: + IMAGE_NAME: ghcr.io/trickfirerobotics/simulations-vsg-chrono + +jobs: + build: + strategy: + matrix: + include: + - platform: amd64 + runner: ubuntu-24.04 + - platform: arm64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v7 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push by digest + id: build + uses: docker/build-push-action@v6 + with: + context: . + file: docker/vsg-chrono.Dockerfile + platforms: linux/${{ matrix.platform }} + outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true + cache-from: type=gha,scope=vsg-chrono-${{ matrix.platform }} + cache-to: type=gha,mode=max,scope=vsg-chrono-${{ matrix.platform }} + + - name: Export digest + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-${{ matrix.platform }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + merge: + needs: build + runs-on: ubuntu-24.04 + permissions: + packages: write + steps: + - name: Download digests + uses: actions/download-artifact@v5 + with: + path: /tmp/digests + pattern: digests-* + merge-multiple: true + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Create manifest list and push + working-directory: /tmp/digests + run: | + docker buildx imagetools create \ + -t ${{ env.IMAGE_NAME }}:latest \ + -t ${{ env.IMAGE_NAME }}:${{ github.sha }} \ + $(printf '${{ env.IMAGE_NAME }}@sha256:%s ' *) + + - name: Inspect image + run: docker buildx imagetools inspect ${{ env.IMAGE_NAME }}:latest diff --git a/docker/Dockerfile b/docker/Dockerfile index 71c9678..8e8da77 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,3 +1,6 @@ +ARG VSG_CHRONO_TAG=latest +FROM ghcr.io/trickfirerobotics/simulations-vsg-chrono:${VSG_CHRONO_TAG} AS vsg-chrono + FROM ubuntu:24.04 AS sim ARG DEBIAN_FRONTEND=noninteractive @@ -22,6 +25,11 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ locale-gen en_US en_US.UTF-8 && \ update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update && \ + apt-get install --no-install-recommends -y libvulkan1 mesa-vulkan-drivers + ENV LANG=en_US.UTF-8 RUN ln -snf "/usr/share/zoneinfo/${TZ}" /etc/localtime && \ @@ -46,125 +54,17 @@ RUN printf 'int drmCloseBufferHandle(int fd, unsigned int handle){ return 0; }\n ldconfig # ---------------------------------------------------------------------------- # -# VSG # +# VSG + CHRONO # # ---------------------------------------------------------------------------- # -RUN echo "-------------------- VSG -------------------" - -# Build tools + Vulkan/Eigen deps - shared by VSG and Chrono -RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ - --mount=type=cache,target=/var/lib/apt,sharing=locked \ - apt-get update && apt-get install -y --no-install-recommends \ - git build-essential cmake \ - libeigen3-dev \ - libvulkan-dev \ - libxcb1-dev \ - mesa-vulkan-drivers \ - ninja-build \ - pkg-config \ - vulkan-tools +RUN echo "----------------- VSG + CHRONO --------------" +# Prebuilt in the vsg-chrono image (see top of file) instead of compiled here. ENV VSG_INSTALL_DIR=/opt/vsg - -RUN git clone -c advice.detachedHead=false --depth 1 --branch 16.1.0 \ - https://github.com/KhronosGroup/glslang.git /tmp/glslang && \ - cmake -GNinja -B /tmp/build_glslang -S /tmp/glslang \ - -DBUILD_SHARED_LIBS=ON -DENABLE_OPT=0 && \ - cmake --build /tmp/build_glslang --parallel ${BUILD_JOBS:-$(nproc)} && \ - cmake --install /tmp/build_glslang --prefix ${VSG_INSTALL_DIR} && \ - rm -rf /tmp/glslang /tmp/build_glslang - -RUN git clone -c advice.detachedHead=false --depth 1 --branch v4.4.2 \ - https://github.com/KhronosGroup/KTX-Software.git /tmp/ktx && \ - cmake -GNinja -B /tmp/build_ktx -S /tmp/ktx \ - -DBUILD_SHARED_LIBS=ON \ - -DKTX_FEATURE_TESTS=OFF \ - -DKTX_FEATURE_TOOLS=OFF \ - -DKTX_FEATURE_DOC=OFF && \ - cmake --build /tmp/build_ktx --parallel ${BUILD_JOBS:-$(nproc)} && \ - cmake --install /tmp/build_ktx --prefix ${VSG_INSTALL_DIR} && \ - rm -rf /tmp/ktx /tmp/build_ktx - -RUN git clone -c advice.detachedHead=false --depth 1 --branch 1.5.7 \ - https://github.com/google/draco.git /tmp/draco && \ - cmake -GNinja -B /tmp/build_draco -S /tmp/draco \ - -DBUILD_SHARED_LIBS=ON \ - -DDRACO_TESTS=OFF && \ - cmake --build /tmp/build_draco --parallel ${BUILD_JOBS:-$(nproc)} && \ - cmake --install /tmp/build_draco --prefix ${VSG_INSTALL_DIR} && \ - rm -rf /tmp/draco /tmp/build_draco - -RUN git clone -c advice.detachedHead=false --depth 1 --branch v6.0.5 \ - https://github.com/assimp/assimp /tmp/assimp && \ - cmake -GNinja -B /tmp/build_assimp -S /tmp/assimp \ - -DBUILD_SHARED_LIBS=OFF \ - -DASSIMP_BUILD_TESTS=OFF \ - -DASSIMP_BUILD_ASSIMP_TOOLS=OFF \ - -DASSIMP_BUILD_ZLIB=ON && \ - cmake --build /tmp/build_assimp --parallel ${BUILD_JOBS:-$(nproc)} && \ - cmake --install /tmp/build_assimp --prefix ${VSG_INSTALL_DIR} && \ - rm -rf /tmp/assimp /tmp/build_assimp - -RUN git clone -c advice.detachedHead=false --depth 1 --branch v1.1.15 \ - https://github.com/vsg-dev/VulkanSceneGraph.git /tmp/vsg && \ - cmake -GNinja -B /tmp/build_vsg -S /tmp/vsg \ - -DCMAKE_PREFIX_PATH=${VSG_INSTALL_DIR} \ - -DBUILD_SHARED_LIBS=ON && \ - cmake --build /tmp/build_vsg --parallel ${BUILD_JOBS:-$(nproc)} && \ - cmake --install /tmp/build_vsg --prefix ${VSG_INSTALL_DIR} && \ - rm -rf /tmp/vsg /tmp/build_vsg - -RUN git clone -c advice.detachedHead=false --depth 1 --branch v1.1.12 \ - https://github.com/vsg-dev/vsgXchange.git /tmp/vsgXchange && \ - ASSIMP_CMAKE=$(find ${VSG_INSTALL_DIR}/lib/cmake -maxdepth 1 -name "assimp-*" -type d | head -1) && \ - cmake -GNinja -B /tmp/build_vsgXchange -S /tmp/vsgXchange \ - -DCMAKE_PREFIX_PATH=${VSG_INSTALL_DIR} \ - -DBUILD_SHARED_LIBS=ON \ - -Dvsg_DIR=${VSG_INSTALL_DIR}/lib/cmake/vsg \ - -Dassimp_DIR=${ASSIMP_CMAKE} && \ - cmake --build /tmp/build_vsgXchange --parallel ${BUILD_JOBS:-$(nproc)} && \ - cmake --install /tmp/build_vsgXchange --prefix ${VSG_INSTALL_DIR} && \ - rm -rf /tmp/vsgXchange /tmp/build_vsgXchange - -RUN git clone -c advice.detachedHead=false --depth 1 --branch v0.7.0 \ - https://github.com/vsg-dev/vsgImGui.git /tmp/vsgImGui && \ - cmake -GNinja -B /tmp/build_vsgImGui -S /tmp/vsgImGui \ - -DCMAKE_PREFIX_PATH=${VSG_INSTALL_DIR} \ - -DBUILD_SHARED_LIBS=ON \ - -Dvsg_DIR=${VSG_INSTALL_DIR}/lib/cmake/vsg && \ - cmake --build /tmp/build_vsgImGui --parallel ${BUILD_JOBS:-$(nproc)} && \ - cmake --install /tmp/build_vsgImGui --prefix ${VSG_INSTALL_DIR} && \ - rm -rf /tmp/vsgImGui /tmp/build_vsgImGui - ENV LD_LIBRARY_PATH=/opt/vsg/lib -# ---------------------------------------------------------------------------- # -# CHRONO # -# ---------------------------------------------------------------------------- # - -RUN echo "------------------ CHRONO ------------------" - -RUN git clone --depth 1 https://github.com/projectchrono/chrono.git /home/trickfire/chrono - -# Fix upstream bug: AddActiveDomain appends without clearing the null-body default domain -# added by SetupInitial, causing a crash when OnBindAssets iterates all domains. -RUN sed -i \ - 's| m_loader->m_active_domains.push_back(ad);| if (!m_loader->m_user_domains)\n m_loader->m_active_domains.clear();\n m_loader->m_active_domains.push_back(ad);|' \ - /home/trickfire/chrono/src/chrono_vehicle/terrain/SCMTerrain.cpp - -RUN cmake -S /home/trickfire/chrono -B /home/trickfire/chrono/build \ - -GNinja \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_PREFIX_PATH=${VSG_INSTALL_DIR} \ - -DCH_ENABLE_MODULE_VEHICLE=ON \ - -DCH_ENABLE_MODULE_VSG=ON \ - -Dvsg_DIR=${VSG_INSTALL_DIR}/lib/cmake/vsg \ - -DvsgXchange_DIR=${VSG_INSTALL_DIR}/lib/cmake/vsgXchange \ - -DvsgImGui_DIR=${VSG_INSTALL_DIR}/lib/cmake/vsgImGui - -RUN cmake --build /home/trickfire/chrono/build \ - --target demo_VEH_SCMTerrain_RigidTire \ - --parallel ${BUILD_JOBS:-$(nproc)} +COPY --from=vsg-chrono /opt/vsg /opt/vsg +COPY --from=vsg-chrono /home/trickfire/chrono /home/trickfire/chrono RUN chown -R trickfire:trickfire /home/trickfire/chrono diff --git a/docker/vsg-chrono.Dockerfile b/docker/vsg-chrono.Dockerfile new file mode 100644 index 0000000..4000738 --- /dev/null +++ b/docker/vsg-chrono.Dockerfile @@ -0,0 +1,121 @@ +# Prebuilt VulkanSceneGraph + Chrono base image. + +FROM ubuntu:24.04 + +ARG DEBIAN_FRONTEND=noninteractive +ARG BUILD_JOBS + +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update && apt-get install -y --no-install-recommends \ + git build-essential cmake \ + libeigen3-dev \ + libvulkan-dev \ + libxcb1-dev \ + mesa-vulkan-drivers \ + ninja-build \ + pkg-config \ + vulkan-tools + +# ---------------------------------------------------------------------------- # +# VSG # +# ---------------------------------------------------------------------------- # + +ENV VSG_INSTALL_DIR=/opt/vsg + +RUN git clone -c advice.detachedHead=false --depth 1 --branch 16.1.0 \ + https://github.com/KhronosGroup/glslang.git /tmp/glslang && \ + cmake -GNinja -B /tmp/build_glslang -S /tmp/glslang \ + -DBUILD_SHARED_LIBS=ON -DENABLE_OPT=0 && \ + cmake --build /tmp/build_glslang --parallel ${BUILD_JOBS:-$(nproc)} && \ + cmake --install /tmp/build_glslang --prefix ${VSG_INSTALL_DIR} && \ + rm -rf /tmp/glslang /tmp/build_glslang + +RUN git clone -c advice.detachedHead=false --depth 1 --branch v4.4.2 \ + https://github.com/KhronosGroup/KTX-Software.git /tmp/ktx && \ + cmake -GNinja -B /tmp/build_ktx -S /tmp/ktx \ + -DBUILD_SHARED_LIBS=ON \ + -DKTX_FEATURE_TESTS=OFF \ + -DKTX_FEATURE_TOOLS=OFF \ + -DKTX_FEATURE_DOC=OFF && \ + cmake --build /tmp/build_ktx --parallel ${BUILD_JOBS:-$(nproc)} && \ + cmake --install /tmp/build_ktx --prefix ${VSG_INSTALL_DIR} && \ + rm -rf /tmp/ktx /tmp/build_ktx + +RUN git clone -c advice.detachedHead=false --depth 1 --branch 1.5.7 \ + https://github.com/google/draco.git /tmp/draco && \ + cmake -GNinja -B /tmp/build_draco -S /tmp/draco \ + -DBUILD_SHARED_LIBS=ON \ + -DDRACO_TESTS=OFF && \ + cmake --build /tmp/build_draco --parallel ${BUILD_JOBS:-$(nproc)} && \ + cmake --install /tmp/build_draco --prefix ${VSG_INSTALL_DIR} && \ + rm -rf /tmp/draco /tmp/build_draco + +RUN git clone -c advice.detachedHead=false --depth 1 --branch v6.0.5 \ + https://github.com/assimp/assimp /tmp/assimp && \ + cmake -GNinja -B /tmp/build_assimp -S /tmp/assimp \ + -DBUILD_SHARED_LIBS=OFF \ + -DASSIMP_BUILD_TESTS=OFF \ + -DASSIMP_BUILD_ASSIMP_TOOLS=OFF \ + -DASSIMP_BUILD_ZLIB=ON && \ + cmake --build /tmp/build_assimp --parallel ${BUILD_JOBS:-$(nproc)} && \ + cmake --install /tmp/build_assimp --prefix ${VSG_INSTALL_DIR} && \ + rm -rf /tmp/assimp /tmp/build_assimp + +RUN git clone -c advice.detachedHead=false --depth 1 --branch v1.1.15 \ + https://github.com/vsg-dev/VulkanSceneGraph.git /tmp/vsg && \ + cmake -GNinja -B /tmp/build_vsg -S /tmp/vsg \ + -DCMAKE_PREFIX_PATH=${VSG_INSTALL_DIR} \ + -DBUILD_SHARED_LIBS=ON && \ + cmake --build /tmp/build_vsg --parallel ${BUILD_JOBS:-$(nproc)} && \ + cmake --install /tmp/build_vsg --prefix ${VSG_INSTALL_DIR} && \ + rm -rf /tmp/vsg /tmp/build_vsg + +RUN git clone -c advice.detachedHead=false --depth 1 --branch v1.1.12 \ + https://github.com/vsg-dev/vsgXchange.git /tmp/vsgXchange && \ + ASSIMP_CMAKE=$(find ${VSG_INSTALL_DIR}/lib/cmake -maxdepth 1 -name "assimp-*" -type d | head -1) && \ + cmake -GNinja -B /tmp/build_vsgXchange -S /tmp/vsgXchange \ + -DCMAKE_PREFIX_PATH=${VSG_INSTALL_DIR} \ + -DBUILD_SHARED_LIBS=ON \ + -Dvsg_DIR=${VSG_INSTALL_DIR}/lib/cmake/vsg \ + -Dassimp_DIR=${ASSIMP_CMAKE} && \ + cmake --build /tmp/build_vsgXchange --parallel ${BUILD_JOBS:-$(nproc)} && \ + cmake --install /tmp/build_vsgXchange --prefix ${VSG_INSTALL_DIR} && \ + rm -rf /tmp/vsgXchange /tmp/build_vsgXchange + +RUN git clone -c advice.detachedHead=false --depth 1 --branch v0.7.0 \ + https://github.com/vsg-dev/vsgImGui.git /tmp/vsgImGui && \ + cmake -GNinja -B /tmp/build_vsgImGui -S /tmp/vsgImGui \ + -DCMAKE_PREFIX_PATH=${VSG_INSTALL_DIR} \ + -DBUILD_SHARED_LIBS=ON \ + -Dvsg_DIR=${VSG_INSTALL_DIR}/lib/cmake/vsg && \ + cmake --build /tmp/build_vsgImGui --parallel ${BUILD_JOBS:-$(nproc)} && \ + cmake --install /tmp/build_vsgImGui --prefix ${VSG_INSTALL_DIR} && \ + rm -rf /tmp/vsgImGui /tmp/build_vsgImGui + +# ---------------------------------------------------------------------------- # +# CHRONO # +# ---------------------------------------------------------------------------- # + +RUN git clone --depth 1 https://github.com/projectchrono/chrono.git /home/trickfire/chrono + +# fix upstream bug: +# AddActiveDomain appends without clearing the null-body default domain +# added by SetupInitial, causing a crash when OnBindAssets iterates all domains. +RUN sed -i \ + 's| m_loader->m_active_domains.push_back(ad);| if (!m_loader->m_user_domains)\n m_loader->m_active_domains.clear();\n m_loader->m_active_domains.push_back(ad);|' \ + /home/trickfire/chrono/src/chrono_vehicle/terrain/SCMTerrain.cpp + +RUN cmake -S /home/trickfire/chrono -B /home/trickfire/chrono/build \ + -GNinja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_PREFIX_PATH=${VSG_INSTALL_DIR} \ + -DCH_ENABLE_MODULE_VEHICLE=ON \ + -DCH_ENABLE_MODULE_VSG=ON \ + -Dvsg_DIR=${VSG_INSTALL_DIR}/lib/cmake/vsg \ + -DvsgXchange_DIR=${VSG_INSTALL_DIR}/lib/cmake/vsgXchange \ + -DvsgImGui_DIR=${VSG_INSTALL_DIR}/lib/cmake/vsgImGui + +RUN cmake --build /home/trickfire/chrono/build \ + --target demo_VEH_SCMTerrain_RigidTire \ + --parallel ${BUILD_JOBS:-$(nproc)} From ab0e5a9b61b16a8c517ccde99c31e1b3b0dae9c4 Mon Sep 17 00:00:00 2001 From: Matej Stastny Date: Wed, 5 Aug 2026 19:28:10 -0700 Subject: [PATCH 12/38] fix: added ca-certs to chrono image --- docker/vsg-chrono.Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/vsg-chrono.Dockerfile b/docker/vsg-chrono.Dockerfile index 4000738..e04c1e5 100644 --- a/docker/vsg-chrono.Dockerfile +++ b/docker/vsg-chrono.Dockerfile @@ -8,6 +8,7 @@ ARG BUILD_JOBS RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ git build-essential cmake \ libeigen3-dev \ libvulkan-dev \ From 8b984d48c599e7bd129ff4f423207b9f703f284a Mon Sep 17 00:00:00 2001 From: Matej Stastny Date: Thu, 6 Aug 2026 04:55:32 +0000 Subject: [PATCH 13/38] fix: add git to dockerfile --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 8e8da77..ac0ceb8 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -21,7 +21,7 @@ RUN echo "------------------- BASE -------------------" RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ apt-get update && \ - apt-get install --no-install-recommends -y locales curl gcc libc6-dev sudo ca-certificates && \ + apt-get install --no-install-recommends -y locales curl gcc git libc6-dev sudo ca-certificates && \ locale-gen en_US en_US.UTF-8 && \ update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 From 7732d42493ca7984c6dd5e46b93aa7225560f601 Mon Sep 17 00:00:00 2001 From: Matej Stastny Date: Thu, 6 Aug 2026 05:31:44 +0000 Subject: [PATCH 14/38] fix: makefile got turned to some generated slop --- chrono/Makefile | 184 +++--------------------------------------------- 1 file changed, 9 insertions(+), 175 deletions(-) diff --git a/chrono/Makefile b/chrono/Makefile index 3e02ffe..4547346 100644 --- a/chrono/Makefile +++ b/chrono/Makefile @@ -1,181 +1,15 @@ -# CMAKE generated file: DO NOT EDIT! -# Generated by "Unix Makefiles" Generator, CMake Version 3.28 +BUILD_DIR := ./build -# Default target executed when no arguments are given to make. -default_target: all -.PHONY : default_target +.PHONY: all run clean -# Allow only one "make -f Makefile2" at a time, but pass parallelism. -.NOTPARALLEL: +all: $(BUILD_DIR)/sim -#============================================================================= -# Special targets provided by cmake. +$(BUILD_DIR)/sim: ./CMakeLists.txt ./main.cpp + cmake -GNinja -B $(BUILD_DIR) -S . -DCMAKE_BUILD_TYPE=Release -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + cmake --build $(BUILD_DIR) -# Disable implicit rules so canonical targets will work. -.SUFFIXES: +run: $(BUILD_DIR)/sim + cd $(BUILD_DIR) && ./sim -# Disable VCS-based implicit rules. -% : %,v - -# Disable VCS-based implicit rules. -% : RCS/% - -# Disable VCS-based implicit rules. -% : RCS/%,v - -# Disable VCS-based implicit rules. -% : SCCS/s.% - -# Disable VCS-based implicit rules. -% : s.% - -.SUFFIXES: .hpux_make_needs_suffix_list - -# Command-line flag to silence nested $(MAKE). -$(VERBOSE)MAKESILENT = -s - -#Suppress display of executed commands. -$(VERBOSE).SILENT: - -# A target that is always out of date. -cmake_force: -.PHONY : cmake_force - -#============================================================================= -# Set environment variables for the build. - -# The shell in which to execute make rules. -SHELL = /bin/sh - -# The CMake executable. -CMAKE_COMMAND = /usr/bin/cmake - -# The command to remove a file. -RM = /usr/bin/cmake -E rm -f - -# Escaping for special characters. -EQUALS = = - -# The top-level source directory on which CMake was run. -CMAKE_SOURCE_DIR = /home/trickfire/simulations/chrono - -# The top-level build directory on which CMake was run. -CMAKE_BINARY_DIR = /home/trickfire/simulations/chrono - -#============================================================================= -# Targets provided globally by CMake. - -# Special rule for the target edit_cache -edit_cache: - @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "No interactive CMake dialog available..." - /usr/bin/cmake -E echo No\ interactive\ CMake\ dialog\ available. -.PHONY : edit_cache - -# Special rule for the target edit_cache -edit_cache/fast: edit_cache -.PHONY : edit_cache/fast - -# Special rule for the target rebuild_cache -rebuild_cache: - @$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Running CMake to regenerate build system..." - /usr/bin/cmake --regenerate-during-build -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) -.PHONY : rebuild_cache - -# Special rule for the target rebuild_cache -rebuild_cache/fast: rebuild_cache -.PHONY : rebuild_cache/fast - -# The main all target -all: cmake_check_build_system - $(CMAKE_COMMAND) -E cmake_progress_start /home/trickfire/simulations/chrono/CMakeFiles /home/trickfire/simulations/chrono//CMakeFiles/progress.marks - $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 all - $(CMAKE_COMMAND) -E cmake_progress_start /home/trickfire/simulations/chrono/CMakeFiles 0 -.PHONY : all - -# The main clean target clean: - $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 clean -.PHONY : clean - -# The main clean target -clean/fast: clean -.PHONY : clean/fast - -# Prepare targets for installation. -preinstall: all - $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 preinstall -.PHONY : preinstall - -# Prepare targets for installation. -preinstall/fast: - $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 preinstall -.PHONY : preinstall/fast - -# clear depends -depend: - $(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-build-system CMakeFiles/Makefile.cmake 1 -.PHONY : depend - -#============================================================================= -# Target rules for targets named sim - -# Build rule for target. -sim: cmake_check_build_system - $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 sim -.PHONY : sim - -# fast build rule for target. -sim/fast: - $(MAKE) $(MAKESILENT) -f CMakeFiles/sim.dir/build.make CMakeFiles/sim.dir/build -.PHONY : sim/fast - -main.o: main.cpp.o -.PHONY : main.o - -# target to build an object file -main.cpp.o: - $(MAKE) $(MAKESILENT) -f CMakeFiles/sim.dir/build.make CMakeFiles/sim.dir/main.cpp.o -.PHONY : main.cpp.o - -main.i: main.cpp.i -.PHONY : main.i - -# target to preprocess a source file -main.cpp.i: - $(MAKE) $(MAKESILENT) -f CMakeFiles/sim.dir/build.make CMakeFiles/sim.dir/main.cpp.i -.PHONY : main.cpp.i - -main.s: main.cpp.s -.PHONY : main.s - -# target to generate assembly for a file -main.cpp.s: - $(MAKE) $(MAKESILENT) -f CMakeFiles/sim.dir/build.make CMakeFiles/sim.dir/main.cpp.s -.PHONY : main.cpp.s - -# Help Target -help: - @echo "The following are some of the valid targets for this Makefile:" - @echo "... all (the default if no target is provided)" - @echo "... clean" - @echo "... depend" - @echo "... edit_cache" - @echo "... rebuild_cache" - @echo "... sim" - @echo "... main.o" - @echo "... main.i" - @echo "... main.s" -.PHONY : help - - - -#============================================================================= -# Special targets to cleanup operation of make. - -# Special rule to run CMake to check the build system integrity. -# No rule that depends on this can have commands that come from listfiles -# because they might be regenerated. -cmake_check_build_system: - $(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-build-system CMakeFiles/Makefile.cmake 0 -.PHONY : cmake_check_build_system - + rm -rf $(BUILD_DIR) From 5f8797d35c236e8c88b849aa4c6c60e0cf2d15dc Mon Sep 17 00:00:00 2001 From: Matej Stastny Date: Thu, 6 Aug 2026 05:32:04 +0000 Subject: [PATCH 15/38] fix: bashrc missing new line --- docker/bashrc.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/bashrc.sh b/docker/bashrc.sh index 742ad58..a798cf6 100644 --- a/docker/bashrc.sh +++ b/docker/bashrc.sh @@ -24,7 +24,8 @@ _tf_find_repo() { "/workspaces/simulations" \ "/workspace/simulations"; do if [ -d "$d/gazebo" ] && [ -f "$d/pyproject.toml" ]; then - printf "%s\n" "$d" return 0 + printf "%s\n" "$d" + return 0 fi done From 695dd1f1b91c0a9467b6aea82a650521f619466b Mon Sep 17 00:00:00 2001 From: Matej Stastny Date: Thu, 6 Aug 2026 05:33:03 +0000 Subject: [PATCH 16/38] fix: chrono cli process was eating a lot of things from it --- cli/chrono/chrono.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cli/chrono/chrono.py b/cli/chrono/chrono.py index ff7c4a7..5e8bc37 100644 --- a/cli/chrono/chrono.py +++ b/cli/chrono/chrono.py @@ -7,9 +7,7 @@ def run(): - result = subprocess.run(["make"], cwd=CHRONO_TERRAIN_DIR, check=False, stdout=open("/dev/null", 'w')) - result = subprocess.run(["./sim"], cwd=CHRONO_TERRAIN_DIR, check=False) - + result = subprocess.run(["make", "run"], cwd=CHRONO_TERRAIN_DIR, check=False) if result.returncode != 0: sys.exit(result.returncode) From a90db3962d861c71bae95b1f4b54210459a1bfd3 Mon Sep 17 00:00:00 2001 From: Matej Stastny Date: Thu, 6 Aug 2026 05:33:19 +0000 Subject: [PATCH 17/38] fix: env reference (I am not done with this file more slop) --- cli/gazebo/launch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/gazebo/launch.py b/cli/gazebo/launch.py index 64e4336..204aa7f 100644 --- a/cli/gazebo/launch.py +++ b/cli/gazebo/launch.py @@ -265,7 +265,7 @@ def _configure_virtualgl_rendering(env: dict[str, str]) -> list[str]: " working GL context on this host - expect a blank Gazebo window and\n" " rviz2 dying with 'Unable to create the rendering window'.\n" " \n" - " Rebuild the container to pick it up, or set FORCE_VNC=1 in docker/.env\n" + " Rebuild the container to pick it up, or set FORCE_VNC=1 in docker/.env.local\n" " and recreate the container to render over VNC instead." ) return [] From b76e61725002a2e05f43f7ddaa741f7b08dd39ae Mon Sep 17 00:00:00 2001 From: Matej Stastny Date: Thu, 6 Aug 2026 15:50:14 -0700 Subject: [PATCH 18/38] fix: restore build toolchain and fix arm64 SIGILL in chrono image sim chrono run builds chrono/ locally via make+cmake+ninja, but the 5d31df8 prebuilt-image refactor dropped those tools from the dev image. Add them back. Also patch FindSIMD.cmake in the vsg-chrono base image build: Chrono unconditionally compiles with -march=native, baking in whatever SIMD extensions the CI build machine has (SVE on GitHub's arm64 runners). Since the image is built once and pulled onto arbitrary hardware, this crashes with SIGILL on CPUs without that exact ISA, e.g. Apple Silicon which has no SVE support. Skip the native shortcut on aarch64 so it falls back to Chrono's portable -march=armv8-a NEON detection. --- docker/Dockerfile | 4 +++- docker/vsg-chrono.Dockerfile | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index ac0ceb8..935c22e 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -168,7 +168,9 @@ RUN echo "--------------- DEV TOOLING ----------------" RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ apt-get update && apt-get install -y --no-install-recommends \ - clangd openssh-server python3.12-venv wget zstd + clangd openssh-server python3.12-venv wget zstd \ + build-essential cmake ninja-build \ + libeigen3-dev libvulkan-dev libxcb1-dev pkg-config RUN --mount=type=cache,target=/root/.cache/pip \ pip3 install ruff pre-commit --break-system-packages --ignore-installed diff --git a/docker/vsg-chrono.Dockerfile b/docker/vsg-chrono.Dockerfile index e04c1e5..da85277 100644 --- a/docker/vsg-chrono.Dockerfile +++ b/docker/vsg-chrono.Dockerfile @@ -107,6 +107,19 @@ RUN sed -i \ 's| m_loader->m_active_domains.push_back(ad);| if (!m_loader->m_user_domains)\n m_loader->m_active_domains.clear();\n m_loader->m_active_domains.push_back(ad);|' \ /home/trickfire/chrono/src/chrono_vehicle/terrain/SCMTerrain.cpp +# fix upstream portability bug: +# FindSIMD.cmake unconditionally compiles with -march=native on any recent +# GCC/Clang, baking in whatever SIMD extensions the *build* machine has +# (e.g. SVE on GitHub's arm64 runners). Since this image is built once and +# distributed to arbitrary machines, that produces SIGILL on hosts without +# the exact same ISA (e.g. Apple Silicon, which has no SVE). Skip the +# native shortcut on aarch64 so it falls through to Chrono's NEON detection, +# which resolves to the portable "-march=armv8-a" baseline instead. +RUN sed -i \ + -e 's|GCC_VERSION_STRING VERSION_GREATER 4.2 AND NOT APPLE AND NOT CMAKE_CROSSCOMPILING|GCC_VERSION_STRING VERSION_GREATER 4.2 AND NOT APPLE AND NOT CMAKE_CROSSCOMPILING AND NOT CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64\|arm64"|' \ + -e 's|CLANG_VERSION_STRING VERSION_GREATER_EQUAL 15.0 AND NOT CMAKE_CROSSCOMPILING|CLANG_VERSION_STRING VERSION_GREATER_EQUAL 15.0 AND NOT CMAKE_CROSSCOMPILING AND NOT CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64\|arm64"|' \ + /home/trickfire/chrono/cmake/FindSIMD.cmake + RUN cmake -S /home/trickfire/chrono -B /home/trickfire/chrono/build \ -GNinja \ -DCMAKE_BUILD_TYPE=Release \ From 187e5672cea8d7d323a02a5af6af7d66af5206de Mon Sep 17 00:00:00 2001 From: Matej Stastny Date: Thu, 6 Aug 2026 21:43:09 -0700 Subject: [PATCH 19/38] chore: rewrite host-env detection to generate a single docker/.env --- .../{detect-host-env.sh => host-env.sh} | 54 +++++++++++-------- 1 file changed, 31 insertions(+), 23 deletions(-) rename .devcontainer/{detect-host-env.sh => host-env.sh} (77%) diff --git a/.devcontainer/detect-host-env.sh b/.devcontainer/host-env.sh similarity index 77% rename from .devcontainer/detect-host-env.sh rename to .devcontainer/host-env.sh index bbd1726..0fc0504 100755 --- a/.devcontainer/detect-host-env.sh +++ b/.devcontainer/host-env.sh @@ -3,13 +3,15 @@ set -eu script_dir="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" docker_dir="$(cd "$script_dir/../docker" && pwd)" -env_out="$docker_dir/.env" -host_out="$docker_dir/.env.host" + defaults="$docker_dir/.env.defaults" overrides="$docker_dir/.env.local" +env_out="$docker_dir/.env" uname_s="$(uname -s 2>/dev/null || echo unknown)" +log() { printf "\033[1;36m%s\033[0m\n" "$1"; } + gpu_available() { [ "$uname_s" = "Linux" ] || return 1 command -v nvidia-smi >/dev/null 2>&1 || return 1 @@ -18,24 +20,9 @@ gpu_available() { } gpu_runtime="" -if gpu_available; then - gpu_runtime="nvidia" -fi - -{ - cat "$defaults" - if [ -f "$overrides" ]; then - echo "" - echo "# --- user overrides (docker/.env.local) ---" - cat "$overrides" - fi - echo "" - echo "# --- host-detected (generated, do not edit) ---" - echo "SIM_GPU_RUNTIME=$gpu_runtime" -} >"$env_out" +gpu_available && gpu_runtime="nvidia" -{ - echo "# generated by .devcontainer/detect-host-env.sh — do not edit or commit" +write_display_env() { case "$uname_s" in Darwin) echo "# host: macOS (XQuartz over TCP + VirtualGL)" @@ -54,16 +41,37 @@ fi echo "DISPLAY=${DISPLAY:-:0}" ;; *) - echo "# host: unknown ($uname_s) — falling back to local display :0" + echo "# host: unknown ($uname_s) - falling back to local display :0" echo "DISPLAY=${DISPLAY:-:0}" ;; esac +} + +write_env() { + echo "# generated by .devcontainer/host-env.sh - do not edit or commit" + echo "# to change ports/flags, edit docker/.env.defaults (shared) or docker/.env.local (yours)" + echo "" + + cat "$defaults" + + if [ -f "$overrides" ]; then + echo "" + echo "# --- user overrides (docker/.env.local) ---" + cat "$overrides" + fi + + echo "" + echo "# --- host-detected (generated, do not edit) ---" + write_display_env + echo "SIM_GPU_RUNTIME=$gpu_runtime" + if [ -n "$gpu_runtime" ]; then - echo "# nvidia GPU detected (runtime: nvidia)" echo "NVIDIA_VISIBLE_DEVICES=all" echo "NVIDIA_DRIVER_CAPABILITIES=graphics,display,compute,utility" echo "__EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/10_nvidia.json" fi -} >"$host_out" +} + +write_env >"$env_out" -echo "[detect-host-env] host=$uname_s gpu=${gpu_runtime:-none} -> wrote docker/.env, docker/.env.host" +echo "[INFO] host=$uname_s gpu=${gpu_runtime:-none}" From fe228f9002f882d896358978b8671028349aad84 Mon Sep 17 00:00:00 2001 From: Matej Stastny Date: Thu, 6 Aug 2026 21:43:12 -0700 Subject: [PATCH 20/38] chore: merge docker-compose-dev.yml into docker-compose.yml --- docker/docker-compose-dev.yml | 31 ------------------------------- docker/docker-compose.yml | 35 +++++++++++++++++++++++++++++++---- 2 files changed, 31 insertions(+), 35 deletions(-) delete mode 100644 docker/docker-compose-dev.yml diff --git a/docker/docker-compose-dev.yml b/docker/docker-compose-dev.yml deleted file mode 100644 index 1af67cf..0000000 --- a/docker/docker-compose-dev.yml +++ /dev/null @@ -1,31 +0,0 @@ -services: - sim: - environment: - WAYLAND_DISPLAY: "${WAYLAND_DISPLAY:-}" - XDG_RUNTIME_DIR: /run/host-runtime - QT_SCALE_FACTOR: "${QT_SCALE_FACTOR:-}" - QT_ENABLE_HIGHDPI_SCALING: "${QT_ENABLE_HIGHDPI_SCALING:-}" - GDK_SCALE: "${GDK_SCALE:-}" - GDK_DPI_SCALE: "${GDK_DPI_SCALE:-}" - FORCE_VNC: "${FORCE_VNC:-}" - build: - args: - USER_UID: "${UID:-1000}" - volumes: - - ${XDG_RUNTIME_DIR:-/tmp}:/run/host-runtime:ro - - type: bind - source: /tmp/.X11-unix - target: /tmp/.X11-unix - bind: - create_host_path: true - - type: bind - source: ${HOME}/.config/trickfire - target: /home/trickfire/.config/trickfire - bind: - create_host_path: true - - type: bind - source: ${HOME}/.ssh - target: /home/trickfire/.ssh - read_only: true - bind: - create_host_path: true diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 7159f67..bdbac3e 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -9,22 +9,32 @@ services: VNC_PORT: "${VNC_PORT:?VNC_PORT must be set, see docker/.env}" NOVNC_PORT: "${NOVNC_PORT:?NOVNC_PORT must be set, see docker/.env}" ROSBRIDGE_PORT: "${ROSBRIDGE_PORT:?ROSBRIDGE_PORT must be set, see docker/.env}" + USER_UID: "${UID:-1000}" image: simulations:latest container_name: simulations user: trickfire runtime: "${SIM_GPU_RUNTIME:-}" - env_file: - - path: .env.host - required: false - environment: VNC_PORT: "${VNC_PORT:?VNC_PORT must be set, see docker/.env}" NOVNC_PORT: "${NOVNC_PORT:?NOVNC_PORT must be set, see docker/.env}" ROSBRIDGE_PORT: "${ROSBRIDGE_PORT:?ROSBRIDGE_PORT must be set, see docker/.env}" QT_X11_NO_MITSHM: "1" TZ: ${TZ:-UTC} + WAYLAND_DISPLAY: "${WAYLAND_DISPLAY:-}" + XDG_RUNTIME_DIR: /run/host-runtime + QT_SCALE_FACTOR: "${QT_SCALE_FACTOR:-}" + QT_ENABLE_HIGHDPI_SCALING: "${QT_ENABLE_HIGHDPI_SCALING:-}" + GDK_SCALE: "${GDK_SCALE:-}" + GDK_DPI_SCALE: "${GDK_DPI_SCALE:-}" + FORCE_VNC: "${FORCE_VNC:-}" + DISPLAY: "${DISPLAY:-}" + VGL_DISPLAY: "${VGL_DISPLAY:-}" + VGL_COMPRESS: "${VGL_COMPRESS:-}" + NVIDIA_VISIBLE_DEVICES: "${NVIDIA_VISIBLE_DEVICES:-}" + NVIDIA_DRIVER_CAPABILITIES: "${NVIDIA_DRIVER_CAPABILITIES:-}" + __EGL_VENDOR_LIBRARY_FILENAMES: "${__EGL_VENDOR_LIBRARY_FILENAMES:-}" volumes: - ..:/home/trickfire/simulations @@ -34,6 +44,23 @@ services: target: /run/user/1000 bind: create_host_path: true + - ${XDG_RUNTIME_DIR:-/tmp}:/run/host-runtime:ro + - type: bind + source: /tmp/.X11-unix + target: /tmp/.X11-unix + bind: + create_host_path: true + - type: bind + source: ${HOME}/.config/trickfire + target: /home/trickfire/.config/trickfire + bind: + create_host_path: true + - type: bind + source: ${HOME}/.ssh + target: /home/trickfire/.ssh + read_only: true + bind: + create_host_path: true working_dir: /home/trickfire/simulations From 0618fd2116a51c1cfe3b03adf653e9e6f03fcb08 Mon Sep 17 00:00:00 2001 From: Matej Stastny Date: Thu, 6 Aug 2026 21:43:15 -0700 Subject: [PATCH 21/38] chore: point devcontainer.json at merged compose file and host-env.sh --- .devcontainer/devcontainer.json | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 826ee19..b08d79c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "simulations", - "dockerComposeFile": ["../docker/docker-compose.yml", "../docker/docker-compose-dev.yml"], + "dockerComposeFile": "../docker/docker-compose.yml", "service": "sim", "runServices": ["sim"], @@ -8,7 +8,7 @@ "remoteEnv": { "HOST_WORKSPACE": "${localWorkspaceFolder}" }, - "initializeCommand": "bash .devcontainer/detect-host-env.sh || true", + "initializeCommand": "bash .devcontainer/host-env.sh || true", "postCreateCommand": "pip3 install --break-system-packages -e .", // install the sim cli "postStartCommand": "bash ./.devcontainer/x_server.sh || true", @@ -19,34 +19,26 @@ "shfmt.executablePath": "/usr/local/bin/shfmt" }, "extensions": [ - // --- Python --- "ms-python.python", "ms-python.vscode-pylance", "charliermarsh.ruff", "ms-python.mypy-type-checker", - - // --- C and C++ --- "ms-vscode.cmake-tools", "josetr.cmake-language-support-vscode", "llvm-vs-code-extensions.vscode-clangd", "ms-vscode.makefile-tools", - - // --- Robotics / ROS / URDF / meshes --- "Ranch-Hand-Robotics.rde-ros-2", "smilerobotics.urdf", "morningfrog.urdf-visualizer", "misiekhardcore.stl-previewer", - - // --- Container --- "ms-azuretools.vscode-docker", - - // --- Formatters --- "esbenp.prettier-vscode", "yzhang.markdown-all-in-one", "DotJoshJohnson.xml", "tamasfe.even-better-toml", "github.vscode-github-actions", - "mkhl.shfmt" + "mkhl.shfmt", + "unifiedjs.vscode-mdx" ] } } From 71286388e5c3e0c2ce9cd3ab240ea15314bb932a Mon Sep 17 00:00:00 2001 From: Matej Stastny Date: Thu, 6 Aug 2026 21:43:18 -0700 Subject: [PATCH 22/38] chore: add mdx formatter to vscode settings --- .vscode/settings.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 482d539..6a9767a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -29,6 +29,10 @@ "editor.formatOnSave": true, "editor.formatOnPaste": true }, + "[mdx]": { + "editor.wordWrap": "on", + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, "[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, @@ -74,6 +78,5 @@ "${workspaceFolder}/gazebo/sim_common" ], "ROS2.distro": "jazzy", - "cmake.ignoreCMakeListsMissing": true, - "autoDevcontainer.enabled": true + "cmake.ignoreCMakeListsMissing": true } From 69d54762838bddb660c7153a14e2d51ce35cc25d Mon Sep 17 00:00:00 2001 From: Matej Stastny Date: Thu, 6 Aug 2026 21:43:18 -0700 Subject: [PATCH 23/38] docs: update x_server.sh comments for merged compose file --- .devcontainer/x_server.sh | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.devcontainer/x_server.sh b/.devcontainer/x_server.sh index 6eab0e2..de5d28e 100755 --- a/.devcontainer/x_server.sh +++ b/.devcontainer/x_server.sh @@ -1,10 +1,6 @@ #!/usr/bin/env bash - -# -------------------------------------------------------------------------------------------- -# Starts a headless X11 desktop (Xorg/Xvfb + Openbox + x11vnc + noVNC) inside the container. -# Intended for running GUI apps (Gazebo) in Docker. -# Supports --verbose flag for debugging (prints all output to console instead of log file). -# -------------------------------------------------------------------------------------------- +# Sets up the container's display: Wayland/X11 passthrough when available, otherwise a +# headless Xorg/Xvfb + Openbox + x11vnc + noVNC stack. See docs/reference/docker-environment.mdx. set -eo pipefail trap '' HUP @@ -101,7 +97,7 @@ try_display_passthrough() { fi # Case 1: Linux host with a Wayland compositor, or WSL2 with WSLg. The host socket is - # bind-mounted into /run/host-runtime by docker-compose-dev.yml. + # bind-mounted into /run/host-runtime by docker-compose.yml. local wayland_sock="/run/host-runtime/${WAYLAND_DISPLAY:-wayland-0}" if [ -S "$wayland_sock" ]; then log "[X11] Using Wayland socket at $wayland_sock" From a094ad4e99fc522afa8073e5939e85426e07b425 Mon Sep 17 00:00:00 2001 From: Matej Stastny Date: Thu, 6 Aug 2026 21:43:19 -0700 Subject: [PATCH 24/38] chore: simplify gitignore for merged env file flow --- .gitignore | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index 813f0e2..f27adf1 100644 --- a/.gitignore +++ b/.gitignore @@ -7,22 +7,19 @@ !.vscode/launch.json !.vscode/extensions.json -# pixi -.pixi/ - -# Python +# python .mypy_cache __pycache__ *.egg-info/ build/ **/.venv -# ROS2 +# ros gazebo/install gazebo/build gazebo/log -# sim create tests +# sim tests cli/gazebo/create/tests # chrono outputs @@ -31,14 +28,8 @@ chrono/results/* # env **/*.env -# docker/.env is generated each `devcontainer up` by .devcontainer/detect-host-env.sh -# (from docker/.env.defaults + docker/.env.local + host detection). docker/.env.defaults -# is the committed source of truth; docker/.env.local holds per-machine overrides. docker/.env -docker/.env.host docker/.env.local # trickfire-docs -.trickfire-docs/ dist/ -.cache/ From 4b4c5e42967e4e170fd8c877275a0bc475e02819 Mon Sep 17 00:00:00 2001 From: Matej Stastny Date: Thu, 6 Aug 2026 21:43:19 -0700 Subject: [PATCH 25/38] chore: trim comments in docker/.env.defaults --- docker/.env.defaults | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/docker/.env.defaults b/docker/.env.defaults index c056411..c7c6c2b 100644 --- a/docker/.env.defaults +++ b/docker/.env.defaults @@ -1,14 +1,6 @@ -# Committed defaults for docker exposed ports (and optional flags). -# -# This file is the source of truth. On `devcontainer up`, .devcontainer/detect-host-env.sh -# generates the real (gitignored) docker/.env from: these defaults + your optional -# docker/.env.local overrides + host-detected values (DISPLAY/GPU). To change ports or -# force VNC on YOUR machine, create docker/.env.local with the keys you want to override -# — do not edit this file (it's shared) or docker/.env (it's regenerated each up). +# do not edit! +# create "docker/.env.local" to override VNC_PORT=5900 NOVNC_PORT=6080 ROSBRIDGE_PORT=9090 - -# set to 1 in docker/.env.local if you want to force vnc startup -# FORCE_VNC=1 From f0d10eb20fcb700df3c5331b20157425eb760b74 Mon Sep 17 00:00:00 2001 From: Matej Stastny Date: Thu, 6 Aug 2026 21:43:20 -0700 Subject: [PATCH 26/38] chore: trim comment in vsg-chrono.Dockerfile --- docker/vsg-chrono.Dockerfile | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/docker/vsg-chrono.Dockerfile b/docker/vsg-chrono.Dockerfile index da85277..03cc76f 100644 --- a/docker/vsg-chrono.Dockerfile +++ b/docker/vsg-chrono.Dockerfile @@ -102,19 +102,14 @@ RUN git clone --depth 1 https://github.com/projectchrono/chrono.git /home/trickf # fix upstream bug: # AddActiveDomain appends without clearing the null-body default domain -# added by SetupInitial, causing a crash when OnBindAssets iterates all domains. +# added by SetupInitial, causing a crash when OnBindAssets iterates all domains RUN sed -i \ 's| m_loader->m_active_domains.push_back(ad);| if (!m_loader->m_user_domains)\n m_loader->m_active_domains.clear();\n m_loader->m_active_domains.push_back(ad);|' \ /home/trickfire/chrono/src/chrono_vehicle/terrain/SCMTerrain.cpp # fix upstream portability bug: # FindSIMD.cmake unconditionally compiles with -march=native on any recent -# GCC/Clang, baking in whatever SIMD extensions the *build* machine has -# (e.g. SVE on GitHub's arm64 runners). Since this image is built once and -# distributed to arbitrary machines, that produces SIGILL on hosts without -# the exact same ISA (e.g. Apple Silicon, which has no SVE). Skip the -# native shortcut on aarch64 so it falls through to Chrono's NEON detection, -# which resolves to the portable "-march=armv8-a" baseline instead. +# GCC/Clang, baking in whatever SIMD extensions the build machine has RUN sed -i \ -e 's|GCC_VERSION_STRING VERSION_GREATER 4.2 AND NOT APPLE AND NOT CMAKE_CROSSCOMPILING|GCC_VERSION_STRING VERSION_GREATER 4.2 AND NOT APPLE AND NOT CMAKE_CROSSCOMPILING AND NOT CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64\|arm64"|' \ -e 's|CLANG_VERSION_STRING VERSION_GREATER_EQUAL 15.0 AND NOT CMAKE_CROSSCOMPILING|CLANG_VERSION_STRING VERSION_GREATER_EQUAL 15.0 AND NOT CMAKE_CROSSCOMPILING AND NOT CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64\|arm64"|' \ From 0871c0f2d6d557dc76f07dbe3ac627d9d871d603 Mon Sep 17 00:00:00 2001 From: Matej Stastny Date: Thu, 6 Aug 2026 21:43:26 -0700 Subject: [PATCH 27/38] refactor: shorten launch.py diagnostics, link to docs --- cli/gazebo/launch.py | 124 +++++++------------------------------------ 1 file changed, 18 insertions(+), 106 deletions(-) diff --git a/cli/gazebo/launch.py b/cli/gazebo/launch.py index 204aa7f..9c99913 100644 --- a/cli/gazebo/launch.py +++ b/cli/gazebo/launch.py @@ -117,51 +117,26 @@ def _x11_port_for(display: str) -> int: return 6000 -def _diagnose_display(display: str, xdpyinfo_stderr: str) -> str: - """Build a specific, actionable explanation for why `display` couldn't be opened. +_DOCKER_DOCS = "https://docs.trickfirerobotics.com/simulations/setup/docker" + - Runs its own DNS/TCP checks (independent of xdpyinfo) so the error points at the - layer that's actually broken, instead of a bare "cannot connect". - """ +def _diagnose_display(display: str, xdpyinfo_stderr: str) -> str: + """Pin down which layer (local socket, DNS, TCP, or X11 auth) is broken.""" lines = [f"Cannot connect to display {display}", ""] - is_local = display.startswith(":") - if is_local: + if display.startswith(":"): lines += [ - "This is a local display spec - the container expected a Wayland/X11 socket to", - "already be forwarded in (native Linux host, or WSL2/WSLg).", - "", - "Checks to run inside the container:", - " 1. grep X11 /tmp/start_x_server.log", - " Look for '[X11] Using host X11 display' or '[X11] Using Wayland socket'.", - " If you see Xvfb/vkms/dummy-driver/noVNC lines instead, passthrough failed", - " at container startup and it fell back to the internal VNC stack - connect", - " a VNC viewer to localhost:5900 (or http://localhost:6080/vnc.html), or fix", - " passthrough on the host and restart the container to retry it.", - " 2. ls -la /tmp/.X11-unix/", - " Empty means the host's X11 socket wasn't bind-mounted in, or nothing is", - " listening on it on the host.", + "Expected a Wayland/X11 socket forwarded in from the hosts", ] return "\n".join(lines) host = display.split(":", 1)[0] - lines += [ - f"This is a remote display spec (host '{host}') - used by the macOS/Windows", - "devcontainer configs to forward GUI windows to XQuartz/VcXsrv over TCP.", - "", - ] try: ip = socket.gethostbyname(host) lines.append(f" [OK] DNS: '{host}' resolves to {ip}") except OSError as e: - lines += [ - f" [FAIL] DNS: '{host}' did not resolve ({e})", - "", - " Docker Desktop provides this name automatically to containers. If it's", - " missing, Docker Desktop may not be running, or this isn't actually", - " running inside the container (check your shell prompt).", - ] + lines += [f" [FAIL] DNS: '{host}' did not resolve ({e})", "", "Is Docker Desktop running?"] return "\n".join(lines) port = _x11_port_for(display) @@ -171,37 +146,14 @@ def _diagnose_display(display: str, xdpyinfo_stderr: str) -> str: except OSError as e: lines += [ f" [FAIL] TCP: could not connect to {host}:{port} ({e})", - "", - " macOS + XQuartz:", - " - Is XQuartz actually running? (`ps aux | grep -i xquartz` on the Mac)", - " - XQuartz > Settings > Security > 'Allow connections from network", - " clients' must be checked, then XQuartz fully restarted for it to", - " take effect.", - " Windows + VcXsrv/X410:", - " - Is the X server running? For VcXsrv, XLaunch must have 'Disable", - " access control' checked.", - " - Windows Defender Firewall may be silently blocking it - check for a", - " blocked-app prompt, or allow it manually for Private networks.", + f"See {_DOCKER_DOCS} for XQuartz/VcXsrv setup.", ] return "\n".join(lines) lines += [ " [FAIL] X11: connected over TCP, but the X server rejected the session:", f" {xdpyinfo_stderr.strip() or '(no error output captured)'}", - "", - " DNS and TCP are both fine, so this is an X11 access-control problem, not a", - " network problem:", - "", - " macOS:", - " Run on the Mac (not in the container): `DISPLAY=:0 xhost + 127.0.0.1`", - " Do NOT use `xhost -display :0 + ...` - this is a documented xhost bug:", - " '-display' is parsed as 'remove a host named display', not a real flag.", - " xhost always connects using your shell's $DISPLAY env var instead, so set", - " it as a one-off prefix like above. This resets every time XQuartz", - " restarts, so you'll need to re-run it after any XQuartz restart.", - " Windows:", - " Relaunch VcXsrv/X410 with 'Disable access control' checked - there's no", - " separate allow-list step needed once that's set.", + f"See {_DOCKER_DOCS} for X11 authorization (xhost) setup.", ] return "\n".join(lines) @@ -223,12 +175,7 @@ def _check_display() -> None: info("Checking for display...") display = os.environ.get("DISPLAY") if not display: - die( - "DISPLAY environment variable not set\n\n" - " This is normally set by the devcontainer's compose config. If you're\n" - " seeing this, something stripped it from your shell - try a fresh\n" - " terminal/container restart, or run `env | grep DISPLAY` to confirm." - ) + die("DISPLAY not set! Try restarting the container") result = subprocess.run( ["xdpyinfo", "-display", display], @@ -242,42 +189,19 @@ def _check_display() -> None: def _configure_virtualgl_rendering(env: dict[str, str]) -> list[str]: - """Route GL rendering through VirtualGL when displaying on a remote X server. - - macOS (XQuartz) and Windows (VcXsrv/X410) can display X11 windows over TCP, but can't - hand back a usable OpenGL context: their indirect GLX is deprecated and broken, so OGRE2 - - which both Gazebo and RViz use - fails at glXMakeCurrent and never creates a renderer. - - VirtualGL splits the two concerns. GL rendering runs against a container-local headless X - server (Mesa llvmpipe, OpenGL 4.5) started by .devcontainer/x_server.sh, and only the - finished frames go to the host's X server as ordinary X11 images - which it handles fine. - - Returns the command prefix to launch under, or an empty prefix if VirtualGL isn't needed - or isn't usable (in which case the launch still proceeds, just without GL acceleration). - """ + """Route GL rendering through VirtualGL when displaying on a remote X server""" display = os.environ.get("DISPLAY", "") if display.startswith(":"): - return [] # local passthrough (Linux/WSLg) - the app's GL already works directly + return [] if not shutil.which("vglrun"): - warn( - "VirtualGL (vglrun) is not installed, so Gazebo/RViz have no way to get a\n" - " working GL context on this host - expect a blank Gazebo window and\n" - " rviz2 dying with 'Unable to create the rendering window'.\n" - " \n" - " Rebuild the container to pick it up, or set FORCE_VNC=1 in docker/.env.local\n" - " and recreate the container to render over VNC instead." - ) + warn(f"vglrun not installed - GL rendering will fail. See {_DOCKER_DOCS}") return [] vgl_display = os.environ.get("VGL_DISPLAY", ":88") if not _display_reachable(vgl_display): warn( - f"VirtualGL's 3D X server on {vgl_display} isn't running, so Gazebo/RViz can't\n" - " get a working GL context - expect rendering to fail.\n" - " \n" - " Start it with: bash .devcontainer/x_server.sh\n" - " (it normally starts automatically when the container starts)" + f"VirtualGL's 3D X server on {vgl_display} isn't running - run .devcontainer/x_server.sh" ) return [] @@ -294,7 +218,7 @@ def _configure_virtualgl_rendering(env: dict[str, str]) -> list[str]: def _configure_rendering(env: dict[str, str]) -> list[str]: """Pick how Gazebo/OGRE2 should get its GL context, based on where it's being displayed.""" if os.environ.get("FORCE_VNC"): - info("FORCE_VNC: forcing software rendering (llvmpipe) - no direct GPU access") + info("FORCE_VNC: forcing software rendering (llvmpipe)") env["LIBGL_ALWAYS_SOFTWARE"] = "1" return [] @@ -333,11 +257,7 @@ def _setup_pixi_env() -> None: def build_and_launch(robot_name: str, *, build_only: bool = False, no_build: bool = False) -> None: - """Build the ROS 2 workspace and launch a robot simulation. - - Auto-detects the environment: configures pixi paths when running natively, - or checks the X display when running inside the Dev Container. - """ + """Build the ROS 2 workspace and launch a robot simulation.""" if build_only and no_build: die("Use either --build-only or --no-build, not both") @@ -363,12 +283,7 @@ def build_and_launch(robot_name: str, *, build_only: bool = False, no_build: boo log_dir.mkdir(parents=True, exist_ok=True) log_path = log_dir / f"{robot_name}-gazebo-{datetime.now():%Y-%m-%d_%H-%M}.log" # noqa: DTZ005 - print("--------------------------------------------------------------") - print(f"Robot: {robot_name}") - print("Simulator: gazebo") - print(f"Workspace: {WORKSPACE_DIR}") - print(f"Log: {log_path}") - print("--------------------------------------------------------------") + info(f"Launching {robot_name} - log: {log_path}") setup_bash = WORKSPACE_DIR / "install" / "setup.bash" @@ -393,10 +308,7 @@ def build_and_launch(robot_name: str, *, build_only: bool = False, no_build: boo info("Build complete") if not setup_bash.is_file(): - die( - "Missing install/setup.bash.\n" - " Run without --no-build once to generate install artifacts." - ) + die("Missing install/setup.bash - run without --no-build once to generate it") if not launch: info("Build-only requested; skipping launch") From b78b6b5e6599aca011c653a540c4f7999e42114c Mon Sep 17 00:00:00 2001 From: Matej Stastny Date: Thu, 6 Aug 2026 21:43:29 -0700 Subject: [PATCH 28/38] docs: rewrite docker setup guide for merged env flow --- docs/setup/docker.mdx | 311 ++++++++++++++++++++++++------------------ 1 file changed, 177 insertions(+), 134 deletions(-) diff --git a/docs/setup/docker.mdx b/docs/setup/docker.mdx index a836aeb..11b7359 100644 --- a/docs/setup/docker.mdx +++ b/docs/setup/docker.mdx @@ -1,169 +1,212 @@ --- -title: Docker -description: Run the simulation inside a container. +title: Docker & Devcontainers +description: Run the simulation inside a Docker container. --- -With this approcach everything runs inside a Docker container with everything deterministically installed. The native approach is faster (except on Linux system, Docker runs with native speeds there) but you can have issues installing or running the sim using it. This project uses [devcontainers](https://containers.dev/) to build the Docker environment. You can build it either using the [VSCode extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) or the [CLI](https://code.visualstudio.com/docs/devcontainers/devcontainer-cli) if you do not want to/cannot use VSCode. +Everything runs inside a Docker container with all dependencies pinned and pre-built. It's slower to start than [native (pixi)](../setup/pixi) on most systems (not Linux, Docker there works at close-to-native speeds) but works on any OS, including Chrono, without a local ROS/Gazebo install. This project uses [devcontainers](https://containers.dev/), built with the [VS Code extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) or the [CLI](https://code.visualstudio.com/docs/devcontainers/devcontainer-cli). ## 1. Build the devcontainer -There is **one devcontainer config, and it auto-detects your host** — no variant to pick. Before the container starts, its `initializeCommand` runs `.devcontainer/detect-host-env.sh`, which detects your OS and GPU and generates `docker/.env` + `docker/.env.host` (loaded by compose) so GUI forwarding *and* GPU acceleration are configured for you: +Before the container starts, `initializeCommand` runs `.devcontainer/host-env.sh`, which detects your OS and GPU and writes `docker/.env`. These are all possible options: -- **Linux (Wayland or X11) and WSL2/WSLg** → host display socket, passed straight through. -- **macOS with XQuartz** → forwarded over TCP to XQuartz via `host.docker.internal`, rendered with VirtualGL. -- **Windows without WSL2/WSLg** (Hyper-V backend, or a native Windows path) → forwarded over TCP to VcXsrv/X410. -- **NVIDIA GPU** (Linux, with the [nvidia container toolkit](/setup/nvidia) installed) → detected automatically and handed to the container; otherwise the default runtime is used. +| Host | Display path | +| ------------------------------------- | -------------------------------------------------------------------------------------------- | +| **Linux Wayland/X11** & **WSL2/WSLg** | Host socket, passed straight through | +| **macOS** + **XQuartz** | Forwarded over TCP via `host.docker.internal` & rendered with VirtualGL | +| **Vanilla Windows** | Forwarded over TCP to VcXsrv & rendered with VirtualGL | +| **Linux** + **NVIDIA GPU** | Uses the [container toolkit](#nvidia-acceleration) and NVIDIA Docker runtime for faster rendering | -So on every platform you just use the single config below. + + + Open the repo in VS Code and accept the **Reopen in Container** prompt, or run **Dev + Containers: Reopen in Container** from the Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`). + Requires the [Dev Containers + extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers). + + -### VSCode + ```bash + devcontainer up + ``` -Open the cloned folder in VSCode. You should see a prompt to **Reopen in Container** in your bottom right — accept it, or run **Dev Containers: Reopen in Container** from the Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`). If the prompt doesn't appear, make sure you have the [extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed. + -### CLI + -```bash title="Terminal" -devcontainer up -``` - - - Ports and flags are defined in `docker/.env.defaults` (committed). To change them on your machine, put the keys you want in `docker/.env.local` (gitignored) — e.g. `FORCE_VNC=1`. The real `docker/.env` is regenerated from the defaults, your `.env.local`, and host detection on every `devcontainer up`, so don't edit it directly. - - - - This will take a long time if it is your first time (30 mins)! Docker builds Chrono and its dependencies (many lines of C++) from source, and then the same for Gazebo and ROS! Subsequent launches will be very fast though. + + Ports and flags & other configuration is in `docker/.env.defaults`. To change any, add it to + `docker/.env.local`. ## 2. Check if display works -Gazebo and RViz need a display to render their GUIs. When the Dev Container starts, a script detects your host setup and picks the best display path automatically. Check the terminal you launched the container from — the log line it prints tells you which case applies to you. - -### Linux host with Wayland (no VNC needed) - -If your host runs a Wayland compositor (GNOME, KDE, Hyprland, etc.), the log prints: - -``` -[X11] Using Wayland socket at /run/host-runtime/wayland-1 -``` - -The container forwards Wayland and X11 directly to your host compositor. GUI windows appear on your desktop natively — you can skip the VNC steps below. - -### Linux host with X11 (no VNC needed) - -If your host runs Xorg instead of Wayland, the container forwards X11 directly through the host's `/tmp/.X11-unix` socket, which is bind-mounted in automatically. The log prints: - -``` -[X11] Using host X11 display at :0 -``` - -GUI windows appear on your desktop natively — you can skip the VNC steps below. - -### WSL2 with WSLg (no VNC needed) - -WSL2 includes WSLg, a built-in Wayland compositor with X11 forwarding. Gazebo and RViz windows appear via WSLg automatically, the same as on native Linux with Wayland above. - -### macOS with XQuartz (no VNC needed) - -On macOS the container is auto-configured for XQuartz (see [step 1](#1-build-the-devcontainer)). GUI windows are forwarded over the X11 protocol to [XQuartz](https://www.xquartz.org) instead of VNC: - -1. Install XQuartz if you don't already have it: `brew install --cask xquartz`, or download it from [xquartz.org](https://www.xquartz.org). -2. Open XQuartz, go to **XQuartz > Settings > Security**, and check **"Allow connections from network clients"**. -3. Fully quit and reopen XQuartz (log out and back in, or restart) for that setting to take effect. -4. In a Mac Terminal, run `DISPLAY=:0 xhost + 127.0.0.1` to authorize the connection from Docker Desktop's VM. You'll need to run this again every time XQuartz restarts, since it isn't a persistent setting. - - - It's tempting to write `xhost -display :0 + 127.0.0.1`, but that doesn't do what it looks like. `xhost`'s own man page documents this as a bug: `-display` is parsed as "remove a host named `display`", not as a flag - `xhost` always connects using your shell's `$DISPLAY` env var instead, which may be stale (pointing at a dead XQuartz session) if you launched XQuartz manually rather than by opening the app normally. Set `DISPLAY` as an env var prefix on the command instead, as in step 4. - +Gazebo and RViz need a display. On container start, `.devcontainer/x_server.sh` picks the best +path for your host and prints which one it picked. Check the terminal you launched the +container from. + + + + Wayland or X11 is forwarded straight through - no VNC needed. The log prints one of: + + ``` + [X11] Using Wayland socket at /run/host-runtime/wayland-1 + [X11] Using host X11 display at :0 + ``` + + WSL2 uses the same path automatically via WSLg's built-in compositor. + + + GUI windows forward over X11 to [XQuartz](https://www.xquartz.org) instead of VNC. + + + Install XQuartz: `brew install --cask xquartz`, or from [xquartz.org](https://www.xquartz.org). + + Open **XQuartz > Settings > Security** and check **"Allow connections from + network clients"**, then fully quit and reopen XQuartz for it to take effect. + + + In a Mac terminal, run `DISPLAY=:0 xhost + 127.0.0.1` to authorize Docker + Desktop's VM. This resets every time XQuartz restarts, so re-run it after each + restart. + + + + + The log should print `[X11] Using host X11 display at host.docker.internal:0`. If it + falls back to Xvfb + noVNC instead, recheck steps 2-3, then check + `/tmp/start_x_server.log` inside the container. + + + + `XQuartz` can display `X11` windows but can't give them a working `OpenGL` context cause its + indirect `GLX` is deprecated and [unsupported + upstream](https://github.com/XQuartz/XQuartz/issues/144), so `OGRE2` (used by both + Gazebo and RViz) can't create a renderer over it. The container works around this + with [`VirtualGL`](https://virtualgl.org), where `GL` rendering happens against a headless X + server *inside* the container (Mesa `llvmpipe`), and only finished frames go to + `XQuartz` as plain `X11` images. + + + + Only needed without WSL2/WSLg, which uses the Linux path above instead. Requires [bash on + `PATH` (Git for Windows)](https://stackoverflow.com/questions/26620312/git-installing-git-in-path-with-github-client-for-windows) so the host script can run. + + + Install [VcXsrv](https://github.com/marchaesen/vcxsrv/releases/latest) + + Launch it with **network access enabled** - for VcXsrv's XLaunch: "Multiple + windows", display `0`, then check **"Disable access control"** on the Extra + Settings page. + + If Windows Defender Firewall prompts you, allow it on Private networks. + + + The log should print `[X11] Using host X11 display at host.docker.internal:0`. If it + falls back to Xvfb + noVNC, confirm the X server is running with access control disabled + and the firewall isn't blocking it. + + + `VcXsrv` can display `X11` windows but can't give them a working `OpenGL` context cause its + indirect `GLX` is deprecated, so `OGRE2` (used by both + Gazebo and RViz) can't create a renderer over it. The container works around this + with [`VirtualGL`](https://virtualgl.org), where `GL` rendering happens against a headless X + server *inside* the container (Mesa `llvmpipe`), and only finished frames go to + `XQuartz` as plain `X11` images. + + + + Used for headless servers, or when nothing above applies. `x_server.sh` starts a VNC + stack, picking a backend based on available hardware: + + ``` + [X11] Using vkms virtual display (/dev/dri/card1) + [X11] vkms unavailable, falling back to dummy driver (no DRI3) + [X11] Jetson/Tegra detected (no DRI), using Xvfb + EGL + ``` + + Once you see `[MAIN] All services started`, connect a VNC viewer + ([TigerVNC](https://tigervnc.org/), [RealVNC + Viewer](https://www.realvnc.com/en/connect/download/viewer/)) to `localhost:5900`, or + open `http://localhost:6080/vnc.html` for a quick browser check - noVNC doesn't forward + modifier keys correctly, so use a real client for regular use. + + If the script fails, it dumps `/tmp/start_x_server.log` to the terminal. + + + If you are using an NVIDIA GPU follow the [directions below](#nvidia-acceleration). If you have set up everything correctly you should see the following somewhere in the `x-server` output: + + ``` + [X11] Desktop NVIDIA GPU detected, using Xorg NVIDIA driver + ``` + + + This log line specifically is only printed in the VNC/headless fallback path, since it comes from the + container's own Xorg server picking a driver. It does **not** mean the NVIDIA runtime only matters for + VNC: `runtime: nvidia` is applied to the container in every display mode, and Vulkan/Chrono rendering + (`vkcube`, vsg-chrono) always benefits from it regardless of which display path is active. Wayland + passthrough in particular still renders client-side inside the container, so it also needs the runtime + for real GPU acceleration - only the X11 passthrough case can lean on the host's own GPU for rendering. + + + + + + +### Verify it works + +- **X11 (Gazebo/RViz):** run `xeyes` inside the container - a pair of animated eyes should + appear. +- **Vulkan (Chrono):** run `vkcube` inside the container - a spinning textured cube should + render. + +On macOS/Windows, `xeyes` only proves the X11 connection, not GL - check that separately: -Once the container starts, the log should print: - -``` -[X11] Using host X11 display at host.docker.internal:0 +```bash title="Container" +vglrun /opt/VirtualGL/bin/glxspheres64 ``` -If you instead see the container falling back to starting Xvfb + noVNC, double check steps 2-4 above, then check `/tmp/start_x_server.log` inside the container. - -If the display *is* reachable but `sim gazebo launch` (or similar) still fails with "Cannot connect to display", it now prints a diagnostic breakdown - DNS resolution, TCP reachability, and X11 authorization are checked separately, with specific fix suggestions for whichever layer is actually broken. - - - A working X11 connection isn't enough on its own to render Gazebo/RViz. XQuartz can display windows, but it can't give them a usable OpenGL context - its indirect GLX (IGLX) is deprecated and [unsupported upstream](https://github.com/XQuartz/XQuartz/issues/144), so OGRE2 (used by both Gazebo and RViz) fails to create a renderer at all. - - The container works around this with [VirtualGL](https://virtualgl.org), which is set up automatically - there is nothing to install on your Mac beyond XQuartz itself, and no extra ports to open. GL rendering happens against a headless X server *inside* the container (Mesa llvmpipe, OpenGL 4.5), and only the finished frames are sent to XQuartz as ordinary X11 images, which it handles reliably. `sim gazebo` wraps the launch in `vglrun` for you. +A window of spinning spheres should appear, and the terminal should report a frame rate with +`OpenGL Renderer: llvmpipe`. `vglrun: command not found` means rebuild the container; "can't open +the 3D X server" means run `bash .devcontainer/x_server.sh`. - You do **not** need to enable XQuartz's "Enable IGLX" / `enable_iglx` preference - VirtualGL deliberately avoids that path, and leaving it off is fine. - - Rendering is done in software (llvmpipe), so expect modest frame rates rather than GPU performance - macOS has no GPU passthrough into Docker. If rendering still fails, `sim gazebo` prints a warning explaining which piece is missing; setting `FORCE_VNC=1` in `docker/.env` and recreating the container is the fallback. + + `sim gazebo` prefixes `vglrun` for you automatically. Anything else you launch yourself that + uses OpenGL needs it too - `vglrun rviz2`, `vglrun gz sim`, etc. Plain X11 tools like `xeyes` + don't need it. This only applies on macOS/Windows; run normally on Linux/WSLg. -### Windows without WSL2/WSLg (no VNC needed) - -On native Windows the container is auto-configured for a TCP X server (see [step 1](#1-build-the-devcontainer)). If you're using WSL2 with Docker Desktop's WSL2 backend, none of this is needed - detection uses WSLg's socket automatically (see above). Native Windows detection requires bash on PATH (Git for Windows) so the host script can run. - -1. Install an X server that supports TCP connections: [VcXsrv](https://sourceforge.net/projects/vcxsrv) (free) or X410 (Microsoft Store, paid). -2. Launch it with network access enabled. For VcXsrv's XLaunch: choose "Multiple windows", display number `0`, then on the "Extra settings" page check **"Disable access control"**. -3. If Windows Defender Firewall prompts you, allow the X server on Private networks. +## 3. Launch the sim -Once the container starts, the log should print: +Once the display works, head to [Running Gazebo](../gazebo/gazebo) to launch your first sim. -``` -[X11] Using host X11 display at host.docker.internal:0 -``` +--- -If it falls back to Xvfb + noVNC instead, confirm the X server is running with access control disabled and that the firewall isn't blocking it. +# NVIDIA Acceleration - - Same as the macOS/XQuartz note above: VcXsrv/X410 can display windows but can't provide Gazebo/RViz (OGRE2) with a usable OpenGL context over indirect GLX. The container routes GL through [VirtualGL](https://virtualgl.org) automatically - rendering happens inside the container with Mesa llvmpipe and only finished frames are sent to your X server, so there's nothing extra to install on Windows and no extra ports to open. Rendering is in software, so expect modest frame rates. If it still doesn't render, `sim gazebo` warns upfront - `FORCE_VNC=1` in `docker/.env` plus a container recreate is the fallback. + + See the [official install + guide](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) + for other distros. -### All other cases (headless servers, or the above not configured) +If you have a Linux system with a NVIDIA GPU, you can use a special Docker runtime (`nvidia-container-toolkit`) to give the container hardware access to it - this speeds up both GL rendering (Gazebo/RViz) and Vulkan (Chrono) in every display mode, not just VNC. `host-env.sh` detects it automatically and enables it for you, but the runtime has to be installed and registered with Docker first. On a Debian-based distro, run this on your host: -On hosts without a reachable Wayland or X11 display, the script starts a VNC stack. Depending on what GPU or virtual display driver is available, you will see one of the following: +```bash +curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \ + | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg -``` -[X11] Desktop NVIDIA GPU detected, using Xorg nvidia driver -[X11] Using vkms virtual display (/dev/dri/card1) -[X11] vkms unavailable, falling back to dummy driver (no DRI3) -[X11] Jetson/Tegra detected (no DRI), using Xvfb + EGL -``` +distribution=$(. /etc/os-release; echo $ID$VERSION_ID) +curl -fsSL https://nvidia.github.io/libnvidia-container/libnvidia-container.list \ + | sed 's|^deb |deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] |' \ + | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list > /dev/null -followed by: - -``` -[noVNC] Desktop available at: http://localhost:6080/vnc.html -[MAIN] All services started +sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit +sudo nvidia-ctk runtime configure --runtime=docker +sudo systemctl restart docker ``` -Once you see `[MAIN] All services started`, connect with a VNC viewer at `localhost:5900`. You should see a blank desktop. - - - Good VNC viewers I would reccomend are [TigerVNC](https://tigervnc.org/) or [VNCViewer](https://www.realvnc.com/en/connect/download/viewer/?lai_sr=0-4&lai_sl=l) - - - - For a quick check without a VNC client, open `http://localhost:6080/vnc.html` in your browser. Note that noVNC doesn't forward modifier keys (Alt, Super, etc.) correctly, so a native VNC viewer is better for regular use. - - -If the script fails, it automatically dumps `/tmp/start_x_server.log` to the terminal to help you diagnose the problem. - -### Verify the display works - -Before launching the sim, confirm both display paths are functioning: +Verify Docker can see the GPU: -- **X11 (Gazebo / RViz):** Run `xeyes` inside the container. If a pair of animated eyes appears (in your VNC viewer or on your local desktop), X11 is working. -- **Vulkan (Chrono):** Run `vkcube` inside the container. If a spinning textured cube renders without errors, Vulkan is working and Chrono's VSG visualizer will function. - -On macOS/Windows, `xeyes` only proves the X11 connection works - it doesn't use OpenGL, so it can succeed while Gazebo/RViz still fail to render. To check the GL path as well, run this inside the container: - -```bash title="Container" -vglrun /opt/VirtualGL/bin/glxspheres64 +```bash +docker run --rm --runtime=nvidia --gpus all nvidia/cuda:12.0-base-ubuntu22.04 nvidia-smi ``` - -A window of spinning spheres should appear on your desktop, and the terminal should report a frame rate and `OpenGL Renderer: llvmpipe`. If instead you get `vglrun: command not found`, rebuild the container; if it reports that it can't open the 3D X server, run `bash .devcontainer/x_server.sh` to start it. - - - `sim gazebo` adds the `vglrun` prefix for you, but anything you launch yourself that renders with OpenGL needs it too - `vglrun rviz2`, `vglrun gz sim`, and so on. Without it, those apps go straight to XQuartz/VcXsrv's broken indirect GLX and fail to create a renderer. Plain X11 tools like `xeyes` don't need it. This only applies on macOS/Windows; on Linux and WSLg, run them normally. - - -## 3. Launch the sim - -Once the display is running, head to [Running Gazebo](../../guides/gazebo/) to launch your first sim. From c8330c0d931ac278221759a730cf123a8a664a7a Mon Sep 17 00:00:00 2001 From: Matej Stastny Date: Thu, 6 Aug 2026 21:43:32 -0700 Subject: [PATCH 29/38] docs: drop nvidia setup page, fold into docker guide --- docs/setup/nvidia.mdx | 62 ------------------------------------------- docs/setup/pixi.mdx | 28 ++++++++++--------- 2 files changed, 16 insertions(+), 74 deletions(-) delete mode 100644 docs/setup/nvidia.mdx diff --git a/docs/setup/nvidia.mdx b/docs/setup/nvidia.mdx deleted file mode 100644 index 388c6fb..0000000 --- a/docs/setup/nvidia.mdx +++ /dev/null @@ -1,62 +0,0 @@ ---- -title: Docker + Nvidia -description: Run the simulation with NVIDIA GPU passthrough in Docker. ---- - -This guide adds GPU passthrough to the standard Dev Container setup, giving Gazebo and Chrono direct access to your NVIDIA GPU. It works on any Linux machine with an NVIDIA GPU. - -## 1. Install NVIDIA Container Toolkit - - - The toolkit lets Docker access the NVIDIA driver on the host. The commands below are for **Ubuntu/Debian**. For other distros see the [official install guide](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html). - - -Run this on your host machine: - -```bash -curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \ - | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg - -distribution=$(. /etc/os-release; echo $ID$VERSION_ID) -curl -fsSL https://nvidia.github.io/libnvidia-container/libnvidia-container.list \ - | sed 's|^deb |deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] |' \ - | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list > /dev/null - -sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit -sudo nvidia-ctk runtime configure --runtime=docker -sudo systemctl restart docker -``` - -Verify the toolkit can see your GPU: - -```bash title="Terminal" -docker run --rm --runtime=nvidia --gpus all nvidia/cuda:12.0-base-ubuntu22.04 nvidia-smi -``` - -You should see your GPU listed in the output. - -## 2. Open the devcontainer - -There is no separate GPU config to pick. Once the toolkit above is installed, just open the **default** devcontainer the [normal way](../docker/#1-build-the-devcontainer) — on `devcontainer up` (or VS Code "Reopen in Container"), `.devcontainer/detect-host-env.sh` probes the host and, when it finds a working NVIDIA setup (`nvidia-smi` plus the `nvidia` runtime registered with Docker), enables `runtime: nvidia` and hands the GPU to the container. If the toolkit isn't installed, it silently stays on the default runtime — so this same config works with or without a GPU. - -```bash title="Terminal" -devcontainer up -``` - -You can confirm detection fired in the `devcontainer up` output — look for `[detect-host-env] host=Linux gpu=nvidia`. - -## 3. Launch the simulation - -Once inside the container, follow the normal [Docker guide](../docker/) from the display check onward. The GPU is available automatically — no extra flags needed when running `sim`. - ---- - -## Jetson-specific setup - -Jetson boards ship with NVIDIA drivers pre-installed via L4T. The Container Toolkit installation above still applies. - -After installing, configure the board for best simulation performance: - -- **Power mode:** Switch to MAXN (maximum performance) in `nvpmodel` or the power mode GUI -- **Fan speed:** Set fans to full speed with `jetson_clocks --fan` or the fan control utility -- **WiFi power management:** Disable with `sudo iw dev wlan0 set power_save off` to reduce latency if using the sim over SSH diff --git a/docs/setup/pixi.mdx b/docs/setup/pixi.mdx index 42c9e75..20cce26 100644 --- a/docs/setup/pixi.mdx +++ b/docs/setup/pixi.mdx @@ -1,14 +1,20 @@ --- -title: Native Setup +title: Pixi Native description: Run the simulation natively using pixi. --- The native workflow installs ROS 2 Jazzy and Gazebo Harmonic into a self-contained environment inside the repository using [pixi](https://pixi.sh), not requiring Docker and using your system directly without another layer, producing better performance. -> **Supported platforms:** Linux (x86-64, ARM64), macOS (Intel and Apple Silicon), WSL2. + + + 1. This way of running the project does not support the Chrono sim, **only Gazebo**. See + [Docker](../../setup/docker). + + 2. Only the following platforms are supported: + - **Linux** (x86_64, ARM64) + - **MacOS** (Intel, Silicon) + - **WSL2** - - This way of running the project does not support the Chrono sim, only Gazebo! See [Docker](../../setup/docker) ## 1. Install pixi @@ -27,18 +33,16 @@ From the repo root, install all project dependencies using the following command pixi install ``` -This downloads ~3–5 GB on first run, and takes a while. Subsequent runs are instant. +This downloads ~3-5 GB on first run, and takes a while. Subsequent runs are instant as everything is already downloaded. All of the pixi environment data is located in the `.pixi` folder in the project root. ## 3. Launch a simulation +You can now launch the sim. It is the same as you would [normally would](../gazebo/gazebo) but all the commands are always prefixed by `pixi run` to use the pixi environment like so: + ```bash -pixi run sim gazebo native arm +pixi run sim gazebo ``` -This builds the ROS 2 workspace and launches 3 windows, Gazebo, RViz, and the Joint GUI. - -See [Running Gazebo](../../guides/gazebo/) for full CLI options and flags. - - - Do not use `pixi shell` as your regular working shell. It modifies library paths in a way that can break system tools like `git` on macOS. Use `pixi run ` instead. For example: `pixi run sim gazebo native arm`. + + Do not use `pixi shell` as your regular working shell. It modifies library paths in a way that can break system tools like `git`. Use `pixi run ` instead. For example: `pixi run sim gazebo native arm`. From b079e5ef7617effd77b8ce974485f3a8369933da Mon Sep 17 00:00:00 2001 From: Matej Stastny Date: Thu, 6 Aug 2026 21:43:34 -0700 Subject: [PATCH 30/38] docs: replace getting-started.mdx with index.mdx --- docs/getting-started.mdx | 21 --------------------- docs/index.mdx | 20 ++++++++++++++++++++ 2 files changed, 20 insertions(+), 21 deletions(-) delete mode 100644 docs/getting-started.mdx create mode 100644 docs/index.mdx diff --git a/docs/getting-started.mdx b/docs/getting-started.mdx deleted file mode 100644 index e6203aa..0000000 --- a/docs/getting-started.mdx +++ /dev/null @@ -1,21 +0,0 @@ ---- -title: Simulations -description: Clone the repository and pick your simulation environment. ---- - -This repository contains robot simulations for TrickFire Robotics. It provides two complementary environments: [**Gazebo Harmonic**](https://gazebosim.org/docs/harmonic/install/) (with [ROS 2 Jazzy](https://docs.ros.org/en/jazzy/index.html)) for simulating robot subsystems and joint control, and [**Project Chrono**](https://projectchrono.org/) for high-fidelity terrain and wheel-soil physics. Both are driven by a unified Python `sim` CLI that handles building and launching. - -## Clone the repository - -```bash -git clone https://github.com/TrickfireRobotics/simulations.git -cd simulations -``` - -## Choose your workflow - -| Environment | Platforms | Pros & Cons | Requirements | -| --------------------------------------- | --------------------- | -------------------------------- | ----------------------------------------------------------------------------- | -| [Native (pixi)](../setup/pixi/) | Linux, macOS, WSL2 | Performant, Gazebo sim only | [pixi](https://pixi.prefix.dev/latest/) | -| [Dev Container](../setup/devcontainer/) | Any OS with Docker | Consistent, everything will work | [Docker](https://www.docker.com/) & [Devcontainers](https://containers.dev/) | -| [Docker + Nvidia GPU](../setup/nvidia/) | Linux with Nvidia GPU | Docker but GPU-accelerated | [Linux + Docker + Nvidia GPU](https://docs.docker.com/engine/containers/gpu/) | diff --git a/docs/index.mdx b/docs/index.mdx new file mode 100644 index 0000000..1c50223 --- /dev/null +++ b/docs/index.mdx @@ -0,0 +1,20 @@ +--- +title: Simulations +description: Simulation systems for TrickFire Robotics +--- + +This repository contains robot simulations for TrickFire Robotics. It provides two complementary environments: [**Gazebo Harmonic**](https://gazebosim.org/docs/harmonic/install/) (with [ROS 2 Jazzy](https://docs.ros.org/en/jazzy/index.html)) for simulating robot subsystems and joint control, and [**Project Chrono**](https://projectchrono.org/) for high-fidelity terrain and wheel-soil physics. Both are driven by a unified Python `sim` CLI that handles building and launching. + +## Clone the repository + +```bash +git clone https://github.com/TrickfireRobotics/simulations.git +cd simulations +``` + +## Choose your workflow + +| Environment | Platforms | Notes | Requirements | +| -------------------------------------------- | ------------------ | ----------------------------------- | ---------------------------------------------------------------------------- | +| [Docker & Devcontainers](./setup/docker.mdx) | Any OS with Docker | Slower first build; Gazebo + Chrono | [Docker](https://www.docker.com/) & [Devcontainers](https://containers.dev/) | +| [Pixi Native](./setup/pixi.mdx) | Linux, macOS, WSL2 | Fastest; Gazebo only, no Chrono | [pixi](https://pixi.prefix.dev/latest/) | From b20e98a331258bee9fa8838c7e061661b26bb93e Mon Sep 17 00:00:00 2001 From: Matej Stastny Date: Thu, 6 Aug 2026 21:43:36 -0700 Subject: [PATCH 31/38] docs: consolidate reference docs into dev-notes.mdx --- docs/dev-notes.mdx | 230 ++++++++++++++++++++++++++ docs/reference/dev-notes.mdx | 95 ----------- docs/reference/docker-environment.mdx | 155 ----------------- 3 files changed, 230 insertions(+), 250 deletions(-) create mode 100644 docs/dev-notes.mdx delete mode 100644 docs/reference/dev-notes.mdx delete mode 100644 docs/reference/docker-environment.mdx diff --git a/docs/dev-notes.mdx b/docs/dev-notes.mdx new file mode 100644 index 0000000..7f6f326 --- /dev/null +++ b/docs/dev-notes.mdx @@ -0,0 +1,230 @@ +--- +title: Dev Notes +description: Internal notes, tips, and architectural decisions for contributors. +--- + +## Architecture decisions + +### Docker vs. pixi + +Both workflows produce the same ROS 2 Jazzy + Gazebo Harmonic environment. Docker is the primary, +recommended workflow; pixi is a supporting option for a faster Gazebo-only loop. + + + + ROS 2 + Gazebo + Chrono are notoriously painful to install and keep in sync natively, + especially across different OS versions. The Dev Container pins every dependency (ROS 2 + Jazzy, Gazebo Harmonic, VSG, Chrono, VirtualGL) inside a pre-built image, so it behaves the + same on Linux, macOS, and Windows, with or without a GPU. The display/GPU quirks that used + to make Docker painful - X11 forwarding, VirtualGL, NVIDIA runtime detection - are now + handled automatically by `.devcontainer/host-env.sh` and `.devcontainer/x_server.sh` (see + [below](#docker-environment)), so the container works out of the box. It's also the only + workflow that supports Chrono. + + + [pixi](https://pixi.sh) installs ROS 2 Jazzy and Gazebo Harmonic into a self-contained conda + environment inside the repo's `.pixi/` directory - no system-level ROS install, and deleting + the repo removes the environment cleanly. It's a faster loop than Docker since there's no + container layer, but it only supports Gazebo (not Chrono) and leaves you to work around any + OS-specific quirks yourself. Reach for it when you want the fastest Gazebo-only iteration; + use Docker for everything else, especially Chrono. + + + + + + [Gazebo Harmonic](https://gazebosim.org/docs/harmonic/) is the current LTS release of the new-generation Gazebo and + pairs with ROS 2 Jazzy which is used on the Apollo rover this sim is mainly used to test components of and for. It + replaces the old [Gazebo Fortress](https://gazebosim.org/docs/fortress/) that was LTS for ROS Humble Viator uses. + + + The `_description` / `_bringup` split is a ROS convention: + + - **`_description`** contains the robot model (URDF + meshes) - things that change when the CAD changes + - **`_bringup`** contains runtime config (launch files, controller YAML, RViz config) - things you tweak during development + + This separation means `sim gazebo update` can safely replace the description without + touching your launch customizations. See [Architecture](./gazebo/architecture) for the + full package layout. + + + + +## Docker environment + +The [Dev Container](./setup/docker) is built from two Dockerfiles that work together, plus a pair +of host-side scripts that configure display and GPU access before the container starts. + +### Image build + +`docker/vsg-chrono.Dockerfile` builds VSG and Chrono from source - a slow, rarely-changing C++ +build - and publishes it as `ghcr.io/trickfirerobotics/simulations-vsg-chrono` via +[CI](https://github.com/TrickfireRobotics/simulations/blob/main/.github/workflows/vsg-chrono.yml) +on every push to `main` that touches it (or on manual dispatch), for both `amd64` and `arm64`. +`docker/Dockerfile`'s `sim` stage then copies `/opt/vsg` and `/home/trickfire/chrono` out of that +image and builds everything else on top as `simulations:latest` - changing the app Dockerfile +never triggers a Chrono rebuild. + +| Section | Contents | +| ------------- | ------------------------------------------------------------------------ | +| BASE | Locale, `trickfire` user, a libdrm stub needed for Jetson Mesa rendering | +| VSG + CHRONO | Copied in from the `vsg-chrono` base image | +| GAZEBO | ROS 2 Jazzy + Gazebo Harmonic, `ros-gz`, controllers, RViz, xacro | +| VNC / DISPLAY | Headless X stack: Xorg/Xvfb, Openbox, x11vnc, noVNC | +| VirtualGL | GL proxying for remote (XQuartz/VcXsrv) displays | +| DEV TOOLING | SSH server, ruff, pre-commit, shfmt, clangd | + + + A `BUILD_JOBS` build arg controls C++ build parallelism (defaults to `nproc`). Override it in + `docker/.env.local` if a full-parallel build overwhelms your machine. (my Macbook Air reaches + 50°C normally doing a all 8-core build 😭) + + +### Host detection + +`.devcontainer/host-env.sh` runs as the devcontainer's `initializeCommand`, before the container +starts. It writes `docker/.env` (loaded by compose) from three sources, in order: + + + `docker/.env.defaults` - committed defaults (ports, etc.) + `docker/.env.local` - your machine-only overrides, gitignored + + Host/GPU detection - `DISPLAY`, `VGL_DISPLAY`, and `SIM_GPU_RUNTIME` based on `uname` and + whether `nvidia-smi` + the `nvidia` Docker runtime are both present + + + +| Variable | Set by | Purpose | +| ------------------------------------ | --------------------------- | ----------------------------------------------- | +| `DISPLAY` | host-env.sh | Host display to forward (local or TCP) | +| `VGL_DISPLAY` | host-env.sh (macOS/Windows) | VirtualGL's in-container 3D X server | +| `SIM_GPU_RUNTIME` | host-env.sh | `nvidia` or empty - feeds `runtime:` in compose | +| `WAYLAND_DISPLAY`, `XDG_RUNTIME_DIR` | docker-compose.yml | Wayland socket passthrough for Vulkan (Chrono) | + +### Display architecture + +`.devcontainer/x_server.sh` runs as `postStartCommand`, after the container is up, and picks the +cheapest option that works: + +| Situation | What happens | +| --------------------------------------------------- | -------------------------------------------------------------------------- | +| Host Wayland socket (`/run/host-runtime`) reachable | Used directly - no extra services started | +| Host X11 socket (`/tmp/.X11-unix`) reachable | Used directly - no extra services started | +| Neither reachable | Falls back to a self-contained stack: Xorg/Xvfb + Openbox + x11vnc + noVNC | + + + Over TCP it additionally starts a headless VirtualGL 3D X server on `:88` - that's what `vglrun` + (used automatically by `sim gazebo`) renders against, so Gazebo/RViz get a real GL context even + though the host X server can't provide one. See [Docker + setup](./setup/docker#2-check-if-display-works) for the per-OS walkthrough. + + +When falling back to VNC, the backend depends on available hardware: + +- Xorg with the NVIDIA driver on desktop GPUs +- `vkms` for a virtual DRI3-capable display when no GPU is present +- Xvfb + EGL on Jetson/Tegra (no `/dev/dri`) + +Chrono's Vulkan rendering uses the Wayland socket when present, independent of this X11 path. + +### Container user + +Runs as non-root `trickfire` (uid 1000) with passwordless sudo, so files created in the +bind-mounted repo stay owned by your host user. + +### Extending the container + + + + Edit `docker/Dockerfile` (or `docker/vsg-chrono.Dockerfile` for the VSG/Chrono base) to add + system packages. + + Rebuild with **Dev Containers: Rebuild Container**. + + + + Python packages for simulation-time use go in the `pip3 install` line in the GAZEBO section; + dev-only tools go in DEV TOOLING. + + +## Useful CLI commands + +### Gazebo camera position + +To set the camera position in the Gazebo viewer: + +```bash +gz service -s /gui/move_to/pose \ + --reqtype gz.msgs.GUICamera \ + --reptype gz.msgs.Boolean \ + --timeout 2000 \ + --req "pose: {position: {x: 0.0, y: -2.0, z: 2.0} orientation: {x: -0.2706, y: 0.2706, z: 0.6533, w: 0.6533}}" +``` + +To read the current camera position: + +```bash +gz topic -e -t /gui/camera/pose +``` + +### ROS 2 inspection + +```bash +# List all topics +ros2 topic list + +# See joint states in real time +ros2 topic echo /joint_states + +# List active controllers +ros2 control list_controllers + +# Check controller manager status +ros2 control list_hardware_interfaces +``` + +### Building a single package + +```bash +cd gazebo +colcon build --packages-select arm_description +source install/setup.bash +``` + +## Community `apt` repository (Dev Container) + +Some ROS packages live in the `universe` repository rather than `main`. If `apt` can't find a package inside the container: + +```bash title="Inside devcontainer" +apt-get update +apt-get install -y software-properties-common +add-apt-repository -y universe +apt-get update +``` + + + The Dockerfile already enables `universe` for the packages that need it. + + +## Common pitfalls + + + + ROS 2 can't find packages until you run `source gazebo/install/setup.bash`. The launch + script does this automatically, but if you're running ROS commands manually, you need to + source first. + + + If something breaks for no obvious reason, run `sim gazebo clean` and rebuild. Colcon's + incremental builds can get confused after certain types of changes. + + + Gazebo and RViz will fail silently or crash if the X server isn't started. The Dev Container + starts it automatically, but if you need to restart it manually, run + `.devcontainer/x_server.sh`. + + + If port 6080 or 5900 is already in use, the X server script will fail. Make sure no other + VNC sessions or containers are using those ports. + + diff --git a/docs/reference/dev-notes.mdx b/docs/reference/dev-notes.mdx deleted file mode 100644 index 533f8c9..0000000 --- a/docs/reference/dev-notes.mdx +++ /dev/null @@ -1,95 +0,0 @@ ---- -title: Dev Notes -description: Internal notes, tips, and architectural decisions for contributors. ---- - -## Architecture decisions - -### Why pixi for the native workflow? - -ROS 2 + Gazebo + their dependencies are notoriously painful to install natively, especially across different OS versions. [pixi](https://pixi.sh) solves this by storing the entire environment (ROS 2 Jazzy, Gazebo Harmonic, all dependencies) inside the repo's `.pixi/` directory via conda. Deleting the repo also deletes the environment - your system stays completely clean. The same `pixi install` works on Linux, macOS, and WSL2 without any OS-level ROS installation. - -### Why a Dev Container (alternative)? - -The Docker Dev Container is an alternative for users who prefer a containerized environment or already have Docker set up. It provides the same ROS 2 Jazzy + Gazebo Harmonic environment inside a container. The Dev Container is useful for Windows users (via Docker Desktop), CI systems, or anyone who wants strong isolation from their host system. - -### Why Gazebo Harmonic? - -Gazebo Harmonic (gz-sim 8.x) is the current LTS release of the new-generation Gazebo and pairs with ROS 2 Jazzy. It replaces the old Gazebo Fortress (ignition-fortress). The package prefix changed from `ign` to `gz` in this generation - use `gz topic`, `gz service`, etc. - -### Why two packages per robot? - -The `_description` / `_bringup` split is a ROS convention: -- **`_description`** contains the robot model (URDF + meshes) -- things that change when the CAD changes -- **`_bringup`** contains runtime config (launch files, controller YAML, RViz config) -- things you tweak during development - -This separation means `sim gazebo update` can safely replace the description without touching your launch customizations. - -## Useful CLI commands - -### Gazebo camera position - -To set the camera position in the Gazebo viewer: - -```bash title="Terminal" -gz service -s /gui/move_to/pose \ - --reqtype gz.msgs.GUICamera \ - --reptype gz.msgs.Boolean \ - --timeout 2000 \ - --req "pose: {position: {x: 0.0, y: -2.0, z: 2.0} orientation: {x: -0.2706, y: 0.2706, z: 0.6533, w: 0.6533}}" -``` - -To read the current camera position: - -```bash title="Terminal" -gz topic -e -t /gui/camera/pose -``` - -### ROS 2 inspection - -```bash title="Terminal" -# List all topics -ros2 topic list - -# See joint states in real time -ros2 topic echo /joint_states - -# List active controllers -ros2 control list_controllers - -# Check controller manager status -ros2 control list_hardware_interfaces -``` - -### Building a single package - -```bash title="Terminal" -cd gazebo -colcon build --packages-select arm_description -source install/setup.bash -``` - -## Community `apt` repository (Dev Container) - -Some ROS packages live in the `universe` repository rather than `main`. If `apt` can't find a package inside the container: - -```bash title="Inside devcontainer" -apt-get update -apt-get install -y software-properties-common -add-apt-repository -y universe -apt-get update -``` - - - The Dockerfile already enables `universe` for the packages that need it. - - -## Common pitfalls - -**Forgetting to source after build:** ROS 2 can't find packages until you run `source gazebo/install/setup.bash`. The launch script does this automatically, but if you're running ROS commands manually, you need to source first. - -**Stale build artifacts:** If something breaks for no obvious reason, run `sim gazebo clean` and rebuild. Colcon's incremental builds can get confused after certain types of changes. - -**X server not running (Dev Container only):** Gazebo and RViz will fail silently or crash if the X server isn't started. The Dev Container starts it automatically, but if you need to restart it manually, run `.devcontainer/x_server.sh`. - -**Port conflicts (Dev Container only):** If port 6080 or 5900 is already in use, the X server script will fail. Make sure no other VNC sessions or containers are using those ports. diff --git a/docs/reference/docker-environment.mdx b/docs/reference/docker-environment.mdx deleted file mode 100644 index dc234dd..0000000 --- a/docs/reference/docker-environment.mdx +++ /dev/null @@ -1,155 +0,0 @@ ---- -title: Docker Environment -description: How the container is configured, the Dockerfile sections, standalone docker-compose usage, and how the headless display works. ---- - -The Docker container is the alternative to the [native pixi workflow](../setup/macos/). There are two ways to use it: - -1. **Dev Container** (VS Code) -- Open the repo in VS Code and choose **Reopen in Container**. -2. **Standalone container** (docker-compose) -- Run simulations on any host without VS Code. NVIDIA GPU passthrough is enabled automatically when hardware is detected. - -Both methods use the same `sim` image built from `docker/Dockerfile`. - -## Dockerfile - -The Dockerfile at `docker/Dockerfile` produces a single stage named `sim` built on Ubuntu 24.04. It is divided into labelled sections: - -``` -ubuntu:24.04 AS sim - ├── BASE - locale, user setup, libdrm stub - ├── VSG - VulkanSceneGraph stack (built from source, /opt/vsg) - ├── CHRONO - Project Chrono (built from source, /home/trickfire/chrono) - ├── GAZEBO - ROS 2 Jazzy + Gazebo Harmonic - ├── VNC/DISPLAY - headless GUI stack - └── DEV TOOLING - SSH server, ruff, pre-commit, shfmt -``` - -A `BUILD_JOBS` build arg controls parallelism for the C++ builds (defaults to `nproc`). - -### VSG section - -VulkanSceneGraph and its dependencies are built from source and installed to `/opt/vsg`: - -| Library | Version | Purpose | -| -------------------------- | -------- | -------------------------------------------- | -| glslang | 16.1.0 | GLSL shader compiler (runtime library) | -| KTX-Software | v4.4.2 | Texture format support | -| draco | 1.5.7 | Mesh compression | -| assimp | v6.0.5 | 3D asset import | -| VulkanSceneGraph (vsg) | v1.1.15 | Vulkan-based scene graph rendering | -| vsgXchange | v1.1.12 | Asset format bridge for vsg (uses assimp) | -| vsgImGui | v0.7.0 | ImGui integration for vsg overlays | - -`LD_LIBRARY_PATH` is set to `/opt/vsg/lib` so the shared libraries are found at runtime. - -### Chrono section - -Project Chrono is cloned from source and built with the Vehicle and VSG modules enabled: - -``` -CH_ENABLE_MODULE_VEHICLE - wheel/terrain dynamics -CH_ENABLE_MODULE_VSG - Vulkan-based visualizer -``` - -The build target is `demo_VEH_SCMTerrain_RigidTire`. An upstream bug in `SCMTerrain.cpp` (domain list not cleared before `AddActiveDomain`) is patched at image build time. - -The Chrono source and build live at `/home/trickfire/chrono` and are owned by the `trickfire` user. - -### Gazebo section - -**Simulation stack:** - -| Package | Purpose | -| -------------------------------------- | ----------------------------------------------- | -| Gazebo Harmonic | Physics simulation engine | -| `ros-jazzy-ros-gz` | ROS 2 / Gazebo integration + bridge | -| `ros-jazzy-ros2-controllers` | Joint state broadcaster + trajectory controller | -| `ros-jazzy-gz-ros2-control` | Hardware interface for Gazebo | -| `ros-jazzy-rviz2` | Robot visualization | -| `ros-jazzy-xacro` | URDF macro processing | -| `ros-jazzy-joint-state-publisher-gui` | Joint state publishing GUI | - -**Python packages:** `pypresence`, `trimesh`, `pyfqmr` - -### VNC / display section - -**Display stack (headless GUI):** - -| Package | Purpose | -| ------------------------------------------------ | --------------------------------------------- | -| `xserver-xorg-core` + `xserver-xorg-video-dummy` | Virtual X server with dummy driver (no GPU) | -| `openbox` | Lightweight window manager | -| `x11vnc` | VNC server for remote X11 access | -| `novnc` + `websockify` | Browser-based VNC client | -| `xvfb` | Virtual framebuffer for Jetson/Tegra (no DRI) | -| `mesa-utils`, `libgl1-mesa-dri` | Software OpenGL rendering | -| `kmod` | Kernel module tools (for `/lib/modules` mount)| -| `x11-apps` | X11 utilities including `xeyes` | - -### Dev tooling section - -| Package/tool | Purpose | -| -------------- | -------------------------------------------------------- | -| `openssh-server` | SSH server (used by VS Code remote connection) | -| `ruff` | Python linter/formatter | -| `pre-commit` | Git hook framework | -| `shfmt` | Shell script formatter | - -## Container user - -The container runs as a non-root user `trickfire` with passwordless sudo: - -```dockerfile -RUN useradd trickfire --uid 1000 --shell /bin/bash --create-home --no-log-init -RUN echo "trickfire ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/user -``` - -## Dev Container configuration - -The `devcontainer.json` configures how VS Code opens the container: - -**Build target:** `sim` stage of `docker/Dockerfile` - -**Workspace mount:** The repo is bind-mounted into the container at `/home/trickfire/simulations`. - -**Compose config:** `.devcontainer/devcontainer.json` uses `docker/docker-compose.yml` plus `docker/docker-compose-dev.yml`. The override adds Wayland and display environment variables for Vulkan rendering. - -**Port forwarding:** Ports `6080` (noVNC) and `5900` (VNC) are forwarded to the host. - -**Privileged mode:** The container runs with `--privileged` and device access for hardware interaction (USB, CAN bus). - -**VS Code extensions:** Pre-installs Python, C++ (clangd, CMake, Makefile), ROS, URDF, Docker, Prettier, and other formatting extensions. - -## X-server Display Architecture - -``` -Xorg/Xvfb → x11vnc → websockify → Browser - ↑ ↑ - Openbox WM noVNC client - ↑ (port 6080) - Gazebo / RViz / Chrono VSG -``` - -### Why this approach? - -Alternatives like Xvfb don't support GLX properly, which means Gazebo's 3D rendering fails. Using Xorg with a dummy driver + Mesa software rendering gives us full OpenGL support without needing a real GPU. The VNC + noVNC layer makes it accessible from any browser. On Jetsons, we fall back to Xvfb because there's no `/dev/dri`, but GPU rendering still works via EGL through injected Tegra libs. - -Chrono uses Vulkan for rendering via the VSG module. Wayland socket passthrough (`WAYLAND_DISPLAY`, `XDG_RUNTIME_DIR`) is exposed to the container so Vulkan can reach the host compositor when available. - -### Environment variables - -| Variable | Value | Set in | -| ------------------ | ---------------------- | ----------------------------------- | -| `DISPLAY` | Host-dependent | `docker-compose.yml` (standalone) | -| `WAYLAND_DISPLAY` | Host-dependent | `docker-compose-dev.yml` | -| `XDG_RUNTIME_DIR` | `/run/host-runtime` | `docker-compose-dev.yml` | -| `VNC_PORT` | `5900` | Dockerfile | -| `NOVNC_PORT` | `6080` | Dockerfile | -| `VSG_INSTALL_DIR` | `/opt/vsg` | Dockerfile | -| `LD_LIBRARY_PATH` | `/opt/vsg/lib` | Dockerfile | - -## Extending the container - -To add system packages, edit `docker/Dockerfile` and rebuild. For Python packages, add them to the `pip3 install` line in the Gazebo section (simulation-time) or the dev tooling section (development-only tools). - -After changing the Dockerfile, use **Dev Containers: Rebuild Container** in VS Code's Command Palette. From 98d371b873b5d751af5436a3a64298944ab5059e Mon Sep 17 00:00:00 2001 From: Matej Stastny Date: Thu, 6 Aug 2026 21:43:40 -0700 Subject: [PATCH 32/38] docs: consolidate gazebo internals docs into architecture.mdx --- docs/gazebo/architecture.mdx | 274 +++++++++++++++++++++++++++++++++ docs/gazebo/joint-gui.mdx | 88 ----------- docs/gazebo/launch-system.mdx | 175 --------------------- docs/gazebo/robot-packages.mdx | 96 ------------ docs/gazebo/ros-workspace.mdx | 89 ----------- 5 files changed, 274 insertions(+), 448 deletions(-) create mode 100644 docs/gazebo/architecture.mdx delete mode 100644 docs/gazebo/joint-gui.mdx delete mode 100644 docs/gazebo/launch-system.mdx delete mode 100644 docs/gazebo/robot-packages.mdx delete mode 100644 docs/gazebo/ros-workspace.mdx diff --git a/docs/gazebo/architecture.mdx b/docs/gazebo/architecture.mdx new file mode 100644 index 0000000..095d0b5 --- /dev/null +++ b/docs/gazebo/architecture.mdx @@ -0,0 +1,274 @@ +--- +title: Architecture +description: How the gazebo/ colcon workspace is laid out, how sim gazebo builds it, and how the launch files orchestrate simulation startup. +--- + +`gazebo/` at the repo root is a standard [ROS 2](https://docs.ros.org/en/jazzy/index.html) [colcon](https://colcon.readthedocs.io/) workspace. Every robot, plus a couple of shared packages, lives directly under it as a colcon package. + +``` +gazebo/ +├── _description/ # URDF + meshes from OnShape +├── _bringup/ # arm launch file + configs +├── sim_common/ # shared Python nodes & launch helpers +├── sim_worlds/ # world SDFs + Gazebo GUI config +├── build/ ┐ +├── install/ ├─ generated by colcon, gitignored +└── log/ ┘ +``` + + + Every robot splits into a `_description` package containing the [URDF](https://en.wikipedia.org/wiki/URDF) and meshes. + Then a `_bringup` package with the launch file and configs. + + +## Package types + +| Package | Build type | Contains | +| --------------------- | -------------- | --------------------------------------------------- | +| `_description` | `ament_cmake` | `urdf/`, `meshes/` | +| `_bringup` | `ament_cmake` | `launch/`, `config/` (controller YAML, RViz config) | +| `sim_worlds` | `ament_cmake` | `worlds/` (SDF files), `gui/` (Gazebo GUI config) | +| `sim_common` | `ament_python` | Shared launch helpers and standalone ROS 2 nodes | + + + The difference between `ament_cmake` and `ament_python` packages is in its project managers. + `ament_cmake` uses + [CMake](https://cmake.org/cmake/help/book/mastering-cmake/chapter/Writing%20CMakeLists%20Files.html) + with the `CMakeLists.txt` file while `ament_python` uses [Setup + Tools](https://setuptools.pypa.io/en/latest/userguide/declarative_config.html) with the + `setup.cfg` file. + + +## `sim_common` + +This is a shared package containing cross-package repetitive code & also independent ROS nodes like the joint GUI. Its `setup.py` registers three console scripts: + +| Command | Source | Purpose | +| ------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `move_joints` | `sim_common/move_joints.py` | Publishes a single `JointTrajectory` and exits. See [Moving Joints](./moving-joints#move_joints-node-cli). | +| `joint_gui` | `sim_common/joint_gui.py` | Tkinter joint slider GUI. See [Moving Joints](./moving-joints#joint-gui) for usage, or [Joint GUI internals](#joint-gui-internals) below for how it's implemented. | +| `drivebase` | `sim_common/drivebase.py` | Sim equivalent of the real rover's drivebase node - converts joystick deflection messages into wheel velocity commands, mirroring `urc-2023/src/drivebase`. | + +It also exports `sim_common/launch_utils.py`, a library (not a node) of helpers every `.launch.py` imports - `get_asset()` for resolving files in a package's share directory, plus node/launch-action builders for the Gazebo server, spawning, controllers, and the ROS-Gazebo bridge. See [launch_utils.py](#launch_utilspy) below for what each helper does. + +## `sim_worlds` + +Holds the environment the robot spawns into: + +- `worlds/empty.world.sdf` - the default world, passed to `gz_sim.launch.py` as `-r ` +- `gui/gui.config` and `gz_gui.xml` - the Gazebo GUI layout (camera view, panels) + +Its `share/` install path is added to `GZ_SIM_RESOURCE_PATH` at launch time so Gazebo can find world assets - see the `SIM_WORLDS_SHARE` handling in `cli/gazebo/launch.py`. + +## `robots.json` + +The repo-root `robots.json` is the CLI's registry of available robots. For each one the object includes these fields: + +| Field | Description | +| ----------------- | --------------------------------------------------------------------------------- | +| `name` | Robot name, must match the `_description` / `_bringup` package prefix | +| `url` | OnShape assembly URL the packages were generated from | +| `world_base_link` | Whether `base_link` is fixed to the world | + +New entries are added automatically by `sim gazebo create` - see [Adding a New Robot](./adding-robots). + +## Building + +`sim gazebo ` (see [Running Gazebo](./gazebo)) drives colcon for you, under the hood it uses a command like this: + +```bash +colcon build --packages-up-to _bringup _description sim_worlds sim_common \ + --cmake-args -DBUILD_TESTING=OFF +``` + +`--packages-up-to` builds only the requested robot's packages plus their dependencies (not every robot in the workspace), so switching robots doesn't force a full rebuild. By default it also deletes `build/`, `install/`, and `log/` first - equivalent to running `sim gazebo clean` - for a clean rebuild every launch; pass `--no-build` to skip building entirely and reuse the existing `install/`. + + + ```bash + cd gazebo + colcon build --packages-select arm_description + source install/setup.bash + ``` + + Useful when iterating on one package without going through the CLI. Remember to `source install/setup.bash` afterwards - ROS 2 won't find the package otherwise. See [Dev Notes](../dev-notes#building-a-single-package) for more. + + + + + All three are gitignored. Don't edit anything inside them - they're wiped on every build. If + colcon behaves strangely after unrelated changes, delete them and rebuild with `sim gazebo + clean`. + + +## Launch system + +Each robot has a launch file at `gazebo/_bringup/launch/.launch.py` that orchestrates the full simulation startup. These are the commands the sim CLI does under the hood. + +### Flags + +| Argument | Default | Description | +| -------- | ------- | -------------------------------------- | +| `rviz` | `true` | Open RViz with the robot's config file | +| `gui` | `true` | Open the Joint GUI | + +These are set via command line: + +```bash +ros2 launch arm_bringup arm.launch.py gui:=false rviz:=false +``` + +### Startup sequence + +The launch file orchestrates several components with specific ordering dependencies: + + + **Gazebo Simulator** - starts `gz_sim` with the world file and GUI config + + **Spawn Robot + Robot State Publisher** (parallel) - spawns the URDF model into Gazebo and + starts `robot_state_publisher` with the URDF + + + **Joint State Broadcaster** - starts reading joint states from Gazebo once the spawn + finishes. + + + **Joint Trajectory Controller** - accepts trajectory commands and drives joints, once the + broadcaster is ready. + + + **ROS-Gazebo Bridge, RViz, Joint GUI** (all in parallel) - bridges `/clock` between Gazebo + and ROS, opens RViz with the saved config, and opens the Joint GUI with the URDF file. + + + +The robot spawn uses a `TimerAction` with a 5-second delay to give Gazebo time to fully initialize before the model is spawned. Steps 3 and 4 use `RegisterEventHandler` with `OnProcessExit` to enforce ordering -- the trajectory controller can't start until the state broadcaster is ready, and the broadcaster can't start until the robot is spawned. + +### Components + +#### Gazebo simulation + +```python +gz_sim = IncludeLaunchDescription( + PythonLaunchDescriptionSource( + os.path.join(get_package_share_directory("ros_gz_sim"), "launch", "gz_sim.launch.py") + ), + launch_arguments={ + "gz_args": " ".join(["-r", world_file, "--gui-config", gz_gui_config]) + }.items(), +) +``` + +Launches Gazebo Harmonic with: + +- `-r` -- start running immediately (not paused) +- The world SDF file from `sim_worlds` (default: `empty.world.sdf`) +- A custom GUI config from `sim_worlds/gui/gui.config` + +#### URDF processing + +```python +robot_desc = xacro.process_file( + urdf_file, + mappings={"controller_config": controller_config}, +).toxml() +``` + +The URDF is processed through xacro at launch time. The `controller_config` mapping passes the path to the controller YAML so the control xacro can reference it. The result is a fully resolved URDF string used for both spawning and state publishing. + +#### Robot spawn + +```python +spawn_robot = Node( + package="ros_gz_sim", + executable="create", + arguments=["-name", "arm", "-string", robot_desc, "-x", "0", "-y", "0", "-z", "0.1"], +) +``` + +Spawns the robot into the Gazebo world from the processed URDF string. The `-z 0.1` offset prevents the robot from spawning inside the ground plane. + +#### Robot state publisher + +```python +robot_state_publisher = Node( + package="robot_state_publisher", + executable="robot_state_publisher", + parameters=[{"robot_description": robot_desc}], +) +``` + +Publishes the robot's TF tree and makes the URDF available on the `/robot_description` topic. RViz uses this to render the robot model. + +#### Controllers + +Two controllers are spawned in sequence: + +1. **`joint_state_broadcaster`** -- reads joint states from Gazebo hardware interfaces and publishes them to `/joint_states` +2. **`joint_trajectory_controller`** -- listens for `JointTrajectory` messages on `/joint_trajectory_controller/joint_trajectory` and commands Gazebo to move the joints + +The controller configuration lives in `config/.controller.yaml`: + +```yaml +controller_manager: + ros__parameters: + use_sim_time: true + update_rate: 60 # Hz + +joint_trajectory_controller: + ros__parameters: + joints: + - shoulder_1 + - elbow_1 + - wrist_1 + - wrist_2 + command_interfaces: + - position + state_interfaces: + - position + - velocity +``` + +#### ROS-Gazebo bridge + +```python +bridge = Node( + package="ros_gz_bridge", + executable="parameter_bridge", + arguments=["/clock@rosgraph_msgs/msg/Clock[gz.msgs.Clock"], +) +``` + +Bridges the `/clock` topic from Gazebo to ROS 2. This is essential for `use_sim_time: true` -- without it, ROS nodes won't have synchronized time with the simulation. + +#### RViz + +Launches RViz2 with a saved config file. Conditional on the `rviz` launch argument. + +#### Joint GUI + +Launches the Tkinter joint control GUI with the URDF file path as an argument. Conditional on the `gui` launch argument. See [Moving Joints](./moving-joints#joint-gui) for how to use it, or [Joint GUI internals](#joint-gui-internals) below for how it's implemented. + +### launch_utils.py + +The `sim_common` package provides a `get_asset()` helper used throughout launch files: + +```python +from sim_common.launch_utils import get_asset + +controller_config = get_asset("arm_bringup", "config", "arm.controller.yaml") +``` + +It resolves a file path inside a ROS 2 package's share directory and exits with an error if the file doesn't exist. This catches missing files early rather than failing mid-launch. + + + + If you need to customize a robot's launch beyond what `sim gazebo create` generates, edit `gazebo/_bringup/launch/.launch.py`. The file is standard ROS 2 launch Python - you can add nodes, change parameters, or modify the startup sequence. + + Common customizations: + + - Adding sensor bridges (cameras, lidar) + - Changing spawn position + - Adding additional ROS 2 nodes + - Modifying controller parameters + + diff --git a/docs/gazebo/joint-gui.mdx b/docs/gazebo/joint-gui.mdx deleted file mode 100644 index 8331798..0000000 --- a/docs/gazebo/joint-gui.mdx +++ /dev/null @@ -1,88 +0,0 @@ ---- -title: Joint GUI -description: Reference for the Tkinter-based joint control GUI that launches with the simulation. ---- - -The Joint GUI is a Tkinter application that provides interactive slider-based control of robot joints during simulation. It launches automatically as part of the ROS 2 launch file. - -## How it works - -### Startup sequence - -``` -1. Parse the URDF for all revolute joints - → Extract joint names, lower/upper limits - -2. Subscribe to /joint_states - → Wait for first message to discover active joints - → Record origin angles for each joint - -3. Build the GUI - → Create sliders with limits from the URDF - → Auto-discover available JointTrajectory topics - -4. Run two threads - → Main thread: Tkinter event loop (UI updates) - → Background thread: ROS 2 spin (message callbacks) -``` - -### Joint discovery - -The GUI doesn't hardcode joint names. Instead: - -1. It parses the robot's URDF file (passed as an argument) for all joints of type `revolute` -2. It subscribes to `/joint_states` and waits for the first message to arrive -3. The joints reported in that message become the active joints shown in the GUI - -This means the GUI works with any robot -- it adapts to whatever joints exist. - -### Origin angles - -When joints are first discovered, their current positions (from `/joint_states`) are saved as "origin angles." Slider values are offsets from these origins. When you hit **Send**, the published position is `slider_value + origin_angle`. - -This matters because joints may not start at position 0 -- the origin angle ensures the slider's zero position matches the robot's natural resting state. - -## Controls - -| Control | Description | -| --- | --- | -| **Topic dropdown** | Select which `JointTrajectory` topic to publish on. Auto-populated from available topics. | -| **Refresh** | Re-scan for available trajectory topics | -| **Joint sliders** | One per joint. Range is set by the URDF's `` values. | -| **Duration** | Time in seconds for the trajectory to complete (default: 2.0s) | -| **Send** | Publish current slider positions as a trajectory command | -| **Sync** | Read current positions from `/joint_states` and update sliders to match | -| **Reset** | Zero all sliders (back to origin position) | - -## ROS 2 interface - -**Subscribes to:** -- `/joint_states` (`sensor_msgs/msg/JointState`) -- reads current joint positions - -**Publishes to:** -- Configurable trajectory topic (default: `/joint_trajectory_controller/joint_trajectory`) -- Message type: `trajectory_msgs/msg/JointTrajectory` - -## Running standalone - -The GUI is registered as a console script entry point. You can run it outside of the launch file: - -```bash title="Inside devcontainer" -ros2 run sim_common joint_gui -``` - -For example: - -```bash title="Inside devcontainer" -ros2 run sim_common joint_gui \ - $(ros2 pkg prefix arm_description)/share/arm_description/urdf/arm.urdf -``` - -The simulation must already be running with controllers active for the GUI to discover joints. - -## Source - -The implementation is in `gazebo/sim_common/sim_common/joint_gui.py`. Key classes: - -- **`JointPublisher`** (ROS 2 Node) -- handles URDF parsing, joint state subscription, topic discovery, and trajectory publishing -- **`JointGui`** (Tkinter) -- builds the UI, manages sliders, and dispatches commands to `JointPublisher` diff --git a/docs/gazebo/launch-system.mdx b/docs/gazebo/launch-system.mdx deleted file mode 100644 index aeca8fa..0000000 --- a/docs/gazebo/launch-system.mdx +++ /dev/null @@ -1,175 +0,0 @@ ---- -title: Launch System -description: How the ROS 2 launch files orchestrate Gazebo, controllers, RViz, and the Joint GUI. ---- - -Each robot has a launch file at `gazebo/_bringup/launch/.launch.py` that orchestrates the full simulation startup. This page explains the architecture using the arm as an example. - -## Launch arguments - -| Argument | Default | Description | -| --- | --- | --- | -| `rviz` | `true` | Open RViz with the robot's config file | -| `gui` | `true` | Open the Joint GUI | - -These are set via command line: - -```bash title="Terminal" -ros2 launch arm_bringup arm.launch.py gui:=false rviz:=false -``` - -## Startup sequence - -The launch file orchestrates several components with specific ordering dependencies: - -``` -1. Gazebo Simulator - Start gz_sim with the world file and GUI config - ↓ -2. Spawn Robot + Robot State Publisher (parallel) - - Spawn the URDF model into Gazebo - - Start robot_state_publisher with the URDF - ↓ (waits for spawn to finish) -3. Joint State Broadcaster - Starts reading joint states from Gazebo - ↓ (waits for broadcaster to finish) -4. Joint Trajectory Controller - Accepts trajectory commands and drives joints - -5. ROS-Gazebo Bridge, RViz, Joint GUI (parallel) - - Bridge /clock between Gazebo and ROS - - Open RViz with saved config - - Open Joint GUI with URDF file -``` - -The robot spawn uses a `TimerAction` with a 5-second delay to give Gazebo time to fully initialize before the model is spawned. Steps 3 and 4 use `RegisterEventHandler` with `OnProcessExit` to enforce ordering -- the trajectory controller can't start until the state broadcaster is ready, and the broadcaster can't start until the robot is spawned. - -## Components - -### Gazebo simulation - -```python -gz_sim = IncludeLaunchDescription( - PythonLaunchDescriptionSource( - os.path.join(get_package_share_directory("ros_gz_sim"), "launch", "gz_sim.launch.py") - ), - launch_arguments={ - "gz_args": " ".join(["-r", world_file, "--gui-config", gz_gui_config]) - }.items(), -) -``` - -Launches Gazebo Harmonic with: -- `-r` -- start running immediately (not paused) -- The world SDF file from `sim_worlds` (default: `empty.world.sdf`) -- A custom GUI config from `sim_worlds/gui/gui.config` - -### URDF processing - -```python -robot_desc = xacro.process_file( - urdf_file, - mappings={"controller_config": controller_config}, -).toxml() -``` - -The URDF is processed through xacro at launch time. The `controller_config` mapping passes the path to the controller YAML so the control xacro can reference it. The result is a fully resolved URDF string used for both spawning and state publishing. - -### Robot spawn - -```python -spawn_robot = Node( - package="ros_gz_sim", - executable="create", - arguments=["-name", "arm", "-string", robot_desc, "-x", "0", "-y", "0", "-z", "0.1"], -) -``` - -Spawns the robot into the Gazebo world from the processed URDF string. The `-z 0.1` offset prevents the robot from spawning inside the ground plane. - -### Robot state publisher - -```python -robot_state_publisher = Node( - package="robot_state_publisher", - executable="robot_state_publisher", - parameters=[{"robot_description": robot_desc}], -) -``` - -Publishes the robot's TF tree and makes the URDF available on the `/robot_description` topic. RViz uses this to render the robot model. - -### Controllers - -Two controllers are spawned in sequence: - -1. **`joint_state_broadcaster`** -- reads joint states from Gazebo hardware interfaces and publishes them to `/joint_states` -2. **`joint_trajectory_controller`** -- listens for `JointTrajectory` messages on `/joint_trajectory_controller/joint_trajectory` and commands Gazebo to move the joints - -The controller configuration lives in `config/.controller.yaml`: - -```yaml -controller_manager: - ros__parameters: - use_sim_time: true - update_rate: 60 # Hz - -joint_trajectory_controller: - ros__parameters: - joints: - - shoulder_1 - - elbow_1 - - wrist_1 - - wrist_2 - command_interfaces: - - position - state_interfaces: - - position - - velocity -``` - -### ROS-Gazebo bridge - -```python -bridge = Node( - package="ros_gz_bridge", - executable="parameter_bridge", - arguments=["/clock@rosgraph_msgs/msg/Clock[gz.msgs.Clock"], -) -``` - -Bridges the `/clock` topic from Gazebo to ROS 2. This is essential for `use_sim_time: true` -- without it, ROS nodes won't have synchronized time with the simulation. - -### RViz - -Launches RViz2 with a saved config file. Conditional on the `rviz` launch argument. - -### Joint GUI - -Launches the Tkinter joint control GUI with the URDF file path as an argument. Conditional on the `gui` launch argument. - -## launch_utils.py - -The `sim_common` package provides a `get_asset()` helper used throughout launch files: - -```python -from sim_common.launch_utils import get_asset - -controller_config = get_asset("arm_bringup", "config", "arm.controller.yaml") -``` - -It resolves a file path inside a ROS 2 package's share directory and exits with an error if the file doesn't exist. This catches missing files early rather than failing mid-launch. - -## Writing a custom launch file - -If you need to customize a robot's launch beyond what `sim gazebo create` generates: - -1. Edit `gazebo/_bringup/launch/.launch.py` -2. The file is standard ROS 2 launch Python -- you can add nodes, change parameters, or modify the startup sequence -3. `sim gazebo update` will **not** overwrite your launch file changes - -Common customizations: -- Adding sensor bridges (cameras, lidar) -- Changing spawn position -- Adding additional ROS 2 nodes -- Modifying controller parameters diff --git a/docs/gazebo/robot-packages.mdx b/docs/gazebo/robot-packages.mdx deleted file mode 100644 index 0526bd6..0000000 --- a/docs/gazebo/robot-packages.mdx +++ /dev/null @@ -1,96 +0,0 @@ ---- -title: Robot Packages -description: What sim gazebo create generates, how URDF post-processing works, and how the robot registry is structured. ---- - -Reference for `sim gazebo create` and `sim gazebo update` - what they produce and how the underlying pipeline works. - -## Generated package layout - -`sim gazebo create` writes two packages into `gazebo/`: - -``` -_description/ - urdf/.urdf ← Post-processed geometry URDF - urdf/_control.urdf.xacro ← ros2_control hardware interface - meshes/ ← Decimated STL files - CMakeLists.txt - package.xml - -_bringup/ - launch/.launch.py ← Launch orchestration - config/.controller.yaml ← Joint controller definitions - config/.rviz ← RViz config - CMakeLists.txt - package.xml -``` - -The `_description` / `_bringup` split is a ROS convention: description holds the robot model (changes when CAD changes), bringup holds runtime config (things you tune during development). `sim gazebo update` can safely replace the description without touching bringup. - -## URDF post-processing - -`sim gazebo create` downloads a raw URDF from OnShape via `onshape-to-robot`, then runs it through several transforms before writing the final files: - -1. **Xacro namespace** - adds `xmlns:xacro` to the `` tag so the file can use xacro macros -2. **Mesh path rewriting** - rewrites `filename="package://assets/foo.stl"` to `filename="${mesh_path}/foo.stl"` so paths resolve correctly in ROS -3. **World base link** - if `--attach-to-world`, inserts a `world` link and a fixed `world_to_base_link` joint before the first `` -4. **Joint extraction** - finds all `revolute` joints with their limits -5. **Control xacro generation** - produces `_control.urdf.xacro` with a `ros2_control` block containing `position` command interfaces and `position`/`velocity`/`effort` state interfaces for every joint -6. **Control include injection** - appends `` for the control xacro at the end of the geometry URDF -7. **Reindent** - normalises indentation from 2-space (OnShape default) to 4-space - -## What `sim gazebo update` touches - -Only geometry - everything authored by hand is preserved: - -| File | `sim gazebo update` | -| ----------------------------------------------------- | ---------------------------------- | -| `_description/urdf/.urdf` | Replaced | -| `_description/meshes/` | Replaced (old files deleted first) | -| `_description/urdf/_control.urdf.xacro` | **Untouched** | -| `_bringup/` (all files) | **Untouched** | - -## robots.json - -`robots.json` at the repo root is the robot registry. `sim gazebo create` writes to it; `sim gazebo update` reads from it to find the OnShape URL. - -```json -[ - { - "name": "arm", - "url": "https://cad.onshape.com/documents/...", - "world_base_link": true - } -] -``` - -## Debugging raw OnShape output - -If the generated packages look wrong, inspect what OnShape actually produced before post-processing: - -```bash title="Inside devcontainer" -sim gazebo create --raw -``` - -This downloads the raw URDF and assets into `cli/gazebo/create/tests//` (gitignored) without running any post-processing. Inspect `robot.urdf` there to see what OnShape produced. - -Once you've identified a fix, re-run the full generation on those local files without hitting OnShape again: - -```bash title="Inside devcontainer" -sim gazebo create --local -``` - -## Code structure - -| File | Responsibility | -| ----------------------------------- | -------------------------------------------------------- | -| `cli/gazebo/create/__init__.py` | `create()` / `update()` entry points, credential loading | -| `cli/gazebo/create/commands.py` | `cmd_create`, `cmd_update`, `cmd_local`, `cmd_raw` | -| `cli/gazebo/create/onshape.py` | URL parsing, `onshape-to-robot` invocation | -| `cli/gazebo/create/urdf.py` | All URDF transforms (steps 1–7 above) | -| `cli/gazebo/create/ros_packages.py` | File scaffolding from templates | -| `cli/gazebo/create/template.py` | `__ROBOT__` token replacement | -| `cli/gazebo/create/reduce_stl.py` | STL decimation via `open3d` | -| `cli/gazebo/create/registry.py` | `robots.json` read/write | -| `cli/gazebo/create/templates/` | Template files for generated packages | -| `cli/auth.py` | Dashboard API key management (`sim gazebo auth`) | diff --git a/docs/gazebo/ros-workspace.mdx b/docs/gazebo/ros-workspace.mdx deleted file mode 100644 index cf72d15..0000000 --- a/docs/gazebo/ros-workspace.mdx +++ /dev/null @@ -1,89 +0,0 @@ ---- -title: ROS Workspace -description: Overview of the ROS 2 package structure, build system, and conventions used in this project. ---- - -## Package structure - -All ROS 2 packages live in the `gazebo/` directory. Every directory containing a `package.xml` is a package. - -### Robot packages - -Each robot has two packages, generated by [sim gazebo create](../../guides/adding-robots/): - -**`_description`** -- the robot model -- `urdf/.urdf` -- geometry URDF (positions, links, joints, mesh references) -- `urdf/_control.urdf.xacro` -- `ros2_control` hardware interface block (joints, command/state interfaces) -- `meshes/` -- STL mesh files from OnShape -- Built with `ament_cmake` -- `CMakeLists.txt` installs URDFs and meshes to the share directory - -**`_bringup`** -- launch configuration -- `launch/.launch.py` -- orchestrates the full simulation startup -- `config/.controller.yaml` -- controller manager config (joints, update rate, controller types) -- `config/.rviz` -- RViz visualization layout -- Built with `ament_cmake` - -### Shared packages - -**`sim_common`** -- shared Python utilities -- `launch_utils.py` -- helper functions for launch files (`get_asset()` resolves paths in package share directories) -- `move_joints.py` -- ROS 2 node for sending one-shot joint trajectory commands -- `joint_gui.py` -- Tkinter GUI for interactive joint control -- Built with `ament_python` (`setup.py` + `setup.cfg`) -- Registers `move_joints` and `joint_gui` as console script entry points - -**`sim_worlds`** -- Gazebo world files -- `worlds/empty.world.sdf` -- default world with physics, lighting, and a ground plane -- `gui/gui.config` -- Gazebo GUI plugin layout -- Built with `ament_cmake` - -## Building - -The `sim` CLI handles building automatically. To build manually: - -```bash title="Inside devcontainer" -cd gazebo -colcon build --packages-up-to arm_bringup arm_description sim_worlds sim_common \ - --cmake-args -DBUILD_TESTING=OFF -source install/setup.bash -``` - -`--packages-up-to` ensures all dependencies are resolved and built in the correct order. The `install/setup.bash` overlay lets ROS 2 find the built packages. - -### Build outputs - -After building, `gazebo/` contains three generated directories: - -| Directory | Contents | -| --- | --- | -| `build/` | Intermediate build artifacts | -| `install/` | Installed packages (what ROS 2 uses at runtime) | -| `log/` | Build logs | - -## Adding or modifying packages - -When you add or change a package: - -1. **`package.xml`** -- declares the package name, version, description, and dependencies. `colcon` uses the ``, ``, and `` tags to determine build order. - -2. **`CMakeLists.txt`** (CMake packages) -- defines what gets installed to the share directory. Typically installs URDFs, meshes, configs, and launch files. - -3. **`setup.py`** (Python packages) -- lists Python modules and console script entry points. - - - If colcon can't find a new package, make sure `package.xml` exists and the package name matches the directory name. - - -## System flowchart - -Here is a flowchart showing how the arm simulation works end-to-end: - -![Arm system flowchart](../assets/arm-flowchart.excalidraw.png) - -## Troubleshooting - -**Unexplained build errors:** Run `sim gazebo clean` and rebuild. This fixes the majority of mysterious failures. - -**Package not found at runtime:** Make sure you sourced the workspace (`source install/setup.bash`). Opening a new terminal in VS Code doesn't automatically source it. - -**Dependency order issues:** Check that all `` tags in `package.xml` are correct. Missing dependencies cause packages to build before their dependencies are ready. From c847e8fcc2f108e53d74f17a1b0ae1ebc5ebb86c Mon Sep 17 00:00:00 2001 From: Matej Stastny Date: Thu, 6 Aug 2026 21:43:42 -0700 Subject: [PATCH 33/38] docs: update sidebar config for restructured pages --- docs.config.json | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/docs.config.json b/docs.config.json index 973b6b6..421a11b 100644 --- a/docs.config.json +++ b/docs.config.json @@ -4,13 +4,12 @@ "description": "Multi-sim environment: Gazebo Harmonic (ROS 2 Jazzy) for robot subsystems, and Project Chrono for terrain/wheel physics", "icon": "MonitorCloud", "sidebar": [ - { "label": "Getting Started", "slug": "getting-started" }, + { "label": "Getting Started", "slug": "index" }, { "label": "Setup", "items": [ - { "label": "Native (pixi)", "slug": "setup/pixi" }, { "label": "Docker", "slug": "setup/docker" }, - { "label": "Docker + Nvidia", "slug": "setup/nvidia" } + { "label": "Native (pixi)", "slug": "setup/pixi" } ] }, { @@ -25,28 +24,13 @@ "label": "Adding a New Robot", "slug": "gazebo/adding-robots" }, - { "label": "ROS Workspace", "slug": "gazebo/ros-workspace" }, - { "label": "Launch System", "slug": "gazebo/launch-system" }, - { - "label": "Robot Packages", - "slug": "gazebo/robot-packages" - }, - { "label": "Joint GUI", "slug": "gazebo/joint-gui" } + { "label": "Architecture", "slug": "gazebo/architecture" } ] }, { "label": "Chrono", "items": [{ "label": "Running Simulations", "slug": "chrono/chrono" }] }, - { - "label": "Reference", - "items": [ - { "label": "Dev Notes", "slug": "reference/dev-notes" }, - { - "label": "Docker Environment", - "slug": "reference/docker-environment" - } - ] - } + { "label": "Dev Notes", "slug": "dev-notes" } ] } From 54999c3bf9b9803f6d9fedce1b10f37256b0493a Mon Sep 17 00:00:00 2001 From: Matej Stastny Date: Thu, 6 Aug 2026 21:43:44 -0700 Subject: [PATCH 34/38] docs: update gazebo docs cross-links for restructure --- docs/gazebo/adding-robots.mdx | 62 +++++++++---------------- docs/gazebo/gazebo.mdx | 66 +++++++++++++-------------- docs/gazebo/moving-joints.mdx | 86 ++++++++++++++++++----------------- 3 files changed, 96 insertions(+), 118 deletions(-) diff --git a/docs/gazebo/adding-robots.mdx b/docs/gazebo/adding-robots.mdx index 16a6c2b..28f6f70 100644 --- a/docs/gazebo/adding-robots.mdx +++ b/docs/gazebo/adding-robots.mdx @@ -3,42 +3,18 @@ title: Adding a New Robot description: How to generate ROS 2 packages from an OnShape CAD model and get a new robot running in Gazebo. --- -`sim gazebo create` takes a robot from OnShape and produces two ready-to-build ROS 2 packages - no manual URDF editing or package scaffolding required. +The command `sim gazebo create` takes a robot from OnShape and produces two ROS 2 packages. To make sure we do not hit the low OnShape API limits, requests to OnShape are routed through the [dashboard](https://github.com/TrickfireRobotics/dashboard.git) that authenticates you and caches common request long-term. Authentication is required. You can do it with the following command: -## Via GitHub Actions (recommended) - -You do not need any credentials or local setup. The **Create new robot** workflow runs on CI and opens a PR with the generated packages. - -1. Go to **Actions** and then to **Create new robot** -2. Click **Run workflow** and fill in the inputs: - - **Mode:** `create` - - **Robot name:** unique, lowercase, no spaces (e.g. `arm`) - - **OnShape URL:** the full document URL from OnShape - - **Fix robot to world:** yes for stationary robots (arms, turrets), no for mobile -3. Wait for the workflow to finish, then review and merge the generated PR - -## Via CLI (developers only) - -Running `sim gazebo create` locally requires authentication via the TrickFire dashboard: - - - This method uses your GitHub `ssh` key. Make sure it is set up. In the Dev Container, your SSH keys are mounted automatically from the host. - - -```bash title="Terminal" +```bash sim gazebo auth ``` Then you can create the robot using: -```bash title="Terminal" +```bash sim gazebo create ``` - - This sim will work with any Onshape API key and secret as long as you put them in the `cli/onshape.env` file under `ONSHAPE_API_KEY` and `ONSHAPE_API_SECRET` (in standard env format). The above is just for TrickFire specific robots. - - ## Flags | Flag | Description | @@ -50,24 +26,28 @@ sim gazebo create ## Updating an existing robot -When the OnShape CAD changes, pull in the new geometry without touching your bringup config. - -**Via CI (recommended):** Actions → **Create new robot** → mode: `update` - -**Locally:** +When the OnShape CAD changes, pull in the new geometry without touching your bringup config using this command: -```bash title="Terminal" +```bash sim gazebo update arm ``` -This replaces only `arm_description/urdf/arm.urdf` and `arm_description/meshes/`. Everything else - your control xacro edits, launch file, controller YAML, RViz config - is left untouched. +This replaces only `arm_description/urdf/arm.urdf` and `arm_description/meshes/`. Everything else, your control xacro edits, launch file, controller YAML, RViz config is left untouched. ## Troubleshooting -**Missing joints:** Joints must be defined as mates in the OnShape assembly. Check the OnShape model if joints don't appear in the generated controller YAML. - -**URDF errors after generation:** See [Robot Packages](../../reference/robot-packages/) for details on what post-processing happens and how to inspect the raw OnShape output. - -**Large mesh files:** `sim gazebo create` decimates STL triangle counts automatically. If meshes are still too large, simplify the geometry in OnShape before re-running. - -**`sim gazebo auth` fails with "no access":** Your API key may be invalid or expired. Generate a new one at [dashboard.trickfirerobotics.com](https://dashboard.trickfirerobotics.com) → Settings → CLI access. + + + Joints must be defined as mates in the OnShape assembly. Check the OnShape model if joints + don't appear in the generated controller YAML. + + + See [Robot Packages](../reference/robot-packages/) for details on what post-processing + happens and how to inspect the raw OnShape output. + + + Your API key may be invalid or expired. Generate a new one at + [dashboard.trickfirerobotics.com](https://dashboard.trickfirerobotics.com) → Settings → CLI + access. + + diff --git a/docs/gazebo/gazebo.mdx b/docs/gazebo/gazebo.mdx index 3f505ce..fcf9fde 100644 --- a/docs/gazebo/gazebo.mdx +++ b/docs/gazebo/gazebo.mdx @@ -3,56 +3,52 @@ title: Running Gazebo description: How to build and launch robot simulations with the Gazebo sim CLI. --- -Gazebo simulations are launched through the `sim gazebo` CLI. It auto-detects whether you are running natively (pixi) or inside the Dev Container and applies the right setup automatically. To launch the sim use: +Gazebo simulations are launched through the `sim gazebo` CLI: ```bash sim gazebo ``` -Of course, if you are using pixi, it is going to be: +With **pixi**, prefix it all of the `sim ...` commands here with `pixi run` -```bash -pixi run sim gazebo -``` - -## Options - -| Option | Description | -| -------------- | --------------------------------------------------------------------------------------------------- | -| `--no-build` | Skip the `colcon build` step. Use this when you haven't changed any code and want a faster startup. | -| `--build-only` | Build the workspace but don't launch the simulation. Useful for checking if your changes compile. | - -```bash -# Skip building (already built) -pixi run sim gazebo arm --no-build +## Flags -# Only build, don't launch -pixi run sim gazebo arm --build-only -``` +| Option | Description | +| -------------- | ---------------------------------------------------- | +| `--no-build` | Skip the `colcon build` step. | +| `--build-only` | Build the workspace but don't launch the simulation. | ## Cleaning the workspace To remove build artifacts and do a clean rebuild: -```bash title="Terminal" -pixi run sim gazebo clean +```bash +sim gazebo clean ``` This deletes `gazebo/build/`, `gazebo/install/`, and `gazebo/log/`. Use this when you encounter unexplained build failures. ## Troubleshooting -**Build fails with cryptic errors:** -Run `sim gazebo clean` to delete stale build artifacts, then try again. Stale artifacts are the most common cause of unexplained build failures. - -**Gazebo window doesn't appear (Dev Container):** -Connect via your VNC viewer at `localhost:5900` and verify it works with `xeyes`. If the display isn't running, restart it with `.devcontainer/x_server.sh`. - -**RViz crashes with "Unable to create the rendering window", or Gazebo shows a blank window (macOS/Windows):** -OGRE2 (used by both Gazebo and RViz) needs a real GL context, which XQuartz/VcXsrv can't provide over their indirect GLX - the X11 connection works, but rendering doesn't. The container routes GL through VirtualGL to work around this, and `sim gazebo` launches under `vglrun` automatically. Seeing this error means that path isn't active. Check for a warning from `sim gazebo` at startup, then verify GL directly with `vglrun /opt/VirtualGL/bin/glxspheres64` - if `vglrun` is missing, rebuild the container; if the 3D X server isn't running, start it with `bash .devcontainer/x_server.sh`. `FORCE_VNC=1` in `docker/.env` plus a container recreate is the fallback - see [Docker setup](../../setup/docker/) for details. - -**Package not found errors after launch:** -Try running `sim gazebo clean` and building again. - -**"Package '``_bringup' not found" right after launch:** -`sim gazebo ` takes a robot name (`arm`, `chassis`, etc. - see `robots.json`), not a subcommand. `sim gazebo launch` gets parsed as robot name `"launch"`, which doesn't exist. Use `sim gazebo arm` or `sim gazebo chassis` instead. + + + Run `sim gazebo clean` to delete stale build artifacts, then try again. Stale artifacts + are the most common cause of unexplained build failures. + + + Connect via your VNC viewer at `localhost:5900` and verify it works with `xeyes`. If the + display isn't running, restart it with `.devcontainer/x_server.sh`. + + + The `VirtualGL` path that gives OGRE2 a working GL context isn't active. Check for a + warning from `sim gazebo` at startup, then see [Docker setup](../../setup/docker/) for + how to verify and fix it. + + + Try running `sim gazebo clean` and building again. + + + `sim gazebo ` takes a robot name (`arm`, `chassis` ...) from configured robots in `robots.json`, + not a subcommand. Check the `robots.json` file for list of available robots or [create a new one](./adding-robots.mdx) + + diff --git a/docs/gazebo/moving-joints.mdx b/docs/gazebo/moving-joints.mdx index 454a3ef..62d8e29 100644 --- a/docs/gazebo/moving-joints.mdx +++ b/docs/gazebo/moving-joints.mdx @@ -1,13 +1,11 @@ --- title: Moving Joints -description: Control robot joints using the Joint GUI or the move_joints CLI node. +description: Control robot joints using the GUI or CLI --- -There are two ways to move joints in the simulation: the **Joint GUI** (graphical, interactive) and the **`move_joints` CLI node** (scriptable, one-shot commands). +There are two ways to move joints in the simulation: the **Joint GUI** and the **`move_joints` CLI node**. Both publish `JointTrajectory` messages to the `joint_trajectory_controller`. -Both publish `JointTrajectory` messages to the `joint_trajectory_controller`. - -## Joint GUI (interactive) +## Joint GUI The Joint GUI launches automatically with the simulation. It provides sliders for each joint discovered from the robot's URDF. @@ -25,65 +23,69 @@ You can also: - Select which **Topic** to publish on (auto-discovered from available `JointTrajectory` topics) - Click **Refresh** to re-scan for topics -### How it works + -1. On launch, the GUI parses the URDF file for all `revolute` joints, reading their `lower` and `upper` limits -2. It subscribes to `/joint_states` to discover which joints are actually active and their starting positions -3. When you hit **Send**, it publishes a `JointTrajectory` message to the selected topic with positions offset by each joint's origin angle + 1. On launch, the GUI parses the URDF file for all `revolute` joints, reading their `lower` and `upper` limits + 2. It subscribes to `/joint_states` to discover which joints are actually active and their starting positions + 3. When you hit **Send**, it publishes a `JointTrajectory` message to the selected topic with positions offset by each joint's origin angle - - If the GUI shows "Discovering joints from /joint_states..." for a long time, the joint controllers may not have started yet. Wait for Gazebo to fully load. -## move_joints node (CLI) +## `move_joints` node (CLI) -The `move_joints` node sends a single trajectory command and exits. It's useful for scripting or quick tests. +The `move_joints` node sends a single trajectory command and exits. It's useful for deterministic move sequences and scripting. ### Usage -```bash title="Terminal" +```bash ros2 run sim_common move_joints --ros-args \ - -p joints:="['shoulder_1', 'elbow_1', 'wrist_1', 'wrist_2']" \ - -p positions:="[0.5, 0.5, 0.2, 0.0]" \ + -p joints:="['joint1', 'joint2', 'joint3', 'joint4']" \ + -p positions:="[0.1, 0.5, 0.2, 0.0]" \ -p duration:=2.0 ``` ### Parameters -| Parameter | Required | Default | Description | -| ----------- | -------- | --------- | ----------------------------------------- | -| `joints` | Yes | -- | List of joint names to move | -| `positions` | Yes | -- | Target positions (radians), one per joint | -| `duration` | No | `2.0` | Seconds to reach the target | -| `topic` | No | See below | Trajectory topic to publish on | - -Default topic: `/joint_trajectory_controller/joint_trajectory` - -### Behavior +| Parameter | Required | Default | Description | +| ----------- | -------- | ----------------------------------------------- | ----------------------------------------- | +| `joints` | Yes | -- | List of joint names to move | +| `positions` | Yes | -- | Target positions (radians), one per joint | +| `duration` | No | `2.0` | Seconds to reach the target | +| `topic` | No | `/joint_trajectory_controller/joint_trajectory` | Trajectory topic to publish on | -The node: + 1. Creates a publisher on the trajectory topic -2. Waits (polling every 0.5s) until at least one subscriber is connected +2. Waits until at least one subscriber is connected 3. Publishes the trajectory message 4. Logs the command and exits This means the joint trajectory controller must be running before `move_joints` can send its command. If you run it right after launching the simulation, it will wait automatically until the controller is ready. -### Example: move two joints + -```bash title="Terminal" -ros2 run sim_common move_joints --ros-args \ - -p joints:="['shoulder_1', 'elbow_1']" \ - -p positions:="[1.0, -0.5]" \ - -p duration:=3.0 -``` +## Examples -### Example: move a single joint slowly + + -```bash title="Terminal" -ros2 run sim_common move_joints --ros-args \ - -p joints:="['wrist_1']" \ - -p positions:="[0.8]" \ - -p duration:=5.0 -``` + ```bash + ros2 run sim_common move_joints --ros-args \ + -p joints:="['shoulder_1', 'elbow_1']" \ + -p positions:="[1.0, -0.5]" \ + -p duration:=3.0 + ``` + + + + + ```bash + ros2 run sim_common move_joints --ros-args \ + -p joints:="['wrist_1']" \ + -p positions:="[0.8]" \ + -p duration:=5.0 + ``` + + + + From 13e610855fe508fe022c8aafa04a5a2a2484899b Mon Sep 17 00:00:00 2001 From: Matej Stastny Date: Thu, 6 Aug 2026 21:43:47 -0700 Subject: [PATCH 35/38] docs: update chrono docs cross-links for restructure --- docs/chrono/chrono.mdx | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/docs/chrono/chrono.mdx b/docs/chrono/chrono.mdx index 36e0c0f..6562703 100644 --- a/docs/chrono/chrono.mdx +++ b/docs/chrono/chrono.mdx @@ -13,15 +13,23 @@ sim chrono run This builds the C++ simulation from `chrono/` (if not already built) and runs it. A Vulkan window opens in the VNC desktop showing the terrain simulation. -## Cleaning + -```bash title="Inside devcontainer" -sim chrono clean -``` + To force a clean rebuild (could help during any unexpectable error) run: + + ```bash title="Inside devcontainer" + sim chrono clean + ``` -Deletes `chrono/build/`. Use this to force a clean rebuild. + This deletes `chrono/build/`. + + ## Troubleshooting -**Chrono window doesn't appear:** -Chrono uses Vulkan for rendering. Make sure the VNC desktop is running (`localhost:5900`) and that the container has Wayland/display access. If the window still doesn't appear, check the terminal output for Vulkan errors. + + + Chrono uses [Vulkan](https://en.wikipedia.org/wiki/Vulkan) for rendering. Make sure you followed all the steps for your platform [during setup](../setup/docker) + + + From 598c0d286b71f4a59da4862d940057d7aa2f6537 Mon Sep 17 00:00:00 2001 From: Matej Stastny Date: Thu, 6 Aug 2026 21:43:47 -0700 Subject: [PATCH 36/38] docs: fix README links for docs restructure --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 77cc424..12638b5 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ Robot simulations for TrickFire Robotics - Gazebo Harmonic (ROS 2 Jazzy) for rob **Full documentation is at [docs.trickfirerobotics.com/simulations](https://docs.trickfirerobotics.com/simulations)** -- [Getting Started](https://docs.trickfirerobotics.com/simulations/getting-started) - native setup, Dev Container alternative, first launch +- [Getting Started](https://docs.trickfirerobotics.com/simulations) - native setup, Dev Container alternative, first launch - [Running Gazebo](https://docs.trickfirerobotics.com/simulations/gazebo/gazebo) - `sim gazebo` CLI usage and flags - [Running Chrono](https://docs.trickfirerobotics.com/simulations/chrono/chrono) - `sim chrono` CLI usage - [Adding a New Robot](https://docs.trickfirerobotics.com/simulations/gazebo/adding-robots) - OnShape to Gazebo with `sim gazebo create` -- [Environment](https://docs.trickfirerobotics.com/simulations/reference/docker-environment) - Docker environment in depth +- [Dev Notes](https://docs.trickfirerobotics.com/simulations/dev-notes) - architecture decisions, Docker environment internals, CLI tips From 969e50c916a915d13ad55b842bbfb535a8154997 Mon Sep 17 00:00:00 2001 From: Matej Stastny Date: Thu, 6 Aug 2026 21:43:53 -0700 Subject: [PATCH 37/38] chore: update imgui window size from local run --- chrono/data/vsg/imgui.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chrono/data/vsg/imgui.ini b/chrono/data/vsg/imgui.ini index 2f804f6..ce7c8a2 100644 --- a/chrono/data/vsg/imgui.ini +++ b/chrono/data/vsg/imgui.ini @@ -8,6 +8,6 @@ Size=531,121 [Window][Simulation] Pos=5,5 -Size=349,458 +Size=238,362 Collapsed=1 From a4ee25b3d76d0af21669c852a814bf67fa366fb4 Mon Sep 17 00:00:00 2001 From: Matej Stastny Date: Fri, 7 Aug 2026 05:20:52 +0000 Subject: [PATCH 38/38] chore: minor fixes --- .devcontainer/x_server.sh | 2 +- docs/chrono/chrono.mdx | 2 +- docs/gazebo/adding-robots.mdx | 5 +++-- docs/setup/docker.mdx | 14 +++++++------- docs/setup/pixi.mdx | 2 +- 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/.devcontainer/x_server.sh b/.devcontainer/x_server.sh index de5d28e..411a7df 100755 --- a/.devcontainer/x_server.sh +++ b/.devcontainer/x_server.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # Sets up the container's display: Wayland/X11 passthrough when available, otherwise a -# headless Xorg/Xvfb + Openbox + x11vnc + noVNC stack. See docs/reference/docker-environment.mdx. +# headless Xorg/Xvfb + Openbox + x11vnc + noVNC stack. See docs/setup/docker.mdx. set -eo pipefail trap '' HUP diff --git a/docs/chrono/chrono.mdx b/docs/chrono/chrono.mdx index 6562703..81570cc 100644 --- a/docs/chrono/chrono.mdx +++ b/docs/chrono/chrono.mdx @@ -15,7 +15,7 @@ This builds the C++ simulation from `chrono/` (if not already built) and runs it - To force a clean rebuild (could help during any unexpectable error) run: + To force a clean rebuild (could help during any unexpected error) run: ```bash title="Inside devcontainer" sim chrono clean diff --git a/docs/gazebo/adding-robots.mdx b/docs/gazebo/adding-robots.mdx index 28f6f70..b1e2e31 100644 --- a/docs/gazebo/adding-robots.mdx +++ b/docs/gazebo/adding-robots.mdx @@ -42,8 +42,9 @@ This replaces only `arm_description/urdf/arm.urdf` and `arm_description/meshes/` don't appear in the generated controller YAML. - See [Robot Packages](../reference/robot-packages/) for details on what post-processing - happens and how to inspect the raw OnShape output. + `sim gazebo create` post-processes the raw OnShape URDF (mesh paths, control xacro + generation, indentation). Check the OnShape assembly for unexpected geometry or joint + definitions if the generated URDF still looks wrong afterwards. Your API key may be invalid or expired. Generate a new one at diff --git a/docs/setup/docker.mdx b/docs/setup/docker.mdx index 11b7359..2e4c95a 100644 --- a/docs/setup/docker.mdx +++ b/docs/setup/docker.mdx @@ -9,11 +9,11 @@ Everything runs inside a Docker container with all dependencies pinned and pre-b Before the container starts, `initializeCommand` runs `.devcontainer/host-env.sh`, which detects your OS and GPU and writes `docker/.env`. These are all possible options: -| Host | Display path | -| ------------------------------------- | -------------------------------------------------------------------------------------------- | -| **Linux Wayland/X11** & **WSL2/WSLg** | Host socket, passed straight through | -| **macOS** + **XQuartz** | Forwarded over TCP via `host.docker.internal` & rendered with VirtualGL | -| **Vanilla Windows** | Forwarded over TCP to VcXsrv & rendered with VirtualGL | +| Host | Display path | +| ------------------------------------- | ------------------------------------------------------------------------------------------------- | +| **Linux Wayland/X11** & **WSL2/WSLg** | Host socket, passed straight through | +| **macOS** + **XQuartz** | Forwarded over TCP via `host.docker.internal` & rendered with VirtualGL | +| **Vanilla Windows** | Forwarded over TCP to VcXsrv & rendered with VirtualGL | | **Linux** + **NVIDIA GPU** | Uses the [container toolkit](#nvidia-acceleration) and NVIDIA Docker runtime for faster rendering | @@ -44,7 +44,7 @@ Gazebo and RViz need a display. On container start, `.devcontainer/x_server.sh` path for your host and prints which one it picked. Check the terminal you launched the container from. - + Wayland or X11 is forwarded straight through - no VNC needed. The log prints one of: @@ -114,7 +114,7 @@ container from. `XQuartz` as plain `X11` images. - + Used for headless servers, or when nothing above applies. `x_server.sh` starts a VNC stack, picking a backend based on available hardware: diff --git a/docs/setup/pixi.mdx b/docs/setup/pixi.mdx index 20cce26..a0704fb 100644 --- a/docs/setup/pixi.mdx +++ b/docs/setup/pixi.mdx @@ -43,6 +43,6 @@ You can now launch the sim. It is the same as you would [normally would](../gaze pixi run sim gazebo ``` - + Do not use `pixi shell` as your regular working shell. It modifies library paths in a way that can break system tools like `git`. Use `pixi run ` instead. For example: `pixi run sim gazebo native arm`.