Turing RK1
The Turing RK1 is an RK3588 compute
module that seats in a Turing Pi 2 cluster board. boot2deb ships it as a small family
of recipes over one hardware base. That base is kernel v7.2 (linux-stable), u-boot
v2026.07, and the RGA / VEPU / VDPU (and NPU) drivers carried in-kernel via the
rk3588-accel patch series. It is a supported configuration in its own right, and a good
starting point for any RK3588 board.
The variants differ along two independent axes, the Debian suite and whether the Rockchip media userspace is built in:
| Recipe | Suite | Media userspace |
|---|---|---|
turing-rk1/forky | forky | — (base) |
turing-rk1/trixie | trixie | — (base) |
turing-rk1/media-accel-forky | forky | ffmpeg-rk + MPP + RGA + Vulkan |
turing-rk1/media-accel-trixie | trixie | ffmpeg-rk + MPP + RGA + Vulkan |
turing-rk1/jellyfin-forky | forky | ffmpeg-rk + MPP + RGA, plus Jellyfin |
turing-rk1/jellyfin-trixie | trixie | ffmpeg-rk + MPP + RGA, plus Jellyfin |
The jellyfin-* pair is the media-server build: media-accel plus the Jellyfin
server, pre-pointed at ffmpeg-rk. See
Accelerated Jellyfin. turing-rk1/util is not an image along
these axes at all but a u-boot-only recovery tool — see
Writing the NVMe from u-boot.
Every variant carries the same accel kernel. The VEPU / VDPU / RGA and NPU drivers
are present in all of them, because the patches and kconfig live on the kernel axis. A
base image simply omits the Rockchip media userspace — the hardware blocks are
there but dark. A media-accel image adds the media-accel-rockchip feature, which
builds and installs ffmpeg-rk, librockchip-mpp1, and librga2 on top. The split is
deliberate: because the kernel already carries the capability, those debs can equally be
installed onto a running base image later. forky is the RK1’s validated suite.
Two RGA drivers exist; this image builds the out-of-tree one
Kernel 7.2 added an in-tree V4L2 driver for the RK3588’s RGA3 cores, so from that
release the SoC has two drivers to choose between. The choice is a kconfig one:
ROCKCHIP_MULTI_RGA for the vendor driver, VIDEO_ROCKCHIP_RGA for the in-tree one.
These images build the vendor driver and leave the in-tree one unset, for reasons that
are about what reaches FFmpeg rather than about code quality:
- The ABI.
librga, and thereforescale_rkrga,vpp_rkrgaandoverlay_rkrga, speak the vendor/dev/rgainterface. The in-tree driver exposes V4L2 video nodes, and FFmpeg ships no V4L2 mem2mem filter to drive them. - The cores. The in-tree driver deliberately exposes one core, to avoid an ABI break when multi-core scheduling is added later. The vendor driver schedules across all three.
- 10-bit. The in-tree driver implements scaling and color conversion only, and
10-bit YUV is on its own list of what is not done yet. A 10-bit decode on this
hardware produces NV15. RGA is the only block that converts it without a trip
through system memory — see 10-bit, and why
NV15is the whole story.
The in-tree driver is a supported thing to switch to. Reach for it if you want a kernel
with no out-of-tree code and can live without the FFmpeg filter path. Unset
CONFIG_ROCKCHIP_MULTI_RGA and set CONFIG_VIDEO_ROCKCHIP_RGA in an overlay fragment.
Then drop media-accel/kernel/072 from the series, so the device-tree nodes keep their
mainline compatibles.
The media-accel-* pair also carries the vulkan feature, which is Mesa’s Vulkan
drivers and the loader. That is what makes the Vulkan filters ffmpeg-rk is already
built with actually open. On a box driven from the command line those are the fastest
scale, tone-map and composite route the hardware has. scale_vulkan measures 53.8 dB
against swscale at 18x the efficiency and 2.8x the speed. It costs about 305 MiB
installed, two thirds of which is the LLVM that Mesa’s software rasterizer pulls in
rather than the Mali driver itself.
The jellyfin-* pair deliberately does not carry it. Jellyfin’s
HardwareAccelerationType is a closed enum with no Vulkan member, so the server can
never emit a Vulkan filter. The packages would be reachable only from a shell. Add
it there with a feature selection if you want the command line too.
Two feature selections carry Jellyfin together with vulkan and avs-decode (see
Decode), and they differ only in the server:
turing-rk1/forky+media-accel-rockchip+jellyfin+jellyfin-rockchip+vulkan+avs-decodeinstalls the stock server from Jellyfin’s repository. It decodes in software and encodes on the VEPU580.turing-rk1/forky+media-accel-rockchip+jellyfin+jellyfin-v4l2request+vulkan+avs-decodebuilds the patched server that also decodes in hardware, as Accelerated Jellyfin describes.
Their locks pin the same kernel, patches, userspace and FFmpeg. Pass
--image-size 3G with either, since a feature selection takes the device’s 2G and
cannot carry the jellyfin-* recipes’ own larger volume.
Every image here ships a redistributable FFmpeg. See The FFmpeg a build ships is redistributable for the opt-in nonfree flavor and why no hardware path depends on it.
Build the base image as in Getting started:
boot2deb build turing-rk1/forky
or, for a ready hardware-transcode host, the media-accel variant:
boot2deb build turing-rk1/media-accel-forky
Either produces a whole-disk image (GPT, u-boot in the reserved gap ahead of the first
partition, then the ext4 rootfs), so a single write lays down everything, bootloader
included. Artifacts are named for the whole build point, so turing-rk1/forky writes
build/turing-rk1/forky/artifacts/turing-rk1-forky.img.xz and the media-accel variant
writes turing-rk1-media-accel-forky.img.xz. The flashing and boot notes below use
turing-rk1/forky. They are identical for any variant, since the bootloader and disk
layout do not change, so substitute your recipe name throughout.
Flash
Press the built artifact into a verified, optionally personalized raw image first — one master, one file per unit:
boot2deb press turing-rk1/forky rk1-03.img \
--hostname rk1-03 --ssh-key "$(cat ~/.ssh/id_ed25519.pub)"
The RK1 is a compute module, not a board you plug a card reader into. The usual
write path is therefore the Turing Pi’s BMC, which writes the module’s eMMC.
Both BMC routes take the raw file press produces:
tpi flash -n 2 -l -i rk1-03.img # the tpi CLI, node 1-4
or the BMC web UI’s flash upload. For a removable or NVMe/USB medium you
write on another machine, use any flasher, dd included. The same image boots
from any medium the board scans, since u-boot discovers its root device at
runtime:
lsblk # confirm the device; dd overwrites it whole
sudo dd if=rk1-03.img of=/dev/sdX bs=4M status=progress conv=fsync
See Producing images for verification, the seed keys, and
per-site additions. The tpi CLI and web UI evolve, so see Turing Pi’s
flashing docs for the
current specifics.
Streaming to the eMMC over USB mass storage
tpi flash stages the whole image on the BMC before it writes. The BMC has
another mode that does not:
tpi advanced msd --node 2
That reboots the node into USB mass-storage mode, after which its eMMC is an ordinary SCSI disk on the BMC. There is no custom firmware, no u-boot in the loop, and no UART. Allow about ten seconds for it to enumerate, then find it by the vendor string the RK1’s eMMC reports rather than by guessing a letter:
ssh root@<bmc> 'grep -l Rockchip /sys/block/*/device/vendor'
# /sys/block/sda/device/vendor -> the node's eMMC is /dev/sda
With the disk present, the image streams straight through with nothing staged on either machine:
xzcat rk1-03.img.xz | ssh root@<bmc> 'dd of=/dev/sda bs=4M conv=fsync'
conv=sparse is worth knowing about here, and worth understanding before you
use it. It makes dd seek over runs of NULs instead of writing them. That is
a real saving across a USB mass-storage link, because most of a fresh image is
zeros. The catch is that seeking leaves whatever was there before, so it is a
faster write and not a clean one.
On a node whose eMMC has held another system, the stale bytes can include an old
backup GPT. They can also include a filesystem signature that blkid will still find.
Use it on a disk you do not mind reading as half-overwritten. Leave it off when you
want the medium to say exactly what the image says.
The same mode is the route by which an already-flashed node can be edited in
place rather than reflashed. Mount the exposed rootfs on the BMC and change
what you need, which is how a boot2deb seed key can be applied after the
fact. Whether that half works is a property of the BMC firmware rather than of
this mode. It needs ext4 in the BMC’s kernel, and enough userland to be useful.
One command tells you before you plan around it:
ssh root@<bmc> 'grep -w ext4 /proc/filesystems && command -v mount'
The write half needs neither, so it stands on its own where the mount half does not.
This exposes the eMMC and nothing else, exactly as tpi flash does. The
M.2 disk stays invisible to the BMC in this mode as in every other. See
Writing the NVMe from u-boot and
Installing to the NVMe from the booted node
for the two routes that reach it.
u-boot on eMMC, OS on a separate disk
A common RK1 setup keeps only u-boot on the eMMC and runs the OS from an NVMe or USB disk. The builder produces the two pieces for this directly.
The whole split at once. Build the split layout, which emits two images instead
of one:
boot2deb build turing-rk1/forky --layout split
turing-rk1-forky-boot.img— u-boot only (idbloader +u-boot.itbat their offsets, no GPT), for the eMMC.turing-rk1-forky-rootfs.img— GPT + rootfs, for the NVMe/USB disk.
press emits the same pair as personalized copies —
press turing-rk1/forky --layout split --boot-out emmc.img --rootfs-out nvme.img --hostname rk1-03 — with the seed riding the rootfs half.
Just the bootloader. If you only need the eMMC u-boot image (e.g. to re-flash the bootloader across nodes) without building a whole OS, the u-boot stage emits it on its own:
boot2deb build turing-rk1/forky --stage uboot
This writes turing-rk1-forky-boot.img (a few MiB, gap-sized) alongside the raw
turing-rk1-forky-idbloader.img and turing-rk1-forky-u-boot.itb. Flash the
-boot.img to the eMMC with tpi/web UI, and write the rootfs image to the target
disk.
Because tpi/web UI flash the eMMC only, the rootfs image goes onto the NVMe/USB disk
by another route. The bootloader itself is the shortest one — see below.
Installing to the NVMe from the booted node
The shortest way onto the M.2 disk uses no serial console and no bootloader prompt at all. Flash the eMMC with the BMC, carrying the image inside itself. The BMC does that in one step. Then boot it, and let the node write its own NVMe.
boot2deb press turing-rk1/forky rk1-03.img --embed-image \
--hostname rk1-03 --ssh-key "$(cat ~/.ssh/id_ed25519.pub)"
tpi flash -n 2 -l -i rk1-03.img
--embed-image carries the recipe’s own compressed artifact inside the pressed
image at /var/lib/boot2deb/install/. Power the node on, let first boot finish,
then from an ssh session:
sudo boot2deb-install-to /dev/nvme0n1
boot2deb-install-to ships in every image. It refuses anything that is not a
whole disk, and refuses the disk the system is running from. It refuses a disk with
anything mounted on it, and requires you to type the device’s name. It then writes
the embedded artifact and syncs.
Power off, and the node boots from the NVMe. The rootfs grows to fill it on that first boot. Re-running it is safe and still writes, so an interrupted write is repaired by repeating it.
That is one BMC flash and one ssh session. The eMMC keeps a complete, bootable copy of the same system. That is a useful thing to have on a node whose OS disk you are about to replace.
Writing the NVMe from u-boot
The BMC writes eMMC and nothing else. The loader it streams into the module speaks
eMMC, so the M.2 disk is invisible to tpi flash, to the web UI, and to gadget
mode. The RK1’s own u-boot has no such limit, since it enumerates the disk over
pcie3x4. The shipped bootloader therefore carries the two commands that let a host
reach it.
This route needs a UART session and interrupting the boot countdown. Reach for it when you want the disk written from outside the node. That is a bare M.2 with no system on it yet, or a node whose OS will not boot. To install onto the NVMe of a node that boots, the previous section does it with no console at all. Build the tool variant for the full set:
boot2deb build turing-rk1/util --stage uboot # writes turing-rk1-util-boot.img
Flash that to the eMMC with tpi, open the node’s UART, and interrupt the
countdown. Two routes from the prompt:
Export the disk to the BMC. ums presents any block device u-boot can see as
USB mass storage. With the node’s USB in device mode, the BMC therefore sees the NVMe
as a normal disk:
=> nvme scan
=> ums 0 nvme 0
then, from your machine, stream the image through the BMC — nothing is staged on the node or the BMC:
xzcat turing-rk1-forky-rootfs.img.xz | ssh root@<bmc> 'dd of=/dev/sdX bs=4M'
Or pull the image in over the network and let u-boot write it. This needs a gzip image, since u-boot has no xz decompressor:
boot2deb build turing-rk1/forky --layout split --compress gz
=> dhcp
=> tftp ${loadaddr} turing-rk1-forky-rootfs.img.gz
=> gzwrite nvme 0 ${loadaddr} ${filesize}
gzwrite decompresses and writes in one pass. Hash the image first with md5sum
if the link is one you do not trust. Several GiB over TFTP has no integrity check of
its own. Images at or above 4 GiB uncompressed need gzwrite’s explicit
outsize argument, and the shipped recipes are well under it.
Either way the eMMC still needs a bootloader afterwards. boot2deb build turing-rk1/forky --stage uboot emits the shipping one, which also carries ums.
A node that boots from NVMe therefore keeps a route back to its disks without
reflashing the tool.
Or keep the OS on eMMC and use the NVMe for data
Often the better answer, and it makes the whole errand above unnecessary. Flash the entire system to the eMMC, which the BMC can do in one step, and let the M.2 disk hold data only. Reimaging then never touches the data, because the new image finds the volume by label and adopts it.
The RK1’s 29 GB eMMC has room for any of the shipped images several times over, so nothing is given up by keeping the OS there. No shipped recipe assumes this layout, because where the data lives is an installation’s choice rather than the board’s. You add it to your own recipe. See Data volumes.
Serial console
To watch u-boot and the kernel come up, open the node’s UART from the BMC:
tpi uart --node <n> get
# or, on the BMC directly:
picocom /dev/ttyS<n> -b 115200
On BMC firmware 2.1.0 and newer the node number maps 1:1 to the ttyS number
(node 1 → ttyS1, node 2 → ttyS2, …). On 2.0.5 and older the mapping was offset
(node 1 → ttyS2, node 2 → ttyS1, …), so check your firmware version. The baud rate
is 115200. See Turing Pi’s UART docs.
Forcing one boot from a chosen medium
A node carrying a system on both its eMMC and its M.2 disk boots whichever its
boot_targets list reaches first. To boot the other one once, override the list
at the prompt instead of writing it. Checking that a freshly written eMMC copy comes
up, without disturbing a node that normally runs from NVMe, is the case for it:
tpi uart --node 2 get # watch this while the node powers on
tpi power on --node 2
Interrupt the countdown at Hit any key to stop autoboot: with any key, then:
=> printenv boot_targets # the shipped order, whatever it is on your build
=> setenv boot_targets mmc0 # or nvme0, or "mmc0 nvme0" to try both
=> boot
setenv without saveenv lives until the next reset, so this cannot strand the
node: power-cycle it and the shipped order is back, unchanged. That is the whole
reason to prefer it over editing the environment.
Driving that conversation for you means powering the node and answering the prompt in one command. That is device tooling rather than an image builder’s job, and boot2deb does not do it. It is the same boundary that keeps it from writing devices.
First boot
Power the node on. On first boot the image does two things:
- Regenerates its SSH host keys.
- Grows the rootfs to fill the whole medium, so the 2 GB image expands to the disk’s capacity. That happens online, in the same boot, with no reboot involved.
Log in as user debian with the password the build printed. It is expired, so you
are required to set a new one immediately. sudo asks for that password, and the
hostname is turing-rk1.
That is a booted Debian system. The kernel’s transcode devices come up on every
variant, so check for /dev/dri and /dev/rga. A media-accel image also installs
the ffmpeg-rk userspace, so you can exercise the rkmpp / rkrga paths directly. On
a base image the blocks are present but idle until you install the media-accel debs, or
build a turing-rk1/media-accel-* image.
Running the accelerated FFmpeg
ffmpeg-rk installs under /opt/ffmpeg-rk and is on PATH as ffmpeg-rk (and
ffprobe-rk). The suffix is deliberate. The build ships the same library sonames as
Debian’s own FFmpeg, so it is kept out of the system’s paths and the loader’s search
path. The plain ffmpeg name stays with Debian’s package.
ffmpeg-rk -hide_banner -filters | grep rkrga # scale_rkrga, vpp_rkrga, overlay_rkrga
ffmpeg-rk -hide_banner -encoders | grep rkmpp # h264_rkmpp, hevc_rkmpp
Hardware decode is reached with -hwaccel v4l2request, not with the *_rkmpp
decoders. Those are compiled in but do not open on a mainline kernel, where rkvdec
is a V4L2 stateless driver rather than an MPP service.
A transcode that stays in hardware from decode through scale to encode names an RKMPP
device for the RGA filters to allocate from. A v4l2request chain does not supply one
on its own:
ffmpeg-rk -init_hw_device rkmpp=rk -filter_hw_device rk \
-hwaccel v4l2request -hwaccel_output_format drm_prime -i in.mkv \
-vf "scale_rkrga=w=1280:h=720:format=nv12" \
-c:v hevc_rkmpp out.mp4
Without -init_hw_device rkmpp=rk -filter_hw_device rk the filter fails with No RKMPP hardware context provided, which reads like a frame-format problem and is not one.
Both of those are stated as caveats on the media-accel-rockchip feature, so they print
at the end of a build of any recipe composing it. See
Support matrix. What each block can actually
decode and encode is tabulated under What the hardware decodes and
encodes.
What the hardware decodes and encodes
Three hardware blocks behind two decode drivers, plus one encoder. The table is what the
kernel this image builds actually offers. Whether a given codec has been validated on
this board is a different question, answered by boot2deb support-matrix.
Decode
| Codec | Block / node | 8-bit | 10-bit | Notes |
|---|---|---|---|---|
| HEVC | rkvdec VDPU381, /dev/video0 | NV12 | NV15 | 4:2:0, with two cores on the one node |
| H.264 | rkvdec VDPU381, /dev/video0 | NV12, NV16 | NV15, NV20 | 4:2:0 and 4:2:2 |
| VP9 | rkvdec VDPU381, /dev/video0 | NV12 (profile 0) | NV15 (profile 2) | profiles 1 and 3 are 4:2:2/4:4:4, which the hardware cannot do |
| AV1 | Hantro VPU981, /dev/video3 | NV12 | NV15, P010 | to 8192x4352. Decodes to a 4x4-tiled buffer and post-processes out |
| VP8 | Hantro VDPU2, /dev/video1 | NV12 | — | to 3840x2160 |
| MPEG-2 | Hantro VDPU2, /dev/video1 | NV12 | — | 1080p ceiling. Within ±2 of software decode and almost never bit-exact. Bottom-field-first field pictures carry one reference-parity defect |
FFmpeg’s v4l2request decides where a stream goes. Of the decoders that take the
stream’s codec, it tries the one reporting the widest frame-size range first
(media-accel/ffmpeg/0023). The table is the result, and every H.264 stream goes to
rkvdec. Hantro’s VDPU2 node also decodes H.264, 8-bit 4:2:0 only and up to 1920x1088,
and takes a stream only if rkvdec refuses it. It is the slower block, at about 3x
realtime of 1080p in total against rkvdec’s 18x across two cores. Both score
identically on conformance (JVT-AVC_V1 129/135, the same six failures).
Conformance, measured on this board against the ITU/ISO suites with the software decoder as control:
- HEVC (JCT-VC-HEVC_V1): 146/147. The one failure is unequal luma/chroma bit
depth, which software fails too. A stream that keeps its reference picture sets in the
SPS needs the
EXT_SPS_ST_RPS/EXT_SPS_LT_RPScontrols filled.ffmpeg-rkcarries the patch that does (media-accel/ffmpeg/0016), and with it the ffmpeg harness reaches its own software ceiling (141/147). Without it such streams (HM-encoded material, not x265 output) decode against wrong references. - H.264 (JVT-AVC_V1): 129/135 on either decoder. The failures are flexible
macroblock ordering and SP slices, which no decoder here does, and one
left/top-crop-offset vector. FRExt (JVT-FR-EXT): 65/69 — 4:2:2 at 8- and 10-bit
(
NV16/NV20) decode correctly. The failures are 4:0:0 monochrome and MBAFF 4:2:2. - VP9 (VP9-TEST-VECTORS): 224/305 driver-attributable, the upstream series’ exact number. The failure classes are sub-64x64 frames, mid-stream resolution changes, and chroma formats the hardware lacks.
- VP8 (VP8-TEST-VECTORS): 59/61, identical to the software score — zero hardware-attributable failures.
- MPEG-2 (MPEG2_VIDEO-MAIN): 20/43, where a pass means every sample within ±2 of the software decoder. The path is almost never bit-exact, progressive streams included. Of the failures traced, nine are IDCT drift the standard permits. Six are one defect, reference field parity on bottom-field-first field pictures, the same under ffmpeg and GStreamer. MPEG-1-syntax streams do not reach the stateless path at all.
The *_rkmpp decoders are compiled into ffmpeg-rk but never open, because MPP finds no
decode client on a mainline kernel. Decode is -hwaccel v4l2request, for every codec
above.
AVS2 and AVS3 decode on the CPU, and only with the avs-decode feature. The
silicon has no V4L2 route to either codec, so there is nothing to accelerate. What the
feature adds is libdavs2 and libuavs3d, which Debian does not package and which
ffmpeg-rk is otherwise not configured against. Without it such a stream demuxes and
then finds no decoder, since the avs2 and avs3 demuxers are always present. The
avs and cavs entries a -decoders listing shows are a 1990s game FMV format and
AVS1-P2 JiZhun. Only AVS1 decodes without the feature.
Measured on this board, it is a 1080p capability. A demanding 1080p AVS2 stream decodes at 49.2 fps, occupying 2.9 of the eight cores at realtime. At 2160p, AVS2 reaches 15.6 fps with all eight busy. It wants 10.4 cores’ worth, so it does not play. AVS3 is much cheaper: 113.3 fps at 1080p for 1.3 cores, about what software HEVC costs.
Both are bit-exact against their encoders’ own reconstruction. Neither direction encodes. FFmpeg has no AVS2 encoder wrapper at all, and its AVS3 one is for a library this image does not carry.
boot2deb build turing-rk1/forky+media-accel-rockchip+avs-decode
Encode
| Codec | Block | Depth | Reached as |
|---|---|---|---|
| H.264 | VEPU580 | 8-bit | -c:v h264_rkmpp |
| HEVC | VEPU580 | 8-bit | -c:v hevc_rkmpp |
| JPEG | VEPU121 | 8-bit | -c:v mjpeg_v4l2m2m |
H.264 and HEVC encode is the one part of the stack that does go through MPP, over the
vendor /dev/mpp_service the 060 patch adds.
The JPEG encoder is a separate mainline V4L2 M2M device (/dev/video2). Its input set
differs from the VEPU580’s, and offers no drm_prime:
yuv420pnv12yuyv422uyvy422
Pick the quality with -quality (5–100, larger is better) or with -q:v, which takes
the same percentage. Note that is the opposite of the software mjpeg encoder,
where -q:v is a quantizer and lower means better. -q:v 5 here asks for 5% quality
rather than near-lossless. Leaving both off keeps the driver’s default of 50, which
lands a 1080p frame at 116 KB and 40.6 dB.
ffmpeg-rk -i input.mkv -frames:v 1 -c:v mjpeg_v4l2m2m -quality 80 -update 1 out.jpg
Two things to expect. What it buys is CPU, not speed. It spends 4.9 CPU-seconds per 300 1080p frames, against 8.1 for a single-threaded software encode. That is a fixed-function cost, steady across content, and most of what is left is the copy of each frame into the V4L2 buffer.
Software mjpeg threads across all eight cores while this path submits serially. A
1080p batch therefore finishes about 2.4x slower in elapsed time, for roughly half the
CPU. That is what you want for trickplay or snapshots running beside a transcode, and
not what you want if throughput is the goal. At matched quality its files also run
7–30% larger than the software encoder’s, which uses optimized Huffman tables where the
hardware has fixed ones.
Jellyfin’s own trickplay cannot select it, so that trade is only available to
something you can hand an encoder name. Jellyfin maps its acceleration type to a
hardware MJPEG encoder through a fixed four-entry table: mjpeg_vaapi, mjpeg_qsv,
mjpeg_videotoolbox, mjpeg_rkmpp. There is no v4l2m2m member, and its rkmpp entry
names an MPP encoder this build does not contain. It therefore falls back to software
mjpeg whatever the hardware settings say. Confirmed on this board with trickplay
hardware acceleration and hardware encoding both enabled. Reaching it would need a patch
to Jellyfin rather than configuration.
It encodes up to 8176x8176, which is 511 macroblocks in each axis. That is the most the
hardware’s nine-bit dimension registers hold, and the reason kernel patch 085 corrects
an advertised maximum of 8192. That maximum produced a JPEG whose header said 0x0,
silently, in GStreamer as well as FFmpeg. Widths are rounded up to a multiple of 4 (854
becomes 856) and the encoder says so.
Clamp your own input to 8176, because asking for more is still wrong. V4L2 has the
driver substitute a size it can do, so a 8192-wide request is honored at 8176. FFmpeg
goes on feeding the wider frame in, exits 0, and writes a JPEG whose header looks right
and whose picture is garbage. It does log encoder coded size is 8176x..., not 8192x... first, so treat that message as an error when the size it reports is
smaller than the one you asked for.
The silicon has four of these cores. The driver registers the first and logs missing multi-core support, ignoring this instance for the other three, so the limit is
throughput, not capability.
Filtering
RGA does scale, color conversion and alpha blending across three cores, as
scale_rkrga, vpp_rkrga and overlay_rkrga. Vulkan filters are available too when the
vulkan feature is selected, and scale_vulkan is the faster of the two for 8-bit work.
RGA is the only one of the two that handles 10-bit, because Vulkan cannot import NV15
at all. overlay_rkrga composites BGRA straight over a 10-bit decoded frame with no
scale stage in front. Subtitle burn-in over 1080p HDR runs at 63.7 fps, and over 4K at
19.1 fps. The RGA filters need the RKMPP device named explicitly when the frames come
from -hwaccel v4l2request. Pass -init_hw_device rkmpp=rk -filter_hw_device rk, or
they fail to configure with No RKMPP hardware context provided.
There is no hardware deinterlacer. RGA does not deinterlace. bwdif_vulkan
produces correct fields, but costs the same CPU as the CPU filter at about a seventh of
its throughput. That is once the upload/download round-trips are paid: 15 fps against
110 fps at 1080i on this box.
Deinterlace on the CPU with bwdif, and budget for it. A hardware-decoded 1080i to
1080p60 transcode costs about 1.1 cores per realtime stream, where the same transcode
without deinterlacing costs 0.15. bwdif=mode=send_frame cuts that to 0.66 by emitting
30p instead of 60p. Standard-definition interlaced content is free — 576i to 576p50 runs
at 12x realtime, about a fifth of a core.
10-bit, and why NV15 is the whole story
Every 10-bit decode on this SoC produces NV15 — packed 4:2:0, four samples in five
bytes, no padding. rkvdec declares no P010 at all and cannot be asked for one, because
the hardware writes packed. So anything that wants a 10-bit frame has to consume NV15,
and exactly two things can:
-
RGA, which converts it in hardware on the way out. This is the cheapest route and the only one that keeps the frame in hardware end to end.
-
hwdownload, which brings it to system memory as a realNV15frame, whereformat=yuv420p10leunpacks it in swscale. This is what lets a GPU tone mapper see a hardware-decoded HDR frame:ffmpeg-rk -init_hw_device rkmpp=rk -filter_hw_device rk \ -hwaccel v4l2request -hwaccel_output_format drm_prime -i hdr.mkv \ -vf "scale_rkrga=w=1280:h=720,hwdownload,format=nv15,format=yuv420p10le,\ libplacebo=tonemapping=bt.2390:format=nv12:w=1280:h=720" \ -c:v hevc_rkmpp out.mp4Three things about that graph are load-bearing:
Scale on RGA first. The unpack is a serial CPU stage, so its cost follows the frame size. Putting
scale_rkrgain front hands swscale a 720p frame instead of a 1080p one, which is most of the difference in the table below.scale_rkrgakeepsNV15on the way out, so nothing is lost by doing it early.format=nv15right afterhwdownloadis required. The download only offers the frame’s own format, so asking foryuv420p10ledirectly leaves the graph with no viable target. Convert in a second step.hwmapdoes not substitute for the download here, since deriving a Vulkan frames context from the mapped frame fails withUnsupported pixel format: nv15.There is no
hwupload, and that is deliberate. A filtergraph carries one-filter_hw_device.scale_rkrganeeds the RKMPP one, and an explicithwuploadinto Vulkan would need the Vulkan one. They cannot both be satisfied.hwupload=derive_device=vulkanfrom an RKMPP device isENOSYS, and naming Vulkan instead makesscale_rkrgafail withNo RKMPP hardware context provided.The way out is to drop the filter. Given no Vulkan device,
libplacebocreates its own and does the upload and download itself, leaving-filter_hw_devicefree for RGA.Measured on this board, 1080p HDR10 to 720p SDR over 900 frames,
user+sys:Route CPU-s fps the chain above 18.8 26.5 same, but unpacking at 1080p with an explicit hwupload32.8 21.9 software decode + libplacebo50.4 24.6 floor: hardware decode + RGA scale, no tone mapping 1.8 335.2 So the hardware path costs about a third of the CPU of software decode and runs faster. At 4K to 1080p the same chain is 12.5 CPU-s at 12.1 fps against 56.9 at 9.9. That is under a quarter of the CPU, though 4K tone mapping is still short of realtime. The floor row is the point worth remembering: tone mapping, not decode, is what this chain spends its time on.
Output correctness was checked, not assumed: this chain and the software-decode chain produce bit-identical frames, so the
NV15unpack is exact.
Vulkan cannot import NV15. There is no VkFormat for a packed 10-bit 4:2:0 sample,
so the swscale unpack above is not avoidable. The upload must be yuv420p10le, and
uploading p010le produces a silently black frame rather than an error.
Both consumers take every 10-bit decoder’s output at any width. The kernel pads the
decoded stride to 64 bytes on rkvdec (HEVC, H.264, VP9) and on the AV1 decoder
alike, which is what RGA’s importer requires.
The AV1 decoder’s format list does also offer P010, but nothing can reach it. The
v4l2request hwaccel negotiates NV15, and hwdownload,format=p010le on an AV1 frame
is refused. That costs nothing: P010 would spend 60% more write bandwidth per frame than
NV15 for the same samples.
What the silicon has that this image does not drive
The tables above are what runs. The RK3588 carries more, and knowing which gaps are whose saves chasing them:
-
AVS2 decode is in the datasheet’s decoder line, and MPEG-4 ASP / H.263 decode is within the VDPU2-class block’s reach. Neither has a V4L2 stateless uAPI at all, so no driver alone could add them. Only the vendor stack decodes them in hardware. AVS2 and AVS3 are reachable in software with the
avs-decodefeature, at the cost measured above. MPEG-1 is nominally a subset of the MPEG-2 silicon, but MPEG-1-syntax streams never engage the stateless path in any userspace. Software carries them, cheaply. -
A dedicated JPEG decoder block exists, and the devicetree carries its clock and QoS plumbing. A mainline driver is in flight upstream. It is a stateful V4L2 M2M device, so once it lands ffmpeg reaches it the same way the encoder above is reached. Nothing new is needed beyond a decoder counterpart to that wrapper.
-
An IEP block (the vendor kernels’ hardware deinterlacer) has power-domain and QoS plumbing in the mainline devicetree and no mainline driver. Deinterlacing here is CPU
bwdif, which the measurements above prefer over the Vulkan route anyway. That costs real cores for HD, and none for SD. -
8K decode is validated on this board for the
rkvdeccodecs. H.264, HEVC and VP9 decode 7680x4320 bit-exact at 8 and 10 bits, andhevc_rkmppencodes 8K that decodes back cleanly. What made 10-bit look like a ceiling was the RCB SRAM mapping kernel patch082fixes. The decoder’s line buffers were mapped into the DMA domain at an address the IOVA allocator did not know was taken. A workload whose cumulative demand reached it therefore collided, and fell back to software. Three concurrent 8-bit 8K streams hit the same address, which showed it was never about bit depth.Six concurrent 8K streams now run with no allocation failure. Patch
084raises the AV1 decoder’s own limit from 4096x2304 to 8192x4352 on the strength of that fix. AV1 is bit-exact at 4096x2304, 7680x4320 and 8192x4352 alike. -
10-bit encode does not exist on this stack, or any other. The VEPU580’s own vendor userspace maps every 10-bit input format to “unsupported”, so the 8-bit encode ceiling is structural. An HDR transcode tone-maps to SDR, which is what the chain above is for. VC-1 decode is similar: silicon the vendor stack itself declines to drive.
-
There is no VP8, VP9 or AV1 encoder in the silicon. H.264, HEVC and JPEG are the complete hardware encode set.
Decode coverage on this image is complete for the codecs a media library actually holds, at conformance parity with each driver’s upstream results. Those codecs are H.264, HEVC, VP9, AV1, VP8 and MPEG-2, at 8 and 10 bit.