diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..d5376078 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,89 @@ +# VRX 4 development image +# ROS 2 Lyrical Luth (Ubuntu 26.04 "Resolute") + Gazebo Jetty. +# +# Unlike the legacy image, this does NOT fake the gz vendor packages or build +# ros_gz from source. VRX 4's gz_waves packages `find_package(gz_sim_vendor)`, +# so we install the REAL ros--gz-*-vendor packages (they carry the CMake +# shims and pull in Gazebo Jetty). This image only provides the environment; +# build the VRX workspace at runtime in the mounted ~/vrx_ws. +FROM ros:lyrical-ros-base AS vrx-base + +ENV DEBIAN_FRONTEND=noninteractive +ENV TZ=Etc/UTC + +# ---- Locale + base tooling ------------------------------------------------ +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates curl gnupg lsb-release locales tzdata \ + && ln -fs /usr/share/zoneinfo/$TZ /etc/localtime \ + && echo $TZ > /etc/timezone \ + && locale-gen en_US en_US.UTF-8 \ + && update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 \ + && rm -rf /var/lib/apt/lists/* +ENV LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 + +# ---- OSRF Gazebo apt repo ------------------------------------------------- +# Provides Gazebo Jetty and the libs the gz_*_vendor packages depend on. +RUN curl -s https://packages.osrfoundation.org/gazebo.gpg \ + -o /usr/share/keyrings/pkgs-osrf-archive-keyring.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/pkgs-osrf-archive-keyring.gpg] http://packages.osrfoundation.org/gazebo/ubuntu-stable $(lsb_release -cs) main" \ + > /etc/apt/sources.list.d/gazebo-stable.list + +# ---- Gazebo Jetty + ROS gz vendor shims + ros_gz -------------------------- +# The gz-*-vendor packages provide the CMake shims that VRX 4's gz_waves +# packages find_package(). The quoted glob is passed literally to apt, which +# expands it to every ros--gz--vendor package (msgs, transport, +# fuel-tools, sim, rendering, ...). Gazebo itself arrives via these vendor +# packages + ros-gz; Resolute (26.04) has no standalone `gz-jetty` metapackage +# in the OSRF repo, and installing one alongside the vendors would be redundant. +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ros-${ROS_DISTRO}-ros-gz \ + "ros-${ROS_DISTRO}-gz-*-vendor" \ + ros-${ROS_DISTRO}-sdformat-vendor \ + && rm -rf /var/lib/apt/lists/* + +# ---- Standard ROS packages + build tooling VRX uses ----------------------- +# NOTE: nav2 (navigation2, nav2-bringup, nav2-minimal-*) is intentionally +# omitted — it is often not yet released early in a new distro's life. Add +# ros-${ROS_DISTRO}-navigation2 etc. once available on Lyrical. +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ros-${ROS_DISTRO}-ackermann-msgs \ + ros-${ROS_DISTRO}-actuator-msgs \ + ros-${ROS_DISTRO}-ament-cmake-pycodestyle \ + ros-${ROS_DISTRO}-image-transport \ + ros-${ROS_DISTRO}-image-transport-plugins \ + ros-${ROS_DISTRO}-joy-teleop \ + ros-${ROS_DISTRO}-joy-linux \ + ros-${ROS_DISTRO}-mavros-msgs \ + ros-${ROS_DISTRO}-radar-msgs \ + ros-${ROS_DISTRO}-vision-msgs \ + ros-${ROS_DISTRO}-xacro \ + python3-colcon-common-extensions \ + python3-rosdep \ + python3-vcstool \ + && rm -rf /var/lib/apt/lists/* + +# ---- Dev tools + NVIDIA GL (rocker --nvidia) ------------------------------ +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + atop expect gdb gnutls-bin iputils-ping libbluetooth-dev libcwiid-dev \ + net-tools python3-dbg python3-pip python3-venv vim wget xvfb \ + && dpkg --add-architecture i386 \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + libxau6 libxau6:i386 libxdmcp6 libxdmcp6:i386 libxcb1 libxcb1:i386 \ + libxext6 libxext6:i386 libx11-6 libx11-6:i386 libglvnd0 libglvnd0:i386 \ + libgl1 libgl1:i386 libglx0 libglx0:i386 libegl1 libegl1:i386 \ + libgles2 libgles2:i386 \ + && rm -rf /var/lib/apt/lists/* + +# ---- Source ROS for interactive + non-interactive shells ------------------ +RUN echo "source /opt/ros/${ROS_DISTRO}/setup.bash" > /etc/profile.d/ros.sh \ + && chmod +x /etc/profile.d/ros.sh \ + && echo "source /etc/profile.d/ros.sh" >> /etc/bash.bashrc +ENV BASH_ENV=/etc/profile.d/ros.sh + +WORKDIR /ws +CMD ["/bin/bash"] diff --git a/docker/build.bash b/docker/build.bash new file mode 100755 index 00000000..5a266e74 --- /dev/null +++ b/docker/build.bash @@ -0,0 +1,49 @@ +#!/usr/bin/env bash + +# +# Copyright (C) 2018 Open Source Robotics Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# + +# Builds a Docker image. +# Usage: build.bash [tag] +# tag defaults to the resolved directory name, so passing '.' works too. +# +# For the VRX dev container, build it as vrx_dev:lyrical so it matches the +# image referenced in compose.yaml: +# ./build.bash lyrical +image_name=vrx_dev + +if [ $# -lt 1 ] +then + echo "Usage: $0 [tag]" + exit 1 +fi + +context="$1" +if [ ! -f "${context}/Dockerfile" ] +then + echo "Err: ${context} does not contain a Dockerfile to build." + exit 1 +fi + +# Tag: explicit 2nd arg, else the resolved directory basename (handles '.'). +distro="${2:-$(basename "$(cd "${context}" && pwd)")}" + +image_plus_tag=$image_name:$(export LC_ALL=C; date +%Y_%m_%d_%H%M) +docker build --rm -t "$image_plus_tag" -f "${context}/Dockerfile" "${context}" && \ +docker tag "$image_plus_tag" "$image_name:$distro" && \ +echo "Built $image_plus_tag and tagged as $image_name:$distro" && \ +echo "To run: ./docker/run_compose.bash" \ No newline at end of file diff --git a/docker/compose.nvidia.yaml b/docker/compose.nvidia.yaml new file mode 100644 index 00000000..30971ba8 --- /dev/null +++ b/docker/compose.nvidia.yaml @@ -0,0 +1,29 @@ +# NVIDIA overlay for the VRX 4 dev container. +# +# run_compose.bash layers this on top of compose.yaml ONLY when it detects an +# NVIDIA GPU on the host, so the base stays portable (Intel/AMD iGPU via +# /dev/dri) and NVIDIA hosts render on the dGPU via PRIME offload. +# +# Manual use: docker compose -f docker/compose.yaml -f docker/compose.nvidia.yaml run --rm dev +services: + dev: + # nvidia-container-toolkit injects the driver libs + /dev/nvidia* when these + # are set and the reservation below requests the GPU. + environment: + - NVIDIA_VISIBLE_DEVICES=all + - NVIDIA_DRIVER_CAPABILITIES=all + # PRIME render offload: on a hybrid/Optimus host the X display is driven by + # the iGPU and NVIDIA is the offload GPU. /dev/dri (base) is the present path. + - __NV_PRIME_RENDER_OFFLOAD=1 + - __GLX_VENDOR_LIBRARY_NAME=nvidia + - __EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/10_nvidia.json + + # Modern Compose + nvidia-container-toolkit. If your Docker still uses the + # legacy runtime, replace this block with `runtime: nvidia`. + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] diff --git a/docker/compose.yaml b/docker/compose.yaml new file mode 100644 index 00000000..ed167ecf --- /dev/null +++ b/docker/compose.yaml @@ -0,0 +1,65 @@ +# Dev container for VRX 4 (ROS 2 Lyrical + Gazebo Jetty), via plain Docker +# Compose so it does NOT depend on rocker knowing Ubuntu 26.04. +# +# Mirrors: rocker --home --user --x11 --nvidia --devices /dev/input/js0 ... bash +# +# Easiest: ./docker/run_compose.bash +# +# Manually: +# xhost +SI:localuser:$(id -un) # let the container reach your X server (once per login) +# cd ~/WorkingCopies/oceansim/vrx_ws/src/vrx +# MY_UID=$(id -u) MY_GID=$(id -g) docker compose -f docker/compose.yaml run --rm --remove-orphans dev +# ( `UID` is a bash readonly var, so we pass MY_UID/MY_GID instead. ) +# +# (`run --rm` gives a fresh interactive shell and removes the container on exit, +# which is the rocker-like behaviour. Use `exec` instead if you prefer a +# long-lived container: `docker compose ... up -d` then `... exec dev bash`.) + +services: + dev: + image: vrx_dev:lyrical # <- the tag you built; adjust if different + + # --- interactive shell (rocker's default) --- + stdin_open: true # docker -i + tty: true # docker -t + command: bash + + # --- run as your host user so files created in the mounted home keep your + # ownership (rocker --user). UID/GID come from the run command; the + # prompt may read "I have no name!" — cosmetic, since this UID has no + # /etc/passwd entry in the image. Drop this line to run as root. --- + user: "${MY_UID:-1000}:${MY_GID:-1000}" + + # --- host networking: ROS 2 DDS discovery + simplest X11 (DISPLAY reaches + # the host X server directly) --- + network_mode: host + ipc: host + + # Workspace root. run_compose.bash derives VRX_WS from the compose file's + # location; the default (current directory) covers a bare `docker compose ...` + # invocation, dropping you in wherever you're standing. + working_dir: ${VRX_WS:-${PWD}} + + environment: + - DISPLAY=${DISPLAY} + - HOME=${HOME} + - QT_X11_NO_MITSHM=1 + # This base runs on the Intel/AMD iGPU via the /dev/dri passthrough below. + # NVIDIA access + PRIME render offload live in compose.nvidia.yaml, which + # run_compose.bash layers in automatically when an NVIDIA GPU is detected. + + volumes: + - ${HOME}:${HOME} # rocker --home + - /tmp/.X11-unix:/tmp/.X11-unix:rw # X11 socket + # No .Xauthority mount: run_compose.bash's `xhost +SI:localuser:$(id -un)` + # grants local access without it, and GDM's $XAUTHORITY + # (/run/user/.../gdm/Xauthority) collides with the home mount if + # bind-mounted onto ~/.Xauthority. + + devices: + # - /dev/input/js0:/dev/input/js0 # joystick (delete if absent) + - /dev/dri:/dev/dri # direct rendering (Intel/AMD/GL) + + # --- GPU: the NVIDIA reservation + PRIME offload are in compose.nvidia.yaml, + # layered in by run_compose.bash only when an NVIDIA GPU is present. + # This base alone renders on the Intel/AMD iGPU via /dev/dri above. --- diff --git a/docker/run_compose.bash b/docker/run_compose.bash new file mode 100755 index 00000000..029f4a34 --- /dev/null +++ b/docker/run_compose.bash @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# +# Minimal launcher for the VRX 4 dev container via Docker Compose +# (ROS 2 Lyrical + Gazebo Jetty). Mirrors rocker: interactive shell, home mount, +# GPU, X11, joystick. Service definition lives in docker/compose.yaml. +# +# Usage: +# ./docker/run_compose.bash # interactive bash in the container +# ./docker/run_compose.bash # run a command instead of bash +# +set -euo pipefail + +# Resolve compose.yaml next to this script, so it works from any directory. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Workspace root = three levels up from docker/ (…//src/vrx/docker). +# Derived from the script's own location so the repo can live anywhere; the +# compose file falls back to a default if this isn't exported. +export VRX_WS +VRX_WS="$(cd "${SCRIPT_DIR}/../../.." && pwd)" + +# Let the container's (host-UID) process reach the host X server. Harmless to +# re-run; warn but don't abort if there's no X server (e.g. headless). +xhost +SI:localuser:"$(id -un)" >/dev/null 2>&1 \ + || echo "WARN: xhost failed (no X server?); GUI apps may not display." >&2 + +# Host UID/GID so files created in the mounted home keep your ownership. +# (`UID` is a bash readonly variable, hence the MY_ prefix the compose expects.) +export MY_UID MY_GID +MY_UID="$(id -u)" +MY_GID="$(id -g)" + +# GPU selection: prefer the NVIDIA dGPU when one is present and usable on the +# host, otherwise fall back to the Intel/AMD iGPU (base compose only). The +# NVIDIA reservation refuses to start the container on a host without an NVIDIA +# GPU, so only layer the overlay in when nvidia-smi actually reports one. +compose_files=(-f "${SCRIPT_DIR}/compose.yaml") +if command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi -L >/dev/null 2>&1; then + compose_files+=(-f "${SCRIPT_DIR}/compose.nvidia.yaml") + echo "GPU: NVIDIA detected -> dGPU via PRIME offload." >&2 +else + echo "GPU: no usable NVIDIA -> Intel/AMD iGPU via /dev/dri." >&2 +fi + +exec docker compose "${compose_files[@]}" run --rm --remove-orphans dev "$@" diff --git a/gz_waves_rendering/CMakeLists.txt b/gz_waves_rendering/CMakeLists.txt index db035748..159a0e7b 100644 --- a/gz_waves_rendering/CMakeLists.txt +++ b/gz_waves_rendering/CMakeLists.txt @@ -28,27 +28,28 @@ find_package(gz-rendering REQUIRED COMPONENTS ogre2) # registry (CreateWaveSimulation); the GUI-side factory registration lives in # the per-engine GUI plugins (gz-sim-waves--gui), loaded by the world. -# Discover Ogre Next headers. Different Gazebo installs sit on different Ogre -# Next patch versions (ROS gz_ogre_next_vendor 2.3.3 vs Ubuntu libogre-next-dev -# 2.3.1). Compiling against one but loading the other silently swaps in slow -# Hlms/barrier code paths (no crash, but RTF tanks), so probe which Ogre Next -# the *resolved* gz-rendering-ogre2 links against and match our header path. -get_target_property(_gz_rend_ogre2_lib gz-rendering::ogre2 LOCATION) -if(_gz_rend_ogre2_lib MATCHES "/opt/ros/([^/]+)/") - set(_ros_distro "${CMAKE_MATCH_1}") - set(OGRE_NEXT_INCLUDE_DIR - "/opt/ros/${_ros_distro}/opt/gz_ogre_next_vendor/include/OGRE-Next" - CACHE PATH "Ogre Next include dir matched to gz-rendering-ogre2 distro") -else() - # Non-ROS-distro install (e.g. local jetty_ws build) → system Ogre Next. - set(OGRE_NEXT_INCLUDE_DIR "/usr/include/OGRE-2.3" - CACHE PATH "Ogre Next include dir from libogre-next-dev") -endif() -if(NOT EXISTS "${OGRE_NEXT_INCLUDE_DIR}/OgreRoot.h") +# Discover the Ogre Next headers. We must compile against the SAME Ogre Next the +# resolved gz-rendering-ogre2 uses (mixing patch versions silently swaps in slow +# Hlms/barrier paths). The previous approach parsed gz-rendering::ogre2's +# LOCATION, but that property is empty for imported/interface targets (it is +# NOTFOUND on the ROS gz vendor + Gazebo Jetty), so it wrongly fell back to a +# hardcoded /usr/include/OGRE-2.3. Instead, search for the OGRE-Next headers, +# seeded by the ogre2 target's own interface include dirs. PATH_SUFFIXES exclude +# a bare "OGRE" so we never pick up classic Ogre 1.x (/usr/include/OGRE, +# rviz_ogre_vendor), whose ABI-incompatible headers would break the build. +get_target_property(_ogre2_incs gz-rendering::ogre2 INTERFACE_INCLUDE_DIRECTORIES) +find_path(OGRE_NEXT_INCLUDE_DIR + NAMES OgreRoot.h + HINTS ${_ogre2_incs} + PATHS + /opt/ros/$ENV{ROS_DISTRO}/opt/gz_ogre_next_vendor/include + /usr/include + PATH_SUFFIXES OGRE-Next OGRE-2.4 OGRE-2.3 +) +if(NOT OGRE_NEXT_INCLUDE_DIR) message(FATAL_ERROR - "OgreRoot.h not found under ${OGRE_NEXT_INCLUDE_DIR} " - "(probed from gz-rendering::ogre2 = ${_gz_rend_ogre2_lib}). " - "Install libogre-next-dev or gz_ogre_next_vendor as appropriate.") + "OGRE-Next headers (OgreRoot.h) not found. Install libogre-next-dev or the " + "gz_ogre_next_vendor package.") endif() message(STATUS "Ogre Next headers: ${OGRE_NEXT_INCLUDE_DIR}")