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 |
|
vector? |
|---|---|---|
Ubuntu’s |
|
yes — 997 |
our |
|
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 |
|
|
ONNX Runtime |
|
|
Rust |
|
|
gst-plugins-rs |
its |
|
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 |
|---|---|
|
|
|
the same and |
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:
|
|
|
|
|---|---|---|---|
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.