# 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](#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](consumer-image-contract.md#building-the-web-lane-tools-from-source).

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`](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.
