#!/usr/bin/env bash # Bring up the headless display, then hand over to the command. # # Xvfb and openbox are started HERE, as children of PID 1 (tini), rather than by # the toolkit scripts. That is the fix for the long-standing "Xvfb and the # emulator die on their own every few minutes" note: nothing owned those # processes, so nothing kept them alive, and a run could sit for 300 s in front # of a visible MAIN MENU reporting "no main menu" because the display had gone. # A container-lifetime display makes that failure mode impossible. set -euo pipefail log() { printf '[entrypoint] %s\n' "$*" >&2; } DISPLAY="${DISPLAY:-:98}" GEOM="${SCREEN_GEOMETRY:-1280x720x24}" export DISPLAY # ── Display ────────────────────────────────────────────────────────────────── if ! xdpyinfo -display "$DISPLAY" >/dev/null 2>&1; then rm -f "/tmp/.X${DISPLAY#:}-lock" "/tmp/.X11-unix/X${DISPLAY#:}" 2>/dev/null || true # GLX and RANDR are both required: Canary's window is GTK+OpenGL even when the # graphics backend is Vulkan, and xwininfo-based screen oracles need RANDR. Xvfb "$DISPLAY" -screen 0 "$GEOM" -ac -nolisten tcp \ +extension GLX +extension RANDR >/tmp/xvfb.log 2>&1 & for _ in $(seq 1 50); do xdpyinfo -display "$DISPLAY" >/dev/null 2>&1 && break sleep 0.2 done fi if ! xdpyinfo -display "$DISPLAY" >/dev/null 2>&1; then log "FATAL: no display on $DISPLAY — see /tmp/xvfb.log" exit 1 fi # A window manager is not cosmetic here: without one the emulator window is # never mapped at a known position, and every pixel oracle reads the wrong rows. if ! pgrep -x openbox >/dev/null 2>&1; then openbox >/tmp/openbox.log 2>&1 & sleep 0.5 fi log "display $DISPLAY ready ($GEOM)" # ── Vulkan ─────────────────────────────────────────────────────────────────── # Prefer the real GPU when /dev/dri was passed through; fall back to lavapipe, # which is slow but correct and needs no host device. LVP=$(ls /usr/share/vulkan/icd.d/lvp_icd*.json 2>/dev/null | head -1) if [ "${SYLPH_VULKAN:-auto}" = "sw" ] || [ ! -e /dev/dri/renderD128 ]; then [ -n "$LVP" ] && export VK_ICD_FILENAMES="$LVP" else unset LIBGL_ALWAYS_SOFTWARE fi # Report what Vulkan ACTUALLY enumerated, not what we asked for. Announcing # "host GPU via /dev/dri" because the device node exists is how an agent ends up # believing it has hardware while running llvmpipe — an NVIDIA card needs the # NVIDIA Container Toolkit, and /dev/dri alone does nothing for it. if command -v vulkaninfo >/dev/null 2>&1; then vkdev=$(vulkaninfo --summary 2>/dev/null | awk -F= '/deviceName/{gsub(/^ +| +$/,"",$2); print $2; exit}') case "${vkdev:-}" in "") log "Vulkan: NO DEVICE — vulkaninfo enumerated nothing" ;; llvmpipe*|lavapipe*) log "Vulkan: $vkdev (SOFTWARE — correct but slow)" ;; *) log "Vulkan: $vkdev (hardware)" ;; esac fi # ── Build parallelism ──────────────────────────────────────────────────────── # Bounded by MEMORY, not just cores. A full-parallel build of this tree has # OOM-killed the host outright, and inside a half-the-box container the ceiling # is lower still. ~1.5 GiB per C++ TU is the rule of thumb that has held. # nproc reports the HOST's core count: --cpus is a CFS quota, not a mask. Using # it would oversubscribe a half-the-box container by exactly 2x, so read the # quota the cgroup actually grants. cpus=$(nproc) if [ -r /sys/fs/cgroup/cpu.max ]; then read -r _q _p < /sys/fs/cgroup/cpu.max || true if [ "${_q:-max}" != max ] && [ "${_p:-0}" -gt 0 ]; then cpus=$(( (_q + _p - 1) / _p )) [ "$cpus" -lt 1 ] && cpus=1 fi fi mem_gib=$(awk '/MemAvailable/{printf "%d", $2/1048576}' /proc/meminfo) # MemAvailable is the HOST's too under cgroup v2; prefer the container's cap. if [ -r /sys/fs/cgroup/memory.max ]; then _m=$(cat /sys/fs/cgroup/memory.max) [ "$_m" != max ] && mem_gib=$(( _m / 1073741824 )) fi [ "${mem_gib:-0}" -lt 1 ] && mem_gib=1 by_mem=$(( mem_gib * 2 / 3 )) [ "$by_mem" -lt 1 ] && by_mem=1 jobs=$(( cpus < by_mem ? cpus : by_mem )) export SYLPH_JOBS="$jobs" CARGO_BUILD_JOBS="$jobs" CMAKE_BUILD_PARALLEL_LEVEL="$jobs" log "build parallelism: $jobs (cpus=$cpus, mem=${mem_gib}GiB avail)" mkdir -p "$HOME/shots" "$HOME/logs" # ── Claude Code ────────────────────────────────────────────────────────────── if [ "${SYLPH_AUTONOMOUS:-0}" = "1" ]; then # The flag the user asked for. It is refused under root, which is why this # image runs as `agent`. set -- claude --dangerously-skip-permissions "$@" log "starting Claude Code with --dangerously-skip-permissions" fi exec "$@"