▲ 8 r/u_That_Direction3907+3 crossposts

Orange Pi 5 Plus (RK3588) as a fully open-source daily driver — mainline Ubuntu 26.04, EDK2 UEFI, dual 4K HDR10+, HW decode for AV1/H.264/H.265/VP9, Vulkan 1.4 + Zink + OpenCL on Mali, NPU — all off a 64 GB SD card

Orange Pi 5 Plus (RK3588) as a fully open-source daily driver — mainline Ubuntu 26.04, EDK2 UEFI, dual 4K HDR10+, HW decode for AV1/H.264/H.265/VP9, Vulkan 1.4 + Zink + OpenCL on Mali, NPU — all off a 64 GB SD card

TL;DR: No vendor BSP, no 5.10/6.1 legacy kernel, no MPP blobs, no libmali. A mainline-first RK3588 build that boots through real UEFI, drives two 4K HDR10+ displays, hardware-decodes all four major codecs (including VP9, which mainline still doesn't ship — solved out-of-tree + DKMS), plays HW video in a browser, runs conformant Vulkan 1.4, OpenCL 3.0, and Zink on the Mali-G610 — and Zink turns out to be 2× faster than native GL on the heaviest glmark2 scene. Every number below comes from original logs run on this exact box. Long post — grab a coffee.

Why this build is interesting (30-second explainer)

Rockchip boards traditionally get their video/GPU powers from the vendor BSP: an old kernel, the MPP userspace, /dev/mpp_service, and the closed libmali blob. It works, but you're frozen in time. The mainline way is different: V4L2 stateless decoding (kernel programs decoder registers, userspace parses bitstreams — GStreamer/browsers plug straight in), Panfrost/PanVK for graphics, and everything upstream, auditable and upgradeable. This build is 100% on that path. The one big missing piece on mainline was VP9 decode — that's the centerpiece of this post.

Hardware & boot chain

  • Board: Orange Pi 5 Plus — RK3588 (4× Cortex-A76 + 4× Cortex-A55), 16 GB LPDDR4X
  • Storage: 64 GB SD card. The entire OS runs off it. (Yes. Really. It's fine.)
  • Boot: EDK2 UEFI in SPI NOR → GRUB → mainline kernel. The board behaves like a normal UEFI PC — boot menu, update-grub, standard workflows. No U-Boot scripting.
  • Displays: both HDMI 2.1 ports driving 4K HDR10+ (daily pairing: one at 144 Hz, second up to 144 Hz depending on monitor; 144+144 and 144+30 both validated)
  • Connectivity: Wi-Fi 7 (320 MHz, 2×2) + Bluetooth 5.4 — Qualcomm WCN7850 on the M.2 slot (ath12k)
  • Extra silicon in use: HDMI input (Synopsys hdmirx as a V4L2 capture device), RGA 2D engine, NPU

Software stack

Layer What
OS Ubuntu 26.04 LTS ARM, KDE Plasma on Wayland
Kernel Custom 7.0.0-rk3588-hdr+ — Collabora-based mainline tree + HDMI/HDR patches + merged OC device tree, self-packaged .debs
GPU (GL) Panfrost, OpenGL ES 3.1 — Mesa 26.2.0-devel (git-c4a1d9583c)
GPU (Vulkan) PanVK, Vulkan 1.4.352, conformanceVersion 1.4.1.2 (yes, conformant open-source Vulkan on Mali)
GL-on-Vulkan Zink (and it has a surprise below)
OpenCL rusticl, OpenCL 3.0 on the Mali-G610
NPU Mainline rocket Gallium driver + Teflon TFLite delegate (built into my Mesa)
Video decode Mainline V4L2 stateless + GStreamer v4l2codecs
VP9 decode Out-of-tree VDPU381 backend packaged as DKMS (below)
Browser ungoogled-chromium 147 custom build with V4L2 stateless HW decode

The "default OC" I daily-drive — and why exactly these values

CPU little (4× A55): 2.2 GHz @ 1.15 V
CPU big   (4× A76): 2.6 GHz @ 1.20 V
GPU (Mali-G610):    1.2 GHz @ 1.15 V
RAM (LPDDR4X):      2750–3200 MHz, stock voltage & timings

My rule: an overclock only counts if the whole stack stays clean under it. Every benchmark and every decode test in this post ran at these clocks with zero decoder timeouts and zero GPU faults — with one deliberate exception I'm keeping in the post because it's instructive: the glmark2 [terrain] scene, which exposes a native-GL driver edge at these clocks (full story in the Zink section — it's worth it). Pushing clocks further is possible for suicide runs; these are the daily values.

(Heads-up for GPU clock chasers: on Mesa/firmware combos newer than ~2026-04-20 there are reports of panthor MCU fatal panics around 1.19 GHz. My Mesa build is stable at 1.2 GHz; if you ever see that line under GPU load, drop the GPU OC first.)

[FILL IN: cooling / temps under load / power draw]

Proof: all four codecs decoded in hardware (output from this box)

$ v4l2-ctl --list-devices  +  --list-formats-out (per device)

rkvdec (VDPU381):   [0] 'S265' HEVC · [1] 'S264' H.264 · [2] 'VP9F' VP9   ← VP9 = the DKMS module
AV1 decoder:        [0] 'AV1F' AV1
hantro (G1):        [0] 'S264' H.264 · [1] 'MG2S' MPEG-2 · [2] 'VP8F' VP8
VEPU121 (encoder):  raw NV12/YUV in → H.264 out
rockchip-rga:       2D blit / colorspace conversion
snps_hdmirx:        HDMI capture input (V4L2)

$ gst-inspect-1.0 | grep v4l2sl
v4l2slav1dec · v4l2slh264dec · v4l2slh265dec · v4l2slvp9dec

Three independent decode engines, all plain V4L2 stateless devices, all driven by the same GStreamer plugin family. (Tip: /dev/videoN numbers shuffle between module reloads — match devices by driver name, never hardcode.)

The VP9 story (the part mainline doesn't give you)

Mainline kernel 7.0 ships H.264 + HEVC for the RK3588's VDPU381 decoder (Collabora's work). VP9 is not upstream yet. The working backend exists out-of-tree — written by D.V.A.B. Sarma on Collabora/Rockchip groundwork, integrated in the beryllium-org 7.0.y tree, adapted to mainline by the dongioia/rock5bplus project.

My integration: full rkvdec driver directory (VP9 wired in), built out-of-tree against my installed kernel headers, packaged as DKMS. Why DKMS matters: the module lands in /updates/dkms/, which depmod prioritizes — the stock kernel module stays untouched (revert = one command), and every future kernel (re)install rebuilds VP9 automatically. Verified across reboots and kernel reinstalls.

Measured decode performance (GStreamer fpsdisplaysink → fakesink, decode-only):

Test Result
VP9 Profile 0, 1080p30 392 fps, 0 dropped (~13× realtime)
VP9 Profile 0, 4K30 ~94 fps avg, 0 dropped (~3× realtime)
VP9 Profile 3 (4:4:4 12-bit — not in silicon) ~10 fps with clean per-frame timeouts, no oops — graceful failure (negative test)
dmesg across all runs clean

Honest limits: Profile 0 (8-bit SDR) only — Profile 2 (10-bit HDR) isn't reported by this backend yet; HDR streams should use AV1 (HW-decoded anyway). And VP9 encode does not exist in RK3588 silicon, period — the encoder block does H.264/H.265/VP8/MJPEG. For scale: my 4× A76 encode the same 4K clip at ~20 fps that the VDPU381 decodes at 94.

GPU deep-dive: Panfrost, PanVK, Zink, OpenCL — all numbers from original logs

Vulkan identity (vulkaninfo)

deviceName         = Mali-G610 MC4
driverName         = panvk
apiVersion         = 1.4.352
driverInfo         = Mesa 26.2.0-devel (git-c4a1d9583c)
conformanceVersion = 1.4.1.2

A conformant Vulkan 1.4 implementation on a Mali GPU, fully open source. Two years ago this sentence didn't exist.

⭐ glmark2 — full suite, native Panfrost vs Zink, scene by scene

Same box, same clocks, same Mesa — two driver paths. Native = Panfrost OpenGL ES 3.1. Zink = OpenGL running on top of PanVK Vulkan 1.4. Full runs, every scene.

Native overall scores: glmark2-es2 = 4200 · glmark2-es2-wayland = 4000 (two separate benchmarks). Table below uses the es2-wayland run against the matching Zink es2-wayland run:

Scene Native (FPS) Zink (FPS) Zink vs native
build (vbo) 5808 3456 −40%
texture (linear) 6920 3485 −50%
shading (phong) 4075 3227 −21%
bump (normals) 6953 3480 −50%
effect2d (blur 5×5) 2804 2283 −19%
pulsar 6583 3380 −49%
desktop (blur) 842 1331 +58%
desktop (shadow) 3357 2365 −30%
buffer (map) 566 642 +13%
buffer (subdata) 546 951 +74%
buffer (interleave) 696 843 +21%
ideas 2278 1974 −13%
jellyfish 3977 2681 −33%
terrain ~120 † 182–187 ≈ +52%
shadow 2770 2683 −3%
refract 334 514 +54%
conditionals (avg of 3) ~5720 ~3448 −40%
glmark2 Score 4200 2716

† terrain: my full-suite native runs trip a Mesa-devel batch-submit edge on this one scene at my OC (it logs a degraded ~88 fps while recovering), so I'm citing my typical isolated native result (~120 fps) here instead — a clean isolated log to lock the exact number is pending. Zink completed terrain cleanly in every run (182 fps ES2 context / 187 fps desktop-GL context; scores 2716 / 2711 — fully consistent). The ~+52% Zink advantage lines up with its wins on refract (+54%) and desktop-blur (+58%), so it's the representative figure. Native scores: glmark2-es2 → 4200, glmark2-es2-wayland → 4000 (two separate benchmarks, not two runs of one).

The pattern is the story: native GLES wins every light scene on raw throughput — but Zink wins every heavy one: buffer-subdata +74%, desktop-blur +58%, refract +54%, terrain ≈ +52%, buffer-interleave +21%. Wherever the workload gets complex (heavy geometry, blur passes, buffer streaming, refraction), the Vulkan-backed path is both faster and more robust. On modern Mali in 2026, GL-on-Vulkan is not a compatibility fallback — for complex workloads it's the fast path. Same silicon, same clocks, same Mesa; different driver architecture, different outcome.

vkmark — PanVK native Vulkan

Score: 5467 (runs: 5408 / 5467). Highlights:

Scene FPS
cube 7577
clear 7302
desktop 5936
vertex (device-local) 5888
texture (aniso 16×) 5882
shading (phong) 4471

OpenCL on Mali — clinfo + clpeak (yes, really)

clinfo: platform rusticl, device Mali-G610 MC4 (Panfrost), OpenCL 3.0, 4 CUs, 11.09 GiB visible global memory (unified with system RAM). GL/Vulkan come from my custom Mesa 26.2-devel build; the OpenCL ICD is Ubuntu's Mesa rusticl package. (clinfo reports the 800 MHz base OPP; devfreq scales under load.)

clpeak:

Metric Result
Global memory bandwidth 31.8 GB/s (float)
Single-precision compute 572 GFLOPS (float4)
Half-precision compute 1057 GFLOPS (half4) — >1 TFLOPS FP16 on an open-source stack
Integer compute 148 GIOPS
Transfer (write / read) 10.3 / 1.6 GB/s
Kernel launch latency 109.6 µs

The Mesa build behind all of this

Mesa 26.2.0-devel (git-c4a1d9583c), built from source, tuned for this exact CPU:

meson setup build \
  -Dprefix=/usr/local \
  -Dbuildtype=release \
  -Dgallium-drivers=panfrost,zink,rocket \
  -Dvulkan-drivers=panfrost \
  -Dplatforms=x11,wayland \
  -Dteflon=true \
  -Dglx=dri -Dopengl=true -Dgles1=enabled -Dgles2=enabled \
  -Degl=enabled -Dgbm=enabled \
  -Dllvm=enabled -Dshared-llvm=enabled \
  -Dvideo-codecs=all \
  -Doptimization=3 -Db_ndebug=true -Dshader-cache=enabled \
  -Dc_args='-O3 -mcpu=cortex-a76.cortex-a55 -mtune=cortex-a76.cortex-a55' \
  -Dcpp_args='-O3 -mcpu=cortex-a76.cortex-a55 -mtune=cortex-a76.cortex-a55' \
  -Dc_link_args='-Wl,-O1' -Dcpp_link_args='-Wl,-O1'

Note rocket in gallium-drivers and teflon=true — that's the mainline NPU path: Mesa's rocket driver for the Rockchip NPU with the Teflon TensorFlow Lite delegate. [FILL IN: NPU inference benchmark]

Hardware video in the browser

Stock Chromium/Firefox on ARM Linux won't touch the V4L2 stateless path (Firefox is VA-API-only → on Rockchip that means the BSP/MPP stack → dead end on mainline). The working route is a custom ungoogled-chromium 147 build (dongioia) with V4L2 stateless decode patched in.

Verified on this box (chrome://gpu + chrome://media-internals):

  • Video Decode: Hardware accelerated
  • kVideoDecoderName: V4L2VideoDecoder · kIsPlatformVideoDecoder: true
  • Enumerated HW profiles: h264, hevc main/main10, vp9 profile0, vp8

[SCREENSHOTS: chrome://gpu + media-internals — incoming]

Two board-specific traps, both solved and documented so you don't repeat them:

  1. Mali Valhall Skia YUV shader bug — Chromium's Ganesh-GL NV12 two-plane (R8/RG8) shader miscompiles on Panfrost → tile garbage on VP9. Fix: the build's CHROMIUM_RK3588_FORCE_LIBYUV=1 env (CPU NV12→RGBA conversion). Clean picture, at a CPU cost.
  2. Never enable Vulkan in the browser here. Chromium tells you itself: '--ozone-platform=wayland' is not compatible with Vulkan. PanVK is for vkmark and games; the browser stays on ANGLE/GLES. (Ironic given the Zink story above? Welcome to 2026.)

Real-world playback at my clocks: 1080p60 & 1440p60 smooth · 4K30 smooth · 4K60 drops (the LibYUV CPU conversion is the ceiling — the decoder itself does 4K at 94 fps). For 4K60 I use gst-play/Clapper on the pure HW path. Also tested an experimental "zero-copy" 148 build: no measurable gain on my box, so 147 stays production.

CHROMIUM_RK3588_FORCE_LIBYUV=1 chromium \
  --ozone-platform=wayland \
  --enable-features=AcceleratedVideoDecodeLinuxV4L2,AcceleratedVideoDecodeLinuxZeroCopyGL,WaylandWindowDecorations \
  --disable-features=UseChromeOSDirectVideoDecoder \
  --use-gl=angle --use-angle=gles

What does NOT work (so you don't waste a weekend)

  • VP9 encode — not in the silicon. Use HW H.265 encode or libvpx on CPU.
  • VP9 Profile 2 (10-bit HDR) decode — not in this backend yet (watch chewitt/LibreELEC & Collabora).
  • 4K60 video inside the browser — CPU-bound by the LibYUV workaround; use a GStreamer player.
  • Vulkan compositing in Chromium on Wayland — incompatible; GLES only.
  • glmark2 [terrain] on native Panfrost GL — trips a Mesa-devel batch-submit edge at my clocks (see the asterisk in the comparison table). Zink runs the same scene clean at 182–187 fps — the recommended path for that class of workload anyway.
  • MPP / /dev/mpp_service — BSP-only by design; this build is mainline V4L2 instead.
  • rkvdec second core — mainline ignores it for now ("missing multi-core support"); one core is already this fast.
  • Widevine DRM in the ungoogled build — Netflix & co. need a stock browser.

Reproducibility — the fresh-install kit

The point of the whole project: everything lands on a fresh Ubuntu 26.04 ARM from a small kit, in stages:

deps → kernel (.deb ×4) → GRUB + OC DTB → VP9 (DKMS) → cold boot
     → Mesa → NPU env → browser → verification

Kit: 4 kernel .debs · OC + stock device trees · staged master setup script · enable-vp9-opi5plus.sh (DKMS install/status/remove, dynamic device discovery) · install-browser-hw-opi5plus.sh (sha256-verified, validated flags baked in) · the Mesa build above · diagnostics script.

Credits — this stands on a lot of shoulders

  • D.V.A.B. Sarma — a special, personal, huge thank-you. 🙏 The VP9 VDPU381 backend at the heart of this post is his code, and he personally encouraged me to run his VP9 driver on top of my Collabora-based stack. Every VP9 number above exists because of him.
  • Collabora (Detlev Casanova, Benjamin Gaignard & team) — mainline VDPU381 H.264/HEVC, the dw_hdmi_qp HDMI/HDR display work, and the rockchip-3588 kernel tree this build stands on.
  • dongioia / rock5bplus-rkvdec2 — the mainline VP9 adaptation, the Chromium V4L2 builds, and the best public analysis of the RK3588 browser video pipeline.
  • beryllium-org — the 7.0.y integration tree.
  • Mesa Panfrost / PanVK / Zink / rusticl / rocket developers and GStreamer v4l2codecs developers — the reason "conformant open-source Vulkan on Mali" and "OpenCL on Mali without blobs" are sentences now.

How this was built — pair-engineering with AI (full transparency)

I started learning programming only a few months ago. This entire project was built in pair-engineering sessions with Anthropic's Claude — Claude Opus 4.8 (via Claude Code, doing the agentic heavy lifting) and Claude Fable 5 (chat), on the Max plan. Claude did the research across kernel trees and mailing lists, made the integration calls (which VP9 tree, why DKMS over a raw module, MMAP vs DMABUF), wrote the install scripts, and debugged everything from a stale-object kernel crash to a Mali shader bug inside Chromium — while I did all the hardware-side work: flashing, building, testing, breaking things and un-breaking them, and validating every number on real silicon. The upstream developers above wrote the actual driver code — but turning it into this system was a human+AI job, and honestly, most of the integration-engineering credit goes to Claude. Take that as you will — the logs are original and the dmesg is clean.

Appendix — reproduce my numbers

# VP9 present on the decoder?
v4l2-ctl -d /dev/videoN --list-formats-out          # expect 'VP9F' next to S264/S265

# VP9 decode benchmark (no display):
gst-launch-1.0 filesrc location=clip.webm ! matroskademux ! vp9parse ! \
  v4l2slvp9dec ! fpsdisplaysink video-sink=fakesink text-overlay=false sync=false

# make a Profile-0 test clip:
ffmpeg -f lavfi -i testsrc2=size=3840x2160:rate=30 -t 10 -c:v libvpx-vp9 \
  -pix_fmt yuv420p -b:v 12M -deadline realtime -cpu-used 8 -row-mt 1 4k-vp9-p0.webm

# watch with HW decode:
gst-play-1.0 4k-vp9-p0.webm        # or: clapper

# GPU / compute:
glmark2-es2-wayland                                       # native Panfrost
MESA_LOADER_DRIVER_OVERRIDE=zink glmark2-es2-wayland      # the Zink run
vkmark
vulkaninfo | grep -E 'deviceName|apiVersion|conformance'
clinfo | head -40
clpeak

# browser check: chrome://media-internals → kVideoDecoderName: V4L2VideoDecoder
# health check after any test:
sudo dmesg | grep -iE 'rkvdec|timeout|panthor'      # should be empty

Questions welcome — happy to share scripts, logs, or run more benchmarks in the comments.

reddit.com
u/That_Direction3907 — 2 days ago
▲ 23 r/OrangePI+1 crossposts

Orange Pi5 Plus 16Gb + Ubuntu 26.04 LTS ARM + EDK2 UEFI + OC + HW encode/decode 4K 60Hz HDR .

OC high like me 😊 but HW O drop frame with stock mesa and without forcing 4K 60Hz . Happy and Excited i go to Work for 3h 🥱 Thanks

youtu.be
u/That_Direction3907 — 25 days ago

Enabling the mainline rkvdec (4K H.264/HEVC) hardware decoder on Orange Pi 5 Plus

# Enabling the mainline rkvdec (4K H.264/HEVC) hardware decoder on Orange Pi 5 Plus via a Device-Tree overlay

**Goal:** Wake up the RK3588's `rkvdec` (VDPU381) 4K H.264 / HEVC hardware decoder on a **pure mainline** kernel, while booting from **EDK2 UEFI** and **keeping the 4K60 firmware framebuffer display** untouched.

This is done with a small **device-tree overlay** — no kernel rebuild, no BSP/vendor kernel.

1. Environment

Item Value
Board Orange Pi 5 Plus (RK3588, Mali-G610 MC4, 16 GB)
Firmware / boot EDK2 UEFI → GRUB → kernel
OS Ubuntu 26.04 (resolute), ARM64
Kernel `7.0.0-15-generic`
GPU stack Mesa 26.2.0-devel (Panfrost + PanVK)
Display EDK2 **firmware framebuffer** via `simpledrm`, mode forced to **4K60** in EDK2 setup
Decoder driver `rockchip-vdec` (`CONFIG_VIDEO_ROCKCHIP_VDEC=m`)

>

2. The problem

The `rockchip-vdec` driver is present and **already supports RK3588**:

$ modinfo rockchip-vdec | grep alias
alias: of:N\*T\*Crockchip,rk3588-vdec
alias: of:N\*T\*Crockchip,rk3576-vdec
alias: of:N\*T\*Crockchip,rk3399-vdec
...

But the EDK2 device tree contains **no decoder node** for it. A Linux driver binds to hardware by matching a `compatible` string in the driver against a `compatible` string in a DT node. The driver exists, but there is no matching node — so nothing binds.

The EDK2 DT only exposes these video-codec nodes: hantro g1 (`fdb50000`), the `vepu121` encoder + cores (`fdba0000`/`fdba4000`/`fdba8000`/`fdbac000`), and the AV1 decoder (`fdc70000`). It has the `qos_rkvdec0`/`qos_rkvdec1` **labels** (the SoC `.dtsi` knows rkvdec exists) but **neither the decoder node nor its IOMMU**.

So the overlay must add **two** nodes: the decoder (`video-codec@fdc38000`) **and** its IOMMU (`iommu@fdc38700`).

3. Where the values come from (do not guess these)

The Ubuntu kernel package already ships the full upstream board DTB, which contains a correct `rkvdec0` node for this exact kernel:

/usr/lib/firmware/7.0.0-15-generic/device-tree/rockchip/rk3588-orangepi-5-plus.dtb

Every register address, clock/reset index, interrupt and power-domain in the overlay below was extracted from that DTB (decompiled with `dtc`). The overlay references **existing labels** in the EDK2 DT (`&cru`, `&power`) so `fdtoverlay` can resolve them at merge time.

>

4. The overlay (rkvdec.dts)

dts

/dts-v1/;
/plugin/;

/ {
    fragment@0 {
        target-path = "/";
        \_\_overlay\_\_ {

            rkvdec0\_mmu: iommu@fdc38700 {
                compatible = "rockchip,rk3588-iommu", "rockchip,rk3568-iommu";
                reg = <0x0 0xfdc38700 0x0 0x40>, <0x0 0xfdc38740 0x0 0x40>;
                interrupts = <0x0 0x60 0x4 0x0>;
                clocks = <&cru 0x181>, <&cru 0x180>;
                clock-names = "aclk", "iface";
                power-domains = <&power 0x0e>;
                #iommu-cells = <0x0>;
            };

            rkvdec0: video-codec@fdc38000 {
                compatible = "rockchip,rk3588-vdec";
                reg = <0x0 0xfdc38100 0x0 0x500>,
                      <0x0 0xfdc38000 0x0 0x100>,
                      <0x0 0xfdc38600 0x0 0x100>;
                reg-names = "function", "link", "cache";
                interrupts = <0x0 0x5f 0x4 0x0>;
                clocks = <&cru 0x181>, <&cru 0x180>, <&cru 0x182>, <&cru 0x184>, <&cru 0x183>;
                clock-names = "axi", "ahb", "cabac", "core", "hevc\_cabac";
                assigned-clocks = <&cru 0x181>, <&cru 0x184>, <&cru 0x182>, <&cru 0x183>;
                assigned-clock-rates = <0x2faf0800 0x23c34600 0x23c34600 0x3b9aca00>;
                iommus = <&rkvdec0\_mmu>;
                power-domains = <&power 0x0e>;
                resets = <&cru 0x143>, <&cru 0x142>, <&cru 0x146>, <&cru 0x148>, <&cru 0x147>;
                reset-names = "axi", "ahb", "cabac", "core", "hevc\_cabac";
            };
        };
    };
};

What each part does

* `iommu@fdc38700` — the MMU `rkvdec` uses to reach memory. Defined first and labelled `rkvdec0_mmu` so the decoder can reference it. * `reg` **(3 windows)** — decoder register banks: `function` / `link` / `cache`. * `clocks` **/** `resets` — referenced through `&cru` \+ indices. `&cru` resolves to your clock-controller at merge time. * `assigned-clock-rates` — `0x2faf0800`=800 MHz (axi), `0x23c34600`=600 MHz (core/cabac), `0x3b9aca00`=1 GHz (hevc\_cabac). * `power-domains = <&power 0x0e>` — RKVDEC0 power domain (index 14). * `iommus = <&rkvdec0_mmu>` — binds the decoder to the MMU above. * `sram` **is deliberately omitted** — it's an optional performance buffer (row-cache in on-chip SRAM). Without it the driver stores the RCB in RAM and prints `No sram node, RCB will be stored in RAM` (benign). Omitting it avoids one more phandle dependency.

>

5. Build, merge, verify (no boot risk yet)

bash

sudo apt install -y device-tree-compiler

# 1) compile the overlay (-@ generates \_\_fixups\_\_ so fdtoverlay can resolve &amp;cru/&amp;power)
dtc -@ -I dts -O dtb -o \~/rkvdec.dtbo \~/rkvdec.dts

# 2) use the RAW firmware DTB as the merge base (byte-faithful to what EDK2 provides)
sudo cp /sys/firmware/fdt \~/edk2-raw.dtb
sudo chown $USER:$USER \~/edk2-raw.dtb

# 3) merge overlay into the base
fdtoverlay -i \~/edk2-raw.dtb -o \~/merged.dtb \~/rkvdec.dtbo
echo "fdtoverlay exit: $?"        # must be 0

# 4) confirm the node is present in the merged DTB
dtc -I dtb -O dts \~/merged.dtb 
2
&gt;/dev/null | grep -A14 "video-codec@fdc38000"

Compile-time `dtc` warnings about `reg_format` / missing `interrupt-parent` are **benign overlay lint** — at compile time `dtc` doesn't know the root's `#address-cells`/`#size-cells` or that the nodes inherit `interrupt-parent` from the GIC. They resolve correctly on merge.

>

6. Test it BEFORE making it permanent (one-shot, reversible)

Copy the merged DTB, then boot it once from the GRUB shell/editor — nothing is saved, so a bad DTB just means a hard-reset back to normal.

bash

sudo cp \~/merged.dtb /boot/rkvdec-merged.dtb

At the GRUB command shell (`grub>`), or in the entry editor (`e`), boot manually:

search --no-floppy --fs-uuid --set=root &lt;YOUR-ROOTFS-UUID&gt;
insmod fdt
devicetree /boot/rkvdec-merged.dtb
linux /boot/vmlinuz-7.0.0-15-generic root=UUID=&lt;YOUR-ROOTFS-UUID&gt; ro
initrd /boot/initrd.img-7.0.0-15-generic
boot

After boot, verify:

bash

sudo dmesg | grep -iE "rkvdec|rk3588-vdec|iommu@fdc38"
# expect: rkvdec fdc38100.video-codec: No sram node, RCB will be stored in RAM

v4l2-ctl --list-devices | grep -A2 rkvdec
# expect: rkvdec (platform:rkvdec): /dev/videoN  /dev/mediaM

v4l2-ctl -d /dev/video2 --list-formats-out
# expect: 'S265' (HEVC Parsed Slice Data) and 'S264' (H.264 Parsed Slice Data)

`S265`/`S264` are **stateless** (parsed-slice) formats — the decoder is driven via the V4L2 stateless / request API.

7. Make it permanent (survives kernel updates)

A failed `devicetree` line is **safe**: GRUB just falls back to the firmware DTB and the system boots normally (without rkvdec). It cannot brick the board.

bash

sudo tee /etc/grub.d/09\_rkvdec\_dtb &gt; /dev/null &lt;&lt; 'EOF'
#!/bin/sh
echo "insmod fdt"
echo "devicetree /boot/rkvdec-merged.dtb"
EOF
sudo chmod +x /etc/grub.d/09\_rkvdec\_dtb
sudo update-grub
sudo grep -n "rkvdec-merged" /boot/grub/grub.cfg   # confirm the line landed

**Disable** temporarily: `sudo chmod -x /etc/grub.d/09_rkvdec_dtb && sudo update-grub` **Remove** permanently: `sudo rm /etc/grub.d/09_rkvdec_dtb && sudo update-grub`

8. Proving hardware decode (GStreamer, no ffmpeg rebuild needed)

GStreamer 1.28+ has V4L2 **stateless** decoder elements (`v4l2slh265dec`, `v4l2slh264dec`) that drive `rkvdec` directly.

bash

# make an 8-bit 4K HEVC test clip (software encode, one-off; slow is expected)
ffmpeg -i some\_4k\_source.webm -t 10 -an -c:v libx265 -crf 23 \\
       -pix\_fmt yuv420p -vf scale=3840:2160 \~/test\_hevc.mp4

# decode through rkvdec to fakesink; watch CPU stay low
gst-launch-1.0 filesrc location=$HOME/test\_hevc.mp4 ! qtdemux ! h265parse ! \\
  v4l2slh265dec ! fakesink

# actual on-screen playback with HW decode forced
GST\_PLUGIN\_FEATURE\_RANK=v4l2slh265dec:MAX gst-play-1.0 $HOME/test\_hevc.mp4

**Result on this setup:** a 10 s 4K (3840×2160) HEVC clip decoded in \~3.1 s (\~190 fps, far above real-time) at only \~10–15 % CPU — and that CPU is just userspace overhead (demux/parse/buffer copy); the decode itself runs on the VPU. For comparison, swapping `v4l2slh265dec` for the software `avdec_h265` spikes CPU sharply. Any GStreamer-based player (Totem, etc.) gets HW HEVC decode the same way.

8.1 Bonus: 4K AV1 (the YouTube codec) on hardware — including in a browser

`rkvdec` covers H.264/HEVC. **YouTube 4K is AV1**, which is handled by a *separate* block: the AV1 VPU (`rk3588-av1-vpu-dec`, `/dev/video4`), present on this stack independently of the rkvdec overlay. GStreamer 1.28 ships a stateless AV1 element (`v4l2slav1dec`), so 4K AV1 decodes on hardware with **no extra build**:

bash

gst-inspect-1.0 | grep v4l2slav1dec          # V4L2 Stateless AV1 Video Decoder
v4l2-ctl -d /dev/video4 --list-formats-out   # 'AV1F' (AV1 Frame, compressed)

# decode a 4K 10-bit AV1 clip — use parsebin (it auto-detects the container)
gst-launch-1.0 -v filesrc location=clip.webm ! parsebin ! v4l2slav1dec ! fakesink
# decoder src caps: video/x-raw, format=NV12\_10LE40\_4L4 (10-bit), colorimetry=bt2100-pq -&gt; full HDR path

YouTube 4K HDR in a browser, hardware-decoded

WebKitGTK browsers (**GNOME Web / Epiphany**) use GStreamer for media, so bumping the AV1 decoder's rank lets YouTube AV1 run on the VPU:

bash

sudo apt install -y epiphany-browser
GST\_PLUGIN\_FEATURE\_RANK=v4l2slav1dec:MAX epiphany

**Confirmed result:** YouTube playing `av01` **10-bit**, `smpte2084 (PQ) / bt2020` **HDR**, **3840×2160@60, 0 dropped frames of 11271**, with CPU only at compositing-overhead levels — the decode runs on the VPU, not the cores. (Software 4K60 10-bit AV1 on these cores would peg the CPU and drop frames.) Prove the VPU is in use while the video plays:

bash

sudo fuser /dev/video4   # prints the browser's GStreamer PID holding the AV1 decoder

Caveats: YouTube may serve **VP9** (no HW path here) instead of AV1 depending on what the browser advertises — if CPU pegs and frames drop, it fell back to VP9/software. **Firefox** does *not* use this path (it uses its own bundled ffmpeg/VA-API, which has no driver for the VPU here).

9. What works / what does not (be honest)

**Works**

* 4K **H.264** and **HEVC (8-bit)** hardware decode via `rkvdec` (`/dev/video2`). * 4K **AV1, 10-bit, HDR** hardware decode via the AV1 VPU (`/dev/video4`) + GStreamer `v4l2slav1dec`. * Any GStreamer-based player (Totem, …) and **WebKitGTK browsers (Epiphany) → YouTube 4K60 AV1 HDR on hardware**. * Display stays on the 4K60 firmware framebuffer; GPU compositing (Panfrost) untouched.

**Does NOT (yet) / caveats**

* **No VP9** on the mainline `rockchip-vdec` driver (only `S264`/`S265` exposed). The hardware can do VP9, but it isn't surfaced here — and YouTube falls back to VP9 (software) unless it serves AV1. * **Firefox / Chromium** don't use the GStreamer path. Firefox needs a VA-API → V4L2-stateless bridge (`libva-v4l2-request`); Chromium needs a patched build (ChromeOS V4L2 path) + udev rules. Use **Epiphany** for the working HW browser path. * **mpv** needs an ffmpeg built with `v4l2request`. * **Desktop tearing on motion** (moving windows) is inherent to the firmware framebuffer (`simpledrm` has no hardware vsync/vblank). It is **not** caused by this overlay. The only real fix is native KMS (`vop2`), which on this stack drops you to \~4K30 RGB. Static content (reading) is unaffected.

10. Summary

A \~35-line device-tree overlay, merged onto the firmware DTB and loaded via GRUB, enables the mainline RK3588 4K H.264/HEVC hardware decoder on EDK2-booted Ubuntu — without a kernel rebuild and without disturbing the 4K60 firmware-framebuffer display. Separately, the AV1 VPU (`/dev/video4`, already present) plus GStreamer's `v4l2slav1dec` gives **4K AV1 10-bit HDR** hardware decode, and a WebKitGTK browser (Epiphany) leverages it for **YouTube 4K60 AV1 HDR with 0 dropped frames** — the full original goal, achieved entirely on mainline. The only remaining gaps are Firefox/Chromium (their own userspace decode plumbing) and the inherent firmware-framebuffer tearing (a display, not decode, limitation).Enabling the mainline rkvdec (4K H.264/HEVC) hardware decoder on Orange Pi 5 Plus via a Device-Tree overlay
Goal: Wake up the RK3588's rkvdec (VDPU381) 4K H.264 / HEVC hardware decoder on a pure mainline kernel, while booting from EDK2 UEFI and keeping the 4K60 firmware framebuffer display untouched.
This is done with a small device-tree overlay — no kernel rebuild, no BSP/vendor kernel.

  1. Environment
    Item Value
    Board Orange Pi 5 Plus (RK3588, Mali-G610 MC4, 16 GB)
    Firmware / boot EDK2 UEFI → GRUB → kernel
    OS Ubuntu 26.04 (resolute), ARM64
    Kernel 7.0.0-15-generic
    GPU stack Mesa 26.2.0-devel (Panfrost + PanVK)
    Display EDK2 firmware framebuffer via simpledrm, mode forced to 4K60 in EDK2 setup
    Decoder driver rockchip-vdec (CONFIG\_VIDEO\_ROCKCHIP\_VDEC=m)

The native Rockchip KMS (vop2/hdmi) stack does not load here, because the EDK2-provided device tree has no display nodes. Display is the firmware framebuffer (simpledrm). That is intentional — it gives a solid 4K60 desktop. See Limitations below.

  1. The problem
    The rockchip-vdec driver is present and already supports RK3588:
    $ modinfo rockchip-vdec | grep alias
    alias: of:N\*T\*Crockchip,rk3588-vdec
    alias: of:N\*T\*Crockchip,rk3576-vdec
    alias: of:N\*T\*Crockchip,rk3399-vdec
    ...
    But the EDK2 device tree contains no decoder node for it. A Linux driver binds to hardware by matching a compatible string in the driver against a compatible string in a DT node. The driver exists, but there is no matching node — so nothing binds.
    The EDK2 DT only exposes these video-codec nodes: hantro g1 (fdb50000), the vepu121 encoder + cores (fdba0000/fdba4000/fdba8000/fdbac000), and the AV1 decoder (fdc70000). It has the qos\_rkvdec0/qos\_rkvdec1 labels (the SoC .dtsi knows rkvdec exists) but neither the decoder node nor its IOMMU.
    So the overlay must add two nodes: the decoder (video-codec@fdc38000) and its IOMMU (iommu@fdc38700).

  2. Where the values come from (do not guess these)
    The Ubuntu kernel package already ships the full upstream board DTB, which contains a correct rkvdec0 node for this exact kernel:
    /usr/lib/firmware/7.0.0-15-generic/device-tree/rockchip/rk3588-orangepi-5-plus.dtb
    Every register address, clock/reset index, interrupt and power-domain in the overlay below was extracted from that DTB (decompiled with dtc). The overlay references existing labels in the EDK2 DT (&cru, &power) so fdtoverlay can resolve them at merge time.

Important: clock/reset indices (0x181, 0x143, …) and the power-domain index (0x0e) are specific to the RK3588 CRU as defined for this kernel's DTB. On a different kernel/DTB, re-extract them from your own shipped rk3588-orangepi-5-plus.dtb rather than copying these verbatim.

  1. The overlay (rkvdec.dts)
    dts
    /dts-v1/;
    /plugin/;

/ {
fragment@0 {
target-path = "/";
\_\_overlay\_\_ {

rkvdec0\_mmu: iommu@fdc38700 {
compatible = "rockchip,rk3588-iommu", "rockchip,rk3568-iommu";
reg = <0x0 0xfdc38700 0x0 0x40>, <0x0 0xfdc38740 0x0 0x40>;
interrupts = <0x0 0x60 0x4 0x0>;
clocks = <&cru 0x181>, <&cru 0x180>;
clock-names = "aclk", "iface";
power-domains = <&power 0x0e>;
\#iommu-cells = <0x0>;
};

rkvdec0: video-codec@fdc38000 {
compatible = "rockchip,rk3588-vdec";
reg = <0x0 0xfdc38100 0x0 0x500>,
<0x0 0xfdc38000 0x0 0x100>,
<0x0 0xfdc38600 0x0 0x100>;
reg-names = "function", "link", "cache";
interrupts = <0x0 0x5f 0x4 0x0>;
clocks = <&cru 0x181>, <&cru 0x180>, <&cru 0x182>, <&cru 0x184>, <&cru 0x183>;
clock-names = "axi", "ahb", "cabac", "core", "hevc\_cabac";
assigned-clocks = <&cru 0x181>, <&cru 0x184>, <&cru 0x182>, <&cru 0x183>;
assigned-clock-rates = <0x2faf0800 0x23c34600 0x23c34600 0x3b9aca00>;
iommus = <&rkvdec0\_mmu>;
power-domains = <&power 0x0e>;
resets = <&cru 0x143>, <&cru 0x142>, <&cru 0x146>, <&cru 0x148>, <&cru 0x147>;
reset-names = "axi", "ahb", "cabac", "core", "hevc\_cabac";
};
};
};
};
What each part does
iommu@fdc38700 — the MMU rkvdec uses to reach memory. Defined first and labelled rkvdec0\_mmu so the decoder can reference it.
reg (3 windows) — decoder register banks: function / link / cache.
clocks / resets — referenced through &cru + indices. &cru resolves to your clock-controller at merge time.
assigned-clock-rates — 0x2faf0800=800 MHz (axi), 0x23c34600=600 MHz (core/cabac), 0x3b9aca00=1 GHz (hevc\_cabac).
power-domains = <&power 0x0e> — RKVDEC0 power domain (index 14).
iommus = <&rkvdec0\_mmu> — binds the decoder to the MMU above.
sram is deliberately omitted — it's an optional performance buffer (row-cache in on-chip SRAM). Without it the driver stores the RCB in RAM and prints No sram node, RCB will be stored in RAM (benign). Omitting it avoids one more phandle dependency.

Only rkvdec0 (core 0) is added. The mainline driver currently drives one core; rkvdec1 (video-codec@fdc40000 / iommu@fdc40700, power-domain 0x0f) can be added later the same way.

  1. Build, merge, verify (no boot risk yet)
    bash
    sudo apt install -y device-tree-compiler

\# 1) compile the overlay (-@ generates \_\_fixups\_\_ so fdtoverlay can resolve &cru/&power)
dtc -@ -I dts -O dtb -o \~/rkvdec.dtbo \~/rkvdec.dts

\# 2) use the RAW firmware DTB as the merge base (byte-faithful to what EDK2 provides)
sudo cp /sys/firmware/fdt \~/edk2-raw.dtb
sudo chown $USER:$USER \~/edk2-raw.dtb

\# 3) merge overlay into the base
fdtoverlay -i \~/edk2-raw.dtb -o \~/merged.dtb \~/rkvdec.dtbo
echo "fdtoverlay exit: $?" # must be 0

\# 4) confirm the node is present in the merged DTB
dtc -I dtb -O dts \~/merged.dtb 2>/dev/null | grep -A14 "video-codec@fdc38000"
Compile-time dtc warnings about reg\_format / missing interrupt-parent are benign overlay lint — at compile time dtc doesn't know the root's #address-cells/#size-cells or that the nodes inherit interrupt-parent from the GIC. They resolve correctly on merge.

Using /sys/firmware/fdt (the raw blob the kernel actually received) is preferable to dtc -I fs -O dtb /proc/device-tree, which reconstructs the tree and is not always byte-identical.

  1. Test it BEFORE making it permanent (one-shot, reversible)
    Copy the merged DTB, then boot it once from the GRUB shell/editor — nothing is saved, so a bad DTB just means a hard-reset back to normal.
    bash
    sudo cp \~/merged.dtb /boot/rkvdec-merged.dtb
    At the GRUB command shell (grub>), or in the entry editor (e), boot manually:
    search --no-floppy --fs-uuid --set=root <YOUR-ROOTFS-UUID>
    insmod fdt
    devicetree /boot/rkvdec-merged.dtb
    linux /boot/vmlinuz-7.0.0-15-generic root=UUID=<YOUR-ROOTFS-UUID> ro
    initrd /boot/initrd.img-7.0.0-15-generic
    boot
    After boot, verify:
    bash
    sudo dmesg | grep -iE "rkvdec|rk3588-vdec|iommu@fdc38"
    \# expect: rkvdec fdc38100.video-codec: No sram node, RCB will be stored in RAM

v4l2-ctl --list-devices | grep -A2 rkvdec
\# expect: rkvdec (platform:rkvdec): /dev/videoN /dev/mediaM

v4l2-ctl -d /dev/video2 --list-formats-out
\# expect: 'S265' (HEVC Parsed Slice Data) and 'S264' (H.264 Parsed Slice Data)
S265/S264 are stateless (parsed-slice) formats — the decoder is driven via the V4L2 stateless / request API.

  1. Make it permanent (survives kernel updates)
    A failed devicetree line is safe: GRUB just falls back to the firmware DTB and the system boots normally (without rkvdec). It cannot brick the board.
    bash
    sudo tee /etc/grub.d/09\_rkvdec\_dtb > /dev/null << 'EOF'
    \#!/bin/sh
    echo "insmod fdt"
    echo "devicetree /boot/rkvdec-merged.dtb"
    EOF
    sudo chmod +x /etc/grub.d/09\_rkvdec\_dtb
    sudo update-grub
    sudo grep -n "rkvdec-merged" /boot/grub/grub.cfg # confirm the line landed
    Disable temporarily: sudo chmod -x /etc/grub.d/09\_rkvdec\_dtb && sudo update-grub Remove permanently: sudo rm /etc/grub.d/09\_rkvdec\_dtb && sudo update-grub

  2. Proving hardware decode (GStreamer, no ffmpeg rebuild needed)
    GStreamer 1.28+ has V4L2 stateless decoder elements (v4l2slh265dec, v4l2slh264dec) that drive rkvdec directly.
    bash
    \# make an 8-bit 4K HEVC test clip (software encode, one-off; slow is expected)
    ffmpeg -i some\_4k\_source.webm -t 10 -an -c:v libx265 -crf 23 \\
    \-pix\_fmt yuv420p -vf scale=3840:2160 \~/test\_hevc.mp4

\# decode through rkvdec to fakesink; watch CPU stay low
gst-launch-1.0 filesrc location=$HOME/test\_hevc.mp4 ! qtdemux ! h265parse ! \\
v4l2slh265dec ! fakesink

\# actual on-screen playback with HW decode forced
GST\_PLUGIN\_FEATURE\_RANK=v4l2slh265dec:MAX gst-play-1.0 $HOME/test\_hevc.mp4
Result on this setup: a 10 s 4K (3840×2160) HEVC clip decoded in \~3.1 s (\~190 fps, far above real-time) at only \~10–15 % CPU — and that CPU is just userspace overhead (demux/parse/buffer copy); the decode itself runs on the VPU. For comparison, swapping v4l2slh265dec for the software avdec\_h265 spikes CPU sharply. Any GStreamer-based player (Totem, etc.) gets HW HEVC decode the same way.

8.1 Bonus: 4K AV1 (the YouTube codec) on hardware — including in a browser
rkvdec covers H.264/HEVC. YouTube 4K is AV1, which is handled by a separate block: the AV1 VPU (rk3588-av1-vpu-dec, /dev/video4), present on this stack independently of the rkvdec overlay. GStreamer 1.28 ships a stateless AV1 element (v4l2slav1dec), so 4K AV1 decodes on hardware with no extra build:
bash
gst-inspect-1.0 | grep v4l2slav1dec # V4L2 Stateless AV1 Video Decoder
v4l2-ctl -d /dev/video4 --list-formats-out # 'AV1F' (AV1 Frame, compressed)

\# decode a 4K 10-bit AV1 clip — use parsebin (it auto-detects the container)
gst-launch-1.0 -v filesrc location=clip.webm ! parsebin ! v4l2slav1dec ! fakesink
\# decoder src caps: video/x-raw, format=NV12\_10LE40\_4L4 (10-bit), colorimetry=bt2100-pq -> full HDR path
YouTube 4K HDR in a browser, hardware-decoded
WebKitGTK browsers (GNOME Web / Epiphany) use GStreamer for media, so bumping the AV1 decoder's rank lets YouTube AV1 run on the VPU:
bash
sudo apt install -y epiphany-browser
GST\_PLUGIN\_FEATURE\_RANK=v4l2slav1dec:MAX epiphany
Confirmed result: YouTube playing av01 10-bit, smpte2084 (PQ) / bt2020 HDR, 3840×2160@60, 0 dropped frames of 11271, with CPU only at compositing-overhead levels — the decode runs on the VPU, not the cores. (Software 4K60 10-bit AV1 on these cores would peg the CPU and drop frames.) Prove the VPU is in use while the video plays:
bash
sudo fuser /dev/video4 # prints the browser's GStreamer PID holding the AV1 decoder
Caveats: YouTube may serve VP9 (no HW path here) instead of AV1 depending on what the browser advertises — if CPU pegs and frames drop, it fell back to VP9/software. Firefox does not use this path (it uses its own bundled ffmpeg/VA-API, which has no driver for the VPU here).

  1. What works / what does not (be honest)
    Works
    4K H.264 and HEVC (8-bit) hardware decode via rkvdec (/dev/video2).
    4K AV1, 10-bit, HDR hardware decode via the AV1 VPU (/dev/video4) + GStreamer v4l2slav1dec.
    Any GStreamer-based player (Totem, …) and WebKitGTK browsers (Epiphany) → YouTube 4K60 AV1 HDR on hardware.
    Display stays on the 4K60 firmware framebuffer; GPU compositing (Panfrost) untouched.
    Does NOT (yet) / caveats
    No VP9 on the mainline rockchip-vdec driver (only S264/S265 exposed). The hardware can do VP9, but it isn't surfaced here — and YouTube falls back to VP9 (software) unless it serves AV1.
    Firefox / Chromium don't use the GStreamer path. Firefox needs a VA-API → V4L2-stateless bridge (libva-v4l2-request); Chromium needs a patched build (ChromeOS V4L2 path) + udev rules. Use Epiphany for the working HW browser path.
    mpv needs an ffmpeg built with v4l2request.
    Desktop tearing on motion (moving windows) is inherent to the firmware framebuffer (simpledrm has no hardware vsync/vblank). It is not caused by this overlay. The only real fix is native KMS (vop2), which on this stack drops you to \~4K30 RGB. Static content (reading) is unaffected.

  2. Summary
    A \~35-line device-tree overlay, merged onto the firmware DTB and loaded via GRUB, enables the mainline RK3588 4K H.264/HEVC hardware decoder on EDK2-booted Ubuntu — without a kernel rebuild and without disturbing the 4K60 firmware-framebuffer display. Separately, the AV1 VPU (/dev/video4, already present) plus GStreamer's v4l2slav1dec gives 4K AV1 10-bit HDR hardware decode, and a WebKitGTK browser (Epiphany) leverages it for YouTube 4K60 AV1 HDR with 0 dropped frames — the full original goal, achieved entirely on mainline. The only remaining gaps are Firefox/Chromium (their own userspace decode plumbing) and the inherent firmware-framebuffer tearing (a display, not decode, limitation).

After 1h7m of continuous 4K60 AV1 10-bit HDR playback in Epiphany: 12 dropped frames of 218,788 (0.005%). `fuser /dev/video4` confirms the AV1 VPU is in use — hardware decode on pure mainline.

All credits go to CLOUDE-CODE OPUS 4.8

reddit.com
u/That_Direction3907 — 1 month ago
▲ 17 r/kernel+3 crossposts

Enabling the mainline rkvdec (4K H.264/HEVC) hardware decoder on Orange Pi 5 Plus

Enabling the mainline rkvdec (4K H.264/HEVC) hardware decoder on Orange Pi 5 Plus via a Device-Tree overlay

Goal: Wake up the RK3588's rkvdec (VDPU381) 4K H.264 / HEVC hardware decoder on a pure mainline kernel, while booting from EDK2 UEFI and keeping the 4K60 firmware framebuffer display untouched.

This is done with a small device-tree overlay — no kernel rebuild, no BSP/vendor kernel.

1. Environment

Item Value
Board Orange Pi 5 Plus (RK3588, Mali-G610 MC4, 16 GB)
Firmware / boot EDK2 UEFI → GRUB → kernel
OS Ubuntu 26.04 (resolute), ARM64
Kernel 7.0.0-15-generic
GPU stack Mesa 26.2.0-devel (Panfrost + PanVK)
Display EDK2 firmware framebuffer via simpledrm, mode forced to 4K60 in EDK2 setup
Decoder driver rockchip-vdec (CONFIG_VIDEO_ROCKCHIP_VDEC=m)

>

2. The problem

The rockchip-vdec driver is present and already supports RK3588:

$ modinfo rockchip-vdec | grep alias
alias: of:N*T*Crockchip,rk3588-vdec
alias: of:N*T*Crockchip,rk3576-vdec
alias: of:N*T*Crockchip,rk3399-vdec
...

But the EDK2 device tree contains no decoder node for it. A Linux driver binds to hardware by matching a compatible string in the driver against a compatible string in a DT node. The driver exists, but there is no matching node — so nothing binds.

The EDK2 DT only exposes these video-codec nodes: hantro g1 (fdb50000), the vepu121 encoder + cores (fdba0000/fdba4000/fdba8000/fdbac000), and the AV1 decoder (fdc70000). It has the qos_rkvdec0/qos_rkvdec1 labels (the SoC .dtsi knows rkvdec exists) but neither the decoder node nor its IOMMU.

So the overlay must add two nodes: the decoder (video-codec@fdc38000) and its IOMMU (iommu@fdc38700).

3. Where the values come from (do not guess these)

The Ubuntu kernel package already ships the full upstream board DTB, which contains a correct rkvdec0 node for this exact kernel:

/usr/lib/firmware/7.0.0-15-generic/device-tree/rockchip/rk3588-orangepi-5-plus.dtb

Every register address, clock/reset index, interrupt and power-domain in the overlay below was extracted from that DTB (decompiled with dtc). The overlay references existing labels in the EDK2 DT (&amp;cru, &amp;power) so fdtoverlay can resolve them at merge time.

>

4. The overlay (rkvdec.dts)

dts

/dts-v1/;
/plugin/;

/ {
    fragment@0 {
        target-path = "/";
        __overlay__ {

            rkvdec0_mmu: iommu@fdc38700 {
                compatible = "rockchip,rk3588-iommu", "rockchip,rk3568-iommu";
                reg = &lt;0x0 0xfdc38700 0x0 0x40&gt;, &lt;0x0 0xfdc38740 0x0 0x40&gt;;
                interrupts = &lt;0x0 0x60 0x4 0x0&gt;;
                clocks = &lt;&amp;cru 0x181&gt;, &lt;&amp;cru 0x180&gt;;
                clock-names = "aclk", "iface";
                power-domains = &lt;&amp;power 0x0e&gt;;
                #iommu-cells = &lt;0x0&gt;;
            };

            rkvdec0: video-codec@fdc38000 {
                compatible = "rockchip,rk3588-vdec";
                reg = &lt;0x0 0xfdc38100 0x0 0x500&gt;,
                      &lt;0x0 0xfdc38000 0x0 0x100&gt;,
                      &lt;0x0 0xfdc38600 0x0 0x100&gt;;
                reg-names = "function", "link", "cache";
                interrupts = &lt;0x0 0x5f 0x4 0x0&gt;;
                clocks = &lt;&amp;cru 0x181&gt;, &lt;&amp;cru 0x180&gt;, &lt;&amp;cru 0x182&gt;, &lt;&amp;cru 0x184&gt;, &lt;&amp;cru 0x183&gt;;
                clock-names = "axi", "ahb", "cabac", "core", "hevc_cabac";
                assigned-clocks = &lt;&amp;cru 0x181&gt;, &lt;&amp;cru 0x184&gt;, &lt;&amp;cru 0x182&gt;, &lt;&amp;cru 0x183&gt;;
                assigned-clock-rates = &lt;0x2faf0800 0x23c34600 0x23c34600 0x3b9aca00&gt;;
                iommus = &lt;&amp;rkvdec0_mmu&gt;;
                power-domains = &lt;&amp;power 0x0e&gt;;
                resets = &lt;&amp;cru 0x143&gt;, &lt;&amp;cru 0x142&gt;, &lt;&amp;cru 0x146&gt;, &lt;&amp;cru 0x148&gt;, &lt;&amp;cru 0x147&gt;;
                reset-names = "axi", "ahb", "cabac", "core", "hevc_cabac";
            };
        };
    };
};

What each part does

  • iommu@fdc38700 — the MMU rkvdec uses to reach memory. Defined first and labelled rkvdec0_mmu so the decoder can reference it.
  • reg (3 windows) — decoder register banks: function / link / cache.
  • clocks / resets — referenced through &amp;cru + indices. &amp;cru resolves to your clock-controller at merge time.
  • assigned-clock-rates0x2faf0800=800 MHz (axi), 0x23c34600=600 MHz (core/cabac), 0x3b9aca00=1 GHz (hevc_cabac).
  • power-domains = &lt;&amp;power 0x0e&gt; — RKVDEC0 power domain (index 14).
  • iommus = &lt;&amp;rkvdec0_mmu&gt; — binds the decoder to the MMU above.
  • sram is deliberately omitted — it's an optional performance buffer (row-cache in on-chip SRAM). Without it the driver stores the RCB in RAM and prints No sram node, RCB will be stored in RAM (benign). Omitting it avoids one more phandle dependency.

>

5. Build, merge, verify (no boot risk yet)

bash

sudo apt install -y device-tree-compiler

# 1) compile the overlay (-@ generates __fixups__ so fdtoverlay can resolve &amp;cru/&amp;power)
dtc -@ -I dts -O dtb -o ~/rkvdec.dtbo ~/rkvdec.dts

# 2) use the RAW firmware DTB as the merge base (byte-faithful to what EDK2 provides)
sudo cp /sys/firmware/fdt ~/edk2-raw.dtb
sudo chown $USER:$USER ~/edk2-raw.dtb

# 3) merge overlay into the base
fdtoverlay -i ~/edk2-raw.dtb -o ~/merged.dtb ~/rkvdec.dtbo
echo "fdtoverlay exit: $?"        # must be 0

# 4) confirm the node is present in the merged DTB
dtc -I dtb -O dts ~/merged.dtb 
2
&gt;/dev/null | grep -A14 "video-codec@fdc38000"

Compile-time dtc warnings about reg_format / missing interrupt-parent are benign overlay lint — at compile time dtc doesn't know the root's #address-cells/#size-cells or that the nodes inherit interrupt-parent from the GIC. They resolve correctly on merge.

>

6. Test it BEFORE making it permanent (one-shot, reversible)

Copy the merged DTB, then boot it once from the GRUB shell/editor — nothing is saved, so a bad DTB just means a hard-reset back to normal.

bash

sudo cp ~/merged.dtb /boot/rkvdec-merged.dtb

At the GRUB command shell (grub&gt;), or in the entry editor (e), boot manually:

search --no-floppy --fs-uuid --set=root &lt;YOUR-ROOTFS-UUID&gt;
insmod fdt
devicetree /boot/rkvdec-merged.dtb
linux /boot/vmlinuz-7.0.0-15-generic root=UUID=&lt;YOUR-ROOTFS-UUID&gt; ro
initrd /boot/initrd.img-7.0.0-15-generic
boot

After boot, verify:

bash

sudo dmesg | grep -iE "rkvdec|rk3588-vdec|iommu@fdc38"
# expect: rkvdec fdc38100.video-codec: No sram node, RCB will be stored in RAM

v4l2-ctl --list-devices | grep -A2 rkvdec
# expect: rkvdec (platform:rkvdec): /dev/videoN  /dev/mediaM

v4l2-ctl -d /dev/video2 --list-formats-out
# expect: 'S265' (HEVC Parsed Slice Data) and 'S264' (H.264 Parsed Slice Data)

S265/S264 are stateless (parsed-slice) formats — the decoder is driven via the V4L2 stateless / request API.

7. Make it permanent (survives kernel updates)

A failed devicetree line is safe: GRUB just falls back to the firmware DTB and the system boots normally (without rkvdec). It cannot brick the board.

bash

sudo tee /etc/grub.d/09_rkvdec_dtb &gt; /dev/null &lt;&lt; 'EOF'
#!/bin/sh
echo "insmod fdt"
echo "devicetree /boot/rkvdec-merged.dtb"
EOF
sudo chmod +x /etc/grub.d/09_rkvdec_dtb
sudo update-grub
sudo grep -n "rkvdec-merged" /boot/grub/grub.cfg   # confirm the line landed

Disable temporarily: sudo chmod -x /etc/grub.d/09_rkvdec_dtb &amp;&amp; sudo update-grub Remove permanently: sudo rm /etc/grub.d/09_rkvdec_dtb &amp;&amp; sudo update-grub

8. Proving hardware decode (GStreamer, no ffmpeg rebuild needed)

GStreamer 1.28+ has V4L2 stateless decoder elements (v4l2slh265dec, v4l2slh264dec) that drive rkvdec directly.

bash

# make an 8-bit 4K HEVC test clip (software encode, one-off; slow is expected)
ffmpeg -i some_4k_source.webm -t 10 -an -c:v libx265 -crf 23 \
       -pix_fmt yuv420p -vf scale=3840:2160 ~/test_hevc.mp4

# decode through rkvdec to fakesink; watch CPU stay low
gst-launch-1.0 filesrc location=$HOME/test_hevc.mp4 ! qtdemux ! h265parse ! \
  v4l2slh265dec ! fakesink

# actual on-screen playback with HW decode forced
GST_PLUGIN_FEATURE_RANK=v4l2slh265dec:MAX gst-play-1.0 $HOME/test_hevc.mp4

Result on this setup: a 10 s 4K (3840×2160) HEVC clip decoded in ~3.1 s (~190 fps, far above real-time) at only ~10–15 % CPU — and that CPU is just userspace overhead (demux/parse/buffer copy); the decode itself runs on the VPU. For comparison, swapping v4l2slh265dec for the software avdec_h265 spikes CPU sharply. Any GStreamer-based player (Totem, etc.) gets HW HEVC decode the same way.

8.1 Bonus: 4K AV1 (the YouTube codec) on hardware — including in a browser

rkvdec covers H.264/HEVC. YouTube 4K is AV1, which is handled by a separate block: the AV1 VPU (rk3588-av1-vpu-dec, /dev/video4), present on this stack independently of the rkvdec overlay. GStreamer 1.28 ships a stateless AV1 element (v4l2slav1dec), so 4K AV1 decodes on hardware with no extra build:

bash

gst-inspect-1.0 | grep v4l2slav1dec          # V4L2 Stateless AV1 Video Decoder
v4l2-ctl -d /dev/video4 --list-formats-out   # 'AV1F' (AV1 Frame, compressed)

# decode a 4K 10-bit AV1 clip — use parsebin (it auto-detects the container)
gst-launch-1.0 -v filesrc location=clip.webm ! parsebin ! v4l2slav1dec ! fakesink
# decoder src caps: video/x-raw, format=NV12_10LE40_4L4 (10-bit), colorimetry=bt2100-pq -&gt; full HDR path

YouTube 4K HDR in a browser, hardware-decoded

WebKitGTK browsers (GNOME Web / Epiphany) use GStreamer for media, so bumping the AV1 decoder's rank lets YouTube AV1 run on the VPU:

bash

sudo apt install -y epiphany-browser
GST_PLUGIN_FEATURE_RANK=v4l2slav1dec:MAX epiphany

Confirmed result: YouTube playing av01 10-bit, smpte2084 (PQ) / bt2020 HDR, 3840×2160@60, 0 dropped frames of 11271, with CPU only at compositing-overhead levels — the decode runs on the VPU, not the cores. (Software 4K60 10-bit AV1 on these cores would peg the CPU and drop frames.) Prove the VPU is in use while the video plays:

bash

sudo fuser /dev/video4   # prints the browser's GStreamer PID holding the AV1 decoder

Caveats: YouTube may serve VP9 (no HW path here) instead of AV1 depending on what the browser advertises — if CPU pegs and frames drop, it fell back to VP9/software. Firefox does not use this path (it uses its own bundled ffmpeg/VA-API, which has no driver for the VPU here).

9. What works / what does not (be honest)

Works

  • 4K H.264 and HEVC (8-bit) hardware decode via rkvdec (/dev/video2).
  • 4K AV1, 10-bit, HDR hardware decode via the AV1 VPU (/dev/video4) + GStreamer v4l2slav1dec.
  • Any GStreamer-based player (Totem, …) and WebKitGTK browsers (Epiphany) → YouTube 4K60 AV1 HDR on hardware.
  • Display stays on the 4K60 firmware framebuffer; GPU compositing (Panfrost) untouched.

Does NOT (yet) / caveats

  • No VP9 on the mainline rockchip-vdec driver (only S264/S265 exposed). The hardware can do VP9, but it isn't surfaced here — and YouTube falls back to VP9 (software) unless it serves AV1.
  • Firefox / Chromium don't use the GStreamer path. Firefox needs a VA-API → V4L2-stateless bridge (libva-v4l2-request); Chromium needs a patched build (ChromeOS V4L2 path) + udev rules. Use Epiphany for the working HW browser path.
  • mpv needs an ffmpeg built with v4l2request.
  • Desktop tearing on motion (moving windows) is inherent to the firmware framebuffer (simpledrm has no hardware vsync/vblank). It is not caused by this overlay. The only real fix is native KMS (vop2), which on this stack drops you to ~4K30 RGB. Static content (reading) is unaffected.

10. Summary

A ~35-line device-tree overlay, merged onto the firmware DTB and loaded via GRUB, enables the mainline RK3588 4K H.264/HEVC hardware decoder on EDK2-booted Ubuntu — without a kernel rebuild and without disturbing the 4K60 firmware-framebuffer display. Separately, the AV1 VPU (/dev/video4, already present) plus GStreamer's v4l2slav1dec gives 4K AV1 10-bit HDR hardware decode, and a WebKitGTK browser (Epiphany) leverages it for YouTube 4K60 AV1 HDR with 0 dropped frames — the full original goal, achieved entirely on mainline. The only remaining gaps are Firefox/Chromium (their own userspace decode plumbing) and the inherent firmware-framebuffer tearing (a display, not decode, limitation).Enabling the mainline rkvdec (4K H.264/HEVC) hardware decoder on Orange Pi 5 Plus via a Device-Tree overlay
Goal: Wake up the RK3588's rkvdec (VDPU381) 4K H.264 / HEVC hardware decoder on a pure mainline kernel, while booting from EDK2 UEFI and keeping the 4K60 firmware framebuffer display untouched.
This is done with a small device-tree overlay — no kernel rebuild, no BSP/vendor kernel.

  1. Environment
    Item Value
    Board Orange Pi 5 Plus (RK3588, Mali-G610 MC4, 16 GB)
    Firmware / boot EDK2 UEFI → GRUB → kernel
    OS Ubuntu 26.04 (resolute), ARM64
    Kernel 7.0.0-15-generic
    GPU stack Mesa 26.2.0-devel (Panfrost + PanVK)
    Display EDK2 firmware framebuffer via simpledrm, mode forced to 4K60 in EDK2 setup
    Decoder driver rockchip-vdec (CONFIG_VIDEO_ROCKCHIP_VDEC=m)

The native Rockchip KMS (vop2/hdmi) stack does not load here, because the EDK2-provided device tree has no display nodes. Display is the firmware framebuffer (simpledrm). That is intentional — it gives a solid 4K60 desktop. See Limitations below.

  1. The problem
    The rockchip-vdec driver is present and already supports RK3588:
    $ modinfo rockchip-vdec | grep alias
    alias: of:N*T*Crockchip,rk3588-vdec
    alias: of:N*T*Crockchip,rk3576-vdec
    alias: of:N*T*Crockchip,rk3399-vdec
    ...
    But the EDK2 device tree contains no decoder node for it. A Linux driver binds to hardware by matching a compatible string in the driver against a compatible string in a DT node. The driver exists, but there is no matching node — so nothing binds.
    The EDK2 DT only exposes these video-codec nodes: hantro g1 (fdb50000), the vepu121 encoder + cores (fdba0000/fdba4000/fdba8000/fdbac000), and the AV1 decoder (fdc70000). It has the qos_rkvdec0/qos_rkvdec1 labels (the SoC .dtsi knows rkvdec exists) but neither the decoder node nor its IOMMU.
    So the overlay must add two nodes: the decoder (video-codec@fdc38000) and its IOMMU (iommu@fdc38700).

  2. Where the values come from (do not guess these)
    The Ubuntu kernel package already ships the full upstream board DTB, which contains a correct rkvdec0 node for this exact kernel:
    /usr/lib/firmware/7.0.0-15-generic/device-tree/rockchip/rk3588-orangepi-5-plus.dtb
    Every register address, clock/reset index, interrupt and power-domain in the overlay below was extracted from that DTB (decompiled with dtc). The overlay references existing labels in the EDK2 DT (&cru, &power) so fdtoverlay can resolve them at merge time.

Important: clock/reset indices (0x181, 0x143, …) and the power-domain index (0x0e) are specific to the RK3588 CRU as defined for this kernel's DTB. On a different kernel/DTB, re-extract them from your own shipped rk3588-orangepi-5-plus.dtb rather than copying these verbatim.

  1. The overlay (rkvdec.dts)
    dts
    /dts-v1/;
    /plugin/;

/ {
fragment@0 {
target-path = "/";
__overlay__ {

rkvdec0_mmu: iommu@fdc38700 {
compatible = "rockchip,rk3588-iommu", "rockchip,rk3568-iommu";
reg = <0x0 0xfdc38700 0x0 0x40>, <0x0 0xfdc38740 0x0 0x40>;
interrupts = <0x0 0x60 0x4 0x0>;
clocks = <&cru 0x181>, <&cru 0x180>;
clock-names = "aclk", "iface";
power-domains = <&power 0x0e>;
#iommu-cells = <0x0>;
};

rkvdec0: video-codec@fdc38000 {
compatible = "rockchip,rk3588-vdec";
reg = <0x0 0xfdc38100 0x0 0x500>,
<0x0 0xfdc38000 0x0 0x100>,
<0x0 0xfdc38600 0x0 0x100>;
reg-names = "function", "link", "cache";
interrupts = <0x0 0x5f 0x4 0x0>;
clocks = <&cru 0x181>, <&cru 0x180>, <&cru 0x182>, <&cru 0x184>, <&cru 0x183>;
clock-names = "axi", "ahb", "cabac", "core", "hevc_cabac";
assigned-clocks = <&cru 0x181>, <&cru 0x184>, <&cru 0x182>, <&cru 0x183>;
assigned-clock-rates = <0x2faf0800 0x23c34600 0x23c34600 0x3b9aca00>;
iommus = <&rkvdec0_mmu>;
power-domains = <&power 0x0e>;
resets = <&cru 0x143>, <&cru 0x142>, <&cru 0x146>, <&cru 0x148>, <&cru 0x147>;
reset-names = "axi", "ahb", "cabac", "core", "hevc_cabac";
};
};
};
};
What each part does
iommu@fdc38700 — the MMU rkvdec uses to reach memory. Defined first and labelled rkvdec0_mmu so the decoder can reference it.
reg (3 windows) — decoder register banks: function / link / cache.
clocks / resets — referenced through &cru + indices. &cru resolves to your clock-controller at merge time.
assigned-clock-rates — 0x2faf0800=800 MHz (axi), 0x23c34600=600 MHz (core/cabac), 0x3b9aca00=1 GHz (hevc_cabac).
power-domains = <&power 0x0e> — RKVDEC0 power domain (index 14).
iommus = <&rkvdec0_mmu> — binds the decoder to the MMU above.
sram is deliberately omitted — it's an optional performance buffer (row-cache in on-chip SRAM). Without it the driver stores the RCB in RAM and prints No sram node, RCB will be stored in RAM (benign). Omitting it avoids one more phandle dependency.

Only rkvdec0 (core 0) is added. The mainline driver currently drives one core; rkvdec1 (video-codec@fdc40000 / iommu@fdc40700, power-domain 0x0f) can be added later the same way.

  1. Build, merge, verify (no boot risk yet)
    bash
    sudo apt install -y device-tree-compiler

# 1) compile the overlay (-@ generates __fixups__ so fdtoverlay can resolve &cru/&power)
dtc -@ -I dts -O dtb -o ~/rkvdec.dtbo ~/rkvdec.dts

# 2) use the RAW firmware DTB as the merge base (byte-faithful to what EDK2 provides)
sudo cp /sys/firmware/fdt ~/edk2-raw.dtb
sudo chown $USER:$USER ~/edk2-raw.dtb

# 3) merge overlay into the base
fdtoverlay -i ~/edk2-raw.dtb -o ~/merged.dtb ~/rkvdec.dtbo
echo "fdtoverlay exit: $?" # must be 0

# 4) confirm the node is present in the merged DTB
dtc -I dtb -O dts ~/merged.dtb 2>/dev/null | grep -A14 "video-codec@fdc38000"
Compile-time dtc warnings about reg_format / missing interrupt-parent are benign overlay lint — at compile time dtc doesn't know the root's #address-cells/#size-cells or that the nodes inherit interrupt-parent from the GIC. They resolve correctly on merge.

Using /sys/firmware/fdt (the raw blob the kernel actually received) is preferable to dtc -I fs -O dtb /proc/device-tree, which reconstructs the tree and is not always byte-identical.

  1. Test it BEFORE making it permanent (one-shot, reversible)
    Copy the merged DTB, then boot it once from the GRUB shell/editor — nothing is saved, so a bad DTB just means a hard-reset back to normal.
    bash
    sudo cp ~/merged.dtb /boot/rkvdec-merged.dtb
    At the GRUB command shell (grub>), or in the entry editor (e), boot manually:
    search --no-floppy --fs-uuid --set=root <YOUR-ROOTFS-UUID>
    insmod fdt
    devicetree /boot/rkvdec-merged.dtb
    linux /boot/vmlinuz-7.0.0-15-generic root=UUID=<YOUR-ROOTFS-UUID> ro
    initrd /boot/initrd.img-7.0.0-15-generic
    boot
    After boot, verify:
    bash
    sudo dmesg | grep -iE "rkvdec|rk3588-vdec|iommu@fdc38"
    # expect: rkvdec fdc38100.video-codec: No sram node, RCB will be stored in RAM

v4l2-ctl --list-devices | grep -A2 rkvdec
# expect: rkvdec (platform:rkvdec): /dev/videoN /dev/mediaM

v4l2-ctl -d /dev/video2 --list-formats-out
# expect: 'S265' (HEVC Parsed Slice Data) and 'S264' (H.264 Parsed Slice Data)
S265/S264 are stateless (parsed-slice) formats — the decoder is driven via the V4L2 stateless / request API.

  1. Make it permanent (survives kernel updates)
    A failed devicetree line is safe: GRUB just falls back to the firmware DTB and the system boots normally (without rkvdec). It cannot brick the board.
    bash
    sudo tee /etc/grub.d/09_rkvdec_dtb > /dev/null << 'EOF'
    #!/bin/sh
    echo "insmod fdt"
    echo "devicetree /boot/rkvdec-merged.dtb"
    EOF
    sudo chmod +x /etc/grub.d/09_rkvdec_dtb
    sudo update-grub
    sudo grep -n "rkvdec-merged" /boot/grub/grub.cfg # confirm the line landed
    Disable temporarily: sudo chmod -x /etc/grub.d/09_rkvdec_dtb && sudo update-grub Remove permanently: sudo rm /etc/grub.d/09_rkvdec_dtb && sudo update-grub

  2. Proving hardware decode (GStreamer, no ffmpeg rebuild needed)
    GStreamer 1.28+ has V4L2 stateless decoder elements (v4l2slh265dec, v4l2slh264dec) that drive rkvdec directly.
    bash
    # make an 8-bit 4K HEVC test clip (software encode, one-off; slow is expected)
    ffmpeg -i some_4k_source.webm -t 10 -an -c:v libx265 -crf 23 \
    -pix_fmt yuv420p -vf scale=3840:2160 ~/test_hevc.mp4

# decode through rkvdec to fakesink; watch CPU stay low
gst-launch-1.0 filesrc location=$HOME/test_hevc.mp4 ! qtdemux ! h265parse ! \
v4l2slh265dec ! fakesink

# actual on-screen playback with HW decode forced
GST_PLUGIN_FEATURE_RANK=v4l2slh265dec:MAX gst-play-1.0 $HOME/test_hevc.mp4
Result on this setup: a 10 s 4K (3840×2160) HEVC clip decoded in ~3.1 s (~190 fps, far above real-time) at only ~10–15 % CPU — and that CPU is just userspace overhead (demux/parse/buffer copy); the decode itself runs on the VPU. For comparison, swapping v4l2slh265dec for the software avdec_h265 spikes CPU sharply. Any GStreamer-based player (Totem, etc.) gets HW HEVC decode the same way.

8.1 Bonus: 4K AV1 (the YouTube codec) on hardware — including in a browser
rkvdec covers H.264/HEVC. YouTube 4K is AV1, which is handled by a separate block: the AV1 VPU (rk3588-av1-vpu-dec, /dev/video4), present on this stack independently of the rkvdec overlay. GStreamer 1.28 ships a stateless AV1 element (v4l2slav1dec), so 4K AV1 decodes on hardware with no extra build:
bash
gst-inspect-1.0 | grep v4l2slav1dec # V4L2 Stateless AV1 Video Decoder
v4l2-ctl -d /dev/video4 --list-formats-out # 'AV1F' (AV1 Frame, compressed)

# decode a 4K 10-bit AV1 clip — use parsebin (it auto-detects the container)
gst-launch-1.0 -v filesrc location=clip.webm ! parsebin ! v4l2slav1dec ! fakesink
# decoder src caps: video/x-raw, format=NV12_10LE40_4L4 (10-bit), colorimetry=bt2100-pq -> full HDR path
YouTube 4K HDR in a browser, hardware-decoded
WebKitGTK browsers (GNOME Web / Epiphany) use GStreamer for media, so bumping the AV1 decoder's rank lets YouTube AV1 run on the VPU:
bash
sudo apt install -y epiphany-browser
GST_PLUGIN_FEATURE_RANK=v4l2slav1dec:MAX epiphany
Confirmed result: YouTube playing av01 10-bit, smpte2084 (PQ) / bt2020 HDR, 3840×2160@60, 0 dropped frames of 11271, with CPU only at compositing-overhead levels — the decode runs on the VPU, not the cores. (Software 4K60 10-bit AV1 on these cores would peg the CPU and drop frames.) Prove the VPU is in use while the video plays:
bash
sudo fuser /dev/video4 # prints the browser's GStreamer PID holding the AV1 decoder
Caveats: YouTube may serve VP9 (no HW path here) instead of AV1 depending on what the browser advertises — if CPU pegs and frames drop, it fell back to VP9/software. Firefox does not use this path (it uses its own bundled ffmpeg/VA-API, which has no driver for the VPU here).

  1. What works / what does not (be honest)
    Works
    4K H.264 and HEVC (8-bit) hardware decode via rkvdec (/dev/video2).
    4K AV1, 10-bit, HDR hardware decode via the AV1 VPU (/dev/video4) + GStreamer v4l2slav1dec.
    Any GStreamer-based player (Totem, …) and WebKitGTK browsers (Epiphany) → YouTube 4K60 AV1 HDR on hardware.
    Display stays on the 4K60 firmware framebuffer; GPU compositing (Panfrost) untouched.
    Does NOT (yet) / caveats
    No VP9 on the mainline rockchip-vdec driver (only S264/S265 exposed). The hardware can do VP9, but it isn't surfaced here — and YouTube falls back to VP9 (software) unless it serves AV1.
    Firefox / Chromium don't use the GStreamer path. Firefox needs a VA-API → V4L2-stateless bridge (libva-v4l2-request); Chromium needs a patched build (ChromeOS V4L2 path) + udev rules. Use Epiphany for the working HW browser path.
    mpv needs an ffmpeg built with v4l2request.
    Desktop tearing on motion (moving windows) is inherent to the firmware framebuffer (simpledrm has no hardware vsync/vblank). It is not caused by this overlay. The only real fix is native KMS (vop2), which on this stack drops you to ~4K30 RGB. Static content (reading) is unaffected.

  2. Summary
    A ~35-line device-tree overlay, merged onto the firmware DTB and loaded via GRUB, enables the mainline RK3588 4K H.264/HEVC hardware decoder on EDK2-booted Ubuntu — without a kernel rebuild and without disturbing the 4K60 firmware-framebuffer display. Separately, the AV1 VPU (/dev/video4, already present) plus GStreamer's v4l2slav1dec gives 4K AV1 10-bit HDR hardware decode, and a WebKitGTK browser (Epiphany) leverages it for YouTube 4K60 AV1 HDR with 0 dropped frames — the full original goal, achieved entirely on mainline. The only remaining gaps are Firefox/Chromium (their own userspace decode plumbing) and the inherent firmware-framebuffer tearing (a display, not decode, limitation).

After 1h7m of continuous 4K60 AV1 10-bit HDR playback in Epiphany: 12 dropped frames of 218,788 (0.005%). fuser /dev/video4 confirms the AV1 VPU is in use — hardware decode on pure mainline.

All credits go to CLOUDE-CODE OPUS 4.8

reddit.com
u/That_Direction3907 — 1 month ago
▲ 30 r/kernel+3 crossposts

OrangePi 5 Plus (16GB v2.1) – Vulkan + OC + Mesa 26.2 results (glmark2 / vkmark)

Hey everyone,

Lately I’ve been tinkering a bit more with the OrangePi 5 Plus and managed to get some pretty interesting results, so I figured I’d share my setup and numbers – might be useful to someone.

Hardware:

  • OrangePi 5 Plus 16GB (v2.1)
  • Mali-G610 MC4 GPU

Software / stack:

  • EDK2 UEFI
  • Ubuntu 26.04 ARM, Fedora 45 ARM
  • Kernel 7.0
  • Mesa 26.2 (Panfrost + Zink)
  • Vulkan enabled

Settings:

  • GPU overclock: 1200 MHz (actual ~1188 MHz) @ 1.2V
  • CPU & GPU: performance mode (no throttling during tests)

Results:

  • glmark2-es2: ~3500
  • glmark2-es2-wayland: ~3500
  • glmark2: ~3000
  • glmark2-wayland: ~3500
  • vkmark: ~4300

Honestly, the Vulkan + Zink combo on this chip provides a pretty nice boost compared to older Mesa versions. Stability is fine so far with this OC profile, but I’m still testing longer sessions.

I also have screenshots with clearly visible:

  • test dates
  • versions (kernel, Mesa, etc.)
  • actual results

If needed, I can share a more detailed setup (kernel config, boot parameters, Mesa build options, etc.).

If anyone else has been tweaking the G610 on this board, I’d be interested to hear your experiences—especially regarding HW Aceleration for Chromium.

Cheers 🍻

https://preview.redd.it/g4mcc6ibhiwg1.jpg?width=2559&format=pjpg&auto=webp&s=511cbd8aa35668eda84a03a0a9744e238a8d754c

reddit.com
u/That_Direction3907 — 1 month ago