Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 28 additions & 14 deletions scripts/launch_ui_c3_desktop.sh
Original file line number Diff line number Diff line change
Expand Up @@ -208,15 +208,35 @@ prepare_pandad_host_artifacts() {
}

prepare_acados_host_artifacts() {
# c_generated_code is regenerated byte-for-byte each run, so SCons sees no change and skips
# relinking, leaving a larch64 solver lib from a device build in place. Drop the whole tree.
# Generated ACADOS solver sources can be identical between device and desktop
# builds. SCons may therefore keep an AArch64 solver library from a previous
# device build instead of relinking it for the host. Remove those stale
# solver trees so they are rebuilt for the current architecture.
local rel=""
for rel in "selfdrive/controls/lib/lateral_mpc_lib/c_generated_code/libacados_ocp_solver_lat.so" \
"selfdrive/controls/lib/longitudinal_mpc_lib/c_generated_code/libacados_ocp_solver_long.so"; do
if archive_is_aarch64_elf "${ROOT_DIR}/${rel}"; then
rm -rf "$(dirname "${ROOT_DIR}/${rel}")"
fi
done

# The bundled x86_64 BLASFEO library is marked with an executable GNU_STACK.
# Some Linux systems refuse to load it, causing both ACADOS MPC Python
# extensions to fail at import time. Clear the executable-stack requirement
# before loading the host ACADOS runtime.
if [[ "$(uname -s)" == "Linux" && "$(uname -m)" == "x86_64" ]]; then
local blasfeo="${ROOT_DIR}/third_party/acados/x86_64/lib/libblasfeo.so"

if [[ -f "${blasfeo}" ]] && readelf -W -l "${blasfeo}" 2>/dev/null | grep -qE 'GNU_STACK.*RWE'; then
if ! command -v patchelf >/dev/null 2>&1; then
echo "patchelf is required to clear BLASFEO's executable stack flag."
return 1
fi

echo "Clearing executable stack flag from x86_64 BLASFEO..."
patchelf --clear-execstack "${blasfeo}"
fi
fi
}

