Initial RK1 media-center image project

This commit is contained in:
2026-08-17 18:42:29 +00:00
commit 5fe41e79e9
81 changed files with 5725 additions and 0 deletions
+100
View File
@@ -0,0 +1,100 @@
# RK1 runtime and hardware diagnostics
This directory installs the local RKNN C runtime and supplies the two commands
used to qualify an RK3588 media image. It does not install RKNN Toolkit's model
conversion environment, Python wheels, or `rknn_server`.
## Pinned RKNN runtime
`rknn-version.env` locks RKNN Toolkit2 v2.3.2 to commit
`42aa1d426c0a9e0869b6374edba009f7208a1926`. The installer verifies the SHA-256
of the ARM64 runtime, C header, RK3588 MobileNet model, demo image, and license
before installing anything. A tag move or damaged download therefore fails the
image build.
Run the installer inside the target chroot:
```sh
runtime/install-rknn-runtime.sh
```
Or install into a mounted root filesystem from an ARM64 build host:
```sh
runtime/install-rknn-runtime.sh --rootfs /path/to/rootfs
```
For an offline/reproducible build, provide the pinned checkout explicitly:
```sh
runtime/install-rknn-runtime.sh \
--rootfs /path/to/rootfs \
--source-dir /path/to/rknn-toolkit2-v2.3.2
```
The build host needs Bash, Git when downloading, an ARM64 C compiler,
`coreutils`, and standard install utilities. Native compilation on the RK1 is
the supported default; a cross-build can select a compiler with `--cc`.
Files are installed below `/opt/rknn/2.3.2`, with `/opt/rknn/current` as the
stable link. The runtime path is registered in `/etc/ld.so.conf.d/rknn.conf`,
and the inference test also embeds that path as an ELF rpath. The test uses a
deterministic synthetic tensor and verifies model initialization, selected-core
submission, and finite output. It is a hardware/runtime smoke test, not a
MobileNet accuracy benchmark.
## Hardware self-test
The installed command inventories HDMI/EDID, Panthor or Mali, DRM render
nodes, RKMPP decoder and encoder bindings, AV1, RGA2/RGA3, RKNN, HDMI ALSA,
Ethernet link state, eMMC, NVMe, and the root filesystem:
```sh
rk1-media-selftest
rk1-media-selftest --quick
rk1-media-selftest --json
```
The normal run adds short Vulkan, H.264/HEVC/MJPEG encode, and NPU core
0/1/2/combined workloads when their tools are installed. `--quick` performs no
active workloads. To test 4K decoding and RKMPP-to-RGA zero-copy scaling, place
licensed samples in a directory with names containing `h264`, `hevc`, `vp9`,
and `av1`, then run:
```sh
rk1-media-selftest --media-dir /usr/share/rk1-media/samples
```
Missing tools and tests that cannot apply are `SKIP`; absent or disconnected
hardware is normally `WARN`; an advertised capability whose active workload
fails is `FAIL`. The exit status is zero unless a check fails. `--strict` also
makes warnings return status 1. Invocation errors return status 2.
## Diagnostic bundle
Create a local archive suitable for troubleshooting a blank display or missing
accelerator:
```sh
sudo rk1-media-diagnostics
sudo rk1-media-diagnostics --include-active-tests \
--media-dir /usr/share/rk1-media/samples
```
The collector records the JSON quick test, DRM connectors, decoded EDID,
drivers, filtered kernel messages, relevant packages, ALSA devices, link state,
and storage topology. It does not upload anything. It omits raw EDID, disk
serials and UUIDs, addresses, user files, SSH material, and unfiltered journals,
and applies basic redaction; review the bundle before sharing it.
## Tests
```sh
runtime/tests/run-tests.sh
RKNN_SOURCE_DIR=/path/to/rknn-toolkit2-v2.3.2 \
runtime/tests/run-tests.sh
```
The first form tests syntax, absent-hardware handling, JSON, synthetic sysfs,
and diagnostic collection. Supplying `RKNN_SOURCE_DIR` also compiles and stages
the pinned runtime into a temporary root filesystem.
+209
View File
@@ -0,0 +1,209 @@
#!/usr/bin/env bash
# Install the pinned RKNN C runtime, test model, and RK1 diagnostic tools.
set -Eeuo pipefail
umask 022
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)
# shellcheck source=rknn-version.env
source "$SCRIPT_DIR/rknn-version.env"
ROOTFS="/"
SOURCE_DIR=""
CC_BIN=${CC:-cc}
KEEP_WORK=0
WORK_DIR=""
usage() {
cat <<'EOF'
Usage: install-rknn-runtime.sh [OPTIONS]
Options:
--rootfs PATH Install below PATH (default: /).
--source-dir PATH Use an existing rknn-toolkit2 v2.3.2 checkout.
--cc COMMAND C compiler for the target (default: $CC or cc).
--keep-work Preserve a downloaded checkout for inspection.
-h, --help Show this help.
Without --source-dir, the script fetches only the required paths from the
immutable upstream commit. Network access and git are then required.
EOF
}
die() {
printf 'install-rknn-runtime: error: %s\n' "$*" >&2
exit 1
}
note() {
printf 'install-rknn-runtime: %s\n' "$*" >&2
}
while (($#)); do
case "$1" in
--rootfs)
(($# >= 2)) || die "--rootfs requires a path"
ROOTFS=$2
shift 2
;;
--source-dir)
(($# >= 2)) || die "--source-dir requires a path"
SOURCE_DIR=$2
shift 2
;;
--cc)
(($# >= 2)) || die "--cc requires a compiler command"
CC_BIN=$2
shift 2
;;
--keep-work)
KEEP_WORK=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
die "unknown option: $1"
;;
esac
done
[[ -d "$ROOTFS" ]] || die "rootfs is not a directory: $ROOTFS"
ROOTFS=$(readlink -f -- "$ROOTFS")
[[ -n "$ROOTFS" ]] || die "could not resolve rootfs"
if [[ "$ROOTFS" == "/" && ${EUID:-$(id -u)} -ne 0 ]]; then
die "installing into / requires root"
fi
for tool in install ln readlink sha256sum "$CC_BIN"; do
command -v "$tool" >/dev/null 2>&1 || die "required command not found: $tool"
done
cleanup() {
if [[ -n "$WORK_DIR" && -d "$WORK_DIR" && $KEEP_WORK -eq 0 ]]; then
rm -rf -- "$WORK_DIR"
elif [[ -n "$WORK_DIR" && -d "$WORK_DIR" ]]; then
note "preserved work directory: $WORK_DIR"
fi
}
trap cleanup EXIT
verify_sha256() {
local expected=$1
local path=$2
local actual
[[ -f "$path" ]] || die "required upstream asset is missing: $path"
actual=$(sha256sum -- "$path")
actual=${actual%% *}
[[ "$actual" == "$expected" ]] ||
die "checksum mismatch for $path (expected $expected, got $actual)"
}
if [[ -z "$SOURCE_DIR" ]]; then
command -v git >/dev/null 2>&1 || die "git is required without --source-dir"
WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/rknn-v232.XXXXXXXX")
SOURCE_DIR="$WORK_DIR/source"
note "fetching RKNN Toolkit2 commit $RKNN_COMMIT"
git init -q "$SOURCE_DIR"
git -C "$SOURCE_DIR" remote add origin "$RKNN_REPOSITORY"
git -C "$SOURCE_DIR" config remote.origin.promisor true
git -C "$SOURCE_DIR" config remote.origin.partialclonefilter blob:none
git -C "$SOURCE_DIR" sparse-checkout init --cone
git -C "$SOURCE_DIR" sparse-checkout set \
rknpu2/runtime/Linux/librknn_api \
rknpu2/examples/rknn_api_demo/model
git -C "$SOURCE_DIR" fetch -q --depth 1 --filter=blob:none origin \
"refs/tags/$RKNN_TAG"
git -C "$SOURCE_DIR" checkout -q --detach FETCH_HEAD
fi
SOURCE_DIR=$(readlink -f -- "$SOURCE_DIR")
[[ -d "$SOURCE_DIR" ]] || die "source directory is not a directory"
if [[ -d "$SOURCE_DIR/.git" ]]; then
source_commit=$(git -C "$SOURCE_DIR" rev-parse HEAD)
[[ "$source_commit" == "$RKNN_COMMIT" ]] ||
die "source checkout is $source_commit, expected $RKNN_COMMIT"
fi
HEADER="$SOURCE_DIR/rknpu2/runtime/Linux/librknn_api/include/rknn_api.h"
RUNTIME_SO="$SOURCE_DIR/rknpu2/runtime/Linux/librknn_api/aarch64/librknnrt.so"
MODEL="$SOURCE_DIR/rknpu2/examples/rknn_api_demo/model/RK3588/mobilenet_v1.rknn"
DEMO_IMAGE="$SOURCE_DIR/rknpu2/examples/rknn_api_demo/model/dog_224x224.jpg"
LICENSE_FILE="$SOURCE_DIR/LICENSE"
verify_sha256 "$RKNN_HEADER_SHA256" "$HEADER"
verify_sha256 "$RKNN_RUNTIME_AARCH64_SHA256" "$RUNTIME_SO"
verify_sha256 "$RKNN_MOBILENET_RK3588_SHA256" "$MODEL"
verify_sha256 "$RKNN_DEMO_IMAGE_SHA256" "$DEMO_IMAGE"
verify_sha256 "$RKNN_LICENSE_SHA256" "$LICENSE_FILE"
if [[ -z "$WORK_DIR" ]]; then
WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/rknn-v232-build.XXXXXXXX")
fi
TEST_BINARY="$WORK_DIR/rknn-inference-test"
note "building the ARM64 RKNN smoke test with $CC_BIN"
"$CC_BIN" \
-std=c11 -D_POSIX_C_SOURCE=200809L -O2 -Wall -Wextra -Wpedantic \
-fstack-protector-strong -D_FORTIFY_SOURCE=2 \
-I"$(dirname -- "$HEADER")" \
"$SCRIPT_DIR/rknn-inference-test.c" \
-L"$(dirname -- "$RUNTIME_SO")" -Wl,--as-needed \
-Wl,-z,relro,-z,now -Wl,-rpath,/opt/rknn/current/lib \
-lrknnrt -lm -o "$TEST_BINARY"
VERSION_ROOT="$ROOTFS/opt/rknn/$RKNN_VERSION"
install -d -m 0755 \
"$VERSION_ROOT/bin" \
"$VERSION_ROOT/include" \
"$VERSION_ROOT/lib" \
"$VERSION_ROOT/share/models/rk3588" \
"$VERSION_ROOT/share/demo" \
"$VERSION_ROOT/share/licenses" \
"$ROOTFS/etc/ld.so.conf.d" \
"$ROOTFS/usr/local/bin" \
"$ROOTFS/usr/local/sbin"
install -m 0755 "$TEST_BINARY" "$VERSION_ROOT/bin/rknn-inference-test"
install -m 0644 "$HEADER" "$VERSION_ROOT/include/rknn_api.h"
install -m 0644 "$RUNTIME_SO" "$VERSION_ROOT/lib/librknnrt.so"
ln -sfn librknnrt.so "$VERSION_ROOT/lib/librknn_api.so"
install -m 0644 "$MODEL" \
"$VERSION_ROOT/share/models/rk3588/mobilenet_v1.rknn"
install -m 0644 "$DEMO_IMAGE" "$VERSION_ROOT/share/demo/dog_224x224.jpg"
install -m 0644 "$LICENSE_FILE" \
"$VERSION_ROOT/share/licenses/rknn-toolkit2-LICENSE"
cat >"$VERSION_ROOT/manifest.env" <<EOF
RKNN_VERSION=$RKNN_VERSION
RKNN_TAG=$RKNN_TAG
RKNN_COMMIT=$RKNN_COMMIT
RKNN_REPOSITORY=$RKNN_REPOSITORY
RKNN_RUNTIME_AARCH64_SHA256=$RKNN_RUNTIME_AARCH64_SHA256
RKNN_MOBILENET_RK3588_SHA256=$RKNN_MOBILENET_RK3588_SHA256
RKNN_DEMO_IMAGE_SHA256=$RKNN_DEMO_IMAGE_SHA256
EOF
chmod 0644 "$VERSION_ROOT/manifest.env"
ln -sfn "$RKNN_VERSION" "$ROOTFS/opt/rknn/current"
ln -sfn /opt/rknn/current/bin/rknn-inference-test \
"$ROOTFS/usr/local/bin/rknn-inference-test"
printf '%s\n' '/opt/rknn/current/lib' >"$ROOTFS/etc/ld.so.conf.d/rknn.conf"
chmod 0644 "$ROOTFS/etc/ld.so.conf.d/rknn.conf"
install -m 0755 "$SCRIPT_DIR/rk1-media-selftest" \
"$ROOTFS/usr/local/bin/rk1-media-selftest"
install -m 0755 "$SCRIPT_DIR/rk1-media-diagnostics" \
"$ROOTFS/usr/local/sbin/rk1-media-diagnostics"
if command -v ldconfig >/dev/null 2>&1 &&
[[ -e "$ROOTFS/etc/ld.so.conf" && -d "$ROOTFS/lib" ]]; then
if ! ldconfig -r "$ROOTFS"; then
note "warning: ldconfig failed; the smoke test still has an embedded rpath"
fi
fi
note "installed RKNN Runtime $RKNN_VERSION below $ROOTFS/opt/rknn/current"
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
# Extract only the immutable RKNN assets needed by install-rknn-runtime.sh.
set -Eeuo pipefail
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)
# shellcheck source=rknn-version.env
source "$SCRIPT_DIR/rknn-version.env"
[[ $# -eq 2 ]] || {
echo "Usage: $0 PINNED-RKNN-CHECKOUT OUTPUT-DIRECTORY" >&2
exit 2
}
SOURCE_DIR=$(readlink -f -- "$1")
OUTPUT_DIR=$2
[[ -d "$SOURCE_DIR/.git" ]] || {
echo "Source must be a Git checkout: $SOURCE_DIR" >&2
exit 1
}
[[ "$(git -C "$SOURCE_DIR" rev-parse HEAD)" == "$RKNN_COMMIT" ]] || {
echo "RKNN checkout is not pinned commit $RKNN_COMMIT" >&2
exit 1
}
[[ ! -e "$OUTPUT_DIR" ]] || {
echo "Refusing to replace existing output: $OUTPUT_DIR" >&2
exit 1
}
declare -a assets=(
"rknpu2/runtime/Linux/librknn_api/include/rknn_api.h:$RKNN_HEADER_SHA256"
"rknpu2/runtime/Linux/librknn_api/aarch64/librknnrt.so:$RKNN_RUNTIME_AARCH64_SHA256"
"rknpu2/examples/rknn_api_demo/model/RK3588/mobilenet_v1.rknn:$RKNN_MOBILENET_RK3588_SHA256"
"rknpu2/examples/rknn_api_demo/model/dog_224x224.jpg:$RKNN_DEMO_IMAGE_SHA256"
"LICENSE:$RKNN_LICENSE_SHA256"
)
for entry in "${assets[@]}"; do
relative_path=${entry%%:*}
expected_sha=${entry##*:}
actual_sha=$(sha256sum "$SOURCE_DIR/$relative_path" | awk '{print $1}')
[[ "$actual_sha" == "$expected_sha" ]] || {
echo "Checksum mismatch: $relative_path" >&2
exit 1
}
install -D -m 0644 "$SOURCE_DIR/$relative_path" "$OUTPUT_DIR/$relative_path"
done
cat >"$OUTPUT_DIR/SOURCE.env" <<EOF
RKNN_VERSION=$RKNN_VERSION
RKNN_TAG=$RKNN_TAG
RKNN_COMMIT=$RKNN_COMMIT
RKNN_REPOSITORY=$RKNN_REPOSITORY
EOF
chmod 0644 "$OUTPUT_DIR/SOURCE.env"
echo "Created verified offline RKNN bundle: $OUTPUT_DIR"
+437
View File
@@ -0,0 +1,437 @@
#!/usr/bin/env bash
# Collect a privacy-conscious, read-only RK1 media diagnostic bundle.
set -Eeuo pipefail
export LC_ALL=C
umask 077
OUTPUT=""
DIRECTORY_OUTPUT=0
ACTIVE_TESTS=0
MEDIA_DIR=""
TEMP_DIR=""
SYS_ROOT=${RK1_SYSFS_ROOT:-/sys}
PROC_ROOT=${RK1_PROCFS_ROOT:-/proc}
DEV_ROOT=${RK1_DEV_ROOT:-/dev}
RKNN_HOME=${RKNN_HOME:-/opt/rknn/current}
usage() {
cat <<'EOF'
Usage: rk1-media-diagnostics [OPTIONS]
Options:
--output PATH Output archive or directory path.
--directory Write an unpacked directory instead of a .tar.gz.
--include-active-tests Include short Vulkan, encode, and NPU workloads.
--media-dir PATH Pass media samples to active decode/RGA tests.
-h, --help Show this help.
The bundle is local only. It deliberately excludes environment variables,
home-directory contents, SSH material, disk serials/UUIDs, MAC addresses,
IP addresses, raw EDID, and unfiltered system journals. Review it before
sharing it with anyone.
EOF
}
die() {
printf 'rk1-media-diagnostics: %s\n' "$*" >&2
exit 2
}
while (($#)); do
case "$1" in
--output)
(($# >= 2)) || die "--output requires a path"
OUTPUT=$2
shift 2
;;
--directory)
DIRECTORY_OUTPUT=1
shift
;;
--include-active-tests)
ACTIVE_TESTS=1
shift
;;
--media-dir)
(($# >= 2)) || die "--media-dir requires a path"
MEDIA_DIR=$2
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
die "unknown option: $1"
;;
esac
done
if [[ -n "$MEDIA_DIR" && ! -d "$MEDIA_DIR" ]]; then
die "media directory is not a directory: $MEDIA_DIR"
fi
safe_host=$(hostname 2>/dev/null || printf rk1)
safe_host=${safe_host//[^A-Za-z0-9._-]/_}
timestamp=$(date -u +%Y%m%dT%H%M%SZ)
if [[ -z "$OUTPUT" ]]; then
if ((DIRECTORY_OUTPUT)); then
OUTPUT="$PWD/rk1-media-diagnostics-${safe_host}-${timestamp}"
else
OUTPUT="$PWD/rk1-media-diagnostics-${safe_host}-${timestamp}.tar.gz"
fi
fi
output_parent=$(dirname -- "$OUTPUT")
[[ -d "$output_parent" ]] || die "output parent does not exist: $output_parent"
output_parent=$(cd -- "$output_parent" && pwd -P)
OUTPUT="$output_parent/$(basename -- "$OUTPUT")"
[[ ! -e "$OUTPUT" && ! -L "$OUTPUT" ]] || die "refusing to overwrite $OUTPUT"
TEMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/rk1-media-diagnostics.XXXXXXXX")
REPORT="$TEMP_DIR/report"
mkdir -p "$REPORT"
cleanup() {
if [[ -n "$TEMP_DIR" && -d "$TEMP_DIR" ]]; then
rm -rf -- "$TEMP_DIR"
fi
}
trap cleanup EXIT
sanitize_stream() {
# Redact common MAC, IPv4, root-device, and static-IP command-line forms.
sed -E \
-e 's/([[:xdigit:]]{2}:){5}[[:xdigit:]]{2}/<mac-redacted>/g' \
-e 's/([[:space:]=]|^)([0-9]{1,3}\.){3}[0-9]{1,3}([[:space:]\/:]|$)/\1<ip-redacted>\3/g' \
-e 's/(root=)[^[:space:]]+/\1<root-device-redacted>/g' \
-e 's/(ip=)[^[:space:]]+/\1<ip-config-redacted>/g' \
-e 's/(UUID|PARTUUID)=[A-Za-z0-9-]+/\1=<redacted>/g' \
-e 's/([Ss]erial([ _-]?[Nn]umber)?[=:][[:space:]]*)[^[:space:],;]+/\1<redacted>/g'
}
run_capture() {
local destination=$1
shift
{
printf '$'
printf ' %q' "$@"
printf '\n'
if command -v "$1" >/dev/null 2>&1; then
timeout 45s "$@" 2>&1 ||
printf '[command exited %d]\n' "$?"
else
printf '[command unavailable: %s]\n' "$1"
fi
} | sanitize_stream >"$REPORT/$destination"
}
append_command() {
local destination=$1
shift
{
printf '\n$'
printf ' %q' "$@"
printf '\n'
if command -v "$1" >/dev/null 2>&1; then
timeout 45s "$@" 2>&1 ||
printf '[command exited %d]\n' "$?"
else
printf '[command unavailable: %s]\n' "$1"
fi
} | sanitize_stream >>"$REPORT/$destination"
}
cat >"$REPORT/README.txt" <<'EOF'
RK1 media diagnostic bundle
This is a read-only snapshot for diagnosing HDMI, GPU, RKMPP VPU, RGA,
RKNPU, ALSA, Ethernet, and storage enumeration. Commands that were missing or
permission-denied are recorded rather than treated as collector failures.
The collector attempts to redact IP and MAC addresses and omits raw EDID,
disk serials/UUIDs, environment variables, home directories, SSH material,
and unfiltered journals. Review every file before sharing the bundle.
EOF
{
printf 'collected_utc=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
printf 'collector_version=1\n'
printf 'active_tests=%s\n' "$ACTIVE_TESTS"
printf 'kernel='
uname -srvm 2>/dev/null || true
printf 'architecture='
uname -m 2>/dev/null || true
if [[ -r /etc/os-release ]]; then
printf '\n[os-release]\n'
grep -E '^(NAME|VERSION|VERSION_ID|ID|ID_LIKE)=' /etc/os-release || true
fi
if [[ -r "$PROC_ROOT/device-tree/model" ]]; then
printf '\nboard_model='
tr -d '\000' <"$PROC_ROOT/device-tree/model" || true
printf '\n'
fi
if [[ -r "$PROC_ROOT/cmdline" ]]; then
printf '\n[kernel-command-line-redacted]\n'
sanitize_stream <"$PROC_ROOT/cmdline"
printf '\n'
fi
} >"$REPORT/system.txt"
SELFTEST=""
if command -v rk1-media-selftest >/dev/null 2>&1; then
SELFTEST=$(command -v rk1-media-selftest)
elif [[ -x "$(dirname -- "${BASH_SOURCE[0]}")/rk1-media-selftest" ]]; then
SELFTEST="$(dirname -- "${BASH_SOURCE[0]}")/rk1-media-selftest"
fi
if [[ -n "$SELFTEST" ]]; then
selftest_args=(--json)
if ((ACTIVE_TESTS == 0)); then
selftest_args+=(--quick)
fi
if [[ -n "$MEDIA_DIR" ]]; then
selftest_args+=(--media-dir "$MEDIA_DIR")
fi
if ! timeout 600s "$SELFTEST" "${selftest_args[@]}" \
>"$REPORT/selftest.json" 2>"$REPORT/selftest.stderr"; then
printf 'self-test returned a nonzero status; inspect its JSON and stderr\n' \
>"$REPORT/selftest-status.txt"
fi
else
printf '{"error":"rk1-media-selftest is unavailable"}\n' \
>"$REPORT/selftest.json"
fi
# DRM connector details. EDID is represented by size, hash, and decoded text,
# never by the raw binary blob.
{
shopt -s nullglob
connectors=("$SYS_ROOT"/class/drm/card*-*)
shopt -u nullglob
if ((${#connectors[@]} == 0)); then
printf 'No DRM connectors found.\n'
fi
for connector in "${connectors[@]}"; do
[[ -d "$connector" ]] || continue
printf '\n[%s]\n' "${connector##*/}"
for attribute in status enabled dpms link_status; do
if [[ -r "$connector/$attribute" ]]; then
printf '%s=' "$attribute"
tr -d '\000' <"$connector/$attribute" 2>/dev/null || true
printf '\n'
fi
done
if [[ -r "$connector/modes" ]]; then
printf 'modes:\n'
sed 's/^/ /' "$connector/modes" 2>/dev/null || true
fi
if [[ -s "$connector/edid" ]]; then
printf 'edid_size=%s\n' "$(wc -c <"$connector/edid")"
printf 'edid_sha256=%s\n' \
"$(sha256sum "$connector/edid" | awk '{print $1}')"
if command -v edid-decode >/dev/null 2>&1; then
printf 'decoded_edid:\n'
timeout 15s edid-decode "$connector/edid" 2>&1 |
sed 's/^/ /' || true
fi
fi
done
} | sanitize_stream >"$REPORT/drm-connectors.txt"
run_capture drm-modetest.txt modetest -c -p
run_capture kernel-modules.txt lsmod
{
printf '[platform GPU driver bindings]\n'
for driver in panthor panfrost mali; do
directory="$SYS_ROOT/bus/platform/drivers/$driver"
[[ -d "$directory" ]] || continue
printf '%s:\n' "$driver"
find "$directory" -mindepth 1 -maxdepth 1 \( -type l -o -type d \) \
-printf ' %f\n' 2>/dev/null | sort
done
printf '\n[GPU devfreq]\n'
shopt -s nullglob
gpu_nodes=("$SYS_ROOT"/class/devfreq/*.gpu)
shopt -u nullglob
for node in "${gpu_nodes[@]}"; do
printf '%s\n' "${node##*/}"
for attribute in cur_freq min_freq max_freq governor available_frequencies; do
if [[ -r "$node/$attribute" ]]; then
printf ' %s=' "$attribute"
tr -d '\000' <"$node/$attribute" 2>/dev/null || true
printf '\n'
fi
done
done
} >"$REPORT/gpu-sysfs.txt"
run_capture vulkan.txt vulkaninfo --summary
append_command vulkan.txt eglinfo -B
{
printf '[media and accelerator platform bindings]\n'
for pattern in 'mpp*' 'rga*' 'rockchip-rga' 'RKNPU' 'rknpu'; do
shopt -s nullglob
directories=("$SYS_ROOT"/bus/platform/drivers/$pattern)
shopt -u nullglob
for directory in "${directories[@]}"; do
[[ -d "$directory" ]] || continue
printf '\n%s:\n' "${directory##*/}"
find "$directory" -mindepth 1 -maxdepth 1 \( -type l -o -type d \) \
-printf ' %f\n' 2>/dev/null | sort
done
done
printf '\n[device nodes]\n'
shopt -s nullglob
nodes=("$DEV_ROOT"/dri/* "$DEV_ROOT"/mpp_service \
"$DEV_ROOT"/rga "$DEV_ROOT"/rknpu* "$DEV_ROOT"/mali*)
shopt -u nullglob
for node in "${nodes[@]}"; do
stat -c '%A %U:%G %t:%T %n' "$node" 2>/dev/null || true
done
} >"$REPORT/accelerators.txt"
FFMPEG=""
for candidate in /opt/rkmedia/bin/ffmpeg-rk /usr/local/bin/ffmpeg-rk; do
if [[ -x "$candidate" ]]; then
FFMPEG=$candidate
break
fi
done
if [[ -z "$FFMPEG" ]] && command -v ffmpeg-rk >/dev/null 2>&1; then
FFMPEG=$(command -v ffmpeg-rk)
fi
if [[ -n "$FFMPEG" ]]; then
run_capture ffmpeg-rk.txt "$FFMPEG" -hide_banner -version
append_command ffmpeg-rk.txt "$FFMPEG" -hide_banner -hwaccels
{
printf '\n[Rockchip encoders, decoders, and filters]\n'
"$FFMPEG" -hide_banner -decoders 2>/dev/null |
grep -Ei 'h264|hevc|vp9|av1|rkmpp' || true
"$FFMPEG" -hide_banner -encoders 2>/dev/null |
grep -Ei 'h264|hevc|mjpeg|rkmpp' || true
"$FFMPEG" -hide_banner -filters 2>/dev/null |
grep -Ei 'rkrga|drm' || true
} >>"$REPORT/ffmpeg-rk.txt"
else
printf 'ffmpeg-rk is unavailable\n' >"$REPORT/ffmpeg-rk.txt"
fi
{
printf '[RKNN installation]\n'
if [[ -r "$RKNN_HOME/manifest.env" ]]; then
sed -E 's#(REPOSITORY=).*#\1<upstream-url>#' "$RKNN_HOME/manifest.env"
else
printf 'manifest unavailable at %s\n' "$RKNN_HOME/manifest.env"
fi
for asset in \
"$RKNN_HOME/lib/librknnrt.so" \
"$RKNN_HOME/share/models/rk3588/mobilenet_v1.rknn" \
"$RKNN_HOME/bin/rknn-inference-test"; do
if [[ -r "$asset" ]]; then
sha256sum "$asset"
else
printf 'missing: %s\n' "$asset"
fi
done
printf '\n[NPU devfreq]\n'
shopt -s nullglob
npu_nodes=("$SYS_ROOT"/class/devfreq/*.npu)
shopt -u nullglob
for node in "${npu_nodes[@]}"; do
printf '%s\n' "${node##*/}"
for attribute in cur_freq min_freq max_freq governor available_frequencies; do
if [[ -r "$node/$attribute" ]]; then
printf ' %s=' "$attribute"
tr -d '\000' <"$node/$attribute" 2>/dev/null || true
printf '\n'
fi
done
done
} >"$REPORT/npu.txt"
{
printf '[ALSA cards]\n'
if [[ -r "$PROC_ROOT/asound/cards" ]]; then
cat "$PROC_ROOT/asound/cards"
else
printf 'unavailable\n'
fi
} >"$REPORT/audio.txt"
append_command audio.txt aplay -l
append_command audio.txt aplay -L
{
printf 'Interface state only; addresses and MACs are intentionally omitted.\n\n'
shopt -s nullglob
interfaces=("$SYS_ROOT"/class/net/*)
shopt -u nullglob
for interface in "${interfaces[@]}"; do
name=${interface##*/}
[[ "$name" == lo ]] && continue
printf '[%s]\n' "$name"
for attribute in operstate carrier speed duplex mtu; do
if [[ -r "$interface/$attribute" ]]; then
printf '%s=' "$attribute"
tr -d '\000' <"$interface/$attribute" 2>/dev/null || true
printf '\n'
fi
done
printf '\n'
done
} >"$REPORT/network.txt"
run_capture storage.txt lsblk -o NAME,TYPE,SIZE,FSTYPE,MOUNTPOINTS,ROTA,TRAN
{
printf '\n[root filesystem]\n'
if command -v findmnt >/dev/null 2>&1; then
findmnt -n -o SOURCE,FSTYPE / 2>&1 | sanitize_stream
else
printf 'findmnt unavailable\n'
fi
} >>"$REPORT/storage.txt"
run_capture usb.txt lsusb -t
append_command usb.txt lspci -nnk
{
if command -v dpkg-query >/dev/null 2>&1; then
dpkg-query -W -f='${binary:Package}\t${Version}\n' 2>/dev/null |
grep -Ei 'armbian|linux-image|linux-dtb|mesa|vulkan|kodi|ffmpeg|mpp|rga|rknn|alsa|libcec' |
sort || true
elif command -v rpm >/dev/null 2>&1; then
rpm -qa 2>/dev/null |
grep -Ei 'kernel|mesa|vulkan|kodi|ffmpeg|mpp|rga|rknn|alsa|libcec' |
sort || true
else
printf 'supported package inventory tool unavailable\n'
fi
} >"$REPORT/packages.txt"
kernel_pattern='drm|hdmi|vop|edid|panthor|panfrost|mali|rknpu|npu|rkvdec|rkvenc|av1|mpp|rga|alsa|snd|cec|pcie|nvme|mmc'
{
printf '[filtered current-boot kernel messages]\n'
if command -v journalctl >/dev/null 2>&1; then
timeout 45s journalctl -b -k --no-pager 2>&1 |
grep -Ei "$kernel_pattern" | tail -2500 || true
elif command -v dmesg >/dev/null 2>&1; then
dmesg --color=never 2>&1 |
grep -Ei "$kernel_pattern" | tail -2500 || true
else
printf 'kernel log reader unavailable\n'
fi
} | sanitize_stream >"$REPORT/kernel-media.log"
if ((DIRECTORY_OUTPUT)); then
mkdir -- "$OUTPUT"
cp -a "$REPORT/." "$OUTPUT/"
chmod -R go-rwx "$OUTPUT"
else
command -v tar >/dev/null 2>&1 || die "tar is required for archive output"
tar -C "$REPORT" -czf "$OUTPUT" .
chmod 0600 "$OUTPUT"
fi
printf 'Diagnostic bundle written to %s\n' "$OUTPUT"
+574
View File
@@ -0,0 +1,574 @@
#!/usr/bin/env bash
# Read-only RK3588 media hardware inventory with optional short workloads.
set -uo pipefail
export LC_ALL=C
JSON=0
QUICK=0
STRICT=0
MEDIA_DIR=""
SYS_ROOT=${RK1_SYSFS_ROOT:-/sys}
PROC_ROOT=${RK1_PROCFS_ROOT:-/proc}
DEV_ROOT=${RK1_DEV_ROOT:-/dev}
RKNN_HOME=${RKNN_HOME:-/opt/rknn/current}
declare -a CHECK_GROUP=()
declare -a CHECK_NAME=()
declare -a CHECK_STATUS=()
declare -a CHECK_MESSAGE=()
usage() {
cat <<'EOF'
Usage: rk1-media-selftest [OPTIONS]
Options:
--json Emit one JSON document instead of readable text.
--quick Inventory only; skip GPU, codec, and NPU workloads.
--strict Return nonzero for warnings as well as failures.
--media-dir PATH Exercise RKMPP decoding with named sample files.
Recognized names contain h264, hevc, vp9, or av1.
-h, --help Show this help.
The default run is non-destructive. It executes only short inference, Vulkan,
and encoder probes when the relevant runtime and devices are present. It never
changes clocks, governors, display modes, storage, or network configuration.
EOF
}
invocation_error() {
printf 'rk1-media-selftest: %s\n' "$*" >&2
exit 2
}
while (($#)); do
case "$1" in
--json)
JSON=1
shift
;;
--quick)
QUICK=1
shift
;;
--strict)
STRICT=1
shift
;;
--media-dir)
(($# >= 2)) || invocation_error "--media-dir requires a path"
MEDIA_DIR=$2
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
invocation_error "unknown option: $1"
;;
esac
done
if [[ -n "$MEDIA_DIR" && ! -d "$MEDIA_DIR" ]]; then
invocation_error "media directory is not a directory: $MEDIA_DIR"
fi
add_check() {
CHECK_GROUP+=("$1")
CHECK_NAME+=("$2")
CHECK_STATUS+=("$3")
CHECK_MESSAGE+=("$4")
}
read_text() {
local path=$1
if [[ -r "$path" ]]; then
tr -d '\000' <"$path" 2>/dev/null || true
fi
}
short_message() {
local value=$1
value=${value//$'\r'/}
value=${value//$'\n'/'; '}
value=${value//$'\t'/ }
printf '%s' "${value:0:320}"
}
driver_binding_count() {
local pattern=$1
local directory entry base
local count=0
shopt -s nullglob
for directory in "$SYS_ROOT"/bus/platform/drivers/$pattern; do
[[ -d "$directory" ]] || continue
for entry in "$directory"/*; do
base=${entry##*/}
case "$base" in
bind|unbind|uevent|module|new_id|remove_id) continue ;;
esac
if [[ -L "$entry" || -d "$entry" ]]; then
((count++))
fi
done
done
shopt -u nullglob
printf '%d' "$count"
}
find_sample() {
local token=$1
[[ -n "$MEDIA_DIR" ]] || return 1
find "$MEDIA_DIR" -maxdepth 1 -type f \
\( -iname "*${token}*.mkv" -o -iname "*${token}*.mp4" \
-o -iname "*${token}*.webm" -o -iname "*${token}*.ts" \) \
-print -quit 2>/dev/null
}
# System identity
machine=$(uname -m 2>/dev/null || printf unknown)
if [[ "$machine" == "aarch64" ]]; then
add_check system architecture pass "aarch64 userspace"
else
add_check system architecture fail "expected aarch64, found $machine"
fi
model=$(read_text "$PROC_ROOT/device-tree/model")
if [[ "$model" == *"Turing"*"RK1"* ]]; then
add_check system board pass "$model"
elif [[ -n "$model" ]]; then
add_check system board warn "device-tree model is $model"
else
add_check system board warn "device-tree model is unavailable"
fi
# HDMI / DRM connector state
shopt -s nullglob
hdmi_connectors=("$SYS_ROOT"/class/drm/card*-HDMI-A-*)
shopt -u nullglob
if ((${#hdmi_connectors[@]} == 0)); then
add_check hdmi connector warn "no DRM HDMI connector is registered"
else
connected_count=0
for connector in "${hdmi_connectors[@]}"; do
connector_name=${connector##*/}
state=$(read_text "$connector/status")
case "$state" in
connected)
((connected_count++))
add_check hdmi "$connector_name" pass "connected"
if [[ -s "$connector/edid" ]]; then
edid_bytes=$(wc -c <"$connector/edid" 2>/dev/null || printf 0)
add_check hdmi "$connector_name-edid" pass \
"$edid_bytes bytes of EDID data"
else
add_check hdmi "$connector_name-edid" warn \
"connected but EDID is empty or unreadable"
fi
if [[ -r "$connector/modes" ]] &&
grep -qx '3840x2160' "$connector/modes" 2>/dev/null; then
add_check hdmi "$connector_name-4k" pass \
"EDID advertises a 3840x2160 mode"
else
add_check hdmi "$connector_name-4k" warn \
"3840x2160 is not present in the connector mode list"
fi
;;
disconnected)
add_check hdmi "$connector_name" warn \
"disconnected (expected when no display is attached)"
;;
*)
add_check hdmi "$connector_name" warn \
"connector state is ${state:-unknown}"
;;
esac
done
fi
hdmi_driver_count=$(driver_binding_count 'dwhdmi*')
hdmi_phy_count=$(driver_binding_count '*hdptx*hdmi*')
if ((hdmi_driver_count > 0 && hdmi_phy_count > 0)); then
add_check hdmi drivers pass \
"$hdmi_driver_count controller and $hdmi_phy_count HDMI PHY binding(s)"
else
add_check hdmi drivers warn \
"HDMI controller bindings=$hdmi_driver_count, PHY bindings=$hdmi_phy_count"
fi
# GPU / Vulkan
gpu_driver=""
for candidate in panthor panfrost mali; do
if (($(driver_binding_count "$candidate") > 0)); then
gpu_driver=$candidate
break
fi
done
case "$gpu_driver" in
panthor)
add_check gpu kernel-driver pass "Panthor is bound to the GPU"
;;
panfrost)
add_check gpu kernel-driver warn "Panfrost is bound; the image expects Panthor"
;;
mali)
add_check gpu kernel-driver warn "proprietary Mali kernel driver is bound"
;;
*)
add_check gpu kernel-driver warn "no bound Mali GPU driver was found"
;;
esac
shopt -s nullglob
gpu_devfreq=("$SYS_ROOT"/class/devfreq/*.gpu)
render_nodes=("$DEV_ROOT"/dri/renderD*)
shopt -u nullglob
if ((${#gpu_devfreq[@]} > 0)); then
add_check gpu devfreq pass "${#gpu_devfreq[@]} GPU devfreq device(s)"
else
add_check gpu devfreq warn "GPU devfreq node is missing"
fi
if ((${#render_nodes[@]} > 0)); then
add_check gpu render-node pass "${#render_nodes[@]} DRM render node(s)"
else
add_check gpu render-node warn "no accessible DRM render node"
fi
if ((QUICK)); then
add_check gpu vulkan skip "active Vulkan probe disabled by --quick"
elif command -v vulkaninfo >/dev/null 2>&1; then
vulkan_output=$(timeout 20s vulkaninfo --summary 2>&1)
vulkan_rc=$?
if ((vulkan_rc == 0)); then
gpu_name=$(printf '%s\n' "$vulkan_output" |
sed -n 's/^[[:space:]]*deviceName[[:space:]]*=[[:space:]]*//p' |
head -1)
add_check gpu vulkan pass "${gpu_name:-vulkaninfo completed}"
else
add_check gpu vulkan fail \
"vulkaninfo failed: $(short_message "$vulkan_output")"
fi
else
add_check gpu vulkan skip "vulkaninfo is not installed"
fi
# VPU bindings and FFmpeg integration
decoder_bindings=$(driver_binding_count 'mpp_rkvdec*')
encoder_bindings=$(driver_binding_count 'mpp_rkvenc*')
av1_bindings=$(driver_binding_count '*av1*')
if ((decoder_bindings > 0)); then
add_check vpu decoder-driver pass "$decoder_bindings RKMPP decoder binding(s)"
else
add_check vpu decoder-driver warn "RKMPP decoder bindings are missing"
fi
if ((encoder_bindings > 0)); then
add_check vpu encoder-driver pass "$encoder_bindings RKMPP encoder binding(s)"
else
add_check vpu encoder-driver warn "RKMPP encoder bindings are missing"
fi
if ((av1_bindings > 0)); then
add_check vpu av1-driver pass "$av1_bindings AV1 decoder binding(s)"
else
add_check vpu av1-driver warn "a separate AV1 driver binding was not found"
fi
FFMPEG=""
for candidate in /opt/rkmedia/bin/ffmpeg-rk /usr/local/bin/ffmpeg-rk; do
if [[ -x "$candidate" ]]; then
FFMPEG=$candidate
break
fi
done
if [[ -z "$FFMPEG" ]] && command -v ffmpeg-rk >/dev/null 2>&1; then
FFMPEG=$(command -v ffmpeg-rk)
fi
if [[ -z "$FFMPEG" ]]; then
add_check vpu ffmpeg-rkmpp skip "ffmpeg-rk is not installed"
add_check rga ffmpeg-filter skip "ffmpeg-rk is not installed"
else
hwaccels=$($FFMPEG -hide_banner -hwaccels 2>&1)
if grep -qE '(^|[[:space:]])rkmpp($|[[:space:]])' <<<"$hwaccels"; then
add_check vpu ffmpeg-rkmpp pass "$FFMPEG advertises the rkmpp hwaccel"
else
add_check vpu ffmpeg-rkmpp fail "$FFMPEG does not advertise rkmpp"
fi
encoders=$($FFMPEG -hide_banner -encoders 2>&1)
for codec in h264 hevc mjpeg; do
if grep -q "${codec}_rkmpp" <<<"$encoders"; then
add_check vpu "encode-$codec-capability" pass \
"${codec}_rkmpp is registered"
else
add_check vpu "encode-$codec-capability" fail \
"${codec}_rkmpp is not registered"
fi
done
filters=$($FFMPEG -hide_banner -filters 2>&1)
if grep -q 'scale_rkrga' <<<"$filters"; then
add_check rga ffmpeg-filter pass "scale_rkrga is registered"
else
add_check rga ffmpeg-filter fail "scale_rkrga is not registered"
fi
if ((QUICK)); then
add_check vpu encode-workload skip "active encoder probe disabled by --quick"
else
for codec in h264 hevc mjpeg; do
encode_output=$(timeout 30s "$FFMPEG" -nostdin -v error \
-f lavfi -i 'color=size=320x240:rate=30:duration=0.2' \
-frames:v 3 -c:v "${codec}_rkmpp" -f null - 2>&1)
encode_rc=$?
if ((encode_rc == 0)); then
add_check vpu "encode-$codec-workload" pass \
"three frames submitted successfully"
else
add_check vpu "encode-$codec-workload" fail \
"hardware encode failed: $(short_message "$encode_output")"
fi
done
fi
if [[ -n "$MEDIA_DIR" && $QUICK -eq 0 ]]; then
first_decode_sample=""
for codec in h264 hevc vp9 av1; do
sample=$(find_sample "$codec" || true)
if [[ -z "$sample" ]]; then
add_check vpu "decode-$codec-workload" skip \
"no sample containing '$codec' in $MEDIA_DIR"
continue
fi
[[ -n "$first_decode_sample" ]] || first_decode_sample=$sample
decode_output=$(timeout 180s "$FFMPEG" -nostdin -v error \
-hwaccel rkmpp -i "$sample" -map 0:v:0 -f null - 2>&1)
decode_rc=$?
if ((decode_rc == 0)); then
add_check vpu "decode-$codec-workload" pass \
"decoded ${sample##*/} through RKMPP"
else
add_check vpu "decode-$codec-workload" fail \
"decode failed: $(short_message "$decode_output")"
fi
done
if [[ -n "$first_decode_sample" ]] && grep -q 'scale_rkrga' <<<"$filters"; then
rga_output=$(timeout 60s "$FFMPEG" -nostdin -v error \
-hwaccel rkmpp -hwaccel_output_format drm_prime \
-i "$first_decode_sample" -map 0:v:0 -frames:v 30 \
-vf 'scale_rkrga=w=1280:h=720:format=nv12' -f null - 2>&1)
rga_rc=$?
if ((rga_rc == 0)); then
add_check rga workload pass "RKMPP-to-RGA scaling completed"
else
add_check rga workload fail \
"RGA workload failed: $(short_message "$rga_output")"
fi
else
add_check rga workload skip "no decode sample is available for RGA"
fi
elif [[ -z "$MEDIA_DIR" ]]; then
add_check vpu decode-workload skip "use --media-dir to test hardware decoding"
add_check rga workload skip "use --media-dir to test zero-copy RGA scaling"
fi
fi
rga2_bindings=$(driver_binding_count 'rga2')
rga3_bindings=$(driver_binding_count 'rga3')
if ((rga2_bindings + rga3_bindings > 0)); then
add_check rga kernel-driver pass \
"RGA2 bindings=$rga2_bindings, RGA3 bindings=$rga3_bindings"
else
add_check rga kernel-driver warn "no RGA2/RGA3 binding was found"
fi
# NPU runtime and per-core inference
npu_bindings=$(driver_binding_count 'rknpu')
((npu_bindings += $(driver_binding_count 'RKNPU')))
shopt -s nullglob
npu_devfreq=("$SYS_ROOT"/class/devfreq/*.npu)
shopt -u nullglob
if ((npu_bindings > 0 || ${#npu_devfreq[@]} > 0)); then
add_check npu kernel-driver pass \
"RKNPU bindings=$npu_bindings, devfreq nodes=${#npu_devfreq[@]}"
npu_present=1
else
add_check npu kernel-driver warn "RKNPU driver/device was not found"
npu_present=0
fi
runtime_so="$RKNN_HOME/lib/librknnrt.so"
model_path="$RKNN_HOME/share/models/rk3588/mobilenet_v1.rknn"
npu_test="$RKNN_HOME/bin/rknn-inference-test"
if [[ -r "$runtime_so" ]]; then
runtime_hash=$(sha256sum "$runtime_so" 2>/dev/null)
runtime_hash=${runtime_hash%% *}
if [[ "$runtime_hash" == \
"d31fc19c85b85f6091b2bd0f6af9d962d5264a4e410bfb536402ec92bac738e8" ]]; then
add_check npu runtime pass "RKNN Runtime 2.3.2 checksum matches"
else
add_check npu runtime fail "unexpected librknnrt.so checksum $runtime_hash"
fi
else
add_check npu runtime warn "RKNN runtime is not installed at $runtime_so"
fi
if ((QUICK)); then
add_check npu inference skip "active inference disabled by --quick"
elif ((npu_present == 0)); then
add_check npu inference skip "NPU driver is absent; inference was not attempted"
elif [[ ! -x "$npu_test" || ! -r "$model_path" ]]; then
add_check npu inference skip "test binary or pinned RK3588 model is unavailable"
else
for core in 0 1 2 all; do
npu_output=$(timeout 45s "$npu_test" --model "$model_path" \
--core "$core" --iterations 1 2>&1)
npu_rc=$?
if ((npu_rc == 0)) && grep -q 'RKNN_RESULT status=pass' <<<"$npu_output"; then
add_check npu "core-$core" pass "$(short_message "$npu_output")"
else
add_check npu "core-$core" fail \
"inference failed: $(short_message "$npu_output")"
fi
done
fi
# HDMI ALSA devices
asound_cards=$(read_text "$PROC_ROOT/asound/cards")
if [[ -n "$asound_cards" ]] && grep -qiE 'hdmi|rockchiphdmi' <<<"$asound_cards"; then
add_check audio hdmi-card pass "an HDMI ALSA card is registered"
elif [[ -n "$asound_cards" ]]; then
add_check audio hdmi-card warn "ALSA cards exist, but none is labeled HDMI"
else
add_check audio hdmi-card warn "ALSA card inventory is unavailable"
fi
if command -v aplay >/dev/null 2>&1; then
aplay_output=$(aplay -l 2>&1)
if grep -qi 'hdmi' <<<"$aplay_output"; then
add_check audio pcm-device pass "aplay lists an HDMI PCM device"
else
add_check audio pcm-device warn \
"aplay did not list an HDMI PCM device: $(short_message "$aplay_output")"
fi
else
add_check audio pcm-device skip "aplay is not installed"
fi
# Link state only: no pings, DNS requests, or network mutations.
shopt -s nullglob
interfaces=("$SYS_ROOT"/class/net/*)
shopt -u nullglob
interface_names=()
up_names=()
for interface in "${interfaces[@]}"; do
name=${interface##*/}
[[ "$name" == lo ]] && continue
interface_names+=("$name")
state=$(read_text "$interface/operstate")
carrier=$(read_text "$interface/carrier")
if [[ "$state" == up || "$carrier" == 1 ]]; then
up_names+=("$name")
fi
done
if ((${#interface_names[@]} == 0)); then
add_check network interfaces warn "no non-loopback interface was found"
elif ((${#up_names[@]} > 0)); then
add_check network link pass "up: ${up_names[*]}"
else
add_check network link warn \
"interfaces present but down: ${interface_names[*]}"
fi
# Root filesystem, eMMC, and NVMe presence. No serial numbers are read.
if command -v findmnt >/dev/null 2>&1; then
root_source=$(findmnt -n -o SOURCE / 2>/dev/null || true)
root_fstype=$(findmnt -n -o FSTYPE / 2>/dev/null || true)
if [[ -n "$root_source" ]]; then
add_check storage rootfs pass "$root_source ($root_fstype)"
else
add_check storage rootfs warn "root filesystem source is unavailable"
fi
else
add_check storage rootfs skip "findmnt is not installed"
fi
shopt -s nullglob
emmc_devices=("$SYS_ROOT"/block/mmcblk*)
nvme_devices=("$SYS_ROOT"/block/nvme*n1)
shopt -u nullglob
if ((${#emmc_devices[@]} > 0)); then
add_check storage emmc pass "${#emmc_devices[@]} MMC block device(s)"
else
add_check storage emmc warn "no MMC/eMMC block device was found"
fi
if ((${#nvme_devices[@]} > 0)); then
add_check storage nvme pass "${#nvme_devices[@]} NVMe namespace(s)"
else
add_check storage nvme warn "no NVMe namespace was found"
fi
pass_count=0
warn_count=0
fail_count=0
skip_count=0
for status in "${CHECK_STATUS[@]}"; do
case "$status" in
pass) ((pass_count++)) ;;
warn) ((warn_count++)) ;;
fail) ((fail_count++)) ;;
skip) ((skip_count++)) ;;
esac
done
if ((fail_count > 0)); then
overall=fail
elif ((warn_count > 0)); then
overall=warn
else
overall=pass
fi
json_escape() {
local value=$1
value=${value//\\/\\\\}
value=${value//\"/\\\"}
value=${value//$'\n'/\\n}
value=${value//$'\r'/\\r}
value=${value//$'\t'/\\t}
printf '%s' "$value"
}
if ((JSON)); then
printf '{"schema_version":1,"overall":"%s","quick":%s,' \
"$overall" "$([[ $QUICK -eq 1 ]] && printf true || printf false)"
printf '"summary":{"pass":%d,"warn":%d,"fail":%d,"skip":%d},' \
"$pass_count" "$warn_count" "$fail_count" "$skip_count"
printf '"checks":['
for ((i = 0; i < ${#CHECK_NAME[@]}; ++i)); do
((i == 0)) || printf ','
printf '{"group":"%s","name":"%s","status":"%s","message":"%s"}' \
"$(json_escape "${CHECK_GROUP[i]}")" \
"$(json_escape "${CHECK_NAME[i]}")" \
"${CHECK_STATUS[i]}" \
"$(json_escape "${CHECK_MESSAGE[i]}")"
done
printf ']}\n'
else
printf '%-9s %-24s %-6s %s\n' GROUP CHECK STATUS DETAIL
printf '%-9s %-24s %-6s %s\n' '---------' '------------------------' \
'------' '------'
for ((i = 0; i < ${#CHECK_NAME[@]}; ++i)); do
printf '%-9s %-24s %-6s %s\n' \
"${CHECK_GROUP[i]}" "${CHECK_NAME[i]}" \
"${CHECK_STATUS[i]^^}" "${CHECK_MESSAGE[i]}"
done
printf '\nOverall: %s (pass=%d warn=%d fail=%d skip=%d)\n' \
"${overall^^}" "$pass_count" "$warn_count" "$fail_count" "$skip_count"
fi
if ((fail_count > 0 || (STRICT && warn_count > 0))); then
exit 1
fi
exit 0
+315
View File
@@ -0,0 +1,315 @@
/*
* Minimal RK3588 NPU smoke test for RKNN Runtime 2.3.2.
*
* The program deliberately uses deterministic synthetic inputs. Its purpose is
* to verify model loading, core selection, command submission, and finite
* output, not the semantic accuracy of MobileNet.
*/
#include <errno.h>
#include <inttypes.h>
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include "rknn_api.h"
#define DEFAULT_MODEL "/opt/rknn/current/share/models/rk3588/mobilenet_v1.rknn"
#define MAX_TENSORS 64U
static void usage(FILE *stream, const char *program)
{
fprintf(stream,
"Usage: %s [--model PATH] [--core auto|0|1|2|all] "
"[--iterations N]\n",
program);
}
static int parse_core(const char *value, rknn_core_mask *mask)
{
if (strcmp(value, "auto") == 0) {
*mask = RKNN_NPU_CORE_AUTO;
} else if (strcmp(value, "0") == 0) {
*mask = RKNN_NPU_CORE_0;
} else if (strcmp(value, "1") == 0) {
*mask = RKNN_NPU_CORE_1;
} else if (strcmp(value, "2") == 0) {
*mask = RKNN_NPU_CORE_2;
} else if (strcmp(value, "all") == 0) {
*mask = RKNN_NPU_CORE_0_1_2;
} else {
return -1;
}
return 0;
}
static int load_file(const char *path, void **buffer, uint32_t *size)
{
struct stat info;
FILE *file = NULL;
void *data = NULL;
if (stat(path, &info) != 0) {
fprintf(stderr, "cannot stat model %s: %s\n", path, strerror(errno));
return -1;
}
if (info.st_size <= 0 || (uint64_t)info.st_size > UINT32_MAX) {
fprintf(stderr, "invalid model size: %jd\n", (intmax_t)info.st_size);
return -1;
}
file = fopen(path, "rb");
if (file == NULL) {
fprintf(stderr, "cannot open model %s: %s\n", path, strerror(errno));
return -1;
}
data = malloc((size_t)info.st_size);
if (data == NULL) {
fprintf(stderr, "cannot allocate %jd bytes for model\n",
(intmax_t)info.st_size);
fclose(file);
return -1;
}
if (fread(data, 1, (size_t)info.st_size, file) != (size_t)info.st_size) {
fprintf(stderr, "short read from model %s\n", path);
free(data);
fclose(file);
return -1;
}
fclose(file);
*buffer = data;
*size = (uint32_t)info.st_size;
return 0;
}
static double elapsed_ms(const struct timespec *start, const struct timespec *end)
{
double seconds = (double)(end->tv_sec - start->tv_sec) * 1000.0;
double nanos = (double)(end->tv_nsec - start->tv_nsec) / 1000000.0;
return seconds + nanos;
}
int main(int argc, char **argv)
{
const char *model_path = DEFAULT_MODEL;
const char *core_name = "auto";
rknn_core_mask core_mask = RKNN_NPU_CORE_AUTO;
unsigned long iterations = 1;
void *model = NULL;
uint32_t model_size = 0;
rknn_context context = 0;
rknn_sdk_version sdk_version;
rknn_input_output_num io_count;
rknn_tensor_attr *input_attrs = NULL;
rknn_input *inputs = NULL;
rknn_output *outputs = NULL;
int outputs_acquired = 0;
int context_created = 0;
int result = EXIT_FAILURE;
double total_ms = 0.0;
uint32_t last_top_index = 0;
float last_top_value = -INFINITY;
int ret;
uint32_t i;
for (i = 1; i < (uint32_t)argc; ++i) {
if (strcmp(argv[i], "--model") == 0 && i + 1U < (uint32_t)argc) {
model_path = argv[++i];
} else if (strcmp(argv[i], "--core") == 0 && i + 1U < (uint32_t)argc) {
core_name = argv[++i];
if (parse_core(core_name, &core_mask) != 0) {
fprintf(stderr, "invalid core selector: %s\n", core_name);
usage(stderr, argv[0]);
return 2;
}
} else if (strcmp(argv[i], "--iterations") == 0 &&
i + 1U < (uint32_t)argc) {
char *end = NULL;
errno = 0;
iterations = strtoul(argv[++i], &end, 10);
if (errno != 0 || end == argv[i] || *end != '\0' ||
iterations == 0 || iterations > 1000) {
fprintf(stderr, "iterations must be between 1 and 1000\n");
return 2;
}
} else if (strcmp(argv[i], "--help") == 0 ||
strcmp(argv[i], "-h") == 0) {
usage(stdout, argv[0]);
return 0;
} else {
fprintf(stderr, "unknown or incomplete option: %s\n", argv[i]);
usage(stderr, argv[0]);
return 2;
}
}
if (load_file(model_path, &model, &model_size) != 0) {
goto cleanup;
}
ret = rknn_init(&context, model, model_size, 0, NULL);
if (ret != RKNN_SUCC) {
fprintf(stderr, "rknn_init failed: %d\n", ret);
goto cleanup;
}
context_created = 1;
ret = rknn_set_core_mask(context, core_mask);
if (ret != RKNN_SUCC) {
fprintf(stderr, "rknn_set_core_mask(%s) failed: %d\n", core_name, ret);
goto cleanup;
}
memset(&sdk_version, 0, sizeof(sdk_version));
ret = rknn_query(context, RKNN_QUERY_SDK_VERSION, &sdk_version,
sizeof(sdk_version));
if (ret != RKNN_SUCC) {
fprintf(stderr, "RKNN_QUERY_SDK_VERSION failed: %d\n", ret);
goto cleanup;
}
memset(&io_count, 0, sizeof(io_count));
ret = rknn_query(context, RKNN_QUERY_IN_OUT_NUM, &io_count,
sizeof(io_count));
if (ret != RKNN_SUCC || io_count.n_input == 0 || io_count.n_output == 0 ||
io_count.n_input > MAX_TENSORS || io_count.n_output > MAX_TENSORS) {
fprintf(stderr, "invalid RKNN input/output count (%u/%u), ret=%d\n",
io_count.n_input, io_count.n_output, ret);
goto cleanup;
}
input_attrs = calloc(io_count.n_input, sizeof(*input_attrs));
inputs = calloc(io_count.n_input, sizeof(*inputs));
outputs = calloc(io_count.n_output, sizeof(*outputs));
if (input_attrs == NULL || inputs == NULL || outputs == NULL) {
fprintf(stderr, "cannot allocate tensor metadata\n");
goto cleanup;
}
for (i = 0; i < io_count.n_input; ++i) {
uint32_t byte;
input_attrs[i].index = i;
ret = rknn_query(context, RKNN_QUERY_INPUT_ATTR, &input_attrs[i],
sizeof(input_attrs[i]));
if (ret != RKNN_SUCC || input_attrs[i].n_elems == 0) {
fprintf(stderr, "query for input %u failed: %d\n", i, ret);
goto cleanup;
}
inputs[i].index = i;
inputs[i].size = input_attrs[i].n_elems;
inputs[i].type = RKNN_TENSOR_UINT8;
inputs[i].fmt = input_attrs[i].fmt == RKNN_TENSOR_UNDEFINED
? RKNN_TENSOR_NHWC
: input_attrs[i].fmt;
inputs[i].pass_through = 0;
inputs[i].buf = malloc(inputs[i].size);
if (inputs[i].buf == NULL) {
fprintf(stderr, "cannot allocate input %u (%u bytes)\n", i,
inputs[i].size);
goto cleanup;
}
for (byte = 0; byte < inputs[i].size; ++byte) {
((uint8_t *)inputs[i].buf)[byte] =
(uint8_t)((byte * 17U + i * 23U) & 0xffU);
}
}
ret = rknn_inputs_set(context, io_count.n_input, inputs);
if (ret != RKNN_SUCC) {
fprintf(stderr, "rknn_inputs_set failed: %d\n", ret);
goto cleanup;
}
for (i = 0; i < io_count.n_output; ++i) {
outputs[i].index = i;
outputs[i].want_float = 1;
outputs[i].is_prealloc = 0;
}
for (unsigned long iteration = 0; iteration < iterations; ++iteration) {
struct timespec start;
struct timespec end;
if (clock_gettime(CLOCK_MONOTONIC, &start) != 0) {
fprintf(stderr, "clock_gettime failed: %s\n", strerror(errno));
goto cleanup;
}
ret = rknn_run(context, NULL);
if (ret != RKNN_SUCC) {
fprintf(stderr, "rknn_run failed at iteration %lu: %d\n",
iteration, ret);
goto cleanup;
}
ret = rknn_outputs_get(context, io_count.n_output, outputs, NULL);
if (ret != RKNN_SUCC) {
fprintf(stderr, "rknn_outputs_get failed at iteration %lu: %d\n",
iteration, ret);
goto cleanup;
}
outputs_acquired = 1;
if (clock_gettime(CLOCK_MONOTONIC, &end) != 0) {
fprintf(stderr, "clock_gettime failed: %s\n", strerror(errno));
goto cleanup;
}
total_ms += elapsed_ms(&start, &end);
last_top_value = -INFINITY;
last_top_index = 0;
for (i = 0; i < io_count.n_output; ++i) {
const float *values = outputs[i].buf;
uint32_t count = outputs[i].size / (uint32_t)sizeof(float);
uint32_t value_index;
if (values == NULL || count == 0) {
fprintf(stderr, "output %u is empty\n", i);
goto cleanup;
}
for (value_index = 0; value_index < count; ++value_index) {
if (!isfinite(values[value_index])) {
fprintf(stderr, "output %u contains a non-finite value\n", i);
goto cleanup;
}
if (i == 0 && values[value_index] > last_top_value) {
last_top_value = values[value_index];
last_top_index = value_index;
}
}
}
ret = rknn_outputs_release(context, io_count.n_output, outputs);
outputs_acquired = 0;
if (ret != RKNN_SUCC) {
fprintf(stderr, "rknn_outputs_release failed: %d\n", ret);
goto cleanup;
}
}
printf("RKNN_RESULT status=pass core=%s iterations=%lu avg_ms=%.3f "
"top_index=%u top_value=%.7g api=%s driver=%s\n",
core_name, iterations, total_ms / (double)iterations,
last_top_index, last_top_value, sdk_version.api_version,
sdk_version.drv_version);
result = EXIT_SUCCESS;
cleanup:
if (outputs_acquired) {
(void)rknn_outputs_release(context, io_count.n_output, outputs);
}
if (inputs != NULL) {
for (i = 0; i < io_count.n_input; ++i) {
free(inputs[i].buf);
}
}
free(outputs);
free(inputs);
free(input_attrs);
if (context_created) {
(void)rknn_destroy(context);
}
free(model);
return result;
}
+12
View File
@@ -0,0 +1,12 @@
# Pinned local-inference runtime for the RK3588 NPU.
# This file is sourced by install-rknn-runtime.sh.
RKNN_VERSION="2.3.2"
RKNN_TAG="v2.3.2"
RKNN_COMMIT="42aa1d426c0a9e0869b6374edba009f7208a1926"
RKNN_REPOSITORY="https://github.com/airockchip/rknn-toolkit2.git"
RKNN_HEADER_SHA256="c48e11a6f41b451a5fd1e4ad774ea60252d3d94f78bee9b21ea3d21b21deba9a"
RKNN_RUNTIME_AARCH64_SHA256="d31fc19c85b85f6091b2bd0f6af9d962d5264a4e410bfb536402ec92bac738e8"
RKNN_MOBILENET_RK3588_SHA256="381dae3b7038a98b10f6ec9dcdbb094a49247341856fb294e692ef518218fcfb"
RKNN_DEMO_IMAGE_SHA256="c350299c6283d5f62fecf1f845b6b3be9aafec8dff528ca09a129990f0a584b0"
RKNN_LICENSE_SHA256="d846f57d942c7dfdca7b8b54f9e8bb39e1e226790dc4f5ee205d6fd678961720"
+149
View File
@@ -0,0 +1,149 @@
#!/usr/bin/env bash
set -Eeuo pipefail
TEST_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)
RUNTIME_DIR=$(cd -- "$TEST_DIR/.." && pwd -P)
TEST_TMP=$(mktemp -d "${TMPDIR:-/tmp}/rk1-runtime-tests.XXXXXXXX")
cleanup() {
rm -rf -- "$TEST_TMP"
}
trap cleanup EXIT
fail() {
printf 'FAIL: %s\n' "$*" >&2
exit 1
}
pass() {
printf 'PASS: %s\n' "$*"
}
bash -n \
"$RUNTIME_DIR/install-rknn-runtime.sh" \
"$RUNTIME_DIR/rk1-media-selftest" \
"$RUNTIME_DIR/rk1-media-diagnostics"
pass "shell syntax"
"$RUNTIME_DIR/install-rknn-runtime.sh" --help >/dev/null
"$RUNTIME_DIR/rk1-media-selftest" --help >/dev/null
"$RUNTIME_DIR/rk1-media-diagnostics" --help >/dev/null
pass "command help"
mkdir -p \
"$TEST_TMP/empty/sys" \
"$TEST_TMP/empty/proc" \
"$TEST_TMP/empty/dev" \
"$TEST_TMP/empty/rknn"
RK1_SYSFS_ROOT="$TEST_TMP/empty/sys" \
RK1_PROCFS_ROOT="$TEST_TMP/empty/proc" \
RK1_DEV_ROOT="$TEST_TMP/empty/dev" \
RKNN_HOME="$TEST_TMP/empty/rknn" \
"$RUNTIME_DIR/rk1-media-selftest" --quick --json \
>"$TEST_TMP/empty.json"
grep -q '"overall":"warn"' "$TEST_TMP/empty.json" ||
fail "empty-hardware JSON does not report a warning"
grep -q '"checks":\[' "$TEST_TMP/empty.json" ||
fail "empty-hardware JSON has no checks"
pass "absent hardware degrades safely"
FAKE="$TEST_TMP/fake"
mkdir -p \
"$FAKE/sys/class/drm/card0-HDMI-A-1" \
"$FAKE/sys/class/devfreq/fb000000.gpu" \
"$FAKE/sys/class/devfreq/fdab0000.npu" \
"$FAKE/sys/class/net/eth0" \
"$FAKE/sys/block/mmcblk0" \
"$FAKE/sys/block/nvme0n1" \
"$FAKE/sys/bus/platform/drivers/dwhdmi-rockchip/fde80000.hdmi" \
"$FAKE/sys/bus/platform/drivers/rockchip-hdptx-phy-hdmi/fed60000.hdmiphy" \
"$FAKE/sys/bus/platform/drivers/panthor/fb000000.gpu" \
"$FAKE/sys/bus/platform/drivers/mpp_rkvdec2/fdc38100.rkvdec-core" \
"$FAKE/sys/bus/platform/drivers/mpp_rkvenc2/fdbd0000.rkvenc-core" \
"$FAKE/sys/bus/platform/drivers/mpp_av1dec/av1-decoder" \
"$FAKE/sys/bus/platform/drivers/rga2/fdb80000.rga" \
"$FAKE/sys/bus/platform/drivers/rga3/fdb60000.rga" \
"$FAKE/sys/bus/platform/drivers/RKNPU/fdab0000.npu" \
"$FAKE/proc/device-tree" \
"$FAKE/proc/asound" \
"$FAKE/dev/dri" \
"$FAKE/rknn"
printf 'connected\n' >"$FAKE/sys/class/drm/card0-HDMI-A-1/status"
printf '3840x2160\n1920x1080\n' >"$FAKE/sys/class/drm/card0-HDMI-A-1/modes"
printf 'fake-edid' >"$FAKE/sys/class/drm/card0-HDMI-A-1/edid"
printf 'Turing Machines RK1\0' >"$FAKE/proc/device-tree/model"
printf ' 0 [rockchiphdmi]: rockchip-hdmi - rockchip-hdmi\n' \
>"$FAKE/proc/asound/cards"
printf 'up\n' >"$FAKE/sys/class/net/eth0/operstate"
printf '1\n' >"$FAKE/sys/class/net/eth0/carrier"
touch "$FAKE/dev/dri/renderD128"
RK1_SYSFS_ROOT="$FAKE/sys" \
RK1_PROCFS_ROOT="$FAKE/proc" \
RK1_DEV_ROOT="$FAKE/dev" \
RKNN_HOME="$FAKE/rknn" \
"$RUNTIME_DIR/rk1-media-selftest" --quick --json \
>"$TEST_TMP/fake.json"
grep -q '"name":"card0-HDMI-A-1","status":"pass"' \
"$TEST_TMP/fake.json" || fail "fake connected HDMI was not detected"
grep -q '"name":"kernel-driver","status":"pass","message":"Panthor' \
"$TEST_TMP/fake.json" || fail "fake Panthor binding was not detected"
pass "synthetic hardware inventory"
if command -v python3 >/dev/null 2>&1; then
python3 -m json.tool "$TEST_TMP/empty.json" >/dev/null
python3 -m json.tool "$TEST_TMP/fake.json" >/dev/null
pass "JSON validity"
fi
RK1_SYSFS_ROOT="$FAKE/sys" \
RK1_PROCFS_ROOT="$FAKE/proc" \
RK1_DEV_ROOT="$FAKE/dev" \
RKNN_HOME="$FAKE/rknn" \
"$RUNTIME_DIR/rk1-media-diagnostics" --directory \
--output "$TEST_TMP/diagnostics" >/dev/null
[[ -s "$TEST_TMP/diagnostics/selftest.json" ]] ||
fail "diagnostic bundle is missing selftest.json"
[[ -s "$TEST_TMP/diagnostics/drm-connectors.txt" ]] ||
fail "diagnostic bundle is missing connector data"
[[ -s "$TEST_TMP/diagnostics/kernel-media.log" ]] ||
fail "diagnostic bundle is missing filtered kernel data"
pass "diagnostic collection"
if [[ -n ${RKNN_SOURCE_DIR:-} ]]; then
[[ -d "$RKNN_SOURCE_DIR" ]] || fail "RKNN_SOURCE_DIR is not a directory"
bundled_source="$TEST_TMP/bundled-rknn-source"
mkdir -p \
"$bundled_source/rknpu2/runtime/Linux/librknn_api/include" \
"$bundled_source/rknpu2/runtime/Linux/librknn_api/aarch64" \
"$bundled_source/rknpu2/examples/rknn_api_demo/model/RK3588" \
"$bundled_source/rknpu2/examples/rknn_api_demo/model"
cp "$RKNN_SOURCE_DIR/LICENSE" "$bundled_source/LICENSE"
cp "$RKNN_SOURCE_DIR/rknpu2/runtime/Linux/librknn_api/include/rknn_api.h" \
"$bundled_source/rknpu2/runtime/Linux/librknn_api/include/rknn_api.h"
cp "$RKNN_SOURCE_DIR/rknpu2/runtime/Linux/librknn_api/aarch64/librknnrt.so" \
"$bundled_source/rknpu2/runtime/Linux/librknn_api/aarch64/librknnrt.so"
cp "$RKNN_SOURCE_DIR/rknpu2/examples/rknn_api_demo/model/RK3588/mobilenet_v1.rknn" \
"$bundled_source/rknpu2/examples/rknn_api_demo/model/RK3588/mobilenet_v1.rknn"
cp "$RKNN_SOURCE_DIR/rknpu2/examples/rknn_api_demo/model/dog_224x224.jpg" \
"$bundled_source/rknpu2/examples/rknn_api_demo/model/dog_224x224.jpg"
mkdir -p "$TEST_TMP/rootfs"
"$RUNTIME_DIR/install-rknn-runtime.sh" \
--rootfs "$TEST_TMP/rootfs" --source-dir "$bundled_source"
installed_runtime="$TEST_TMP/rootfs/opt/rknn/2.3.2/lib/librknnrt.so"
installed_model="$TEST_TMP/rootfs/opt/rknn/2.3.2/share/models/rk3588/mobilenet_v1.rknn"
[[ -x "$TEST_TMP/rootfs/opt/rknn/2.3.2/bin/rknn-inference-test" ]] ||
fail "compiled inference test is missing"
[[ $(sha256sum "$installed_runtime" | awk '{print $1}') == \
d31fc19c85b85f6091b2bd0f6af9d962d5264a4e410bfb536402ec92bac738e8 ]] ||
fail "installed runtime checksum differs"
[[ $(sha256sum "$installed_model" | awk '{print $1}') == \
381dae3b7038a98b10f6ec9dcdbb094a49247341856fb294e692ef518218fcfb ]] ||
fail "installed model checksum differs"
pass "pinned RKNN installation"
else
printf 'SKIP: installer integration (set RKNN_SOURCE_DIR to a v2.3.2 checkout)\n'
fi
printf 'All runtime tests passed.\n'