riscv64: build at Ubuntu’s own baseline (RVA23, with vector)

riscv64: build at Ubuntu’s own baseline (RVA23, with vector)#

Every riscv64 artefact this chain builds targets RVA23-class code with vector — rv64gcv_zicsr_zifencei_zba_zbb_zbs_zicond / lp64d (§ Where it is set), a subset of the rva23u64_zifencei ISA Ubuntu 26.04 (resolute) builds its own riscv64 port at. Before 2026-09-01 they were built at the cross toolchain’s default rv64gc, which is below the platform the image already stands on.

Why this costs no compatibility#

The intuition “turning on vector breaks non-vector hardware” is backwards here. Measured in the shipped riscv64 image on 2026-09-01:

object

Tag_RISCV_arch

vector?

Ubuntu’s /lib/riscv64-linux-gnu/libc.so.6

rv64i2p1_…_b1p0_v1p0_…_zvl128b1p0

yes — 997 vsetvli

our /opt/opencv5/lib/libopencv_core.so

rv64i2p1_m2p0_a2p1_f2p2_d2p2_c2p0_zicsr2p0_zifencei2p0_zmmul1p0_zaamo1p0_zalrsc1p0_zca1p0_zcd1p0

no

The image’s own glibc and dynamic loader require RVV 1.0, so a board without a vector unit cannot run this image today, regardless of what we compile. The hardware floor is set by Ubuntu, not by us. Our binaries were simply the only sub-baseline objects in the image, giving up Zba/Zbb/Zicond and every vector kernel for nothing.

Why rva23u64_zifencei and not rv64gcv#

rv64gcv is a strictly narrower subset: it adds V but still leaves out the bit-manipulation and conditional-move extensions the distro assumes. Of the two profile spellings, only rva23u64_zifencei reproduces Ubuntu’s attribute string exactly — plain rva23u64 omits zifencei2p0, which apt’s libc carries. Verified by compiling with each and diffing readelf -A against apt’s libc.so.6.

-mabi stays lp64d, unchanged. e_flags are identical, so rv64gc and rva23u64 objects link and run together — which is what makes it safe to switch our own builds while every apt library stays as it is.

Where it is set#

The value is an ISA string, not the profile name. -march=rva23u64_zifencei works on the command line, but GCC’s build-time arch-canonicalize only parses rv32/rv64 prefixes and dies with Unexpected arch: `rva23` when the profile name reaches --with-arch. That killed a compiler stage on 2026-09-01 after 684 s. The configured default is therefore rv64gcv_zicsr_zifencei_zba_zbb_zbs_zicond — vector plus the bit-manipulation and conditional-move extensions Ubuntu’s own port carries. It is a SUBSET of RVA23 (no zvbb/zfa/zicboz and the rest); matching exactly would need the full ~400-character expansion, which is unreadable and fragile. Verified in the shipped image: the object attribute carries _v1p0, zba and zicond.

The single load-bearing edit is the cross GCC’s built-in default (02-toolchain/build-gcc.sh, the riscv64-* case: --with-arch / --with-abi, overridable with RISCV_GCC_ARCH / RISCV_GCC_ABI). A compiler default cannot be erased by a -DCMAKE_C_FLAGS= whole-string reset, cannot leak into an amd64 host sub-build, and is inherited by meson cross builds — all of which a global CFLAGS export would get wrong.

Four consumers need more than -march, because they gate their vector paths on their own switches:

consumer

what it needs

where

OpenCV

CPU_BASELINE=RVV, WITH_HAL_RVV=ON

03-media/build/opencv/build-opencv.sh

ONNX Runtime

onnxruntime_USE_RVV=ON

03-media/build/onnxruntime/build/lib/common.sh

Rust

RUSTFLAGS -C target-feature=+v,+zvl128b (no gcv triple exists)

01-core/cross-env.sh

gst-plugins-rs

its cargo_wrapper.py overwrote RUSTFLAGS; patched to merge

patches/gstreamer/003-cargo-wrapper-cross-rust-target.patch