python_ui_runtime_ok() {
Expand Down Expand Up @@ -318,18 +338,12 @@ if ! python_ui_runtime_ok >/dev/null 2>&1; then
run_scons "${jobs}" \
selfdrive/controls/lib/lateral_mpc_lib/c_generated_code/acados_ocp_solver_pyx.so \
selfdrive/controls/lib/longitudinal_mpc_lib/c_generated_code/acados_ocp_solver_pyx.so
(
cd "${ROOT_DIR}/msgq_repo"
local_scons_bin="${ROOT_DIR}/.venv/bin/scons"
if [[ -x "${local_scons_bin}" ]]; then
SP_DISABLE_AUTO_DEVICE_SCONS=1 "${local_scons_bin}" -j"${jobs}" msgq/ipc_pyx.so msgq/visionipc/visionipc_pyx.so
elif "${PY_BIN}" -m SCons --version >/dev/null 2>&1; then
SP_DISABLE_AUTO_DEVICE_SCONS=1 "${PY_BIN}" -m SCons -j"${jobs}" msgq/ipc_pyx.so msgq/visionipc/visionipc_pyx.so
else
echo "SCons not found in .venv after sync."
exit 1
fi
)

# Build msgq extensions through the root SConstruct so they link against
# StarPilot's common library instead of msgq_repo's standalone environment.
run_scons "${jobs}" \
msgq_repo/msgq/ipc_pyx.so \
msgq_repo/msgq/visionipc/visionipc_pyx.so
fi

if ! python_ui_runtime_ok >/dev/null 2>&1; then
Expand Down
39 changes: 27 additions & 12 deletions scripts/launch_ui_c4_desktop.sh
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,26 @@ prepare_pandad_host_artifacts() {
rm -f "${ROOT_DIR}/selfdrive/pandad/libcan_list_to_can_capnp.a"
}

prepare_acados_host_artifacts() {
# The bundled x86_64 BLASFEO library is marked with an executable GNU_STACK.
# Some Linux systems refuse to load it, causing both ACADOS MPC Python
# extensions to fail at import time. Clear the executable-stack requirement
# before loading the host ACADOS runtime.
if [[ "$(uname -s)" == "Linux" && "$(uname -m)" == "x86_64" ]]; then
local blasfeo="${ROOT_DIR}/third_party/acados/x86_64/lib/libblasfeo.so"

if [[ -f "${blasfeo}" ]] && readelf -W -l "${blasfeo}" 2>/dev/null | grep -qE 'GNU_STACK.*RWE'; then
if ! command -v patchelf >/dev/null 2>&1; then
echo "patchelf is required to clear BLASFEO's executable stack flag."
return 1
fi

echo "Clearing executable stack flag from x86_64 BLASFEO..."
patchelf --clear-execstack "${blasfeo}"
fi
fi
}

python_ui_runtime_ok() {
"${PY_BIN}" - <<'PY'
import pyray # noqa: F401
Expand Down Expand Up @@ -289,6 +309,7 @@ if ! python_ui_runtime_ok >/dev/null 2>&1; then
prepare_common_host_artifacts
prepare_msgq_host_artifacts
prepare_pandad_host_artifacts
prepare_acados_host_artifacts
remove_if_elf "common/params_pyx.so"
remove_if_elf "common/transformations/transformations.so"
remove_if_elf "msgq/ipc_pyx.so"
Expand All @@ -303,18 +324,12 @@ if ! python_ui_runtime_ok >/dev/null 2>&1; then
run_scons "${jobs}" \
selfdrive/controls/lib/lateral_mpc_lib/c_generated_code/acados_ocp_solver_pyx.so \
selfdrive/controls/lib/longitudinal_mpc_lib/c_generated_code/acados_ocp_solver_pyx.so
(
cd "${ROOT_DIR}/msgq_repo"
local_scons_bin="${ROOT_DIR}/.venv/bin/scons"
if [[ -x "${local_scons_bin}" ]]; then
SP_DISABLE_AUTO_DEVICE_SCONS=1 "${local_scons_bin}" -j"${jobs}" msgq/ipc_pyx.so msgq/visionipc/visionipc_pyx.so
elif "${PY_BIN}" -m SCons --version >/dev/null 2>&1; then
SP_DISABLE_AUTO_DEVICE_SCONS=1 "${PY_BIN}" -m SCons -j"${jobs}" msgq/ipc_pyx.so msgq/visionipc/visionipc_pyx.so
else
echo "SCons not found in .venv after sync."
exit 1
fi
)

# Build msgq extensions through the root SConstruct so they link against
# StarPilot's common library instead of msgq_repo's standalone environment.
run_scons "${jobs}" \
msgq_repo/msgq/ipc_pyx.so \
msgq_repo/msgq/visionipc/visionipc_pyx.so
fi

if ! python_ui_runtime_ok >/dev/null 2>&1; then
Expand Down
26 changes: 25 additions & 1 deletion selfdrive/ui/mici/onroad/augmented_road_view.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import time
import numpy as np
import pyray as rl
Expand Down Expand Up @@ -31,7 +32,7 @@
load_favorite_slots,
toggle_favorite_slot,
)
from openpilot.system.ui.lib.application import FontWeight, gui_app, MousePos, MouseEvent
from openpilot.system.ui.lib.application import FontWeight, gui_app, MousePos, MouseEvent, RECORD_HUD_ONLY, RECORD_CAMERA_ONLY, RECORD_COMBINED
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.wrap_text import wrap_text
from openpilot.system.ui.widgets.label import UnifiedLabel
Expand All @@ -55,6 +56,13 @@
CAMERA_VIEW_WIDE = 3
CAMERA_VIEW_NONE = 4

# Optional camera selection for desktop-only recording modes.
RECORD_CAMERA_VIEW = (
os.getenv("RECORD_CAMERA_VIEW", "").strip().lower()
if RECORD_HUD_ONLY or RECORD_CAMERA_ONLY or RECORD_COMBINED
else ""
)


class BookmarkState(IntEnum):
HIDDEN = 0
Expand Down Expand Up @@ -730,6 +738,11 @@ def _render(self, _):
super()._render(self._content_rect)
gui_app.mark_progress("mici.onroad.after_camera")

# Desktop-only camera recording: stop after the transformed camera frame.
if RECORD_CAMERA_ONLY:
rl.end_scissor_mode()
return

waiting_for_controls = ui_state.started and not self._controls_ready()
if waiting_for_controls:
rl.draw_rectangle(int(self._content_rect.x), int(self._content_rect.y),
Expand Down Expand Up @@ -885,6 +898,17 @@ def is_in_reverse(self) -> bool:

@staticmethod
def _camera_view() -> int:
# Desktop-only camera-view override for recording modes.
# Never changes CameraView or camera selection on comma hardware.
if RECORD_HUD_ONLY or RECORD_CAMERA_ONLY or RECORD_COMBINED:
desktop_views = {
"auto": CAMERA_VIEW_AUTO,
"standard": CAMERA_VIEW_STANDARD,
"wide": CAMERA_VIEW_WIDE,
}
if RECORD_CAMERA_VIEW in desktop_views:
return desktop_views[RECORD_CAMERA_VIEW]

camera_view = ui_state.ui_params.get_int("CameraView", return_default=True, default=CAMERA_VIEW_STANDARD)
if camera_view not in (CAMERA_VIEW_AUTO, CAMERA_VIEW_DRIVER, CAMERA_VIEW_STANDARD, CAMERA_VIEW_WIDE, CAMERA_VIEW_NONE):
return CAMERA_VIEW_STANDARD
Expand Down
21 changes: 20 additions & 1 deletion selfdrive/ui/onroad/augmented_road_view.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import time
import numpy as np
import pyray as rl
Expand All @@ -13,7 +14,7 @@
from openpilot.selfdrive.ui.onroad.model_renderer import ModelRenderer
from openpilot.selfdrive.ui.onroad.cameraview import CameraView
from openpilot.selfdrive.ui.lib.starpilot_status import get_screen_edge_color
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.application import gui_app, RECORD_HUD_ONLY, RECORD_CAMERA_ONLY, RECORD_COMBINED
from openpilot.common.transformations.camera import DEVICE_CAMERAS, DeviceCameraConfig, view_frame_from_device_frame
from openpilot.common.transformations.orientation import rot_from_euler

Expand All @@ -31,6 +32,13 @@
CAMERA_VIEW_WIDE = 3
CAMERA_VIEW_NONE = 4

# Optional camera selection for desktop-only recording modes.
RECORD_CAMERA_VIEW = (
os.getenv("RECORD_CAMERA_VIEW", "").strip().lower()
if RECORD_HUD_ONLY or RECORD_CAMERA_ONLY or RECORD_COMBINED
else ""
)

BORDER_COLORS = {
UIStatus.DISENGAGED: rl.Color(0x12, 0x28, 0x39, 0xFF), # Blue for disengaged state
UIStatus.OVERRIDE: rl.Color(0x89, 0x92, 0x8D, 0xFF), # Gray for override state
Expand Down Expand Up @@ -196,6 +204,17 @@ def _update_reverse_driver_camera_state(self) -> bool:

@staticmethod
def _camera_view() -> int:
# Desktop-only camera-view override for recording modes.
# Never changes CameraView or camera selection on comma hardware.
if RECORD_HUD_ONLY or RECORD_CAMERA_ONLY or RECORD_COMBINED:
desktop_views = {
"auto": CAMERA_VIEW_AUTO,
"standard": CAMERA_VIEW_STANDARD,
"wide": CAMERA_VIEW_WIDE,
}
if RECORD_CAMERA_VIEW in desktop_views:
return desktop_views[RECORD_CAMERA_VIEW]

params = ui_state.ui_params
camera_view = params.get_int("CameraView", return_default=True, default=CAMERA_VIEW_STANDARD)
if camera_view not in (CAMERA_VIEW_AUTO, CAMERA_VIEW_DRIVER, CAMERA_VIEW_STANDARD, CAMERA_VIEW_WIDE, CAMERA_VIEW_NONE):
Expand Down
7 changes: 6 additions & 1 deletion selfdrive/ui/onroad/cameraview.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf
from openpilot.common.swaglog import cloudlog
from openpilot.system.hardware import HARDWARE, PC, TICI
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.application import gui_app, RECORD_HUD_ONLY
from openpilot.system.ui.lib.egl import (
init_egl, is_egl_initialized, finish_gl, create_egl_image, destroy_egl_image,
bind_egl_image_to_texture, create_external_texture, destroy_external_texture, EGLImage,
Expand Down Expand Up @@ -403,6 +403,11 @@ def _render(self, rect: rl.Rectangle):

dst_rect = rl.Rectangle(x_offset, y_offset, scale_x, scale_y)

# Desktop-only transparent HUD recording: keep camera frame processing and
# geometry updates for correctly aligned model overlays, but skip camera pixels.
if RECORD_HUD_ONLY:
return

if self._use_egl:
try:
rendered = self._render_egl(src_rect, dst_rect)
Expand Down
6 changes: 3 additions & 3 deletions selfdrive/ui/ui_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.ui.lib.prime_state import PrimeState
from openpilot.selfdrive.ui.lib.ui_param_cache import shared_ui_params
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.application import gui_app, RECORD_METRIC
from openpilot.starpilot.common.lateral_only_experimental import lateral_only_experimental_available
from openpilot.system.hardware import HARDWARE, PC

Expand Down Expand Up @@ -85,7 +85,7 @@ def _initialize(self):
self._started_prev: bool = False

# Core state variables
self.is_metric: bool = self.params.get_bool("IsMetric")
self.is_metric: bool = RECORD_METRIC or self.params.get_bool("IsMetric")
self.is_release = self.params.get_bool("IsReleaseBranch")
self.always_on_dm: bool = self.params.get_bool("AlwaysOnDM")
self.usbgpu: bool = False
Expand Down Expand Up @@ -210,7 +210,7 @@ def _update_state(self, progress_hook: Callable[[str], None] | None = None) -> N
# Update recording audio state
self.recording_audio = params.get_bool("RecordAudio") and self.started

self.is_metric = params.get_bool("IsMetric")
self.is_metric = RECORD_METRIC or params.get_bool("IsMetric")
self.always_on_dm = params.get_bool("AlwaysOnDM")
self.usbgpu_compiled = params.get_bool("UsbGpuCompiled")
self.usbgpu_active = params.get_bool("UsbGpuActive")
Expand Down
Loading