The cross-built web-lane tools (wasm-pack, flutter_rust_bridge_codegen, since 2026-09-23) take the Rust row’s flags from setup_linux_cross_env. 06-packaging/web-lane-tools.sh keeps a copy for its cache key, and tests/test-web-lane-tools.sh pins that copy to cross-env.sh’s value. Their native fallback keeps its rv64gc Rust; neither adds a hardware floor the image’s glibc does not already set. Contract.

Android riscv64 keeps RVV off (03-media/build/opencv/android/build-android.sh): that disable works around an NDK-clang bug with sizeless RVV types, not a platform choice.

TVM’s llvm target has no host-CPU default on any arch, so on riscv64 it compiles for LLVM’s generic CPU: soft-float, no vector unit. Name the baseline, or the board tag the pinned TVM ships (riscv/spacemit-k3 for the X100, riscv/licheepi3a for the X60):

llvm -mtriple=riscv64-unknown-linux-gnu -mcpu=generic-rv64 -mabi=lp64d -mattr=+m,+a,+f,+d,+c,+v,+zvl128b,+zba,+zbb,+zbs,+zicond

riscv64 ships no IREE compiler (a documented parity exemption). Compile for it elsewhere with --iree-llvmcpu-target-triple=riscv64-unknown-linux-gnu --iree-llvmcpu-target-abi=lp64d and the same --iree-llvmcpu-target-cpu-features, or --iree-llvmcpu-target-cpu=spacemit-x100. Measured 2026-09-26 with amd64 :latest’s iree-compile: without the features the link fails on __mulsf3; with them the module holds 31 vector instructions in 153. Recorded (CON24) in image-decisions.md § Deliberate — not gaps.

The gate#

06-packaging/smoke-runtime-image.sh reads Tag_RISCV_arch off the shipped libopencv_core.so, libavcodec.so, libgstreamer-1.0.so and libc.so.6 and fails any that lacks _v1p0. It asserts the bytes, not the build log, and an empty probe reports NONE rather than passing vacuously.

Cost#

The change invalidates the warm riscv64 compiler cache: the next riscv64 build is cold. LLVM alone is roughly 50 minutes warm against many hours cold.

libyuv RVV#

libcamera builds libyuv as a Meson subproject (subprojects/libyuv.wrap, pinned at 500f4565). Turning the vector baseline on made that build fail to link:

ld.lld: error: undefined symbol: ARGBBlendRow_RVV
ld.lld: error: undefined symbol: BlendPlaneRow_RVV

Header and implementation disagree about who may use RVV:

file

condition

include/libyuv/row.h

!LIBYUV_DISABLE_RVV && __riscv_vector, plus __riscv_v_intrinsic version checks

source/row_rvv.cc, source/scale_rvv.cc

the same and defined(__clang__)

Our cross GCC 16.2.0 defines __riscv_vector and __riscv_v_intrinsic 1000000, so the header declares all 57 HAS_*_RVV entry points and callers such as planar_functions.cc reference them — while the two implementation files compile to nothing. The stale comment above the guard says why it was written that way: “GCC hasn’t supported segment load & store”. That was true for GCC 13; GCC 16 compiles both files without a diagnostic.

Upstream has since dropped the clang gate. linux/scripts/patches/libyuv/ 001-rvv-build-with-gcc.patch backports exactly that change to the pinned revision — the guard and its matching #endif comment, nothing else. Measured by compiling the pinned sources with our own cross compiler:

row_rvv.cc

scale_rvv.cc

ARGBBlendRow_RVV

pinned upstream

0 RVV symbols

0

missing

patched

54

25

defined

build-libcamera.sh applies the patch between meson setup (which downloads the subproject) and ninja, then verify_libyuv_rvv_rows counts _RVV symbols in the installed libyuv.a and fails the build at zero — so the rows cannot silently disappear again.

Do not reach for -DLIBYUV_DISABLE_RVV. It links, but it throws away every vector row in libcamera’s colour conversion, which is the opposite of the point of this baseline.

The general shape to expect from the vector switch: a dependency whose HEADERS do ISA dispatch must be BUILT with the same ISA, and a per-ISA source file may carry its own, stricter compiler gate.