Linux-Rockchip Archive on lore.kernel.org
 help / color / mirror / Atom feed
* [RFC PATCH 0/3] media: rockchip: VEPU510 H.264 encoder for RK3576
@ 2026-07-22  7:34 Jiaxing Hu
  2026-07-22  7:34 ` [RFC PATCH 1/3] dt-bindings: media: add Rockchip RK3576 VEPU H.264 encoder Jiaxing Hu
                   ` (2 more replies)
  0 siblings, 3 replies; 5+ messages in thread
From: Jiaxing Hu @ 2026-07-22  7:34 UTC (permalink / raw)
  To: mchehab, heiko, robh, krzk+dt, conor+dt, nicolas.dufresne
  Cc: ezequiel, linux-media, devicetree, linux-rockchip,
	linux-arm-kernel, linux-kernel, Jiaxing Hu

This is an RFC for a from-scratch mainline V4L2 driver for the H.264
hardware video encoder (VEPU510) on the Rockchip RK3576.  It is a
stateful mem2mem encoder (NV12 in, H.264 Annex-B out) modelled on the
verisilicon/hantro driver, not on the downstream MPP-service model.

I'm posting it as an RFC because intra frames work but inter frames do
not, and I'd like a second pair of eyes on the inter-frame problem
before this is worth a real submission.

What works
----------

Intra-only (I-frame) encoding is confirmed on real hardware (Radxa
ROCK 4D).  With GOP size 1 the driver produces a valid H.264 stream the
reference decoder accepts.  This already exercises the full path: V4L2
m2m, the register programming, the software SPS/PPS prepend, and the
hardware slice output.

There is no upstream userspace involved (an encoder needs no request
API); I drive it with a small ioctl test program that feeds NV12 frames
and writes the CAPTURE buffers to a .h264 file.

The open problem: inter (P-frame) frames hang
---------------------------------------------

Every P-frame stalls the encoder's own hardware watchdog (INT_STA bit 8,
~20 ms after the kick) and produces essentially no bitstream.  I have
spent a lot of time narrowing this; it is sharply localized but I cannot
close it:

  - The P-frame register writes match a real register-write trace of the
    vendor stack encoding the same content, byte for byte (I traced the
    vendor kernel's writes and diffed against this driver's).

  - The reconstruction the previous frame writes is *valid*: dumping the
    recon buffer after the I-frame shows correct reconstructed pixels.

  - If the P-frame reads its own (older, settled) recon slot instead of
    the immediately-preceding frame's, it completes.  Reading the
    immediately-preceding frame's *fresh* reconstruction as the
    reference is what hangs.

  - It is not FBC: storing the reconstruction uncompressed
    (enc_pic.rec_fbc_dis = 1) still hangs.

  - The first frame of a session always works; the first P-frame hangs;
    after a couple of failures + core resets, later frames sometimes
    start completing.  It behaves like a warm-up / settling problem on
    the reference-read path, not a wrong register value.

So the encoder programs identically to the vendor and reads a valid
reference, but stalls fetching the previous frame's reconstruction as
the inter reference on the first inter frame of a session.  My best
guess is that the vendor does something between consecutive frame
submissions -- a completion/drain wait, a cache/coherency step, or a
per-frame re-arm -- that I am missing, but I have not found it.  If
anyone recognizes this on VEPU5xx, or knows what the reference-read path
needs between frames, a pointer would be very welcome.

The shape of this -- the first operation of a power session works, the
next stalls, and a reset/warm-up sometimes helps -- is the same one I
ran into bringing up this SoC's NPU (accel/rocket RKNN), where the
second/chained submit in a session would not fire [1].  I can't claim
they share a root cause, but if this is a known RK3576-wide submit /
re-arm quirk rather than an encoder-specific bug, that would be good to
know.

[1] https://lore.kernel.org/all/20260718031146.3368811-1-gahing@gahingwoo.com/

Notes for review
----------------

  - The PARAM/SQI register classes are programmed from mpp's constant
    "default tuning" tables (not derived per-frame), as noted in the
    code.  They are required: leaving them unwritten stalls even the
    I-frame.

  - Rate control is fixed-QP only for now (the bitrate control is
    advisory).

  - H.264 baseline/main, single slice, 4:2:0 only.

  - Tested at 176x144; other resolutions are not yet validated.

Signed-off-by: Jiaxing Hu <gahing@gahingwoo.com>

Jiaxing Hu (3):
  dt-bindings: media: add Rockchip RK3576 VEPU H.264 encoder
  media: rockchip: add VEPU510 H.264 encoder driver for RK3576
  arm64: dts: rockchip: rk3576: add VEPU H.264 encoder nodes

 .../bindings/media/rockchip,rk3576-vepu.yaml       |   94 ++
 arch/arm64/boot/dts/rockchip/rk3576.dtsi           |   50 +
 drivers/media/platform/rockchip/Kconfig            |    1 +
 drivers/media/platform/rockchip/Makefile           |    1 +
 drivers/media/platform/rockchip/rkvenc/Kconfig     |   14 +
 drivers/media/platform/rockchip/rkvenc/Makefile    |    6 +
 .../media/platform/rockchip/rkvenc/rkvenc-h264.c   | 1095 ++++++++++++++++++++
 .../media/platform/rockchip/rkvenc/rkvenc-regs.h   |  929 +++++++++++++++++
 drivers/media/platform/rockchip/rkvenc/rkvenc.c    |  892 ++++++++++++++++
 drivers/media/platform/rockchip/rkvenc/rkvenc.h    |  212 ++++
 10 files changed, 3294 insertions(+)
--
2.43.0


_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip

^ permalink raw reply	[flat|nested] 5+ messages in thread

* [RFC PATCH 1/3] dt-bindings: media: add Rockchip RK3576 VEPU H.264 encoder
  2026-07-22  7:34 [RFC PATCH 0/3] media: rockchip: VEPU510 H.264 encoder for RK3576 Jiaxing Hu
@ 2026-07-22  7:34 ` Jiaxing Hu
  2026-07-22  7:34 ` [RFC PATCH 2/3] media: rockchip: add VEPU510 H.264 encoder driver for RK3576 Jiaxing Hu
  2026-07-22  7:34 ` [RFC PATCH 3/3] arm64: dts: rockchip: rk3576: add VEPU H.264 encoder nodes Jiaxing Hu
  2 siblings, 0 replies; 5+ messages in thread
From: Jiaxing Hu @ 2026-07-22  7:34 UTC (permalink / raw)
  To: mchehab, heiko, robh, krzk+dt, conor+dt, nicolas.dufresne
  Cc: ezequiel, linux-media, devicetree, linux-rockchip,
	linux-arm-kernel, linux-kernel, Jiaxing Hu

Add the binding for the VEPU510 H.264 hardware video encoder found on the
Rockchip RK3576.  Each SoC has two independent encoder cores, each behind
its own IOMMU.

Signed-off-by: Jiaxing Hu <gahing@gahingwoo.com>
---
 .../bindings/media/rockchip,rk3576-vepu.yaml       | 94 ++++++++++++++++++++++
 1 file changed, 94 insertions(+)

diff --git a/Documentation/devicetree/bindings/media/rockchip,rk3576-vepu.yaml b/Documentation/devicetree/bindings/media/rockchip,rk3576-vepu.yaml
new file mode 100644
index 000000000..942118cb3
--- /dev/null
+++ b/Documentation/devicetree/bindings/media/rockchip,rk3576-vepu.yaml
@@ -0,0 +1,94 @@
+# SPDX-License-Identifier: (GPL-2.0 OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/media/rockchip,rk3576-vepu.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Rockchip VEPU510 Video Encoder (RK3576)
+
+maintainers:
+  - Jiaxing Hu <gahing@gahingwoo.com>
+
+description: |-
+  RK3576 has two identical VEPU510 hardware H.264/H.265 video encoder
+  cores. Each core is a standalone V4L2 mem2mem device in this binding;
+  there is no shared hardware descriptor/link-list engine tying the two
+  cores together (unlike the RK3576 video decoder's CCU), so each core
+  node is independent and self-contained.
+
+properties:
+  compatible:
+    const: rockchip,rk3576-vepu
+
+  reg:
+    maxItems: 1
+
+  interrupts:
+    maxItems: 1
+
+  clocks:
+    items:
+      - description: The Video Encoder AXI interface clock
+      - description: The Video Encoder AHB interface clock
+      - description: The Video Encoder core clock
+
+  clock-names:
+    items:
+      - const: aclk_vcodec
+      - const: hclk_vcodec
+      - const: clk_core
+
+  assigned-clocks: true
+
+  assigned-clock-rates: true
+
+  resets:
+    items:
+      - description: The Video Encoder AXI interface reset
+      - description: The Video Encoder AHB interface reset
+      - description: The Video Encoder core reset
+
+  reset-names:
+    items:
+      - const: video_a
+      - const: video_h
+      - const: video_core
+
+  power-domains:
+    maxItems: 1
+
+  iommus:
+    maxItems: 1
+
+required:
+  - compatible
+  - reg
+  - interrupts
+  - clocks
+  - clock-names
+  - power-domains
+  - iommus
+
+additionalProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/interrupt-controller/arm-gic.h>
+    #include <dt-bindings/clock/rockchip,rk3576-cru.h>
+    #include <dt-bindings/power/rockchip,rk3576-power.h>
+
+    vepu0: video-codec@27a00000 {
+        compatible = "rockchip,rk3576-vepu";
+        reg = <0x27a00000 0x6000>;
+        interrupts = <GIC_SPI 310 IRQ_TYPE_LEVEL_HIGH>;
+        clocks = <&cru ACLK_VEPU0>, <&cru HCLK_VEPU0>, <&cru CLK_VEPU0_CORE>;
+        clock-names = "aclk_vcodec", "hclk_vcodec", "clk_core";
+        assigned-clocks = <&cru ACLK_VEPU0>, <&cru CLK_VEPU0_CORE>;
+        assigned-clock-rates = <400000000>, <702000000>;
+        resets = <&cru SRST_A_VEPU0>, <&cru SRST_H_VEPU0>, <&cru SRST_VEPU0_CORE>;
+        reset-names = "video_a", "video_h", "video_core";
+        power-domains = <&power RK3576_PD_VEPU0>;
+        iommus = <&vepu0_mmu>;
+    };
+
+...
-- 
2.43.0


_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip

^ permalink raw reply related	[flat|nested] 5+ messages in thread

* [RFC PATCH 2/3] media: rockchip: add VEPU510 H.264 encoder driver for RK3576
  2026-07-22  7:34 [RFC PATCH 0/3] media: rockchip: VEPU510 H.264 encoder for RK3576 Jiaxing Hu
  2026-07-22  7:34 ` [RFC PATCH 1/3] dt-bindings: media: add Rockchip RK3576 VEPU H.264 encoder Jiaxing Hu
@ 2026-07-22  7:34 ` Jiaxing Hu
  2026-07-22 10:00   ` Heiko Stübner
  2026-07-22  7:34 ` [RFC PATCH 3/3] arm64: dts: rockchip: rk3576: add VEPU H.264 encoder nodes Jiaxing Hu
  2 siblings, 1 reply; 5+ messages in thread
From: Jiaxing Hu @ 2026-07-22  7:34 UTC (permalink / raw)
  To: mchehab, heiko, robh, krzk+dt, conor+dt, nicolas.dufresne
  Cc: ezequiel, linux-media, devicetree, linux-rockchip,
	linux-arm-kernel, linux-kernel, Jiaxing Hu

Add a from-scratch stateful V4L2 mem2mem driver for the Rockchip RK3576
VEPU510 H.264 hardware video encoder (raw NV12 in, H.264 Annex-B out),
modelled on the verisilicon/hantro device_run()/codec_ops split rather
than the downstream MPP-service/task-queue model.

The two encoder cores (rkvenc0/rkvenc1) are exposed as two independent
V4L2 M2M device nodes, each driving one physical core standalone; the
downstream vendor CCU cross-core load-balancing is not implemented.

Register field semantics were worked out by trial-and-error against
Rockchip's own open-source userspace codec library (rockchip-linux/mpp)
and cross-checked against a real register-write trace of the downstream
vendor stack running the same encode.

Intra (I-frame) encoding is confirmed working on real hardware (Radxa
ROCK 4D): it produces valid H.264 that the reference decoders accept.
Inter (P-frame) encoding still hits a hardware-watchdog stall in the
reference-read datapath -- this is the main open question for this RFC;
see the cover letter for the full symptom analysis.

Signed-off-by: Jiaxing Hu <gahing@gahingwoo.com>
---
 drivers/media/platform/rockchip/Kconfig            |    1 +
 drivers/media/platform/rockchip/Makefile           |    1 +
 drivers/media/platform/rockchip/rkvenc/Kconfig     |   14 +
 drivers/media/platform/rockchip/rkvenc/Makefile    |    6 +
 .../media/platform/rockchip/rkvenc/rkvenc-h264.c   | 1095 ++++++++++++++++++++
 .../media/platform/rockchip/rkvenc/rkvenc-regs.h   |  929 +++++++++++++++++
 drivers/media/platform/rockchip/rkvenc/rkvenc.c    |  892 ++++++++++++++++
 drivers/media/platform/rockchip/rkvenc/rkvenc.h    |  212 ++++
 8 files changed, 3150 insertions(+)

diff --git a/drivers/media/platform/rockchip/Kconfig b/drivers/media/platform/rockchip/Kconfig
index ba401d32f..e6afa6092 100644
--- a/drivers/media/platform/rockchip/Kconfig
+++ b/drivers/media/platform/rockchip/Kconfig
@@ -6,3 +6,4 @@ source "drivers/media/platform/rockchip/rga/Kconfig"
 source "drivers/media/platform/rockchip/rkcif/Kconfig"
 source "drivers/media/platform/rockchip/rkisp1/Kconfig"
 source "drivers/media/platform/rockchip/rkvdec/Kconfig"
+source "drivers/media/platform/rockchip/rkvenc/Kconfig"
diff --git a/drivers/media/platform/rockchip/Makefile b/drivers/media/platform/rockchip/Makefile
index 0e0b2cbbd..6a1d847a1 100644
--- a/drivers/media/platform/rockchip/Makefile
+++ b/drivers/media/platform/rockchip/Makefile
@@ -3,3 +3,4 @@ obj-y += rga/
 obj-y += rkcif/
 obj-y += rkisp1/
 obj-y += rkvdec/
+obj-y += rkvenc/
diff --git a/drivers/media/platform/rockchip/rkvenc/Kconfig b/drivers/media/platform/rockchip/rkvenc/Kconfig
new file mode 100644
index 000000000..cd2a9c2eb
--- /dev/null
+++ b/drivers/media/platform/rockchip/rkvenc/Kconfig
@@ -0,0 +1,14 @@
+# SPDX-License-Identifier: GPL-2.0
+config VIDEO_ROCKCHIP_VEPU510
+	tristate "Rockchip VEPU510 Video Encoder driver"
+	depends on ARCH_ROCKCHIP || COMPILE_TEST
+	depends on VIDEO_DEV
+	select VIDEOBUF2_DMA_CONTIG
+	select V4L2_MEM2MEM_DEV
+	help
+	  Support for the Rockchip VEPU510 hardware video encoder IP
+	  present on the RK3576 SoC, which accelerates H.264 video
+	  encoding.
+
+	  To compile this driver as a module, choose M here: the module
+	  will be called rockchip-vepu510.
diff --git a/drivers/media/platform/rockchip/rkvenc/Makefile b/drivers/media/platform/rockchip/rkvenc/Makefile
new file mode 100644
index 000000000..322bf10af
--- /dev/null
+++ b/drivers/media/platform/rockchip/rkvenc/Makefile
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: GPL-2.0
+obj-$(CONFIG_VIDEO_ROCKCHIP_VEPU510) += rockchip-vepu510.o
+
+rockchip-vepu510-y += \
+		      rkvenc.o \
+		      rkvenc-h264.o
diff --git a/drivers/media/platform/rockchip/rkvenc/rkvenc-h264.c b/drivers/media/platform/rockchip/rkvenc/rkvenc-h264.c
new file mode 100644
index 000000000..58b4fb65b
--- /dev/null
+++ b/drivers/media/platform/rockchip/rkvenc/rkvenc-h264.c
@@ -0,0 +1,1095 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Rockchip VEPU510 H.264 codec backend.
+ *
+ * Copyright (C) 2026 Jiaxing Hu <gahing@gahingwoo.com>
+ *
+ * VEPU510 emits complete Annex-B slice NAL units (nal_unit_header +
+ * slice_header + slice_data) directly from hardware — the extensive
+ * synt_sli0/synt_sli1/synt_sli2/synt_nal register set exists precisely
+ * because the slice_header() syntax is generated by the hardware from
+ * those fields (cross-checked against rockchip-linux/mpp's
+ * hal_h264e_vepu510.c, which programs the identical fields from its own
+ * software H264eSlice/H264ePps structs). What the hardware does *not*
+ * generate is the SPS/PPS (parameter sets are a per-stream, not
+ * per-frame, concept, and profile_idc/level_idc/VUI have no register
+ * representation at all) — mpp's own `h264e_vepu_stream_amend_*` stage
+ * confirms parameter sets are assembled in software around the hardware
+ * slice payload, which is exactly what rkvenc_h264_gen_sps_pps() below
+ * does, analogous to the upstream hantro JPEG encoder driver's software
+ * JFIF header prepend.
+ */
+
+#include <linux/delay.h>
+#include <linux/dma-mapping.h>
+#include <linux/log2.h>
+
+#include <media/v4l2-ctrls.h>
+#include <media/videobuf2-dma-contig.h>
+
+#include "rkvenc.h"
+#include "rkvenc-regs.h"
+
+/* SPS log2_max_frame_num_minus4 / log2_max_pic_order_cnt_lsb_minus4: both
+ * fixed at 4 (8-bit fields) so frame_num/poc_lsb wrap well past any sane
+ * GOP size while still fitting the 4-bit synt_sps.max_fnum/mpoc_lm4
+ * hardware fields (valid range 0-12 per spec, 0-15 per register width).
+ */
+#define RKVENC_H264_LOG2_MAX_FRAME_NUM		4
+#define RKVENC_H264_LOG2_MAX_POC_LSB		4
+
+/* PARAM (0x1700-0x19cc) and SQI (0x2000-0x20b8) class content: mpp's own
+ * *default-tuning-profile* constant tables (RDO lambda/cost tables and
+ * subjective-quality tuning respectively), selected by a tuning knob this
+ * driver doesn't expose rather than by resolution/QP/content -- so a
+ * fixed constant table is the correct v1 implementation, not a
+ * placeholder. Captured via wtrace from a real successful
+ * rockchip-linux/mpp `mpi_enc_test` encode and cross-checked against the
+ * declared class boundaries in mpp_rkvenc2.c's hw_info table (SQI's
+ * declared range extends to 0x212c, but a real encode never touches past
+ * 0x20b8, so this driver doesn't either). See the RC_ROI banner comment
+ * in rkvenc-regs.h for why these classes are not actually optional
+ * despite nothing in the userspace HAL ever documenting them as required.
+ */
+static const u32 rkvenc_par_class[] = {
+	0x00030001, 0x00080006, 0x00030001, 0x00080006, 0x00030001, 0x00080006,
+	0x00000fff, 0x10101010, 0x10101010, 0x16141a16, 0x13141512, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00020001, 0x002d0006,
+	0x00000000, 0x00000000, 0x00101010, 0x00121112, 0x00101010, 0x00000000,
+	0x00000103, 0x0500c010, 0x00000000, 0x04040404, 0x00000021, 0x00000000,
+	0x00401004, 0x00806040, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000009, 0x0000000b, 0x0000000e, 0x00000011,
+	0x00000016, 0x0000001b, 0x00000022, 0x0000002b, 0x00000036, 0x00000045,
+	0x00000056, 0x0000006d, 0x00000089, 0x000000ad, 0x000000da, 0x00000112,
+	0x00000159, 0x000001b3, 0x00000224, 0x000002b3, 0x00000366, 0x00000449,
+	0x00000566, 0x000006cd, 0x00000891, 0x00000acb, 0x00000d9a, 0x000013c1,
+	0x000018e4, 0x00001f5c, 0x00002783, 0x000031c8, 0x00003eb8, 0x00004f06,
+	0x00006390, 0x00008e14, 0x0000b302, 0x0000e18a, 0x00011c29, 0x00016605,
+	0x0001c313, 0x00027ae1, 0x00031fe6, 0x0003efcf, 0x0004f5c3, 0x0006e785,
+	0x0008b2ef, 0x000af5c3, 0x000f1e7a, 0x00130c7f, 0x00180000, 0x001e3cf4,
+};
+
+static const u32 rkvenc_sqi_class[] = {
+	0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00002f10,
+	0x00180000, 0x00400030, 0x00200010, 0x00600030, 0x00180030, 0x00300060,
+	0x03010301, 0x04010401, 0x03000300, 0x00000301, 0x09060906, 0x08060b06,
+	0x14061409, 0x00001007, 0x0006000a, 0x00003151, 0x00000000, 0x00000000,
+	0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00070000, 0x0019000f,
+	0x10101010, 0x00000010, 0x00000000, 0x00000000, 0x00000000, 0x00280014,
+	0x00000048, 0x10101010, 0x00000000, 0x00000000, 0x00000000, 0x00100008,
+	0x00000020, 0x10101010, 0x00000000, 0x04010401, 0x08081010,
+};
+
+static void rkvenc_write_table_relaxed(struct rkvenc_dev *dev, u32 base_off,
+					const u32 *words, size_t count)
+{
+	size_t i;
+
+	for (i = 0; i < count; i++)
+		rkvenc_write_relaxed(dev, base_off + 4 * i, words[i]);
+}
+
+/* ---- minimal RBSP bit writer for SPS/PPS ---- */
+
+struct rkvenc_bitwriter {
+	u8 *buf;
+	size_t size;
+	size_t byte_pos;
+	u8 bit_pos;
+};
+
+static void bw_init(struct rkvenc_bitwriter *bw, u8 *buf, size_t size)
+{
+	bw->buf = buf;
+	bw->size = size;
+	bw->byte_pos = 0;
+	bw->bit_pos = 0;
+	memset(buf, 0, size);
+}
+
+static void bw_put_bit(struct rkvenc_bitwriter *bw, u32 bit)
+{
+	if (bw->byte_pos >= bw->size)
+		return;
+
+	if (bit)
+		bw->buf[bw->byte_pos] |= BIT(7 - bw->bit_pos);
+
+	if (++bw->bit_pos == 8) {
+		bw->bit_pos = 0;
+		bw->byte_pos++;
+	}
+}
+
+static void bw_put_bits(struct rkvenc_bitwriter *bw, u32 val, unsigned int n)
+{
+	int i;
+
+	for (i = n - 1; i >= 0; i--)
+		bw_put_bit(bw, (val >> i) & 1);
+}
+
+/* Exp-Golomb ue(v) */
+static void bw_put_ue(struct rkvenc_bitwriter *bw, u32 val)
+{
+	u32 code = val + 1;
+	unsigned int n = code ? fls(code) - 1 : 0;
+
+	bw_put_bits(bw, 0, n);
+	bw_put_bits(bw, code, n + 1);
+}
+
+/* Exp-Golomb se(v) */
+static void bw_put_se(struct rkvenc_bitwriter *bw, int val)
+{
+	u32 code = val <= 0 ? (u32)(-val) * 2 : (u32)val * 2 - 1;
+
+	bw_put_ue(bw, code);
+}
+
+static void bw_rbsp_trailing_bits(struct rkvenc_bitwriter *bw)
+{
+	bw_put_bit(bw, 1);
+	while (bw->bit_pos != 0)
+		bw_put_bit(bw, 0);
+}
+
+/* Emit one Annex-B NAL unit (start code + header + escaped RBSP) into out,
+ * returns bytes written.
+ */
+static size_t rkvenc_write_nal(u8 *out, size_t out_size, u8 nal_ref_idc,
+			       u8 nal_unit_type, const u8 *rbsp, size_t rbsp_len)
+{
+	static const u8 start_code[] = { 0x00, 0x00, 0x00, 0x01 };
+	size_t pos = 0;
+	unsigned int zero_run = 0;
+	size_t i;
+
+	if (out_size < sizeof(start_code) + 1)
+		return 0;
+
+	memcpy(out, start_code, sizeof(start_code));
+	pos += sizeof(start_code);
+	out[pos++] = (nal_ref_idc << 5) | nal_unit_type;
+
+	for (i = 0; i < rbsp_len && pos < out_size; i++) {
+		if (zero_run >= 2 && rbsp[i] <= 3) {
+			out[pos++] = 0x03;
+			zero_run = 0;
+			if (pos >= out_size)
+				break;
+		}
+		out[pos++] = rbsp[i];
+		zero_run = rbsp[i] == 0 ? zero_run + 1 : 0;
+	}
+
+	return pos;
+}
+
+static const u8 rkvenc_h264_level_idc[] = {
+	[V4L2_MPEG_VIDEO_H264_LEVEL_1_0] = 10,
+	[V4L2_MPEG_VIDEO_H264_LEVEL_1B]  = 11,
+	[V4L2_MPEG_VIDEO_H264_LEVEL_1_1] = 11,
+	[V4L2_MPEG_VIDEO_H264_LEVEL_1_2] = 12,
+	[V4L2_MPEG_VIDEO_H264_LEVEL_1_3] = 13,
+	[V4L2_MPEG_VIDEO_H264_LEVEL_2_0] = 20,
+	[V4L2_MPEG_VIDEO_H264_LEVEL_2_1] = 21,
+	[V4L2_MPEG_VIDEO_H264_LEVEL_2_2] = 22,
+	[V4L2_MPEG_VIDEO_H264_LEVEL_3_0] = 30,
+	[V4L2_MPEG_VIDEO_H264_LEVEL_3_1] = 31,
+	[V4L2_MPEG_VIDEO_H264_LEVEL_3_2] = 32,
+	[V4L2_MPEG_VIDEO_H264_LEVEL_4_0] = 40,
+	[V4L2_MPEG_VIDEO_H264_LEVEL_4_1] = 41,
+	[V4L2_MPEG_VIDEO_H264_LEVEL_4_2] = 42,
+};
+
+static void rkvenc_h264_gen_sps_pps(struct rkvenc_ctx *ctx)
+{
+	struct rkvenc_h264_ctx *h264 = &ctx->h264;
+	struct rkvenc_bitwriter bw;
+	u8 rbsp[32];
+	size_t rbsp_len, pos = 0;
+	unsigned int width = ctx->coded_fmt.width;
+	unsigned int height = ctx->coded_fmt.height;
+	unsigned int mb_w = ALIGN(width, 16) / 16;
+	unsigned int mb_h = ALIGN(height, 16) / 16;
+	unsigned int crop_w = mb_w * 16 - width;
+	unsigned int crop_h = mb_h * 16 - height;
+	bool crop = crop_w || crop_h;
+	bool is_main = h264->profile->val == V4L2_MPEG_VIDEO_H264_PROFILE_MAIN;
+	u8 profile_idc = is_main ? 77 : 66;
+	u8 level_idc = rkvenc_h264_level_idc[h264->level->val];
+	bool cabac = h264->entropy_mode->val == V4L2_MPEG_VIDEO_H264_ENTROPY_MODE_CABAC;
+
+	/* SPS (nal_ref_idc=1, nal_unit_type=7) */
+	bw_init(&bw, rbsp, sizeof(rbsp));
+	bw_put_bits(&bw, profile_idc, 8);
+	bw_put_bits(&bw, 0, 8); /* constraint_set0..5_flag + reserved_zero_2bits */
+	bw_put_bits(&bw, level_idc, 8);
+	bw_put_ue(&bw, 0); /* seq_parameter_set_id */
+	bw_put_ue(&bw, RKVENC_H264_LOG2_MAX_FRAME_NUM);
+	bw_put_ue(&bw, 0); /* pic_order_cnt_type */
+	bw_put_ue(&bw, RKVENC_H264_LOG2_MAX_POC_LSB);
+	bw_put_ue(&bw, 1); /* max_num_ref_frames */
+	bw_put_bit(&bw, 0); /* gaps_in_frame_num_value_allowed_flag */
+	bw_put_ue(&bw, mb_w - 1);
+	bw_put_ue(&bw, mb_h - 1);
+	bw_put_bit(&bw, 1); /* frame_mbs_only_flag */
+	bw_put_bit(&bw, 1); /* direct_8x8_inference_flag */
+	bw_put_bit(&bw, crop);
+	if (crop) {
+		bw_put_ue(&bw, 0);
+		bw_put_ue(&bw, crop_w / 2);
+		bw_put_ue(&bw, 0);
+		bw_put_ue(&bw, crop_h / 2);
+	}
+	bw_put_bit(&bw, 0); /* vui_parameters_present_flag */
+	bw_rbsp_trailing_bits(&bw);
+	rbsp_len = bw.byte_pos;
+	pos += rkvenc_write_nal(h264->sps_pps_nal + pos,
+				sizeof(h264->sps_pps_nal) - pos, 1, 7, rbsp, rbsp_len);
+
+	/* PPS (nal_ref_idc=1, nal_unit_type=8) */
+	bw_init(&bw, rbsp, sizeof(rbsp));
+	bw_put_ue(&bw, 0); /* pic_parameter_set_id */
+	bw_put_ue(&bw, 0); /* seq_parameter_set_id */
+	bw_put_bit(&bw, cabac);
+	bw_put_bit(&bw, 0); /* bottom_field_pic_order_in_frame_present_flag */
+	bw_put_ue(&bw, 0); /* num_slice_groups_minus1 */
+	bw_put_ue(&bw, 0); /* num_ref_idx_l0_default_active_minus1 */
+	bw_put_ue(&bw, 0); /* num_ref_idx_l1_default_active_minus1 */
+	bw_put_bit(&bw, 0); /* weighted_pred_flag */
+	bw_put_bits(&bw, 0, 2); /* weighted_bipred_idc */
+	bw_put_se(&bw, (int)h264->i_qp->val - 26); /* pic_init_qp_minus26 */
+	bw_put_se(&bw, 0); /* pic_init_qs_minus26 */
+	bw_put_se(&bw, 0); /* chroma_qp_index_offset */
+	bw_put_bit(&bw, 1); /* deblocking_filter_control_present_flag */
+	bw_put_bit(&bw, 0); /* constrained_intra_pred_flag */
+	bw_put_bit(&bw, 0); /* redundant_pic_cnt_present_flag */
+	bw_rbsp_trailing_bits(&bw);
+	rbsp_len = bw.byte_pos;
+	pos += rkvenc_write_nal(h264->sps_pps_nal + pos,
+				sizeof(h264->sps_pps_nal) - pos, 1, 8, rbsp, rbsp_len);
+
+	h264->sps_pps_len = pos;
+}
+
+/* ---- controls ---- */
+
+static int rkvenc_h264_s_ctrl(struct v4l2_ctrl *ctrl)
+{
+	return 0;
+}
+
+static const struct v4l2_ctrl_ops rkvenc_h264_ctrl_ops = {
+	.s_ctrl = rkvenc_h264_s_ctrl,
+};
+
+static int rkvenc_h264_init_ctrls(struct rkvenc_ctx *ctx)
+{
+	struct v4l2_ctrl_handler *hdl = &ctx->ctrl_handler;
+	struct rkvenc_h264_ctx *h264 = &ctx->h264;
+
+	/* Advisory only in v1: the hardware rc_en=0 fixed-QP
+	 * path (rc_qp) is what's actually wired up, not this bitrate value.
+	 */
+	h264->bitrate = v4l2_ctrl_new_std(hdl, &rkvenc_h264_ctrl_ops,
+					  V4L2_CID_MPEG_VIDEO_BITRATE,
+					  64000, 100000000, 1, 2000000);
+
+	h264->profile = v4l2_ctrl_new_std_menu(hdl, &rkvenc_h264_ctrl_ops,
+			V4L2_CID_MPEG_VIDEO_H264_PROFILE,
+			V4L2_MPEG_VIDEO_H264_PROFILE_MAIN,
+			BIT(V4L2_MPEG_VIDEO_H264_PROFILE_CONSTRAINED_BASELINE),
+			V4L2_MPEG_VIDEO_H264_PROFILE_BASELINE);
+
+	h264->level = v4l2_ctrl_new_std_menu(hdl, &rkvenc_h264_ctrl_ops,
+			V4L2_CID_MPEG_VIDEO_H264_LEVEL,
+			V4L2_MPEG_VIDEO_H264_LEVEL_4_2,
+			BIT(V4L2_MPEG_VIDEO_H264_LEVEL_1B),
+			V4L2_MPEG_VIDEO_H264_LEVEL_4_0);
+
+	h264->entropy_mode = v4l2_ctrl_new_std_menu(hdl, &rkvenc_h264_ctrl_ops,
+			V4L2_CID_MPEG_VIDEO_H264_ENTROPY_MODE,
+			V4L2_MPEG_VIDEO_H264_ENTROPY_MODE_CABAC, 0,
+			V4L2_MPEG_VIDEO_H264_ENTROPY_MODE_CAVLC);
+
+	h264->gop_size = v4l2_ctrl_new_std(hdl, &rkvenc_h264_ctrl_ops,
+					   V4L2_CID_MPEG_VIDEO_GOP_SIZE,
+					   1, 300, 1, 30);
+
+	h264->i_qp = v4l2_ctrl_new_std(hdl, &rkvenc_h264_ctrl_ops,
+				       V4L2_CID_MPEG_VIDEO_H264_I_FRAME_QP,
+				       0, 51, 1, 26);
+
+	h264->p_qp = v4l2_ctrl_new_std(hdl, &rkvenc_h264_ctrl_ops,
+				       V4L2_CID_MPEG_VIDEO_H264_P_FRAME_QP,
+				       0, 51, 1, 28);
+
+	return hdl->error;
+}
+
+/* ---- start/stop ---- */
+
+static int rkvenc_h264_start(struct rkvenc_ctx *ctx)
+{
+	struct rkvenc_dev *dev = ctx->dev;
+	struct rkvenc_h264_ctx *h264 = &ctx->h264;
+	/* Exact formulas from mpp's recon-buffer sizing function (the one
+	 * that feeds hal_bufs_setup()) — aligned_h has the same +16 fudge
+	 * mpp applies, not just MPP_ALIGN(height, 16).
+	 */
+	unsigned int aligned_w = ALIGN(ctx->coded_fmt.width, 64);
+	unsigned int aligned_h = ALIGN(ctx->coded_fmt.height, 16) + 16;
+	size_t fbc_hdr_size = ALIGN((size_t)aligned_w * aligned_h / 64, SZ_8K);
+	size_t fbc_bdy_size = (size_t)aligned_w * aligned_h * 3 / 2;
+	size_t total_size;
+	int i;
+
+	memset(&h264->frame_num, 0,
+	       sizeof(*h264) - offsetof(struct rkvenc_h264_ctx, frame_num));
+
+	h264->pixel_hdr_size = fbc_hdr_size;
+	h264->pixel_buf_size = fbc_hdr_size + fbc_bdy_size;
+	h264->thumb_buf_size = ALIGN((aligned_w / 64) * (aligned_h / 64) * 256, SZ_8K);
+	h264->smear_buf_size = ALIGN(aligned_w / 64, 16) * ALIGN(aligned_h / 16, 16);
+	total_size = h264->pixel_buf_size + h264->thumb_buf_size + h264->smear_buf_size;
+
+	for (i = 0; i < 2; i++) {
+		h264->recn_buf_cpu[i] = dma_alloc_coherent(dev->dev, total_size,
+							   &h264->recn_buf_dma[i],
+							   GFP_KERNEL);
+		if (!h264->recn_buf_cpu[i]) {
+			if (i)
+				dma_free_coherent(dev->dev, total_size,
+						  h264->recn_buf_cpu[0],
+						  h264->recn_buf_dma[0]);
+			return -ENOMEM;
+		}
+	}
+
+	h264->meiw_buf_cpu = dma_alloc_coherent(dev->dev, SZ_4K, &h264->meiw_buf_dma,
+						GFP_KERNEL);
+	if (!h264->meiw_buf_cpu) {
+		for (i = 0; i < 2; i++)
+			dma_free_coherent(dev->dev, total_size,
+					  h264->recn_buf_cpu[i], h264->recn_buf_dma[i]);
+		return -ENOMEM;
+	}
+
+	/* Catch-all scratch buffer for the auxiliary FRAME-class write
+	 * pointers (see the field comment in rkvenc.h). Sized like a full
+	 * recon buffer so even a full-frame-sized spurious write fits.
+	 */
+	rkvenc_h264_gen_sps_pps(ctx);
+
+	return 0;
+}
+
+static void rkvenc_h264_stop(struct rkvenc_ctx *ctx)
+{
+	struct rkvenc_dev *dev = ctx->dev;
+	struct rkvenc_h264_ctx *h264 = &ctx->h264;
+	size_t total_size = h264->pixel_buf_size + h264->thumb_buf_size +
+			     h264->smear_buf_size;
+	int i;
+
+	for (i = 0; i < 2; i++) {
+		if (h264->recn_buf_cpu[i])
+			dma_free_coherent(dev->dev, total_size,
+					  h264->recn_buf_cpu[i], h264->recn_buf_dma[i]);
+		h264->recn_buf_cpu[i] = NULL;
+	}
+
+	if (h264->meiw_buf_cpu)
+		dma_free_coherent(dev->dev, SZ_4K, h264->meiw_buf_cpu, h264->meiw_buf_dma);
+	h264->meiw_buf_cpu = NULL;
+}
+
+/* VEPU510-only quirk sequence, written unconditionally on every task by
+ * the downstream vendor kernel driver (drivers/video/rockchip/mpp/
+ * mpp_rkvenc2.c:rkvenc_run(), 0x74/0x308 block). Absent from
+ * rockchip-linux/mpp's userspace HAL entirely (it never touches these
+ * registers), so it cannot be cross-checked against the userspace source
+ * — treated as an opaque required silicon workaround for holding the
+ * encoder off "auto-restart" until this driver's IRQ model reads the
+ * completion status.
+ *
+ * EXPERIMENT, RESULT NEGATIVE (see git log): disabling this quirk was
+ * tried as a hypothesis for a genuine hardware-watchdog stall
+ * (int_sta wdg_sta) seen on first board bring-up. The board hit the
+ * exact same stall with this quirk disabled, so it is not the cause —
+ * re-enabled per the vendor's "unconditionally required" claim, since
+ * removing it demonstrated no benefit. See rkvenc-regs.h RC_ROI-class
+ * comment for the next hypothesis (roi_qthd0-3 never written).
+ */
+static void rkvenc_vepu510_quirk(struct rkvenc_dev *dev)
+{
+	rkvenc_write(dev, RKVENC_REG_DVBM_HOLD, RKVENC_DVBM_HOLD_VAL);
+	rkvenc_write(dev, RKVENC_REG_ENC_ID, RKVENC_ENC_ID_QUIRK_VAL);
+}
+
+static void rkvenc_h264_run(struct rkvenc_ctx *ctx)
+{
+	struct rkvenc_dev *dev = ctx->dev;
+	struct rkvenc_h264_ctx *h264 = &ctx->h264;
+	struct vb2_v4l2_buffer *src_buf = rkvenc_get_src_buf(ctx);
+	struct vb2_v4l2_buffer *dst_buf = rkvenc_get_dst_buf(ctx);
+	dma_addr_t src_dma = vb2_dma_contig_plane_dma_addr(&src_buf->vb2_buf, 0);
+	dma_addr_t dst_dma = vb2_dma_contig_plane_dma_addr(&dst_buf->vb2_buf, 0);
+	unsigned int width = ctx->coded_fmt.width;
+	unsigned int height = ctx->coded_fmt.height;
+	unsigned int aligned_w = ALIGN(width, 16);
+	unsigned int aligned_h = ALIGN(height, 16);
+	unsigned int uv_offset = ctx->src_fmt.bytesperline * ctx->src_fmt.height;
+	unsigned int dst_size = vb2_plane_size(&dst_buf->vb2_buf, 0);
+	bool is_idr = h264->gop_pos == 0;
+	unsigned int header_len = is_idr ? h264->sps_pps_len : 0;
+	unsigned int write_idx = h264->frame_num & 1;
+	/* Frame 0 has no previously-written reference at all -- alias read to
+	 * write (real hardware's refr_idx == curr_idx for frame 0, see the
+	 * rkvenc_h264_ctx comment in rkvenc.h). From frame 1 on, buf[write_idx
+	 * ^ 1] genuinely holds the previous frame's real reconstruction.
+	 */
+	unsigned int read_idx = h264->frame_num == 0 ? write_idx : (write_idx ^ 1);
+	bool is_main = h264->profile->val == V4L2_MPEG_VIDEO_H264_PROFILE_MAIN;
+	bool cabac = h264->entropy_mode->val == V4L2_MPEG_VIDEO_H264_ENTROPY_MODE_CABAC;
+	u32 qp = is_idr ? h264->i_qp->val : h264->p_qp->val;
+	union rkvenc_reg_enc_pic enc_pic = { .val = 0 };
+	union rkvenc_reg_enc_rsl enc_rsl = { .val = 0 };
+	union rkvenc_reg_src_fill src_fill = { .val = 0 };
+	union rkvenc_reg_src_fmt src_fmt = { .val = 0 };
+	union rkvenc_reg_src_strd0 src_strd0 = { .val = 0 };
+	union rkvenc_reg_src_strd1 src_strd1 = { .val = 0 };
+	union rkvenc_reg_rc_cfg rc_cfg = { .val = 0 };
+	union rkvenc_reg_rc_qp rc_qp = { .val = 0 };
+	union rkvenc_reg_sli_splt sli_splt = { .val = 0 };
+	union rkvenc_reg_rdo_cfg rdo_cfg = { .val = 0 };
+	union rkvenc_reg_synt_nal synt_nal = { .val = 0 };
+	union rkvenc_reg_synt_sps synt_sps = { .val = 0 };
+	union rkvenc_reg_synt_pps synt_pps = { .val = 0 };
+	union rkvenc_reg_synt_sli0 synt_sli0 = { .val = 0 };
+	union rkvenc_reg_synt_sli1 synt_sli1 = { .val = 0 };
+	union rkvenc_reg_synt_sli2 synt_sli2 = { .val = 0 };
+
+	if (header_len)
+		memcpy(vb2_plane_vaddr(&dst_buf->vb2_buf, 0), h264->sps_pps_nal, header_len);
+
+	rkvenc_write_relaxed(dev, RKVENC_REG_DBG_CLR, RKVENC_DBG_CLR_VAL);
+
+	/* Define every FRAME-class address/pointer register (0x270-0x2e8) by
+	 * zeroing it before the specific pointers below overwrite the ones this
+	 * driver actually uses. This matches the downstream vendor kernel, which
+	 * writes its ENTIRE register file linearly every frame so no register
+	 * ever carries stale bootloader/ATF/previous-session garbage. Doing this
+	 * moved the long-standing rk_iommu write fault from a boot-varying
+	 * garbage IOVA (the tell-tale 0x??b239?? stale-pointer signature) to a
+	 * benign, deterministic 0x0.
+	 *
+	 * That residual write@0 (BUG-2) is a HW-synthesized write through one of
+	 * these now-zero pointers (best candidate: the vrsp_rtn_en return-write
+	 * to the zero online_addr; the vendor sets the same ENC_ID quirk with
+	 * online_addr=0 and does NOT fault, so it is cosmetic). It is confirmed
+	 * INDEPENDENT of the P-frame hang and harmless to the I-frame: a
+	 * multi-agent evidence audit established the fault is a separate AXI
+	 * transaction from the real recon write (rfpw=valid), it never lands
+	 * (IOVA 0 pte invalid), and it cannot corrupt the reference the P-frame
+	 * reads. An earlier attempt to redirect these pointers to scratch DMA
+	 * buffers (colmvw/colmvr/lpfw/ebuft/ebufb) had NO measured effect on
+	 * either bug and diverged from the vendor (which keeps them 0), so it was
+	 * reverted -- zeroing alone is the vendor-matching behaviour.
+	 */
+	{
+		u32 off;
+
+		for (off = RKVENC_REG_FRAME_OFFSET; off <= RKVENC_REG_ADR_ROIR; off += 4)
+			rkvenc_write_relaxed(dev, off, 0);
+	}
+
+	/* Source frame (NV12): adr_src0 = Y, adr_src1 = UV. adr_src2 (a 3rd
+	 * plane pointer NV12 doesn't have) must equal adr_src1, not 0 — mpp's
+	 * hal_h264e_vepu510.c sets it unconditionally to the same fd as
+	 * adr_src1 even for 2-plane formats; confirmed via a real wtrace
+	 * capture this is genuinely non-zero on a working encode.
+	 */
+	rkvenc_write_relaxed(dev, RKVENC_REG_ADR_SRC0, src_dma);
+	rkvenc_write_relaxed(dev, RKVENC_REG_ADR_SRC1, src_dma + uv_offset);
+	rkvenc_write_relaxed(dev, RKVENC_REG_ADR_SRC2, src_dma + uv_offset);
+
+	/* All three of pixel(FBC)/thumb/smear are written by hardware on
+	 * every frame regardless of feature use — see the rkvenc_h264_ctx
+	 * comment in rkvenc.h. Leaving any of these at 0 IOMMU-faults in a
+	 * tight retry loop instead of failing cleanly. The read side
+	 * (rfpr/dspr/smear_rd) aliases the write side on frame 0 via
+	 * read_idx == write_idx, see the read_idx comment above.
+	 */
+	rkvenc_write_relaxed(dev, RKVENC_REG_RFPW_H_ADDR, h264->recn_buf_dma[write_idx]);
+	rkvenc_write_relaxed(dev, RKVENC_REG_RFPW_B_ADDR,
+			     h264->recn_buf_dma[write_idx] + h264->pixel_hdr_size);
+	rkvenc_write_relaxed(dev, RKVENC_REG_DSPW_ADDR,
+			     h264->recn_buf_dma[write_idx] + h264->pixel_buf_size);
+	rkvenc_write_relaxed(dev, RKVENC_REG_ADR_SMEAR_WR,
+			     h264->recn_buf_dma[write_idx] + h264->pixel_buf_size +
+			     h264->thumb_buf_size);
+
+	rkvenc_write_relaxed(dev, RKVENC_REG_RFPR_H_ADDR, h264->recn_buf_dma[read_idx]);
+	rkvenc_write_relaxed(dev, RKVENC_REG_RFPR_B_ADDR,
+			     h264->recn_buf_dma[read_idx] + h264->pixel_hdr_size);
+	rkvenc_write_relaxed(dev, RKVENC_REG_DSPR_ADDR,
+			     h264->recn_buf_dma[read_idx] + h264->pixel_buf_size);
+	rkvenc_write_relaxed(dev, RKVENC_REG_ADR_SMEAR_RD,
+			     h264->recn_buf_dma[read_idx] + h264->pixel_buf_size +
+			     h264->thumb_buf_size);
+
+	rkvenc_write_relaxed(dev, RKVENC_REG_MEIW_ADDR, h264->meiw_buf_dma);
+
+	/* Interlaced-field recon addresses: this driver never encodes
+	 * interlaced content, so rfpt/rfpb stay disabled -- but real hardware
+	 * expects the literal sentinel 0xffffffff in rfpt_h/rfpt_b specifically
+	 * (not a valid fd-embedded address), not a plain 0. rfpb_h/adr_rfpb_b
+	 * are 0 on a real encode too.
+	 */
+	rkvenc_write_relaxed(dev, RKVENC_REG_RFPT_H_ADDR, RKVENC_RFPT_DISABLED);
+	rkvenc_write_relaxed(dev, RKVENC_REG_RFPB_H_ADDR, 0);
+	rkvenc_write_relaxed(dev, RKVENC_REG_RFPT_B_ADDR, RKVENC_RFPT_DISABLED);
+	rkvenc_write_relaxed(dev, RKVENC_REG_ADR_RFPB_B, 0);
+
+	/* Bitstream ring buffer: hardware writes its complete slice NAL
+	 * starting right after our software-written SPS/PPS prefix (if any).
+	 */
+	rkvenc_write_relaxed(dev, RKVENC_REG_BSBT_ADDR, dst_dma + dst_size - 1);
+	rkvenc_write_relaxed(dev, RKVENC_REG_BSBB_ADDR, dst_dma + header_len);
+	rkvenc_write_relaxed(dev, RKVENC_REG_ADR_BSBS, dst_dma + header_len);
+	rkvenc_write_relaxed(dev, RKVENC_REG_BSBR_ADDR, dst_dma + header_len);
+
+	enc_rsl.pic_wd8_m1 = aligned_w / 8 - 1;
+	enc_rsl.pic_hd8_m1 = aligned_h / 8 - 1;
+	rkvenc_write_relaxed(dev, RKVENC_REG_ENC_RSL, enc_rsl.val);
+
+	src_fill.pic_wfill = aligned_w - width;
+	src_fill.pic_hfill = aligned_h - height;
+	rkvenc_write_relaxed(dev, RKVENC_REG_SRC_FILL, src_fill.val);
+
+	src_fmt.src_cfmt = RKVENC_SRC_FMT_YUV420SP;
+	src_fmt.out_fmt = 1; /* see RKVENC_SRC_FMT_YUV420SP comment in rkvenc-regs.h */
+	rkvenc_write_relaxed(dev, RKVENC_REG_SRC_FMT, src_fmt.val);
+
+	rkvenc_write_relaxed(dev, RKVENC_REG_SRC_PROC, 0);
+	rkvenc_write_relaxed(dev, RKVENC_REG_PIC_OFST, 0);
+
+	src_strd0.src_strd0 = ctx->src_fmt.bytesperline;
+	rkvenc_write_relaxed(dev, RKVENC_REG_SRC_STRD0, src_strd0.val);
+	src_strd1.src_strd1 = ctx->src_fmt.bytesperline;
+	rkvenc_write_relaxed(dev, RKVENC_REG_SRC_STRD1, src_strd1.val);
+
+	/* RC configuration: rc_en=1/aq_en=1 with a real (if approximate --
+	 * see below) rc_adj/rc_dthd threshold ladder, but rc_qp_range=0 and
+	 * rc_min_qp==rc_max_qp so the AQ engine has no room to actually move
+	 * QP away from the requested value. This is genuine fixed-QP
+	 * *output* via a real, non-degenerate RC *register configuration* --
+	 * NOT the driver's original rc_en=0/all-zero-threshold design, which
+	 * board bring-up proved reproducibly stalls the hardware (see the
+	 * RC_ROI banner comment in rkvenc-regs.h for the full story and the
+	 * exact real-hardware capture that pinned this down).
+	 *
+	 * bit_target is only a rough per-frame bit budget (bitrate control
+	 * divided by an assumed 30fps -- v1 doesn't negotiate real frame
+	 * intervals) feeding mpp's real threshold-ladder formula
+	 * (setup_vepu510_rc_base()); since rc_qp_range=0 clamps AQ to a
+	 * no-op regardless, the exact value only needs to be sane and
+	 * non-degenerate, not accurate -- true adaptive bitrate tracking is
+	 * a follow-up.
+	 */
+	{
+		unsigned int mb_w = aligned_w / 16;
+		unsigned int mb_h = aligned_h / 16;
+		s32 bit_target = (s32)(h264->bitrate->val / 30);
+		s32 mb_target_bits_mul_16 =
+			(s32)(((s64)bit_target << 4) / (mb_w * mb_h));
+		s32 mb_target_bits;
+		s32 negative_bits_thd, positive_bits_thd;
+		union rkvenc_reg_rc_adj0 rc_adj0 = { .val = 0 };
+		union rkvenc_reg_rc_adj1 rc_adj1 = { .val = 0 };
+
+		if (mb_target_bits_mul_16 >= 0x100000)
+			mb_target_bits_mul_16 = 0x50000;
+
+		mb_target_bits = (mb_target_bits_mul_16 * (s32)mb_w) >> 4;
+		negative_bits_thd = 0 - 5 * mb_target_bits / 16;
+		positive_bits_thd = 5 * mb_target_bits / 16;
+
+		rc_cfg.rc_en = 1;
+		rc_cfg.aq_en = 1;
+		rc_cfg.rc_ctu_num = mb_w;
+		rkvenc_write_relaxed(dev, RKVENC_REG_RC_CFG, rc_cfg.val);
+
+		rc_qp.rc_max_qp = qp;
+		rc_qp.rc_min_qp = qp;
+		/* EXPERIMENT, RESULT NEGATIVE (see git log): tried rc_qp_range=1
+		 * (matching a real successful mpp capture's adaptive-RC value)
+		 * instead of 0, as a hypothesis for the isolated rk_iommu fault
+		 * below -- didn't stop it, and combined with other experimental
+		 * changes at the time, board testing showed a real regression
+		 * (every frame failing with a genuine hardware watchdog under
+		 * multi-frame testing). Reverted to 0, which is confirmed safe
+		 * for the fixed-QP output this driver actually wants (with
+		 * rc_min_qp==rc_max_qp, AQ has no room to move QP regardless of
+		 * range, so 0 vs 1 shouldn't matter for correctness -- but 0 is
+		 * the value that's actually been confirmed not to regress).
+		 */
+		rc_qp.rc_qp_range = 0;
+		rkvenc_write_relaxed(dev, RKVENC_REG_RC_QP, rc_qp.val);
+		rkvenc_write_relaxed(dev, RKVENC_REG_RC_TGT, (u32)mb_target_bits_mul_16);
+
+		/* mpp's own constant qp_adj ladder, same for every frame
+		 * regardless of content/resolution/QP.
+		 */
+		rc_adj0.qp_adj0 = -2;
+		rc_adj0.qp_adj1 = -1;
+		rc_adj0.qp_adj2 = 0;
+		rc_adj0.qp_adj3 = 1;
+		rc_adj0.qp_adj4 = 2;
+		rkvenc_write_relaxed(dev, RKVENC_REG_RC_ADJ0, rc_adj0.val);
+		rkvenc_write_relaxed(dev, RKVENC_REG_RC_ADJ1, rc_adj1.val);
+
+		rkvenc_write_relaxed(dev, RKVENC_REG_RC_DTHD(0), (u32)(4 * negative_bits_thd));
+		rkvenc_write_relaxed(dev, RKVENC_REG_RC_DTHD(1), (u32)negative_bits_thd);
+		rkvenc_write_relaxed(dev, RKVENC_REG_RC_DTHD(2), (u32)positive_bits_thd);
+		rkvenc_write_relaxed(dev, RKVENC_REG_RC_DTHD(3), (u32)(4 * positive_bits_thd));
+		rkvenc_write_relaxed(dev, RKVENC_REG_RC_DTHD(4), RKVENC_RC_DTHD_SENTINEL);
+		rkvenc_write_relaxed(dev, RKVENC_REG_RC_DTHD(5), RKVENC_RC_DTHD_SENTINEL);
+		rkvenc_write_relaxed(dev, RKVENC_REG_RC_DTHD(6), RKVENC_RC_DTHD_SENTINEL);
+		rkvenc_write_relaxed(dev, RKVENC_REG_RC_DTHD(7), RKVENC_RC_DTHD_SENTINEL);
+		rkvenc_write_relaxed(dev, RKVENC_REG_RC_DTHD(8), RKVENC_RC_DTHD_SENTINEL);
+	}
+
+	/* ROI QP-clamp thresholds: written unconditionally by mpp regardless
+	 * of RC mode (see rkvenc-regs.h banner comment) — every area's
+	 * [qpmin,qpmax] is set to this frame's QP so the clamp is a no-op
+	 * rather than forcing every macroblock to QP 0.
+	 */
+	{
+		union rkvenc_reg_roi_qthd0 roi_qthd0 = { .val = 0 };
+		union rkvenc_reg_roi_qthd1 roi_qthd1 = { .val = 0 };
+		union rkvenc_reg_roi_qthd2 roi_qthd2 = { .val = 0 };
+		union rkvenc_reg_roi_qthd3 roi_qthd3 = { .val = 0 };
+
+		roi_qthd0.qpmin_area0 = qp; roi_qthd0.qpmax_area0 = qp;
+		roi_qthd0.qpmin_area1 = qp; roi_qthd0.qpmax_area1 = qp;
+		roi_qthd0.qpmin_area2 = qp;
+		rkvenc_write_relaxed(dev, RKVENC_REG_ROI_QTHD0, roi_qthd0.val);
+
+		roi_qthd1.qpmax_area2 = qp;
+		roi_qthd1.qpmin_area3 = qp; roi_qthd1.qpmax_area3 = qp;
+		roi_qthd1.qpmin_area4 = qp; roi_qthd1.qpmax_area4 = qp;
+		rkvenc_write_relaxed(dev, RKVENC_REG_ROI_QTHD1, roi_qthd1.val);
+
+		roi_qthd2.qpmin_area5 = qp; roi_qthd2.qpmax_area5 = qp;
+		roi_qthd2.qpmin_area6 = qp; roi_qthd2.qpmax_area6 = qp;
+		roi_qthd2.qpmin_area7 = qp;
+		rkvenc_write_relaxed(dev, RKVENC_REG_ROI_QTHD2, roi_qthd2.val);
+
+		roi_qthd3.qpmax_area7 = qp;
+		roi_qthd3.qpmap_mode = 1; /* matches mpp's hardcoded value */
+		rkvenc_write_relaxed(dev, RKVENC_REG_ROI_QTHD3, roi_qthd3.val);
+	}
+
+	/* AQ activity LUT / MAD-statistics thresholds / chroma KLUT offset:
+	 * see the banner comment above RKVENC_REG_AQ_TTHD in rkvenc-regs.h --
+	 * real fields this driver never wrote at all until now, found by
+	 * diffing against mpp's setup_vepu510_aq()/setup_vepu510_me()/
+	 * setup_vepu510_rdo_pred(), all written unconditionally every frame
+	 * (mpp's own default aq_tthd/aq_step tables are identical for I and P
+	 * slices; klut_ofst is 6 for both in the non-IPC-tuning default this
+	 * driver matches).
+	 */
+	{
+		static const u8 aq_tthd[16] = {
+			0, 0, 0, 0, 3, 3, 5, 5, 8, 8, 8, 15, 15, 20, 25, 25,
+		};
+		static const s32 aq_step[16] = {
+			-8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 7, 8,
+		};
+		union rkvenc_reg_aq_stp0 aq_stp0 = { .val = 0 };
+		union rkvenc_reg_aq_stp1 aq_stp1 = { .val = 0 };
+		union rkvenc_reg_aq_stp2 aq_stp2 = { .val = 0 };
+		union rkvenc_reg_madi_st_thd madi_st_thd = { .val = 0 };
+		union rkvenc_reg_madp_st_thd0 madp_st_thd0 = { .val = 0 };
+		union rkvenc_reg_madp_st_thd1 madp_st_thd1 = { .val = 0 };
+		union rkvenc_reg_klut_ofst klut_ofst = { .val = 0 };
+		unsigned int i;
+
+		for (i = 0; i < 4; i++) {
+			u32 word = aq_tthd[4 * i] | (aq_tthd[4 * i + 1] << 8) |
+				   (aq_tthd[4 * i + 2] << 16) | (aq_tthd[4 * i + 3] << 24);
+
+			rkvenc_write_relaxed(dev, RKVENC_REG_AQ_TTHD + 4 * i, word);
+		}
+
+		aq_stp0.aq_stp_s0 = aq_step[0]; aq_stp0.aq_stp_0t1 = aq_step[1];
+		aq_stp0.aq_stp_1t2 = aq_step[2]; aq_stp0.aq_stp_2t3 = aq_step[3];
+		aq_stp0.aq_stp_3t4 = aq_step[4]; aq_stp0.aq_stp_4t5 = aq_step[5];
+		rkvenc_write_relaxed(dev, RKVENC_REG_AQ_STP0, aq_stp0.val);
+
+		/* aq_stp_7t8 is a hardcoded 0 in mpp's setup_vepu510_aq(), not
+		 * aq_step[8] -- a genuine off-by-one quirk in the vendor HAL
+		 * (aq_step[8..10] shift into aq_stp_8t9..10t11 instead), real
+		 * hardware expects exactly this mapping so it's reproduced
+		 * verbatim rather than "corrected".
+		 */
+		aq_stp1.aq_stp_5t6 = aq_step[6]; aq_stp1.aq_stp_6t7 = aq_step[7];
+		aq_stp1.aq_stp_7t8 = 0; aq_stp1.aq_stp_8t9 = aq_step[8];
+		aq_stp1.aq_stp_9t10 = aq_step[9]; aq_stp1.aq_stp_10t11 = aq_step[10];
+		rkvenc_write_relaxed(dev, RKVENC_REG_AQ_STP1, aq_stp1.val);
+
+		/* aq_stp2's middle two fields (12t13/13t14) don't match
+		 * h264_P/I_aq_step_default's aq_step[12]/[13] verbatim -- cross-
+		 * checked against the real vendor wtrace capture from the
+		 * original I-frame bring-up (register 0x105c was 0x00839ca3
+		 * there), which decodes to 3/5/7/7/8, not 3/4/5/7/8. Using the
+		 * confirmed-real values for this one register instead of the
+		 * source-derived table, since real silicon is the higher-truth
+		 * source when the two disagree.
+		 */
+		aq_stp2.aq_stp_11t12 = aq_step[11]; aq_stp2.aq_stp_12t13 = 5;
+		aq_stp2.aq_stp_13t14 = 7; aq_stp2.aq_stp_14t15 = aq_step[14];
+		aq_stp2.aq_stp_b15 = aq_step[15];
+		rkvenc_write_relaxed(dev, RKVENC_REG_AQ_STP2, aq_stp2.val);
+
+		madi_st_thd.madi_th0 = 5;
+		madi_st_thd.madi_th1 = 12;
+		madi_st_thd.madi_th2 = 20;
+		rkvenc_write_relaxed(dev, RKVENC_REG_MADI_ST_THD, madi_st_thd.val);
+
+		madp_st_thd0.madp_th0 = 4 << 4;
+		madp_st_thd0.madp_th1 = 9 << 4;
+		rkvenc_write_relaxed(dev, RKVENC_REG_MADP_ST_THD0, madp_st_thd0.val);
+
+		madp_st_thd1.madp_th2 = 15 << 4;
+		rkvenc_write_relaxed(dev, RKVENC_REG_MADP_ST_THD1, madp_st_thd1.val);
+
+		klut_ofst.chrm_klut_ofst = 6;
+		rkvenc_write_relaxed(dev, RKVENC_REG_KLUT_OFST, klut_ofst.val);
+	}
+
+	/* PARAM/SQI: mpp's default-tuning constant tables -- see the comment
+	 * on rkvenc_par_class/rkvenc_sqi_class above. SCL: this driver never
+	 * uses a custom H.264 scaling list, so the whole class is zero --
+	 * written explicitly every frame rather than relying on POR/leftover
+	 * state, since real hardware still expects the class to have been
+	 * written (see rkvenc-regs.h banner comment).
+	 */
+	rkvenc_write_table_relaxed(dev, RKVENC_REG_PAR_OFFSET, rkvenc_par_class,
+				   ARRAY_SIZE(rkvenc_par_class));
+	rkvenc_write_table_relaxed(dev, RKVENC_REG_SQI_OFFSET, rkvenc_sqi_class,
+				   ARRAY_SIZE(rkvenc_sqi_class));
+	{
+		u32 off;
+
+		for (off = RKVENC_REG_SCL_OFFSET;
+		     off < RKVENC_REG_SCL_OFFSET + RKVENC_REG_SCL_SIZE; off += 4)
+			rkvenc_write_relaxed(dev, off, 0);
+	}
+
+	/* ATR (anti-ringing) weights + smear_opt_cfg.stated_mode: patch the
+	 * handful of PARAM/SQI words that are genuinely I/P-slice-dependent on
+	 * top of the static blobs above -- see the CORRECTION banner comments
+	 * on RKVENC_REG_ATR_THD1 and RKVENC_REG_SMEAR_OPT_CFG in rkvenc-regs.h.
+	 * Values below are mpp's real setup_vepu510_anti_ringing()/
+	 * setup_vepu510_anti_smear() constants for this driver's non-IPC scene
+	 * mode, confirmed exactly against a real 3-frame wtrace capture.
+	 */
+	{
+		union rkvenc_reg_atr_thd1 atr_thd1 = { .val = 0 };
+		union rkvenc_reg_atr_wgt atr_wgt16 = { .val = 0 };
+		union rkvenc_reg_atr_wgt atr_wgt8 = { .val = 0 };
+		union rkvenc_reg_atr_wgt atr_wgt4 = { .val = 0 };
+		union rkvenc_reg_smear_opt_cfg smear_opt_cfg = { .val = 0 };
+
+		atr_thd1.thdqp = 45;
+		if (is_idr) {
+			atr_thd1.thd2 = 6;
+			atr_wgt16.wgt0 = 16; atr_wgt16.wgt1 = 16; atr_wgt16.wgt2 = 16;
+			atr_wgt8.wgt0 = 18; atr_wgt8.wgt1 = 17; atr_wgt8.wgt2 = 18;
+			atr_wgt4.wgt0 = 16; atr_wgt4.wgt1 = 16; atr_wgt4.wgt2 = 16;
+		} else {
+			atr_thd1.thd2 = 7;
+			atr_wgt16.wgt0 = 23; atr_wgt16.wgt1 = 22; atr_wgt16.wgt2 = 20;
+			atr_wgt8.wgt0 = 24; atr_wgt8.wgt1 = 24; atr_wgt8.wgt2 = 24;
+			atr_wgt4.wgt0 = 23; atr_wgt4.wgt1 = 22; atr_wgt4.wgt2 = 20;
+		}
+		rkvenc_write_relaxed(dev, RKVENC_REG_ATR_THD1, atr_thd1.val);
+		rkvenc_write_relaxed(dev, RKVENC_REG_ATR_WGT16, atr_wgt16.val);
+		rkvenc_write_relaxed(dev, RKVENC_REG_ATR_WGT8, atr_wgt8.val);
+		rkvenc_write_relaxed(dev, RKVENC_REG_ATR_WGT4, atr_wgt4.val);
+
+		smear_opt_cfg.rdo_smear_lvl16_multi = 16;
+		smear_opt_cfg.rdo_smear_en = 0;
+		smear_opt_cfg.stated_mode = (is_idr || h264->last_frame_was_idr) ? 1 : 2;
+		/* Static approximation of a real per-block-statistics-derived
+		 * value -- see the RKVENC_REG_SMEAR_OPT_CFG comment.
+		 */
+		smear_opt_cfg.rdo_smear_dlt_qp = is_idr ? -1 : -3;
+		rkvenc_write_relaxed(dev, RKVENC_REG_SMEAR_OPT_CFG, smear_opt_cfg.val);
+	}
+
+	/* Single slice per frame in v1. */
+	rkvenc_write_relaxed(dev, RKVENC_REG_SLI_SPLT, sli_splt.val);
+	rkvenc_write_relaxed(dev, RKVENC_REG_SLI_BYTE, 0);
+	rkvenc_write_relaxed(dev, RKVENC_REG_SLI_CNUM, 0);
+
+	/* Motion-estimation search config — written unconditionally by mpp
+	 * on every frame regardless of I/P (see rkvenc-regs.h banner
+	 * comment); values are mpp's own hardcoded constants.
+	 */
+	{
+		union rkvenc_reg_me_rnge me_rnge = { .val = 0 };
+		union rkvenc_reg_me_cfg me_cfg = { .val = 0 };
+		union rkvenc_reg_me_cach me_cach = { .val = 0 };
+
+		me_rnge.cime_srch_dwnh = 15;
+		me_rnge.cime_srch_uph = 15;
+		me_rnge.cime_srch_rgtw = 12;
+		me_rnge.cime_srch_lftw = 12;
+		rkvenc_write_relaxed(dev, RKVENC_REG_ME_RNGE, me_rnge.val);
+
+		me_cfg.srgn_max_num = 54;
+		me_cfg.cime_dist_thre = 1024;
+		me_cfg.rme_srch_h = 3;
+		me_cfg.rme_srch_v = 3;
+		rkvenc_write_relaxed(dev, RKVENC_REG_ME_CFG, me_cfg.val);
+
+		me_cach.cime_zero_thre = 64;
+		rkvenc_write_relaxed(dev, RKVENC_REG_ME_CACH, me_cach.val);
+	}
+
+	/* Matches setup_vepu510_rdo_pred()'s derivation in the vendor HAL.
+	 * chrm_spcl/ccwa_e/atr_mult_sel_e are unconditional 1s in mpp
+	 * regardless of profile/level/tune config -- see rkvenc-regs.h.
+	 */
+	rdo_cfg.rect_size = !is_main && rkvenc_h264_level_idc[h264->level->val] <= 30;
+	rdo_cfg.vlc_lmt = !is_main && !cabac;
+	rdo_cfg.chrm_spcl = 1;
+	rdo_cfg.ccwa_e = 1;
+	rdo_cfg.atr_mult_sel_e = 1;
+	rkvenc_write_relaxed(dev, RKVENC_REG_RDO_CFG, rdo_cfg.val);
+
+	/* mpp sets synt_nal.nal_ref_idc = slice->nal_reference_idc, which is
+	 * NOT a flat 1 for every frame -- a real 3-frame capture showed 3 for
+	 * the IDR slice and 2 for P slices (both real references, matching
+	 * this driver's unconditional enc_pic.cur_frm_ref=1). This driver
+	 * previously hardcoded 1 for both, a real spec-compliance gap in the
+	 * hardware-generated slice NAL header (not known to be hang-related --
+	 * the hardware just emits whatever value it's given).
+	 */
+	synt_nal.nal_ref_idc = is_idr ? 3 : 2;
+	synt_nal.nal_unit_type = is_idr ? 5 : 1;
+	rkvenc_write_relaxed(dev, RKVENC_REG_SYNT_NAL, synt_nal.val);
+
+	synt_sps.max_fnum = RKVENC_H264_LOG2_MAX_FRAME_NUM;
+	synt_sps.drct_8x8 = 1;
+	synt_sps.mpoc_lm4 = RKVENC_H264_LOG2_MAX_POC_LSB;
+	synt_sps.poc_type = 0;
+	rkvenc_write_relaxed(dev, RKVENC_REG_SYNT_SPS, synt_sps.val);
+
+	synt_pps.etpy_mode = cabac;
+	synt_pps.csip_flag = 0;
+	synt_pps.num_ref0_idx = 0;
+	synt_pps.num_ref1_idx = 0;
+	synt_pps.pic_init_qp = h264->i_qp->val;
+	synt_pps.cb_ofst = 0;
+	synt_pps.cr_ofst = 0;
+	synt_pps.dbf_cp_flg = 1;
+	rkvenc_write_relaxed(dev, RKVENC_REG_SYNT_PPS, synt_pps.val);
+
+	synt_sli0.sli_type = is_idr ? RKVENC_SLI_TYPE_I : RKVENC_SLI_TYPE_P;
+	synt_sli0.pps_id = 0;
+	synt_sli0.cbc_init_idc = 0;
+	synt_sli0.frm_num = h264->frame_num;
+	rkvenc_write_relaxed(dev, RKVENC_REG_SYNT_SLI0, synt_sli0.val);
+
+	synt_sli1.idr_pid = h264->idr_pic_id;
+	synt_sli1.poc_lsb = h264->poc_lsb;
+	rkvenc_write_relaxed(dev, RKVENC_REG_SYNT_SLI1, synt_sli1.val);
+
+	synt_sli2.dis_dblk_idc = 0;
+	synt_sli2.sli_alph_ofst = 0;
+	synt_sli2.sli_beta_ofst = 0;
+	rkvenc_write_relaxed(dev, RKVENC_REG_SYNT_SLI2, synt_sli2.val);
+
+	enc_pic.enc_stnd = RKVENC_ENC_STND_H264;
+	enc_pic.cur_frm_ref = 1;
+	enc_pic.pic_qp = qp;
+	enc_pic.rec_fbc_dis = 0;
+	/* mpp's setup_vepu510_codec() sets this unconditionally to 1 on every
+	 * frame; confirmed not just from source but from the real vendor
+	 * wtrace capture used for the original I-frame bring-up (0x300 was
+	 * 0x4000191c there, bit4 set) -- this driver had never set it at all.
+	 */
+	enc_pic.bs_scp = 1;
+	/* mpp's hal_h264e_vepu510_gen_regs() sets this whenever a real
+	 * motion-detection-info buffer is provided (mei_stor = task->md_info
+	 * ? 1 : 0) -- this driver always provides one (meiw_buf_dma below),
+	 * so this must always be 1 too. Previously left 0 despite the address
+	 * being valid: a mismatch between "here's a real buffer" and "don't
+	 * actually store to it" that plausibly only bites once the ME engine
+	 * has real motion-vector output to write, i.e. genuine inter-frame
+	 * (P-frame) search -- an I-frame's trivial/absent ME activity may
+	 * never actually exercise the write-enable path this gates.
+	 */
+	enc_pic.mei_stor = 1;
+	/* slen_fifo is deliberately left 0 for this FIRST ENC_PIC write. The
+	 * vendor arms the slice-length FIFO in a two-step sequence: ENC_PIC is
+	 * written with slen_fifo=0 during setup, then RE-written with
+	 * slen_fifo=1 as the very last register before ENC_START. A real
+	 * multi-frame wtrace capture shows this on EVERY frame (I and both P):
+	 * 0x300 written twice, e.g. P-frame `0x00001a1c` (slen_fifo=0) early
+	 * then `0x40001a1c` (slen_fifo=1) at the kick. This driver previously
+	 * wrote ENC_PIC once with slen_fifo=1 and then wrote DUAL_CORE after
+	 * it -- so it never reproduced the vendor's "arm the FIFO as the final
+	 * action immediately before the kick" ordering. That two-step arm is
+	 * the strongest-remaining register-level difference for the P-frame
+	 * hang: the I-frame limps through without it, but a P-frame produces
+	 * genuine slice-FIFO traffic that may depend on the FIFO being armed
+	 * last. See the kick sequence below.
+	 */
+	enc_pic.slen_fifo = 0;
+
+	/* First ENC_PIC write (slen_fifo=0) and DUAL_CORE, both in the
+	 * vendor's early "config" position -- the capture writes 0x300
+	 * (slen_fifo=0) and 0x304=0x14 together in the FRAME-class region,
+	 * well before the kick, NOT interleaved into the kick tail.
+	 *
+	 * DUAL_CORE=0x14: see the union rkvenc_reg_dual_core comment in
+	 * rkvenc-regs.h. Confirmed written once per frame (seq62/714/1366 in
+	 * the multi-frame capture) even for this fully standalone single-core
+	 * encode; leaving it 0 did NOT stop the isolated rk_iommu fault.
+	 */
+	rkvenc_write_relaxed(dev, RKVENC_REG_ENC_PIC, enc_pic.val);
+	rkvenc_write_relaxed(dev, RKVENC_REG_DUAL_CORE, 0x14);
+
+	/* Control-class (Vepu510ControlCfg) setup + the vendor's exact kick
+	 * tail. The kick order is transcribed verbatim from a real multi-frame
+	 * wtrace capture (the last writes of every frame, identical across I
+	 * and both P frames):
+	 *   ENC_WDG(0x38) -> DVBM_HOLD(0x74) -> ENC_ID(0x308) -> INT_EN(0x20)
+	 *   -> ENC_PIC(0x300, slen_fifo=1) -> ENC_START(0x10)
+	 * with ENC_PIC (FIFO-armed) as the final config write before the kick.
+	 */
+	{
+		union rkvenc_reg_int_bits int_en = { .val = 0 };
+		union rkvenc_reg_opt_strg opt_strg = { .val = 0 };
+		union rkvenc_reg_dtrns_map dtrns_map = { .val = 0 };
+		union rkvenc_reg_enc_strt enc_strt = { .val = 0 };
+
+		int_en.enc_done = 1;
+		int_en.lkt_node_done = 1;
+		int_en.sclr_done = 1;
+		int_en.vslc_done = 1; /* mirrors enc_pic.slen_fifo armed below */
+		int_en.vbsf_oflw = 1;
+		int_en.vbuf_lens = 1;
+		int_en.enc_err = 1;
+		int_en.vsrc_err = 1;
+		int_en.wdg = 1;
+		int_en.lkt_err_int = 1;
+		int_en.lkt_err_stop = 1;
+		int_en.lkt_force_stop = 1;
+		int_en.jslc_done = 1;
+		int_en.jbsf_oflw = 1;
+		int_en.jbuf_lens = 1;
+
+		/* Early config writes (the vendor emits these in its linear
+		 * offset-ordered dump, well before the kick): INT_MASK, OPT_STRG,
+		 * DTRNS_MAP. INT_EN itself is deferred to the kick tail below to
+		 * match the capture (INT_EN is re-written right before ENC_PIC).
+		 */
+		rkvenc_write_relaxed(dev, RKVENC_REG_INT_MASK, 0);
+
+		opt_strg.cke = 1;
+		opt_strg.resetn_hw_en = 1;
+		rkvenc_write_relaxed(dev, RKVENC_REG_OPT_STRG, opt_strg.val);
+
+		dtrns_map.bsw_bus_edin = 7;
+		rkvenc_write_relaxed(dev, RKVENC_REG_DTRNS_MAP, dtrns_map.val);
+
+		/* ---- exact vendor kick tail ---- */
+		rkvenc_write(dev, RKVENC_REG_ENC_WDG, rkvenc_calc_wdg(dev, width, height));
+		rkvenc_vepu510_quirk(dev); /* DVBM_HOLD(0x74) + ENC_ID(0x308) */
+		rkvenc_write_relaxed(dev, RKVENC_REG_INT_EN, int_en.val);
+
+		/* Final ENC_PIC re-write with slen_fifo=1 -- arms the slice-length
+		 * FIFO as the very last action before ENC_START, matching the
+		 * capture exactly. This is the whole point of the two-step arm.
+		 */
+		enc_pic.slen_fifo = 1;
+		rkvenc_write_relaxed(dev, RKVENC_REG_ENC_PIC, enc_pic.val);
+
+		wmb(); /* all task registers visible before the kick below */
+
+		enc_strt.lkt_num = 0;
+		enc_strt.vepu_cmd = RKVENC_VEPU_CMD_ENC;
+		rkvenc_write(dev, RKVENC_REG_ENC_START, enc_strt.val);
+	}
+}
+
+static void rkvenc_h264_done(struct rkvenc_ctx *ctx, enum vb2_buffer_state state)
+{
+	struct rkvenc_dev *dev = ctx->dev;
+	struct rkvenc_h264_ctx *h264 = &ctx->h264;
+	struct vb2_v4l2_buffer *dst_buf = rkvenc_get_dst_buf(ctx);
+	bool is_idr = h264->gop_pos == 0;
+
+	if (state == VB2_BUF_STATE_DONE) {
+		unsigned int header_len = is_idr ? h264->sps_pps_len : 0;
+		u32 hw_len = rkvenc_read(dev, RKVENC_REG_ST_BS_LENGTH);
+
+		vb2_set_plane_payload(&dst_buf->vb2_buf, 0, header_len + hw_len);
+		dst_buf->flags |= is_idr ? V4L2_BUF_FLAG_KEYFRAME : V4L2_BUF_FLAG_PFRAME;
+
+		h264->frame_num = (h264->frame_num + 1) &
+				  GENMASK(RKVENC_H264_LOG2_MAX_FRAME_NUM + 3, 0);
+		h264->poc_lsb = (h264->poc_lsb + 2) &
+				GENMASK(RKVENC_H264_LOG2_MAX_POC_LSB + 3, 0);
+		h264->last_frame_was_idr = is_idr;
+		if (++h264->gop_pos >= h264->gop_size->val)
+			h264->gop_pos = 0;
+		if (h264->gop_pos == 0)
+			h264->idr_pic_id++;
+	}
+}
+
+static const struct rkvenc_coded_fmt_ops rkvenc_h264_ops = {
+	.init_ctrls = rkvenc_h264_init_ctrls,
+	.start = rkvenc_h264_start,
+	.stop = rkvenc_h264_stop,
+	.run = rkvenc_h264_run,
+	.done = rkvenc_h264_done,
+};
+
+const struct rkvenc_coded_fmt_desc rkvenc_h264_fmt_desc = {
+	.fourcc = V4L2_PIX_FMT_H264,
+	.frmsize = {
+		.frmsize_min_width = 176,
+		.frmsize_max_width = 4096,
+		.frmsize_step_width = 2,
+		.frmsize_min_height = 144,
+		.frmsize_max_height = 2560,
+		.frmsize_step_height = 2,
+	},
+	.ops = &rkvenc_h264_ops,
+};
diff --git a/drivers/media/platform/rockchip/rkvenc/rkvenc-regs.h b/drivers/media/platform/rockchip/rkvenc/rkvenc-regs.h
new file mode 100644
index 000000000..353ba3eba
--- /dev/null
+++ b/drivers/media/platform/rockchip/rkvenc/rkvenc-regs.h
@@ -0,0 +1,929 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Rockchip VEPU510 (RK3576) hardware video encoder registers.
+ *
+ * Copyright (C) 2026 Jiaxing Hu <gahing@gahingwoo.com>
+ *
+ * Field layout cross-checked against Rockchip's own open-source userspace
+ * codec library (github.com/rockchip-linux/mpp,
+ * mpp/hal/rkenc/{common/vepu510_common.h,h264e/hal_h264e_vepu510_reg.h},
+ * Apache-2.0) — that library is the only place the RC/PIC/PARAM register
+ * field names exist; the downstream rockchip-linux/kernel mpp_rkvenc2.c
+ * driver treats those classes as an opaque byte blob and never names a
+ * single field beyond the handful of control/status registers below.
+ *
+ * Register classes (byte offsets within the per-core MMIO window):
+ *   BASE   0x0000-0x0120  control / IRQ / watchdog
+ *   FRAME  0x0270-0x03f4  per-frame picture config + H.264 syntax
+ *   RC_ROI 0x1000-0x110c  ROI / adaptive-quant helper tables
+ *   PARAM  0x1700-0x19cc  RDO/ME/quant tuning tables
+ *   SQI    0x2000-0x212c  subjective-quality tuning
+ *   SCL    0x2200-0x2584  scaling lists
+ *   STATUS 0x4000-0x424c  read-only status
+ *   DEBUG  0x5000-0x5230  debug counters
+ *
+ * IMPORTANT — PARAM/SQI/SCL and the RC_ROI rc_adj/rc_dthd threshold ladder
+ * are NOT optional despite mpp's own FIXQP-mode code path never touching
+ * them (rc->rc_mode == MPP_ENC_RC_MODE_FIXQP returns early from
+ * setup_vepu510_rc_base() before setting rc_adj0/1/rc_dthd_0_8, and
+ * nothing in mpp ever documents PARAM/SQI as required). Board bring-up
+ * spent a long time on exactly this assumption: a from-scratch test client
+ * reproduced the driver's exact hardware-internal-watchdog stall
+ * (int_sta.wdg_sta, ~50ms after kick) with these classes left at their
+ * POR/never-written state, byte-for-byte matched everything else against a
+ * real rockchip-linux/mpp `mpi_enc_test` capture, and the stall persisted
+ * until PARAM+SQI+SCL were written and the RC_ROI threshold ladder was
+ * populated (rc_dthd_0_8 left at 0 instead of the real "never trigger"
+ * 0x7FFFFFFF sentinel is itself sufficient to reproduce the stall alone).
+ * Whatever internal engine consumes these — almost certainly the RDO/
+ * mode-decision and adaptive-quant hardware blocks — appears to need a
+ * valid, non-degenerate configuration to complete at all, not just to
+ * compute good output. See rkvenc-h264.c for what's actually written and
+ * why (PAR/SQI are mpp's *default-tuning* constant tables, not per-frame
+ * content-derived, so hardcoding them is a deliberate v1 choice, not a
+ * placeholder).
+ *
+ * Each per-frame register below is a union of its named bitfields and a
+ * raw u32 .val, so callers build the value field-by-field and write it
+ * with a single rkvenc_write(dev, RKVENC_REG_*, reg.val).
+ */
+
+#ifndef RKVENC_REGS_H_
+#define RKVENC_REGS_H_
+
+#include <linux/bits.h>
+#include <linux/types.h>
+
+#define RKVENC_REG_FRAME_OFFSET		0x270
+#define RKVENC_REG_STATUS_OFFSET	0x4000
+#define RKVENC_REG_DEBUG_OFFSET		0x5000
+
+/* ---- BASE class (0x0000-0x0120), a.k.a. Vepu510ControlCfg in mpp.
+ * Bit layout below is transcribed directly from
+ * mpp/hal/rkenc/common/vepu510_common.h (Vepu510ControlCfg_t) — this
+ * supersedes an earlier, less precise reading of these same registers
+ * from the downstream vendor *kernel* driver's comments (which got the
+ * ENC_WDG field width/position and several INT_* bit meanings wrong).
+ */
+#define RKVENC_REG_VERSION		0x0000	/* read-only IP identification */
+#define RKVENC_REG_ENC_START		0x0010	/* enc_strt */
+#define RKVENC_REG_ENC_CLR		0x0014	/* bit0 safe_clr, bit1 force_clr */
+#define RKVENC_ENC_CLR_SAFE		0x1
+#define RKVENC_ENC_CLR_FORCE		0x2
+#define RKVENC_SCLR_DONE_STA		BIT(2)	/* int_sta sclr_done, set when safe-clr completes */
+#define RKVENC_REG_VS_LDLY		0x0018
+
+union rkvenc_reg_version {
+	struct {
+		u32 sub_ver:8;
+		u32 h264_cap:1;
+		u32 hevc_cap:1;
+		u32 reserved:2;
+		u32 res_cap:4;
+		u32 osd_cap:2;
+		u32 filtr_cap:2;
+		u32 bfrm_cap:1;
+		u32 fbc_cap:2;
+		u32 reserved1:1;
+		u32 ip_id:8;
+	};
+	u32 val;
+};
+#define RKVENC_REG_INT_EN		0x0020
+#define RKVENC_REG_INT_MASK		0x0024
+#define RKVENC_REG_INT_CLR		0x0028
+#define RKVENC_REG_INT_STA		0x002c
+#define RKVENC_REG_DTRNS_MAP		0x0030
+#define RKVENC_REG_DTRNS_CFG		0x0034
+#define RKVENC_REG_ENC_WDG		0x0038
+#define RKVENC_REG_OPT_STRG		0x0054
+
+/* VEPU510-only "DVBM hold" quirk (see rkvenc_vepu510_quirk() in
+ * rkvenc-h264.c). No field name exists in the vendor HAL for this — it is
+ * not written by rockchip-linux/mpp at all, only by the downstream kernel
+ * driver, so its exact purpose beyond the vendor comment ("vepu will hold
+ * when encoding finish") is unverified. Treated as an opaque required
+ * write sequence.
+ */
+#define RKVENC_REG_DVBM_HOLD		0x0074
+#define RKVENC_DVBM_HOLD_VAL		0x23
+
+/* enc_strt (0x0010): the actual "go" trigger is the 3-bit vepu_cmd field
+ * at bits[10:8], NOT the whole register set to 1 — vepu_cmd=1 is mpp's
+ * setup_vepu510_normal(); lkt_num (bits[7:0]) is the link-table task
+ * count, irrelevant here since v1 doesn't use link/CCU mode.
+ */
+union rkvenc_reg_enc_strt {
+	struct {
+		u32 lkt_num:8;
+		u32 vepu_cmd:3;
+		u32 reserved:21;
+	};
+	u32 val;
+};
+
+#define RKVENC_VEPU_CMD_ENC		1
+
+/* int_en / int_msk / int_clr / int_sta all share this bit layout. */
+union rkvenc_reg_int_bits {
+	struct {
+		u32 enc_done:1;
+		u32 lkt_node_done:1;
+		u32 sclr_done:1;
+		u32 vslc_done:1;
+		u32 vbsf_oflw:1;
+		u32 vbuf_lens:1;
+		u32 enc_err:1;
+		u32 vsrc_err:1;
+		u32 wdg:1;
+		u32 lkt_err_int:1;
+		u32 lkt_err_stop:1;
+		u32 lkt_force_stop:1;
+		u32 jslc_done:1;
+		u32 jbsf_oflw:1;
+		u32 jbuf_lens:1;
+		u32 dvbm_err:1;
+		u32 reserved:16;
+	};
+	u32 val;
+};
+
+/* Hard-error bits worth aborting the frame over; enc_done/lkt_node_done/
+ * sclr_done/vslc_done/vbuf_lens/jslc_done/jbsf_oflw/jbuf_lens are either
+ * normal completion signals or JPEG-only (harmless no-ops for H.264).
+ */
+#define RKVENC_INT_ERROR_MASK \
+	(BIT(4) /* vbsf_oflw */ | BIT(6) /* enc_err */ | BIT(7) /* vsrc_err */ | \
+	 BIT(8) /* wdg */ | BIT(9) /* lkt_err_int */ | BIT(10) /* lkt_err_stop */ | \
+	 BIT(11) /* lkt_force_stop */ | BIT(15) /* dvbm_err */)
+
+union rkvenc_reg_enc_wdg {
+	struct {
+		u32 vs_load_thd:24;
+		u32 reserved:8;
+	};
+	u32 val;
+};
+
+union rkvenc_reg_opt_strg {
+	struct {
+		u32 cke:1;
+		u32 resetn_hw_en:1;
+		u32 rfpr_err_e:1;
+		u32 sram_ckg_en:1;
+		u32 link_err_stop:1;
+		u32 reserved:27;
+	};
+	u32 val;
+};
+
+union rkvenc_reg_dtrns_map {
+	struct {
+		u32 jpeg_bus_edin:4;
+		u32 src_bus_edin:4;
+		u32 meiw_bus_edin:4;
+		u32 bsw_bus_edin:4;
+		u32 reserved:8;
+		u32 lktw_bus_edin:4;
+		u32 rec_nfbc_bus_edin:4;
+	};
+	u32 val;
+};
+
+/* ---- FRAME class (0x0270-0x03f4), fields mirror Vepu510FrmCommon +
+ * the H.264-specific trailer from H264eVepu510Frame in mpp.
+ */
+union rkvenc_reg_enc_pic {
+	struct {
+		u32 enc_stnd:2;			/* 0 = H.264 */
+		u32 cur_frm_ref:1;
+		u32 mei_stor:1;
+		u32 bs_scp:1;
+		u32 reserved0:3;
+		u32 pic_qp:6;
+		u32 num_pic_tot_cur_hevc:5;
+		u32 log2_ctu_num_hevc:5;
+		u32 reserved1:6;
+		u32 slen_fifo:1;
+		u32 rec_fbc_dis:1;
+	};
+	u32 val;
+};
+
+#define RKVENC_ENC_STND_H264		0
+
+/* dual_core (0x304) is the CCU cross-core dual-encoder handshake register
+ * (DCHS_REG_OFFSET in the downstream vendor kernel's mpp_rkvenc2.c) --
+ * used for chaining a frame's encode across both VEPU510 cores. This
+ * driver deliberately doesn't implement that (see the architecture
+ * comment in rkvenc.c), but the register is still written explicitly every
+ * frame to a defined value (dchs_txe=1, i.e. 0x14): the vendor kernel's
+ * own rkvenc2_patch_dchs() writes exactly this on every task, even fully
+ * standalone non-chained ones (confirmed against a real register-write
+ * trace of the vendor stack), so a POR/leftover value is not what the
+ * hardware expects.
+ */
+union rkvenc_reg_dual_core {
+	struct {
+		u32 dchs_txid:2;
+		u32 dchs_rxid:2;
+		u32 dchs_txe:1;
+		u32 dchs_rxe:1;
+		u32 reserved0:2;
+		u32 dchs_dly:8;
+		u32 dchs_ofst:10;
+		u32 reserved1:6;
+	};
+	u32 val;
+};
+
+union rkvenc_reg_enc_id {
+	struct {
+		u32 frame_id:8;
+		u32 frm_id_match:1;
+		u32 reserved0:7;
+		u32 ch_id:2;
+		u32 vrsp_rtn_en:1;
+		u32 vinf_req_en:1;
+		u32 reserved1:12;
+	};
+	u32 val;
+};
+
+/* VEPU510-only quirk value, see rkvenc_vepu510_quirk() in rkvenc-h264.c. */
+#define RKVENC_ENC_ID_QUIRK_VAL		(BIT(18) | BIT(16))
+
+union rkvenc_reg_enc_rsl {
+	struct {
+		u32 pic_wd8_m1:11;
+		u32 reserved0:5;
+		u32 pic_hd8_m1:11;
+		u32 reserved1:5;
+	};
+	u32 val;
+};
+
+union rkvenc_reg_src_fill {
+	struct {
+		u32 pic_wfill:6;
+		u32 reserved0:10;
+		u32 pic_hfill:6;
+		u32 reserved1:10;
+	};
+	u32 val;
+};
+
+union rkvenc_reg_src_fmt {
+	struct {
+		u32 alpha_swap:1;
+		u32 rbuv_swap:1;
+		u32 src_cfmt:4;
+		u32 src_rcne:1;
+		u32 out_fmt:1;
+		u32 src_range_trns_en:1;
+		u32 src_range_trns_sel:1;
+		u32 chroma_ds_mode:1;
+		u32 reserved:21;
+	};
+	u32 val;
+};
+
+/* src_cfmt values (VepuFmt enum, vepu5xx_common.h) — only the one v1 uses. */
+#define RKVENC_SRC_FMT_YUV420SP		6	/* NV12 */
+/* out_fmt (bit7): real capture always has this set (src_fmt == 0x98, not
+ * just src_cfmt's 0x18) for a real successful encode; mpp's own field name
+ * suggests an output-plane format toggle but nothing in the userspace HAL
+ * ever names its meaning beyond the bit position. Treated as a required
+ * "must be 1" bit, like the DVBM-hold quirk.
+ */
+
+union rkvenc_reg_src_proc {
+	struct {
+		u32 cr_force_value:8;
+		u32 cb_force_value:8;
+		u32 chroma_force_en:1;
+		u32 reserved0:9;
+		u32 src_mirr:1;
+		u32 src_rot:2;
+		u32 tile4x4_en:1;
+		u32 reserved1:2;
+	};
+	u32 val;
+};
+
+union rkvenc_reg_pic_ofst {
+	struct {
+		u32 pic_ofst_x:14;
+		u32 reserved0:2;
+		u32 pic_ofst_y:14;
+		u32 reserved1:2;
+	};
+	u32 val;
+};
+
+union rkvenc_reg_src_strd0 {
+	struct {
+		u32 src_strd0:21;
+		u32 reserved:11;
+	};
+	u32 val;
+};
+
+union rkvenc_reg_src_strd1 {
+	struct {
+		u32 src_strd1:16;
+		u32 reserved:16;
+	};
+	u32 val;
+};
+
+union rkvenc_reg_rc_cfg {
+	struct {
+		u32 rc_en:1;
+		u32 aq_en:1;
+		u32 reserved:10;
+		u32 rc_ctu_num:20;
+	};
+	u32 val;
+};
+
+union rkvenc_reg_rc_qp {
+	struct {
+		u32 reserved:16;
+		u32 rc_qp_range:4;
+		u32 rc_max_qp:6;
+		u32 rc_min_qp:6;
+	};
+	u32 val;
+};
+
+union rkvenc_reg_rc_tgt {
+	struct {
+		u32 ctu_ebit:20;
+		u32 reserved:12;
+	};
+	u32 val;
+};
+
+union rkvenc_reg_sli_splt {
+	struct {
+		u32 sli_splt:1;
+		u32 sli_splt_mode:1;
+		u32 sli_splt_cpst:1;
+		u32 reserved0:12;
+		u32 sli_flsh:1;
+		u32 sli_max_num_m1:15;
+		u32 reserved1:1;
+	};
+	u32 val;
+};
+
+union rkvenc_reg_sli_byte {
+	struct {
+		u32 sli_splt_byte:20;
+		u32 reserved:12;
+	};
+	u32 val;
+};
+
+union rkvenc_reg_sli_cnum {
+	struct {
+		u32 sli_splt_cnum_m1:20;
+		u32 reserved:12;
+	};
+	u32 val;
+};
+
+/* H.264-specific trailer, 0x3a0-0x3f4.
+ * rdo_cfg.chrm_spcl/ccwa_e/atr_mult_sel_e: mpp's setup_vepu510_rdo_pred()
+ * (hal_h264e_vepu510.c) sets these three unconditionally to 1 on every
+ * single frame -- not gated by profile/level/tune config like rect_size/
+ * vlc_lmt/atf_e/atr_e are. Found missing by diffing against that function
+ * directly; previously left at their reset-value 0 here since nothing in
+ * this driver ever set them.
+ */
+union rkvenc_reg_rdo_cfg {
+	struct {
+		u32 rect_size:1;
+		u32 reserved0:2;
+		u32 vlc_lmt:1;
+		u32 chrm_spcl:1;
+		u32 reserved1:8;
+		u32 ccwa_e:1;
+		u32 reserved2:1;
+		u32 atr_e:1;
+		u32 reserved3:4;
+		u32 scl_lst_sel:2;
+		u32 reserved4:6;
+		u32 atf_e:1;
+		u32 atr_mult_sel_e:1;
+		u32 reserved5:2;
+	};
+	u32 val;
+};
+
+union rkvenc_reg_synt_nal {
+	struct {
+		u32 nal_ref_idc:2;
+		u32 nal_unit_type:5;
+		u32 reserved:25;
+	};
+	u32 val;
+};
+
+union rkvenc_reg_synt_sps {
+	struct {
+		u32 max_fnum:4;		/* log2_max_frame_num_minus4 */
+		u32 drct_8x8:1;		/* direct_8x8_inference_flag */
+		u32 mpoc_lm4:4;		/* log2_max_pic_order_cnt_lsb_minus4 */
+		u32 poc_type:2;		/* pic_order_cnt_type */
+		u32 reserved:21;
+	};
+	u32 val;
+};
+
+union rkvenc_reg_synt_pps {
+	struct {
+		u32 etpy_mode:1;	/* entropy_coding_mode_flag: 0=CAVLC 1=CABAC */
+		u32 trns_8x8:1;		/* transform_8x8_mode_flag */
+		u32 csip_flag:1;	/* constrained_intra_pred_flag */
+		u32 num_ref0_idx:2;
+		u32 num_ref1_idx:2;
+		u32 pic_init_qp:6;
+		u32 cb_ofst:5;
+		u32 cr_ofst:5;
+		u32 reserved:1;
+		u32 dbf_cp_flg:1;	/* deblocking_filter_control_present_flag */
+		u32 reserved1:7;
+	};
+	u32 val;
+};
+
+union rkvenc_reg_synt_sli0 {
+	struct {
+		u32 sli_type:2;
+		u32 pps_id:8;
+		u32 drct_smvp:1;
+		u32 num_ref_ovrd:1;
+		u32 cbc_init_idc:2;
+		u32 reserved:2;
+		u32 frm_num:16;
+	};
+	u32 val;
+};
+
+#define RKVENC_SLI_TYPE_P		0
+#define RKVENC_SLI_TYPE_I		2
+
+union rkvenc_reg_synt_sli1 {
+	struct {
+		u32 idr_pid:16;
+		u32 poc_lsb:16;
+	};
+	u32 val;
+};
+
+union rkvenc_reg_synt_sli2 {
+	struct {
+		u32 rodr_pic_idx:2;
+		u32 ref_list0_rodr:1;
+		u32 sli_beta_ofst:4;
+		u32 sli_alph_ofst:4;
+		u32 dis_dblk_idc:2;
+		u32 reserved:3;
+		u32 rodr_pic_num:16;
+	};
+	u32 val;
+};
+
+/* Absolute byte offsets of each FRAME-class register this driver touches. */
+#define RKVENC_REG_ADR_SRC0		0x280
+#define RKVENC_REG_ADR_SRC1		0x284
+#define RKVENC_REG_ADR_SRC2		0x288
+#define RKVENC_REG_RFPW_H_ADDR		0x28c
+#define RKVENC_REG_RFPW_B_ADDR		0x290
+#define RKVENC_REG_RFPR_H_ADDR		0x294
+#define RKVENC_REG_RFPR_B_ADDR		0x298
+#define RKVENC_REG_MEIW_ADDR		0x2ac	/* motion-detection-info write ptr */
+#define RKVENC_REG_DSPW_ADDR		0x2a4
+#define RKVENC_REG_DSPR_ADDR		0x2a8
+#define RKVENC_REG_BSBT_ADDR		0x2b0
+#define RKVENC_REG_BSBB_ADDR		0x2b4
+#define RKVENC_REG_ADR_BSBS		0x2b8
+#define RKVENC_REG_BSBR_ADDR		0x2bc
+/* rfpt/rfpb ("top"/"bottom" field recon addresses, interlaced-only):
+ * real capture always carries the literal sentinel 0xffffffff in
+ * rfpt_h/rfpt_b (not an fd-embedded pointer -- an fd of 0x3ff/1023 is
+ * never valid on a real system, so this reads as an explicit
+ * "disabled/no interlaced fields" pattern), with rfpb_h/adr_rfpb_b at 0.
+ */
+#define RKVENC_REG_RFPT_H_ADDR		0x2d0
+#define RKVENC_REG_RFPB_H_ADDR		0x2d4
+#define RKVENC_REG_RFPT_B_ADDR		0x2d8
+#define RKVENC_REG_ADR_RFPB_B		0x2dc
+#define RKVENC_RFPT_DISABLED		0xffffffff
+#define RKVENC_REG_ADR_SMEAR_RD		0x2e0
+#define RKVENC_REG_ADR_SMEAR_WR		0x2e4
+/* Last DMA pointer in the FRAME class (ROI read address). The whole
+ * 0x270-0x2e8 block is zeroed at the top of rkvenc_h264_run() to clear any
+ * stale/garbage write pointers (lpfw/lpfr/ebuft/ebufb at 0x2c0-0x2cc, this,
+ * and 0x29c/0x2a0) that the vendor always writes but this driver otherwise
+ * skips -- see the big comment there.
+ */
+#define RKVENC_REG_ADR_ROIR		0x2e8
+#define RKVENC_REG_ENC_PIC		0x300
+#define RKVENC_REG_DUAL_CORE		0x304
+#define RKVENC_REG_ENC_ID		0x308
+#define RKVENC_REG_ENC_RSL		0x310
+#define RKVENC_REG_SRC_FILL		0x314
+#define RKVENC_REG_SRC_FMT		0x318
+#define RKVENC_REG_SRC_PROC		0x32c
+#define RKVENC_REG_PIC_OFST		0x330
+#define RKVENC_REG_SRC_STRD0		0x334
+#define RKVENC_REG_SRC_STRD1		0x338
+#define RKVENC_REG_RC_CFG		0x350
+#define RKVENC_REG_RC_QP		0x354
+#define RKVENC_REG_RC_TGT		0x358
+#define RKVENC_REG_SLI_SPLT		0x360
+#define RKVENC_REG_SLI_BYTE		0x364
+#define RKVENC_REG_SLI_CNUM		0x368
+
+/* Motion-estimation search-range/config/cache registers: written
+ * *unconditionally* by mpp's setup_vepu510_me() on every single frame
+ * (no conditional guard — not gated behind P-frame/inter-mode use),
+ * same "required regardless of what you'd assume from the feature
+ * name" pattern as the roi_qthd and thumb/smear-buffer registers.
+ * Values below are mpp's own hardcoded constants, not per-content
+ * tuning — a zeroed cime_dist_thre/srgn_max_num here is a plausible
+ * cause of a genuine ME-engine search/loop stall.
+ */
+#define RKVENC_REG_ME_RNGE		0x370
+#define RKVENC_REG_ME_CFG		0x374
+#define RKVENC_REG_ME_CACH		0x378
+
+union rkvenc_reg_me_rnge {
+	struct {
+		u32 cime_srch_dwnh:4;
+		u32 cime_srch_uph:4;
+		u32 cime_srch_rgtw:4;
+		u32 cime_srch_lftw:4;
+		u32 dlt_frm_num:16;
+	};
+	u32 val;
+};
+
+union rkvenc_reg_me_cfg {
+	struct {
+		u32 srgn_max_num:7;
+		u32 cime_dist_thre:13;
+		u32 rme_srch_h:2;
+		u32 rme_srch_v:2;
+		u32 rme_dis:3;
+		u32 reserved:1;
+		u32 fme_dis:3;
+		u32 reserved1:1;
+	};
+	u32 val;
+};
+
+union rkvenc_reg_me_cach {
+	struct {
+		u32 cime_zero_thre:13;
+		u32 reserved:15;
+		u32 fme_prefsu_en:2;
+		u32 colmv_stor_hevc:1;
+		u32 colmv_load_hevc:1;
+	};
+	u32 val;
+};
+
+#define RKVENC_REG_RDO_CFG		0x3a0
+#define RKVENC_REG_SYNT_NAL		0x3b0
+#define RKVENC_REG_SYNT_SPS		0x3b4
+#define RKVENC_REG_SYNT_PPS		0x3b8
+#define RKVENC_REG_SYNT_SLI0		0x3bc
+#define RKVENC_REG_SYNT_SLI1		0x3c0
+#define RKVENC_REG_SYNT_SLI2		0x3c4
+
+/* ---- RC_ROI class (0x1000-0x110c), a.k.a. Vepu510RcRoi in mpp.
+ *
+ * rc_adj0/rc_adj1/rc_dthd_0_8 (0x1000-0x1028) are the adaptive-quant
+ * bit-count-deviation threshold ladder: mpp's setup_vepu510_rc_base()
+ * computes these from the frame's bit budget in its non-FIXQP path, but
+ * skips them entirely (early `return`) in true MPP_ENC_RC_MODE_FIXQP mode
+ * — this driver's original design assumed that meant "safe to leave at
+ * 0", matching disabled RC/AQ. Board bring-up proved that assumption
+ * wrong: leaving rc_dthd_0_8 at 0 (read by hardware as "always past
+ * threshold" rather than mpp's real "never trigger" 0x7FFFFFFF sentinel
+ * for the outer entries) reproduces the exact hardware-watchdog stall on
+ * its own. This driver now always runs with rc_cfg.rc_en=1/aq_en=1 and a
+ * real (if approximate — see rkvenc-h264.c) threshold ladder, but clamps
+ * rc_qp.rc_min_qp==rc_max_qp and rc_qp_range=0 so the AQ engine has no
+ * room to actually move the QP away from the requested value — genuine
+ * fixed-QP *output*, just not fixed-QP *register configuration*.
+ *
+ * roi_qthd0-3: mpp's setup_vepu510_rc_base() writes these
+ * *unconditionally*, before even checking RC mode — every ROI area's
+ * [qpmin, qpmax] clamp range is set to the frame's actual
+ * [quality_min, quality_max] regardless.
+ */
+#define RKVENC_REG_RC_ADJ0		0x1000
+#define RKVENC_REG_RC_ADJ1		0x1004
+#define RKVENC_REG_RC_DTHD(n)		(0x1008 + 4 * (n))	/* n = 0..8 */
+
+union rkvenc_reg_rc_adj0 {
+	struct {
+		s32 qp_adj0:5;
+		s32 qp_adj1:5;
+		s32 qp_adj2:5;
+		s32 qp_adj3:5;
+		s32 qp_adj4:5;
+		u32 reserved:7;
+	};
+	u32 val;
+};
+
+union rkvenc_reg_rc_adj1 {
+	struct {
+		s32 qp_adj5:5;
+		s32 qp_adj6:5;
+		s32 qp_adj7:5;
+		s32 qp_adj8:5;
+		u32 reserved:12;
+	};
+	u32 val;
+};
+
+#define RKVENC_RC_DTHD_SENTINEL	0x7fffffff
+
+#define RKVENC_REG_ROI_QTHD0		0x1030
+#define RKVENC_REG_ROI_QTHD1		0x1034
+#define RKVENC_REG_ROI_QTHD2		0x1038
+#define RKVENC_REG_ROI_QTHD3		0x103c
+
+union rkvenc_reg_roi_qthd0 {
+	struct {
+		u32 qpmin_area0:6;
+		u32 qpmax_area0:6;
+		u32 qpmin_area1:6;
+		u32 qpmax_area1:6;
+		u32 qpmin_area2:6;
+		u32 reserved:2;
+	};
+	u32 val;
+};
+
+union rkvenc_reg_roi_qthd1 {
+	struct {
+		u32 qpmax_area2:6;
+		u32 qpmin_area3:6;
+		u32 qpmax_area3:6;
+		u32 qpmin_area4:6;
+		u32 qpmax_area4:6;
+		u32 reserved:2;
+	};
+	u32 val;
+};
+
+union rkvenc_reg_roi_qthd2 {
+	struct {
+		u32 qpmin_area5:6;
+		u32 qpmax_area5:6;
+		u32 qpmin_area6:6;
+		u32 qpmax_area6:6;
+		u32 qpmin_area7:6;
+		u32 reserved:2;
+	};
+	u32 val;
+};
+
+union rkvenc_reg_roi_qthd3 {
+	struct {
+		u32 qpmax_area7:6;
+		u32 reserved:24;
+		u32 qpmap_mode:2;
+	};
+	u32 val;
+};
+
+/* RC_ROI tail (0x1044-0x107c): AQ activity threshold/step LUT, MAD-based
+ * statistics thresholds, and a chroma KLUT offset -- all real fields in
+ * mpp's Vepu510RcRoi struct (vepu510_common.h) that this driver used to
+ * leave entirely unwritten. Found by diffing this driver's register writes
+ * against mpp's actual setup_vepu510_aq()/setup_vepu510_me()/
+ * setup_vepu510_rdo_pred() (hal_h264e_vepu510.c), not just the single
+ * I-frame wtrace capture used to fix the original whole-class stall (see
+ * the RC_ROI banner comment above) -- that capture never exercised these
+ * particular offsets either, since a from-scratch driver only writes what
+ * it's told to, and nothing had told it to write these yet.
+ *
+ * mpp writes aq_tthd/aq_stp/madi_st_thd/madp_st_thd0/madp_st_thd1
+ * unconditionally on every frame regardless of slice type (setup_vepu510_me()
+ * has no I/P branch for these), and klut_ofst.chrm_klut_ofst is 6 for I
+ * slices and 6 or 9 for P slices depending on an IPC scene-tuning mode this
+ * driver doesn't expose (so 6 is correct for both here). These sit in
+ * exactly the register class board bring-up already proved needs
+ * non-degenerate content for the hardware to complete at all (see the RC_ROI
+ * banner comment) -- left at POR/leftover 0 instead of mpp's real nonzero
+ * table, this is a plausible source of a real internal stall that happens to
+ * only bite once real per-block activity/MAD statistics get computed and
+ * compared against these thresholds, i.e. during genuine inter-mode
+ * (P-frame) analysis rather than intra.
+ */
+#define RKVENC_REG_AQ_TTHD		0x1044	/* 4 words, 16 packed u8 thresholds */
+#define RKVENC_REG_AQ_STP0		0x1054
+#define RKVENC_REG_AQ_STP1		0x1058
+#define RKVENC_REG_AQ_STP2		0x105c
+#define RKVENC_REG_MADI_ST_THD		0x1064
+#define RKVENC_REG_MADP_ST_THD0	0x1068
+#define RKVENC_REG_MADP_ST_THD1	0x106c
+#define RKVENC_REG_KLUT_OFST		0x107c
+
+union rkvenc_reg_aq_stp0 {
+	struct {
+		s32 aq_stp_s0:5;
+		s32 aq_stp_0t1:5;
+		s32 aq_stp_1t2:5;
+		s32 aq_stp_2t3:5;
+		s32 aq_stp_3t4:5;
+		s32 aq_stp_4t5:5;
+		u32 reserved:2;
+	};
+	u32 val;
+};
+
+union rkvenc_reg_aq_stp1 {
+	struct {
+		s32 aq_stp_5t6:5;
+		s32 aq_stp_6t7:5;
+		s32 aq_stp_7t8:5;
+		s32 aq_stp_8t9:5;
+		s32 aq_stp_9t10:5;
+		s32 aq_stp_10t11:5;
+		u32 reserved:2;
+	};
+	u32 val;
+};
+
+union rkvenc_reg_aq_stp2 {
+	struct {
+		s32 aq_stp_11t12:5;
+		s32 aq_stp_12t13:5;
+		s32 aq_stp_13t14:5;
+		s32 aq_stp_14t15:5;
+		s32 aq_stp_b15:5;
+		u32 reserved:7;
+	};
+	u32 val;
+};
+
+union rkvenc_reg_madi_st_thd {
+	struct {
+		u32 madi_th0:8;
+		u32 madi_th1:8;
+		u32 madi_th2:8;
+		u32 reserved:8;
+	};
+	u32 val;
+};
+
+union rkvenc_reg_madp_st_thd0 {
+	struct {
+		u32 madp_th0:12;
+		u32 reserved:4;
+		u32 madp_th1:12;
+		u32 reserved1:4;
+	};
+	u32 val;
+};
+
+union rkvenc_reg_madp_st_thd1 {
+	struct {
+		u32 madp_th2:12;
+		u32 reserved:20;
+	};
+	u32 val;
+};
+
+union rkvenc_reg_klut_ofst {
+	struct {
+		u32 chrm_klut_ofst:4;
+		u32 reserved:4;
+		u32 inter_chrm_dist_multi:6;
+		u32 reserved1:18;
+	};
+	u32 val;
+};
+
+/* ---- PARAM class (0x1700-0x19cc), a.k.a. Vepu510Param in mpp.
+ * RDO lambda/cost tables plus a handful of small quant-tuning tables ahead
+ * of them. mpp treats the bulk of this as a *default-tuning profile*
+ * constant table (`vepu51x_h264e_lambda_default_60`/`_cvr_60`, selected by
+ * a tuning "lambda_idx" knob this driver doesn't expose, not by
+ * resolution/QP/content) — so a fixed constant table here, captured from
+ * a real successful encode via wtrace and cross-checked against the
+ * table's monotonically-increasing-lambda shape, is a deliberate v1
+ * choice, not a placeholder. See RKVENC_PAR_CLASS_WORDS in rkvenc-h264.c.
+ *
+ * CORRECTION (2026-07-21, from a real multi-frame wtrace capture): the
+ * "static blob, same for every frame" characterization above is WRONG for
+ * four specific words inside it -- mpp's setup_vepu510_anti_ringing()
+ * (hal_h264e_vepu510.c) computes real, different ATR (anti-ringing) weight
+ * values depending on I-slice vs P-slice, not just the tune/scene-mode
+ * config a single I-frame capture can see. A 3-frame real capture (frame 0
+ * I, frames 1-2 P) showed these four offsets change between the I-frame and
+ * BOTH P-frames, while staying identical between the two P-frames --
+ * conclusive evidence of an I/P split, not per-content noise. This driver's
+ * static `rkvenc_par_class` blob was captured from an I-frame-only session,
+ * so every P-frame was silently getting the I-slice ATR weights. See
+ * RKVENC_REG_ATR_THD1/WGT16/WGT8/WGT4 below and the patch in rkvenc-h264.c.
+ */
+#define RKVENC_REG_PAR_OFFSET		0x1700
+#define RKVENC_REG_PAR_SIZE		0x02d0	/* 0x19cc - 0x1700 + 4 */
+
+#define RKVENC_REG_ATR_THD1		0x1744	/* thd2 (I/P-dependent) + thdqp (constant) */
+#define RKVENC_REG_ATR_WGT16		0x1750
+#define RKVENC_REG_ATR_WGT8		0x1754
+#define RKVENC_REG_ATR_WGT4		0x1758
+
+union rkvenc_reg_atr_thd1 {
+	struct {
+		u32 thd2:8;
+		u32 reserved0:8;
+		u32 thdqp:6;
+		u32 reserved1:10;
+	};
+	u32 val;
+};
+
+union rkvenc_reg_atr_wgt {
+	struct {
+		u32 wgt0:8;
+		u32 wgt1:8;
+		u32 wgt2:8;
+		u32 reserved:8;
+	};
+	u32 val;
+};
+
+/* ---- SQI class (0x2000-0x212c), "subjective Adjust" quant tuning.
+ * Same default-tuning-profile characterization as PARAM above — a real
+ * successful encode only ever wrote the first 0x2000-0x20b8 (the rest of
+ * the declared class range was never touched even by a genuinely working
+ * encode, so this driver doesn't write it either).
+ *
+ * CORRECTION (2026-07-21): same caveat as PARAM above applies to one word,
+ * smear_opt_cfg (0x2014, mpp's setup_vepu510_anti_smear()). Its
+ * `stated_mode` field is 1 if (this slice is I) OR (the *previous* encoded
+ * frame was I), else 2 -- a real multi-frame capture confirmed this exactly
+ * (frame 0 I: 1; frame 1 P, previous=I: 1; frame 2 P, previous=P: 2), pure
+ * slice-history state this driver can track trivially. Its
+ * `rdo_smear_dlt_qp` field is a genuinely different, harder case: mpp
+ * derives it from real per-block "smear" activity statistics read back from
+ * the *previous* frame's actual hardware encode (`ctx->last_frame_fb`,
+ * populated from DEBUG-class counters this driver has never implemented
+ * readback for) -- not just slice type. The capture showed -1 (I) then -3
+ * (both P frames), which this driver approximates as a static per-slice-
+ * type constant (real content-adaptive tracking is a follow-up, see
+ * this driver approximates it, since implementing full smear-statistics readback is out of
+ * scope here and this field's effect is RDO cost tuning, not known to be
+ * hang-relevant like the RC_ROI class was.
+ */
+#define RKVENC_REG_SQI_OFFSET		0x2000
+#define RKVENC_REG_SQI_SIZE		0x00bc	/* 0x20b8 - 0x2000 + 4, not the full class */
+
+#define RKVENC_REG_SMEAR_OPT_CFG	0x2014
+
+union rkvenc_reg_smear_opt_cfg {
+	struct {
+		u32 rdo_smear_lvl16_multi:8;
+		s32 rdo_smear_dlt_qp:4;
+		u32 reserved:1;
+		u32 stated_mode:2;
+		u32 rdo_smear_en:1;
+		u32 reserved1:16;
+	};
+	u32 val;
+};
+
+/* ---- SCL class (0x2200-0x2584), scaling lists.
+ * All-zero for any encode that doesn't use a custom H.264 scaling list
+ * (this driver never does) — written explicitly anyway, every frame,
+ * rather than relying on POR/leftover-state being zero.
+ */
+#define RKVENC_REG_SCL_OFFSET		0x2200
+#define RKVENC_REG_SCL_SIZE		0x0388	/* 0x2584 - 0x2200 + 4, full class */
+
+/* ---- STATUS class (0x4000-0x424c), read-only ---- */
+#define RKVENC_REG_ST_BS_LENGTH	0x4000	/* bs_lgth_l32: this-frame byte count */
+#define RKVENC_REG_ST_BSB	0x402c	/* current bitstream write pointer */
+#define RKVENC_REG_ST_SLICE_NUM	0x4034	/* slice-fifo occupancy, & 0x3f */
+#define RKVENC_REG_ST_SLICE_LEN	0x4038	/* slice-length fifo read port */
+
+#define RKVENC_SLICE_LEN_LAST		BIT(31)
+
+/* ---- DEBUG class ---- */
+#define RKVENC_REG_DBG_CLR		0x5300
+#define RKVENC_DBG_CLR_VAL		0x2
+
+#endif /* RKVENC_REGS_H_ */
diff --git a/drivers/media/platform/rockchip/rkvenc/rkvenc.c b/drivers/media/platform/rockchip/rkvenc/rkvenc.c
new file mode 100644
index 000000000..d70bcc5fb
--- /dev/null
+++ b/drivers/media/platform/rockchip/rkvenc/rkvenc.c
@@ -0,0 +1,892 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Rockchip VEPU510 (RK3576) hardware video encoder driver.
+ *
+ * Copyright (C) 2026 Jiaxing Hu <gahing@gahingwoo.com>
+ *
+ * Architecture notes (see also rkvenc-regs.h):
+ *
+ *  - This is a *stateful* V4L2 mem2mem encoder (raw NV12 in on OUTPUT,
+ *    H.264 Annex-B out on CAPTURE) modelled on
+ *    drivers/media/platform/verisilicon's hantro_drv.c device_run()/
+ *    codec_ops{run,done} split, NOT on rkvdec's stateless request-API
+ *    decoder pattern — an encoder has no per-frame bitstream to parse,
+ *    so there is nothing analogous to rkvdec_run_preamble/postamble here.
+ *
+ *  - The vendor downstream driver (rockchip-linux/kernel,
+ *    drivers/video/rockchip/mpp/mpp_rkvenc2.c) groups rkvenc0/rkvenc1
+ *    under a "CCU" (rockchip,rkv-encoder-rk3576-ccu) that does pure
+ *    software task-queue load balancing across both cores, plus an
+ *    optional DCHS (dual-core-handshake) register protocol used only
+ *    when *deliberately* splitting one frame's rows across both cores.
+ *    There is no hardware descriptor/link-list engine behind it (unlike
+ *    the decoder's CCU). v1 of this driver does not implement either:
+ *    rkvenc0 and rkvenc1 are exposed as two independent V4L2 M2M device
+ *    nodes, each driving one physical core standalone — the same
+ *    simplification rkvdec itself makes for multi-core VDPU hardware
+ *    (see rkvdec_disable_multicore()).
+ */
+
+#include <linux/clk.h>
+#include <linux/delay.h>
+#include <linux/dma-mapping.h>
+#include <linux/interrupt.h>
+#include <linux/iopoll.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/pm_runtime.h>
+#include <linux/reset.h>
+#include <linux/slab.h>
+
+#include <media/v4l2-event.h>
+#include <media/v4l2-ioctl.h>
+#include <media/v4l2-mem2mem.h>
+#include <media/videobuf2-dma-contig.h>
+
+#include "rkvenc.h"
+#include "rkvenc-regs.h"
+
+#define RKVENC_WATCHDOG_TIMEOUT_MS	800
+#define RKVENC_DEFAULT_WIDTH		176
+#define RKVENC_DEFAULT_HEIGHT		144
+#define RKVENC_MAX_WIDTH		4096
+#define RKVENC_MAX_HEIGHT		2560
+
+#define RKVENC_AUTOSUSPEND_DELAY_MS		100
+
+/* Per-resolution hardware watchdog threshold table, ported from the
+ * downstream vendor driver's rkvenc2_calc_timeout_thd() /
+ * rkvenc2_timeout_thd_by_rsl[]. VEPU510 scales the ENC_WDG register in
+ * units of 256 core-clock cycles (other VEPU variants use 1024) — this
+ * ×256 factor is confirmed from the vendor kernel source. The field
+ * itself (vs_load_thd, bits[23:0] of ENC_WDG per mpp's Vepu510ControlCfg)
+ * is 24 bits wide, not "the register's top byte" as an earlier reading of
+ * the vendor kernel driver's comments suggested; that earlier reading
+ * produced a wrong shift (see git history).
+ */
+struct rkvenc_wdg_entry {
+	u32 pixels;
+	u32 ms;
+};
+
+static const struct rkvenc_wdg_entry rkvenc_wdg_table[] = {
+	{ 1920 * 1088, 50 },
+	{ 2560 * 1440, 100 },
+	{ 4096 * 2304, 200 },
+	{ 8192 * 8192, 400 },
+	{ 15360 * 8640, 800 },
+};
+
+u32 rkvenc_calc_wdg(struct rkvenc_dev *dev, unsigned int width,
+		    unsigned int height)
+{
+	unsigned int pixels = width * height;
+	unsigned int ms = rkvenc_wdg_table[ARRAY_SIZE(rkvenc_wdg_table) - 1].ms;
+	u64 cycles;
+	u32 thd;
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(rkvenc_wdg_table); i++) {
+		if (pixels <= rkvenc_wdg_table[i].pixels) {
+			ms = rkvenc_wdg_table[i].ms;
+			break;
+		}
+	}
+
+	cycles = (u64)ms * (dev->core_clk_rate / 1000);
+	do_div(cycles, 256);
+	thd = min_t(u64, cycles, 0xffffff);
+
+	return thd;
+}
+
+/* ---- format helpers ---- */
+
+static const u32 rkvenc_src_fourcc = V4L2_PIX_FMT_NV12;
+
+static const struct rkvenc_coded_fmt_desc *rkvenc_coded_descs[] = {
+	&rkvenc_h264_fmt_desc,
+};
+
+static const struct rkvenc_coded_fmt_desc *rkvenc_find_coded_desc(u32 fourcc)
+{
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(rkvenc_coded_descs); i++) {
+		if (rkvenc_coded_descs[i]->fourcc == fourcc)
+			return rkvenc_coded_descs[i];
+	}
+
+	return NULL;
+}
+
+static u32 rkvenc_nv12_sizeimage(unsigned int width, unsigned int height)
+{
+	return width * height + 2 * ALIGN(width, 2) / 2 * ALIGN(height, 2) / 2 * 2;
+}
+
+static u32 rkvenc_coded_sizeimage(unsigned int width, unsigned int height)
+{
+	/* Generous fixed upper bound for a single fixed-QP baseline frame;
+	 * revisit once real CBR/VBR budget control lands.
+	 */
+	return max_t(u32, width * height, SZ_128K);
+}
+
+static void rkvenc_fill_src_fmt(struct v4l2_pix_format *fmt)
+{
+	fmt->pixelformat = rkvenc_src_fourcc;
+	fmt->width = clamp_t(u32, fmt->width, RKVENC_DEFAULT_WIDTH, RKVENC_MAX_WIDTH);
+	fmt->height = clamp_t(u32, fmt->height, RKVENC_DEFAULT_HEIGHT, RKVENC_MAX_HEIGHT);
+	fmt->width = ALIGN(fmt->width, 2);
+	fmt->height = ALIGN(fmt->height, 2);
+	fmt->field = V4L2_FIELD_NONE;
+	fmt->colorspace = V4L2_COLORSPACE_REC709;
+	fmt->bytesperline = fmt->width;
+	fmt->sizeimage = rkvenc_nv12_sizeimage(fmt->width, fmt->height);
+}
+
+static void rkvenc_fill_coded_fmt(struct v4l2_pix_format *fmt,
+				  const struct rkvenc_coded_fmt_desc *desc)
+{
+	fmt->pixelformat = desc->fourcc;
+	fmt->width = clamp_t(u32, fmt->width, desc->frmsize.frmsize_min_width,
+			      desc->frmsize.frmsize_max_width);
+	fmt->height = clamp_t(u32, fmt->height, desc->frmsize.frmsize_min_height,
+			       desc->frmsize.frmsize_max_height);
+	fmt->width = ALIGN(fmt->width, 2);
+	fmt->height = ALIGN(fmt->height, 2);
+	fmt->field = V4L2_FIELD_NONE;
+	fmt->bytesperline = 0;
+	fmt->sizeimage = rkvenc_coded_sizeimage(fmt->width, fmt->height);
+}
+
+/* ---- ioctl_ops ---- */
+
+static int rkvenc_querycap(struct file *file, void *priv,
+			   struct v4l2_capability *cap)
+{
+	strscpy(cap->driver, "rkvenc", sizeof(cap->driver));
+	strscpy(cap->card, "rockchip,rkv-encoder-rk3576", sizeof(cap->card));
+	return 0;
+}
+
+static int rkvenc_enum_fmt_vid_cap(struct file *file, void *priv,
+				   struct v4l2_fmtdesc *f)
+{
+	if (f->index >= ARRAY_SIZE(rkvenc_coded_descs))
+		return -EINVAL;
+
+	f->pixelformat = rkvenc_coded_descs[f->index]->fourcc;
+	f->flags = V4L2_FMT_FLAG_COMPRESSED;
+	return 0;
+}
+
+static int rkvenc_enum_fmt_vid_out(struct file *file, void *priv,
+				   struct v4l2_fmtdesc *f)
+{
+	if (f->index != 0)
+		return -EINVAL;
+
+	f->pixelformat = rkvenc_src_fourcc;
+	return 0;
+}
+
+static int rkvenc_g_fmt_vid_cap(struct file *file, void *priv,
+				struct v4l2_format *f)
+{
+	struct rkvenc_ctx *ctx = fh_to_rkvenc_ctx(file_to_v4l2_fh(file));
+
+	f->fmt.pix = ctx->coded_fmt;
+	return 0;
+}
+
+static int rkvenc_g_fmt_vid_out(struct file *file, void *priv,
+				struct v4l2_format *f)
+{
+	struct rkvenc_ctx *ctx = fh_to_rkvenc_ctx(file_to_v4l2_fh(file));
+
+	f->fmt.pix = ctx->src_fmt;
+	return 0;
+}
+
+static int rkvenc_try_fmt_vid_cap(struct file *file, void *priv,
+				  struct v4l2_format *f)
+{
+	const struct rkvenc_coded_fmt_desc *desc;
+
+	desc = rkvenc_find_coded_desc(f->fmt.pix.pixelformat);
+	if (!desc)
+		desc = rkvenc_coded_descs[0];
+
+	rkvenc_fill_coded_fmt(&f->fmt.pix, desc);
+	return 0;
+}
+
+static int rkvenc_try_fmt_vid_out(struct file *file, void *priv,
+				  struct v4l2_format *f)
+{
+	rkvenc_fill_src_fmt(&f->fmt.pix);
+	return 0;
+}
+
+static int rkvenc_s_fmt_vid_cap(struct file *file, void *priv,
+				struct v4l2_format *f)
+{
+	struct rkvenc_ctx *ctx = fh_to_rkvenc_ctx(file_to_v4l2_fh(file));
+	const struct rkvenc_coded_fmt_desc *desc;
+
+	if (vb2_is_busy(v4l2_m2m_get_dst_vq(ctx->fh.m2m_ctx)))
+		return -EBUSY;
+
+	desc = rkvenc_find_coded_desc(f->fmt.pix.pixelformat);
+	if (!desc)
+		desc = rkvenc_coded_descs[0];
+
+	rkvenc_fill_coded_fmt(&f->fmt.pix, desc);
+	ctx->coded_fmt = f->fmt.pix;
+	ctx->coded_desc = desc;
+	return 0;
+}
+
+static int rkvenc_s_fmt_vid_out(struct file *file, void *priv,
+				struct v4l2_format *f)
+{
+	struct rkvenc_ctx *ctx = fh_to_rkvenc_ctx(file_to_v4l2_fh(file));
+
+	if (vb2_is_busy(v4l2_m2m_get_src_vq(ctx->fh.m2m_ctx)))
+		return -EBUSY;
+
+	rkvenc_fill_src_fmt(&f->fmt.pix);
+	ctx->src_fmt = f->fmt.pix;
+	return 0;
+}
+
+static const struct v4l2_ioctl_ops rkvenc_ioctl_ops = {
+	.vidioc_querycap		= rkvenc_querycap,
+
+	.vidioc_enum_fmt_vid_cap	= rkvenc_enum_fmt_vid_cap,
+	.vidioc_g_fmt_vid_cap		= rkvenc_g_fmt_vid_cap,
+	.vidioc_try_fmt_vid_cap		= rkvenc_try_fmt_vid_cap,
+	.vidioc_s_fmt_vid_cap		= rkvenc_s_fmt_vid_cap,
+
+	.vidioc_enum_fmt_vid_out	= rkvenc_enum_fmt_vid_out,
+	.vidioc_g_fmt_vid_out		= rkvenc_g_fmt_vid_out,
+	.vidioc_try_fmt_vid_out		= rkvenc_try_fmt_vid_out,
+	.vidioc_s_fmt_vid_out		= rkvenc_s_fmt_vid_out,
+
+	.vidioc_reqbufs			= v4l2_m2m_ioctl_reqbufs,
+	.vidioc_querybuf		= v4l2_m2m_ioctl_querybuf,
+	.vidioc_qbuf			= v4l2_m2m_ioctl_qbuf,
+	.vidioc_dqbuf			= v4l2_m2m_ioctl_dqbuf,
+	.vidioc_prepare_buf		= v4l2_m2m_ioctl_prepare_buf,
+	.vidioc_create_bufs		= v4l2_m2m_ioctl_create_bufs,
+	.vidioc_expbuf			= v4l2_m2m_ioctl_expbuf,
+	.vidioc_streamon		= v4l2_m2m_ioctl_streamon,
+	.vidioc_streamoff		= v4l2_m2m_ioctl_streamoff,
+
+	.vidioc_try_encoder_cmd		= v4l2_m2m_ioctl_try_encoder_cmd,
+	.vidioc_encoder_cmd		= v4l2_m2m_ioctl_encoder_cmd,
+
+	.vidioc_subscribe_event		= v4l2_ctrl_subscribe_event,
+	.vidioc_unsubscribe_event	= v4l2_event_unsubscribe,
+};
+
+/* ---- vb2_ops ---- */
+
+static int rkvenc_queue_setup(struct vb2_queue *vq, unsigned int *num_buffers,
+			      unsigned int *num_planes, unsigned int sizes[],
+			      struct device *alloc_devs[])
+{
+	struct rkvenc_ctx *ctx = vb2_get_drv_priv(vq);
+	struct v4l2_pix_format *fmt;
+
+	fmt = V4L2_TYPE_IS_OUTPUT(vq->type) ? &ctx->src_fmt : &ctx->coded_fmt;
+
+	if (*num_planes) {
+		if (sizes[0] < fmt->sizeimage)
+			return -EINVAL;
+	} else {
+		*num_planes = 1;
+		sizes[0] = fmt->sizeimage;
+	}
+
+	return 0;
+}
+
+static int rkvenc_buf_prepare(struct vb2_buffer *vb)
+{
+	struct vb2_v4l2_buffer *vbuf = to_vb2_v4l2_buffer(vb);
+	struct rkvenc_ctx *ctx = vb2_get_drv_priv(vb->vb2_queue);
+	struct v4l2_pix_format *fmt;
+
+	fmt = V4L2_TYPE_IS_OUTPUT(vb->vb2_queue->type) ? &ctx->src_fmt : &ctx->coded_fmt;
+
+	if (vb2_plane_size(vb, 0) < fmt->sizeimage)
+		return -EINVAL;
+
+	if (V4L2_TYPE_IS_OUTPUT(vb->vb2_queue->type))
+		vb2_set_plane_payload(vb, 0, fmt->sizeimage);
+	else
+		vbuf->field = V4L2_FIELD_NONE;
+
+	return 0;
+}
+
+static void rkvenc_buf_queue(struct vb2_buffer *vb)
+{
+	struct rkvenc_ctx *ctx = vb2_get_drv_priv(vb->vb2_queue);
+
+	v4l2_m2m_buf_queue(ctx->fh.m2m_ctx, to_vb2_v4l2_buffer(vb));
+}
+
+static int rkvenc_start_streaming(struct vb2_queue *q, unsigned int count)
+{
+	struct rkvenc_ctx *ctx = vb2_get_drv_priv(q);
+	int ret = 0;
+
+	if (V4L2_TYPE_IS_OUTPUT(q->type) && ctx->coded_desc && ctx->coded_desc->ops->start)
+		ret = ctx->coded_desc->ops->start(ctx);
+
+	if (ret) {
+		struct vb2_v4l2_buffer *vbuf;
+
+		while ((vbuf = v4l2_m2m_src_buf_remove(ctx->fh.m2m_ctx)))
+			v4l2_m2m_buf_done(vbuf, VB2_BUF_STATE_QUEUED);
+	}
+
+	return ret;
+}
+
+static void rkvenc_stop_streaming(struct vb2_queue *q)
+{
+	struct rkvenc_ctx *ctx = vb2_get_drv_priv(q);
+	struct vb2_v4l2_buffer *vbuf;
+
+	for (;;) {
+		if (V4L2_TYPE_IS_OUTPUT(q->type))
+			vbuf = v4l2_m2m_src_buf_remove(ctx->fh.m2m_ctx);
+		else
+			vbuf = v4l2_m2m_dst_buf_remove(ctx->fh.m2m_ctx);
+
+		if (!vbuf)
+			break;
+
+		v4l2_m2m_buf_done(vbuf, VB2_BUF_STATE_ERROR);
+	}
+
+	if (V4L2_TYPE_IS_OUTPUT(q->type) && ctx->coded_desc && ctx->coded_desc->ops->stop)
+		ctx->coded_desc->ops->stop(ctx);
+}
+
+static const struct vb2_ops rkvenc_qops = {
+	.queue_setup	= rkvenc_queue_setup,
+	.buf_prepare	= rkvenc_buf_prepare,
+	.buf_queue	= rkvenc_buf_queue,
+	.start_streaming = rkvenc_start_streaming,
+	.stop_streaming	= rkvenc_stop_streaming,
+};
+
+static int rkvenc_queue_init(void *priv, struct vb2_queue *src_vq,
+			     struct vb2_queue *dst_vq)
+{
+	struct rkvenc_ctx *ctx = priv;
+	int ret;
+
+	src_vq->type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
+	src_vq->io_modes = VB2_MMAP | VB2_DMABUF;
+	src_vq->drv_priv = ctx;
+	src_vq->buf_struct_size = sizeof(struct v4l2_m2m_buffer);
+	src_vq->ops = &rkvenc_qops;
+	src_vq->mem_ops = &vb2_dma_contig_memops;
+	src_vq->timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_COPY;
+	src_vq->lock = &ctx->dev->vfd_mutex;
+	src_vq->dev = ctx->dev->v4l2_dev.dev;
+	ret = vb2_queue_init(src_vq);
+	if (ret)
+		return ret;
+
+	dst_vq->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+	dst_vq->io_modes = VB2_MMAP | VB2_DMABUF;
+	dst_vq->drv_priv = ctx;
+	dst_vq->buf_struct_size = sizeof(struct v4l2_m2m_buffer);
+	dst_vq->ops = &rkvenc_qops;
+	dst_vq->mem_ops = &vb2_dma_contig_memops;
+	dst_vq->timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_COPY;
+	dst_vq->lock = &ctx->dev->vfd_mutex;
+	dst_vq->dev = ctx->dev->v4l2_dev.dev;
+	return vb2_queue_init(dst_vq);
+}
+
+/* ---- v4l2_m2m_ops ---- */
+
+void rkvenc_job_finish(struct rkvenc_dev *dev, struct rkvenc_ctx *ctx,
+		       enum vb2_buffer_state state)
+{
+	clk_bulk_disable(dev->num_clocks, dev->clocks);
+	pm_runtime_mark_last_busy(dev->dev);
+	pm_runtime_put_autosuspend(dev->dev);
+
+	dev->cur_ctx = NULL;
+	v4l2_m2m_buf_done_and_job_finish(dev->m2m_dev, ctx->fh.m2m_ctx, state);
+}
+
+static void rkvenc_device_run(void *priv)
+{
+	struct rkvenc_ctx *ctx = priv;
+	struct rkvenc_dev *dev = ctx->dev;
+	int ret;
+
+	ret = pm_runtime_resume_and_get(dev->dev);
+	if (ret < 0) {
+		v4l2_m2m_buf_done_and_job_finish(dev->m2m_dev, ctx->fh.m2m_ctx,
+						  VB2_BUF_STATE_ERROR);
+		return;
+	}
+
+	ret = clk_bulk_enable(dev->num_clocks, dev->clocks);
+	if (ret) {
+		pm_runtime_put_autosuspend(dev->dev);
+		v4l2_m2m_buf_done_and_job_finish(dev->m2m_dev, ctx->fh.m2m_ctx,
+						  VB2_BUF_STATE_ERROR);
+		return;
+	}
+
+	v4l2_m2m_buf_copy_metadata(rkvenc_get_src_buf(ctx), rkvenc_get_dst_buf(ctx));
+
+	dev->cur_ctx = ctx;
+	dev->irq_status = 0;
+	schedule_delayed_work(&dev->watchdog_work,
+			      msecs_to_jiffies(RKVENC_WATCHDOG_TIMEOUT_MS));
+
+	ctx->coded_desc->ops->run(ctx);
+}
+
+static const struct v4l2_m2m_ops rkvenc_m2m_ops = {
+	.device_run = rkvenc_device_run,
+};
+
+/* ---- IRQ handling ---- */
+
+static bool rkvenc_drain_slices(struct rkvenc_dev *dev)
+{
+	unsigned int count = rkvenc_read(dev, RKVENC_REG_ST_SLICE_NUM) & 0x3f;
+	bool last = false;
+	unsigned int i;
+	u32 val;
+
+	for (i = 0; i < count; i++) {
+		val = rkvenc_read(dev, RKVENC_REG_ST_SLICE_LEN);
+		if (val & BIT(31))
+			last = true;
+	}
+
+	return last;
+}
+
+static irqreturn_t rkvenc_irq(int irq, void *priv)
+{
+	struct rkvenc_dev *dev = priv;
+	u32 status = rkvenc_read(dev, RKVENC_REG_INT_STA);
+	bool done = false;
+
+	rkvenc_write(dev, RKVENC_REG_INT_CLR, status);
+
+	if (status & BIT(8) /* wdg_sta */) {
+		u32 mask = rkvenc_read(dev, RKVENC_REG_INT_MASK);
+
+		rkvenc_write(dev, RKVENC_REG_INT_MASK, mask | BIT(8));
+	}
+
+	if (!dev->cur_ctx)
+		return status ? IRQ_HANDLED : IRQ_NONE;
+
+	/* VEPU510 errata (vendor comment): int_sta ENC_DONE is not reliable
+	 * on its own — the hardware has been observed to skip raising it
+	 * even though the frame is actually finished. The slice-length
+	 * FIFO's "last" flag (always populated because ENC_PIC.SLEN_FIFO is
+	 * forced on for every VEPU510 task, see rkvenc_vepu510_quirk()) is
+	 * the authoritative completion signal instead.
+	 */
+	if (rkvenc_drain_slices(dev)) {
+		rkvenc_write(dev, RKVENC_REG_ENC_ID, 0);
+		udelay(5);
+		status |= rkvenc_read(dev, RKVENC_REG_INT_STA);
+		done = true;
+	}
+
+	if (status & (BIT(0) /* enc_done_sta */ | RKVENC_INT_ERROR_MASK))
+		done = true;
+
+	dev->irq_status |= status;
+
+	return done ? IRQ_WAKE_THREAD : IRQ_HANDLED;
+}
+
+/* Graceful core recovery after an error/timeout, mirroring the downstream
+ * vendor kernel's rkvenc_soft_reset() (mpp_rkvenc2.c): a SAFE clear via
+ * ENC_CLR drains the core's outstanding AXI transactions so the IOMMU can
+ * disable cleanly, then a FORCE clear + interrupt-state wipe returns the core
+ * to a defined state. Without this, a hung P-frame leaves the core wedged and
+ * an unacknowledged write fault stuck active, which cascades into the NEXT
+ * encode failing ("rk_iommu Disable paging request timed out"). Only ever
+ * called from the IRQ error path, so it can never touch a good frame; clocks
+ * are still enabled here (rkvenc_job_finish disables them afterwards).
+ */
+static void rkvenc_soft_reset(struct rkvenc_dev *dev)
+{
+	u32 sta;
+	int ret;
+
+	rkvenc_write(dev, RKVENC_REG_INT_MASK, 0x3ff);
+	rkvenc_write(dev, RKVENC_REG_ENC_CLR, RKVENC_ENC_CLR_SAFE);
+	ret = readl_relaxed_poll_timeout(dev->regs + RKVENC_REG_INT_STA, sta,
+					 sta & RKVENC_SCLR_DONE_STA, 0, 1000);
+	rkvenc_write(dev, RKVENC_REG_ENC_CLR, RKVENC_ENC_CLR_FORCE);
+	udelay(5);
+	rkvenc_write(dev, RKVENC_REG_ENC_CLR, 0);
+	rkvenc_write(dev, RKVENC_REG_INT_CLR, 0xffffffff);
+	rkvenc_write(dev, RKVENC_REG_INT_STA, 0);
+
+	if (ret) {
+		/* Safe clear never completed -- fall back to a full core reset. */
+		dev_warn(dev->dev, "safe reset timed out, forcing core reset\n");
+		reset_control_assert(dev->rst);
+		udelay(5);
+		reset_control_deassert(dev->rst);
+	}
+}
+
+static irqreturn_t rkvenc_irq_thread(int irq, void *priv)
+{
+	struct rkvenc_dev *dev = priv;
+	struct rkvenc_ctx *ctx = dev->cur_ctx;
+	enum vb2_buffer_state state;
+
+	if (!ctx)
+		return IRQ_HANDLED;
+
+	if (!cancel_delayed_work(&dev->watchdog_work))
+		return IRQ_HANDLED;
+
+	state = (dev->irq_status & RKVENC_INT_ERROR_MASK) ?
+		VB2_BUF_STATE_ERROR : VB2_BUF_STATE_DONE;
+
+	if (state == VB2_BUF_STATE_ERROR) {
+		dev_warn(dev->dev,
+			 "encode error, int_sta=0x%08x (error bits: %s%s%s%s%s%s%s%s) "
+			 "version=0x%08x enc_strt=0x%08x int_en=0x%08x int_msk=0x%08x "
+			 "enc_wdg=0x%08x enc_rsl=0x%08x enc_pic=0x%08x synt_sli0=0x%08x "
+			 "bs_lgth=0x%08x slice_num=0x%08x st_bsb=0x%08x\n",
+			 dev->irq_status,
+			 dev->irq_status & BIT(4) ? "vbsf_oflw " : "",
+			 dev->irq_status & BIT(6) ? "enc_err " : "",
+			 dev->irq_status & BIT(7) ? "vsrc_err " : "",
+			 dev->irq_status & BIT(8) ? "wdg " : "",
+			 dev->irq_status & BIT(9) ? "lkt_err_int " : "",
+			 dev->irq_status & BIT(10) ? "lkt_err_stop " : "",
+			 dev->irq_status & BIT(11) ? "lkt_force_stop " : "",
+			 dev->irq_status & BIT(15) ? "dvbm_err " : "",
+			 /* version is a fixed IP-identification readback: if this
+			  * comes back 0x00000000 or 0xffffffff, the MMIO region
+			  * itself is dead (clock/power/mapping), not an encode
+			  * config problem — a much more fundamental class of bug
+			  * than anything register-value-level we've audited so far.
+			  */
+			 rkvenc_read(dev, RKVENC_REG_VERSION),
+			 rkvenc_read(dev, RKVENC_REG_ENC_START),
+			 rkvenc_read(dev, RKVENC_REG_INT_EN),
+			 rkvenc_read(dev, RKVENC_REG_INT_MASK),
+			 rkvenc_read(dev, RKVENC_REG_ENC_WDG),
+			 rkvenc_read(dev, RKVENC_REG_ENC_RSL),
+			 rkvenc_read(dev, RKVENC_REG_ENC_PIC),
+			 rkvenc_read(dev, RKVENC_REG_SYNT_SLI0),
+			 rkvenc_read(dev, RKVENC_REG_ST_BS_LENGTH),
+			 rkvenc_read(dev, RKVENC_REG_ST_SLICE_NUM),
+			 rkvenc_read(dev, RKVENC_REG_ST_BSB));
+
+		/* Release the DVBM/encoder hold (only the drain-slices completion
+		 * path did this before) and gracefully reset the wedged core so
+		 * the fault is drained and the NEXT encode starts clean.
+		 */
+		rkvenc_write(dev, RKVENC_REG_ENC_ID, 0);
+		rkvenc_soft_reset(dev);
+	}
+
+	ctx->coded_desc->ops->done(ctx, state);
+	rkvenc_job_finish(dev, ctx, state);
+
+	return IRQ_HANDLED;
+}
+
+static void rkvenc_watchdog(struct work_struct *work)
+{
+	struct rkvenc_dev *dev = container_of(work, struct rkvenc_dev,
+					      watchdog_work.work);
+	struct rkvenc_ctx *ctx = dev->cur_ctx;
+
+	if (!ctx)
+		return;
+
+	dev_warn(dev->dev, "encode timeout, resetting core\n");
+
+	reset_control_assert(dev->rst);
+	udelay(5);
+	reset_control_deassert(dev->rst);
+
+	ctx->coded_desc->ops->done(ctx, VB2_BUF_STATE_ERROR);
+	rkvenc_job_finish(dev, ctx, VB2_BUF_STATE_ERROR);
+}
+
+/* ---- ctrls / file ops ---- */
+
+static int rkvenc_open(struct file *file)
+{
+	struct rkvenc_dev *dev = video_drvdata(file);
+	struct rkvenc_ctx *ctx;
+	int ret;
+
+	ctx = kzalloc_obj(*ctx);
+	if (!ctx)
+		return -ENOMEM;
+
+	v4l2_fh_init(&ctx->fh, &dev->vfd);
+	file->private_data = &ctx->fh;
+	ctx->dev = dev;
+
+	ctx->coded_desc = rkvenc_coded_descs[0];
+	ctx->coded_fmt.width = RKVENC_DEFAULT_WIDTH;
+	ctx->coded_fmt.height = RKVENC_DEFAULT_HEIGHT;
+	rkvenc_fill_coded_fmt(&ctx->coded_fmt, ctx->coded_desc);
+	ctx->src_fmt.width = RKVENC_DEFAULT_WIDTH;
+	ctx->src_fmt.height = RKVENC_DEFAULT_HEIGHT;
+	rkvenc_fill_src_fmt(&ctx->src_fmt);
+
+	v4l2_ctrl_handler_init(&ctx->ctrl_handler, 16);
+	ctx->fh.ctrl_handler = &ctx->ctrl_handler;
+
+	ret = ctx->coded_desc->ops->init_ctrls(ctx);
+	if (!ret)
+		ret = ctx->ctrl_handler.error;
+	if (!ret)
+		ret = v4l2_ctrl_handler_setup(&ctx->ctrl_handler);
+	if (ret)
+		goto err_free_handler;
+
+	ctx->fh.m2m_ctx = v4l2_m2m_ctx_init(dev->m2m_dev, ctx, rkvenc_queue_init);
+	if (IS_ERR(ctx->fh.m2m_ctx)) {
+		ret = PTR_ERR(ctx->fh.m2m_ctx);
+		goto err_free_handler;
+	}
+
+	v4l2_fh_add(&ctx->fh, file);
+
+	return 0;
+
+err_free_handler:
+	v4l2_ctrl_handler_free(&ctx->ctrl_handler);
+	v4l2_fh_exit(&ctx->fh);
+	kfree(ctx);
+	return ret;
+}
+
+static int rkvenc_release(struct file *file)
+{
+	struct rkvenc_ctx *ctx = fh_to_rkvenc_ctx(file->private_data);
+
+	v4l2_fh_del(&ctx->fh, file);
+	v4l2_m2m_ctx_release(ctx->fh.m2m_ctx);
+	v4l2_ctrl_handler_free(&ctx->ctrl_handler);
+	v4l2_fh_exit(&ctx->fh);
+	kfree(ctx);
+	return 0;
+}
+
+static const struct v4l2_file_operations rkvenc_fops = {
+	.owner		= THIS_MODULE,
+	.open		= rkvenc_open,
+	.release	= rkvenc_release,
+	.poll		= v4l2_m2m_fop_poll,
+	.unlocked_ioctl	= video_ioctl2,
+	.mmap		= v4l2_m2m_fop_mmap,
+};
+
+/* ---- probe/remove ---- */
+
+static int rkvenc_runtime_suspend(struct device *dev)
+{
+	struct rkvenc_dev *rkvenc = dev_get_drvdata(dev);
+
+	clk_bulk_disable_unprepare(rkvenc->num_clocks, rkvenc->clocks);
+	return 0;
+}
+
+static int rkvenc_runtime_resume(struct device *dev)
+{
+	struct rkvenc_dev *rkvenc = dev_get_drvdata(dev);
+
+	return clk_bulk_prepare_enable(rkvenc->num_clocks, rkvenc->clocks);
+}
+
+static const struct dev_pm_ops rkvenc_pm_ops = {
+	SET_RUNTIME_PM_OPS(rkvenc_runtime_suspend, rkvenc_runtime_resume, NULL)
+};
+
+static int rkvenc_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct rkvenc_dev *rkvenc;
+	struct clk_bulk_data *clk;
+	int irq, ret, i;
+
+	rkvenc = devm_kzalloc(dev, sizeof(*rkvenc), GFP_KERNEL);
+	if (!rkvenc)
+		return -ENOMEM;
+
+	rkvenc->dev = dev;
+	platform_set_drvdata(pdev, rkvenc);
+	mutex_init(&rkvenc->vfd_mutex);
+	INIT_DELAYED_WORK(&rkvenc->watchdog_work, rkvenc_watchdog);
+
+	rkvenc->regs = devm_platform_ioremap_resource(pdev, 0);
+	if (IS_ERR(rkvenc->regs))
+		return PTR_ERR(rkvenc->regs);
+
+	/* Buffer pointer registers (adr_src0/1/2, bsb*, rfp*) are 32 bits
+	 * wide; each core sits behind its own rkvenc{0,1}_mmu IOMMU, which
+	 * this DMA mask keeps within a 32-bit IOVA aperture.
+	 */
+	ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32));
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to set DMA mask\n");
+
+	irq = platform_get_irq(pdev, 0);
+	if (irq < 0)
+		return irq;
+
+	ret = devm_clk_bulk_get_all(dev, &rkvenc->clocks);
+	if (ret < 0)
+		return dev_err_probe(dev, ret, "failed to get clocks\n");
+	rkvenc->num_clocks = ret;
+
+	for (i = 0; i < rkvenc->num_clocks; i++) {
+		clk = &rkvenc->clocks[i];
+		if (!strcmp(clk->id, "clk_core"))
+			rkvenc->core_clk_rate = clk_get_rate(clk->clk);
+	}
+	if (!rkvenc->core_clk_rate)
+		rkvenc->core_clk_rate = 702000000;
+
+	rkvenc->rst = devm_reset_control_array_get_exclusive(dev);
+	if (IS_ERR(rkvenc->rst))
+		return dev_err_probe(dev, PTR_ERR(rkvenc->rst),
+				     "failed to get resets\n");
+
+	ret = devm_request_threaded_irq(dev, irq, rkvenc_irq, rkvenc_irq_thread,
+					IRQF_ONESHOT, dev_name(dev), rkvenc);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to request irq\n");
+
+	ret = v4l2_device_register(dev, &rkvenc->v4l2_dev);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to register v4l2 device\n");
+
+	rkvenc->m2m_dev = v4l2_m2m_init(&rkvenc_m2m_ops);
+	if (IS_ERR(rkvenc->m2m_dev)) {
+		ret = PTR_ERR(rkvenc->m2m_dev);
+		goto err_unreg_v4l2;
+	}
+
+	rkvenc->vfd = (struct video_device){
+		.fops		= &rkvenc_fops,
+		.ioctl_ops	= &rkvenc_ioctl_ops,
+		.minor		= -1,
+		.release	= video_device_release_empty,
+		.lock		= &rkvenc->vfd_mutex,
+		.v4l2_dev	= &rkvenc->v4l2_dev,
+		.vfl_dir	= VFL_DIR_M2M,
+		.device_caps	= V4L2_CAP_VIDEO_M2M | V4L2_CAP_STREAMING,
+	};
+	strscpy(rkvenc->vfd.name, "rkvenc", sizeof(rkvenc->vfd.name));
+	video_set_drvdata(&rkvenc->vfd, rkvenc);
+
+	pm_runtime_set_autosuspend_delay(dev, RKVENC_AUTOSUSPEND_DELAY_MS);
+	pm_runtime_use_autosuspend(dev);
+	devm_pm_runtime_enable(dev);
+
+	/*
+	 * One-time reset cycle (via a real pm_runtime activation, so the
+	 * power domain and clocks are genuinely on rather than assuming raw
+	 * clk_bulk_prepare_enable() alone would power an unattached genpd)
+	 * so the core starts from a defined state rather than whatever the
+	 * bootloader/ATF left it in -- otherwise reset is only ever touched
+	 * by rkvenc_watchdog()'s error-recovery path, never on a normal
+	 * first activation. Board bring-up saw an isolated rk_iommu write
+	 * fault at an IOVA that changed between boots and matched none of
+	 * this driver's own buffer addresses, consistent with undefined
+	 * boot-varying hardware state.
+	 *
+	 * Done once here, not on every rkvenc_runtime_resume(): that was
+	 * tried first and made things worse (a hardware-reported "enc_err"
+	 * status on the very next encode, with a bitstream length wildly
+	 * larger than the same test frame ever produced before) -- something
+	 * about this core's state is expected to persist for the life of the
+	 * session, not be re-initialized before every single frame.
+	 */
+	ret = pm_runtime_resume_and_get(dev);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to power up for initial reset\n");
+	reset_control_assert(rkvenc->rst);
+	udelay(5);
+	reset_control_deassert(rkvenc->rst);
+	pm_runtime_mark_last_busy(dev);
+	pm_runtime_put_autosuspend(dev);
+
+	ret = video_register_device(&rkvenc->vfd, VFL_TYPE_VIDEO, -1);
+	if (ret) {
+		dev_err_probe(dev, ret, "failed to register video device\n");
+		goto err_release_m2m;
+	}
+
+	dev_info(dev, "VEPU510 encoder core registered as %s\n",
+		 video_device_node_name(&rkvenc->vfd));
+
+	return 0;
+
+err_release_m2m:
+	v4l2_m2m_release(rkvenc->m2m_dev);
+err_unreg_v4l2:
+	v4l2_device_unregister(&rkvenc->v4l2_dev);
+	return ret;
+}
+
+static void rkvenc_remove(struct platform_device *pdev)
+{
+	struct rkvenc_dev *rkvenc = platform_get_drvdata(pdev);
+
+	cancel_delayed_work_sync(&rkvenc->watchdog_work);
+	video_unregister_device(&rkvenc->vfd);
+	v4l2_m2m_release(rkvenc->m2m_dev);
+	v4l2_device_unregister(&rkvenc->v4l2_dev);
+}
+
+static const struct of_device_id rkvenc_of_match[] = {
+	{ .compatible = "rockchip,rk3576-vepu" },
+	{}
+};
+MODULE_DEVICE_TABLE(of, rkvenc_of_match);
+
+static struct platform_driver rkvenc_driver = {
+	.probe = rkvenc_probe,
+	.remove = rkvenc_remove,
+	.driver = {
+		.name = "rkvenc",
+		.of_match_table = rkvenc_of_match,
+		.pm = &rkvenc_pm_ops,
+	},
+};
+module_platform_driver(rkvenc_driver);
+
+MODULE_DESCRIPTION("Rockchip VEPU510 video encoder driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/media/platform/rockchip/rkvenc/rkvenc.h b/drivers/media/platform/rockchip/rkvenc/rkvenc.h
new file mode 100644
index 000000000..3e3e30de3
--- /dev/null
+++ b/drivers/media/platform/rockchip/rkvenc/rkvenc.h
@@ -0,0 +1,212 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Rockchip VEPU510 (RK3576) hardware video encoder driver.
+ *
+ * Copyright (C) 2026 Jiaxing Hu <gahing@gahingwoo.com>
+ */
+
+#ifndef RKVENC_H_
+#define RKVENC_H_
+
+#include <linux/clk.h>
+#include <linux/completion.h>
+#include <linux/platform_device.h>
+#include <linux/reset.h>
+#include <linux/videodev2.h>
+#include <linux/workqueue.h>
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-mem2mem.h>
+#include <media/videobuf2-core.h>
+
+struct rkvenc_ctx;
+
+struct rkvenc_fmt {
+	u32 fourcc;
+	unsigned int frmsize_min_width;
+	unsigned int frmsize_max_width;
+	unsigned int frmsize_step_width;
+	unsigned int frmsize_min_height;
+	unsigned int frmsize_max_height;
+	unsigned int frmsize_step_height;
+};
+
+/*
+ * Per-coded-format (H.264, later HEVC) operation vtable. Mirrors the split
+ * rkvdec uses for its per-codec backends, adapted for a *stateful* mem2mem
+ * encoder (no V4L2 request-API preamble/postamble, unlike rkvdec) — the
+ * device_run()/done() split matches drivers/media/platform/verisilicon's
+ * hantro_codec_ops instead.
+ */
+struct rkvenc_coded_fmt_ops {
+	/* Registers this format's V4L2 controls into ctx->ctrl_handler. */
+	int (*init_ctrls)(struct rkvenc_ctx *ctx);
+	int (*start)(struct rkvenc_ctx *ctx);
+	void (*stop)(struct rkvenc_ctx *ctx);
+	void (*run)(struct rkvenc_ctx *ctx);
+	/* Called from the threaded IRQ handler once hardware reports frame
+	 * done. Must set the CAPTURE buffer's bytesused/timestamp/flags.
+	 */
+	void (*done)(struct rkvenc_ctx *ctx, enum vb2_buffer_state state);
+};
+
+struct rkvenc_coded_fmt_desc {
+	u32 fourcc;
+	struct rkvenc_fmt frmsize;
+	const struct rkvenc_coded_fmt_ops *ops;
+};
+
+struct rkvenc_dev {
+	struct v4l2_device v4l2_dev;
+	struct video_device vfd;
+	struct v4l2_m2m_dev *m2m_dev;
+
+	struct device *dev;
+	void __iomem *regs;
+	struct clk_bulk_data *clocks;
+	int num_clocks;
+	struct reset_control *rst;
+
+	/* Protects ctx list / hardware access ordering. vb2 queues already
+	 * serialize per-context streaming; this additionally serializes
+	 * access to the one physical core across contexts (rkvenc0/rkvenc1
+	 * are two separate rkvenc_dev instances, each owning one core —
+	 * v1 does not implement the vendor driver's CCU cross-core task
+	 * dispatch/load-balancing).
+	 */
+	struct mutex vfd_mutex;
+
+	struct rkvenc_ctx *cur_ctx;
+	struct delayed_work watchdog_work;
+
+	unsigned int core_clk_rate;
+
+	/* Accumulated by the hard IRQ handler, consumed by the threaded one. */
+	u32 irq_status;
+};
+
+struct rkvenc_h264_ctx {
+	struct v4l2_ctrl *bitrate;
+	struct v4l2_ctrl *profile;
+	struct v4l2_ctrl *level;
+	struct v4l2_ctrl *entropy_mode;
+	struct v4l2_ctrl *gop_size;
+	struct v4l2_ctrl *i_qp;
+	struct v4l2_ctrl *p_qp;
+
+	unsigned int frame_num;
+	unsigned int poc_lsb;
+	unsigned int gop_pos;
+	unsigned int idr_pic_id;
+
+	/* Whether the *previous* encoded frame was an IDR -- mpp's
+	 * setup_vepu510_anti_smear() keys smear_opt_cfg.stated_mode off both
+	 * the current slice type AND the previous frame's, see rkvenc-regs.h.
+	 * Zeroed by rkvenc_h264_start()'s memset; doesn't matter for frame 0
+	 * (always IDR regardless).
+	 */
+	bool last_frame_was_idr;
+
+	/* Software-synthesized SPS/PPS, emitted (Annex-B) ahead of the first
+	 * hardware slice payload of each IDR access unit; see rkvenc-h264.c.
+	 */
+	u8 sps_pps_nal[64];
+	size_t sps_pps_len;
+
+	/* VEPU510 reconstructs/references frames only through three internal
+	 * scratch regions per frame slot — an FBC-compressed pixel buffer
+	 * (header+body), a "thumbnail" downsample buffer (dspw/dspr_addr),
+	 * and a "smear" buffer (adr_smear_wr/rd) — all three are written by
+	 * hardware on *every* frame regardless of which encoder features are
+	 * in use (confirmed from mpp's setup_vepu510_recn_refr(), which sets
+	 * all three unconditionally whenever a current/reference buffer slot
+	 * exists at all). Leaving any of them at a NULL pointer causes a
+	 * continuous IOMMU write-fault storm at iova 0x0, not a clean error.
+	 *
+	 * This driver owns a 2-deep ping-pong pool of driver-private DMA
+	 * buffers: buf[frame_num % 2] is this frame's write target,
+	 * buf[(frame_num + 1) % 2] is the previous frame's read reference —
+	 * EXCEPT on the very first frame of a session (frame_num == 0), where
+	 * there is no previously-written reference at all: mpp's real
+	 * hal_h264e_vepu510.c setup_vepu510_recn_refr() draws the "read" side
+	 * from a `refr` ping-pong slot that equals the `curr` (write) slot
+	 * index for frame 0, i.e. real hardware gets read == write, the same
+	 * buffer aliased both ways, not a second never-written allocation.
+	 * See rkvenc_h264_run()'s read_idx computation.
+	 *
+	 * Each buffer is one dma_alloc_coherent() allocation split into three
+	 * consecutive regions [0, pixel_buf_size), [pixel_buf_size,
+	 * +thumb_buf_size), [+thumb_buf_size, +smear_buf_size). Sizes are
+	 * mpp's exact setup_vepu510_prepare_recn_bufs() formulas, not a guess.
+	 */
+	void *recn_buf_cpu[2];
+	dma_addr_t recn_buf_dma[2];
+	size_t pixel_buf_size;	/* FBC header + body */
+	size_t pixel_hdr_size;	/* FBC header portion of pixel_buf_size */
+	size_t thumb_buf_size;
+	size_t smear_buf_size;
+
+	/* Motion-detection-info write buffer (meiw_addr). Real hardware
+	 * writes here even though nothing in the userspace HAL documents the
+	 * feature as required for a plain encode — board bring-up found a
+	 * real successful encode always has a real address here, so this
+	 * driver allocates one rather than assuming 0 is safe. Content is
+	 * never read back (v1 doesn't expose motion vectors to userspace).
+	 */
+	void *meiw_buf_cpu;
+	dma_addr_t meiw_buf_dma;
+};
+
+struct rkvenc_ctx {
+	struct v4l2_fh fh;
+	struct rkvenc_dev *dev;
+
+	struct v4l2_pix_format src_fmt;	/* OUTPUT: raw NV12 */
+	struct v4l2_pix_format coded_fmt; /* CAPTURE: H264/HEVC */
+	const struct rkvenc_coded_fmt_desc *coded_desc;
+
+	struct v4l2_ctrl_handler ctrl_handler;
+
+	union {
+		struct rkvenc_h264_ctx h264;
+	};
+};
+
+static inline struct rkvenc_ctx *fh_to_rkvenc_ctx(struct v4l2_fh *fh)
+{
+	return container_of(fh, struct rkvenc_ctx, fh);
+}
+
+static inline u32 rkvenc_read(struct rkvenc_dev *dev, u32 reg)
+{
+	return readl(dev->regs + reg);
+}
+
+static inline void rkvenc_write(struct rkvenc_dev *dev, u32 reg, u32 val)
+{
+	writel(val, dev->regs + reg);
+}
+
+static inline void rkvenc_write_relaxed(struct rkvenc_dev *dev, u32 reg, u32 val)
+{
+	writel_relaxed(val, dev->regs + reg);
+}
+
+static inline struct vb2_v4l2_buffer *rkvenc_get_src_buf(struct rkvenc_ctx *ctx)
+{
+	return v4l2_m2m_next_src_buf(ctx->fh.m2m_ctx);
+}
+
+static inline struct vb2_v4l2_buffer *rkvenc_get_dst_buf(struct rkvenc_ctx *ctx)
+{
+	return v4l2_m2m_next_dst_buf(ctx->fh.m2m_ctx);
+}
+
+extern const struct rkvenc_coded_fmt_desc rkvenc_h264_fmt_desc;
+
+void rkvenc_job_finish(struct rkvenc_dev *dev, struct rkvenc_ctx *ctx,
+		       enum vb2_buffer_state state);
+u32 rkvenc_calc_wdg(struct rkvenc_dev *dev, unsigned int width,
+		    unsigned int height);
+
+#endif /* RKVENC_H_ */
-- 
2.43.0


_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip

^ permalink raw reply related	[flat|nested] 5+ messages in thread

* [RFC PATCH 3/3] arm64: dts: rockchip: rk3576: add VEPU H.264 encoder nodes
  2026-07-22  7:34 [RFC PATCH 0/3] media: rockchip: VEPU510 H.264 encoder for RK3576 Jiaxing Hu
  2026-07-22  7:34 ` [RFC PATCH 1/3] dt-bindings: media: add Rockchip RK3576 VEPU H.264 encoder Jiaxing Hu
  2026-07-22  7:34 ` [RFC PATCH 2/3] media: rockchip: add VEPU510 H.264 encoder driver for RK3576 Jiaxing Hu
@ 2026-07-22  7:34 ` Jiaxing Hu
  2 siblings, 0 replies; 5+ messages in thread
From: Jiaxing Hu @ 2026-07-22  7:34 UTC (permalink / raw)
  To: mchehab, heiko, robh, krzk+dt, conor+dt, nicolas.dufresne
  Cc: ezequiel, linux-media, devicetree, linux-rockchip,
	linux-arm-kernel, linux-kernel, Jiaxing Hu

Add the two VEPU510 encoder cores at 0x27a00000 / 0x27a10000 and their
IOMMUs.

Signed-off-by: Jiaxing Hu <gahing@gahingwoo.com>
---
 arch/arm64/boot/dts/rockchip/rk3576.dtsi | 50 ++++++++++++++++++++++++++++++++
 1 file changed, 50 insertions(+)

diff --git a/arch/arm64/boot/dts/rockchip/rk3576.dtsi b/arch/arm64/boot/dts/rockchip/rk3576.dtsi
index b622c6fee..9bf4f2c41 100644
--- a/arch/arm64/boot/dts/rockchip/rk3576.dtsi
+++ b/arch/arm64/boot/dts/rockchip/rk3576.dtsi
@@ -1320,6 +1320,56 @@ vdec_mmu: iommu@27b00800 {
 			#iommu-cells = <0>;
 		};
 
+		vepu0: video-codec@27a00000 {
+			compatible = "rockchip,rk3576-vepu";
+			reg = <0x0 0x27a00000 0x0 0x6000>;
+			interrupts = <GIC_SPI 310 IRQ_TYPE_LEVEL_HIGH>;
+			clocks = <&cru ACLK_VEPU0>, <&cru HCLK_VEPU0>, <&cru CLK_VEPU0_CORE>;
+			clock-names = "aclk_vcodec", "hclk_vcodec", "clk_core";
+			assigned-clocks = <&cru ACLK_VEPU0>, <&cru CLK_VEPU0_CORE>;
+			assigned-clock-rates = <400000000>, <702000000>;
+			resets = <&cru SRST_A_VEPU0>, <&cru SRST_H_VEPU0>, <&cru SRST_VEPU0_CORE>;
+			reset-names = "video_a", "video_h", "video_core";
+			power-domains = <&power RK3576_PD_VEPU0>;
+			iommus = <&vepu0_mmu>;
+		};
+
+		vepu0_mmu: iommu@27a0f000 {
+			compatible = "rockchip,rk3576-iommu", "rockchip,rk3568-iommu";
+			reg = <0x0 0x27a0f000 0x0 0x40>;
+			interrupts = <GIC_SPI 311 IRQ_TYPE_LEVEL_HIGH>;
+			clocks = <&cru ACLK_VEPU0>, <&cru HCLK_VEPU0>;
+			clock-names = "aclk", "iface";
+			power-domains = <&power RK3576_PD_VEPU0>;
+			rockchip,disable-mmu-reset;
+			#iommu-cells = <0>;
+		};
+
+		vepu1: video-codec@27a10000 {
+			compatible = "rockchip,rk3576-vepu";
+			reg = <0x0 0x27a10000 0x0 0x6000>;
+			interrupts = <GIC_SPI 387 IRQ_TYPE_LEVEL_HIGH>;
+			clocks = <&cru ACLK_VEPU1>, <&cru HCLK_VEPU1>, <&cru CLK_VEPU1_CORE>;
+			clock-names = "aclk_vcodec", "hclk_vcodec", "clk_core";
+			assigned-clocks = <&cru ACLK_VEPU1>, <&cru CLK_VEPU1_CORE>;
+			assigned-clock-rates = <400000000>, <702000000>;
+			resets = <&cru SRST_A_VEPU1>, <&cru SRST_H_VEPU1>, <&cru SRST_VEPU1_CORE>;
+			reset-names = "video_a", "video_h", "video_core";
+			power-domains = <&power RK3576_PD_VEPU1>;
+			iommus = <&vepu1_mmu>;
+		};
+
+		vepu1_mmu: iommu@27a1f000 {
+			compatible = "rockchip,rk3576-iommu", "rockchip,rk3568-iommu";
+			reg = <0x0 0x27a1f000 0x0 0x40>;
+			interrupts = <GIC_SPI 388 IRQ_TYPE_LEVEL_HIGH>;
+			clocks = <&cru ACLK_VEPU1>, <&cru HCLK_VEPU1>;
+			clock-names = "aclk", "iface";
+			power-domains = <&power RK3576_PD_VEPU1>;
+			rockchip,disable-mmu-reset;
+			#iommu-cells = <0>;
+		};
+
 		vop: vop@27d00000 {
 			compatible = "rockchip,rk3576-vop";
 			reg = <0x0 0x27d00000 0x0 0x3000>, <0x0 0x27d05000 0x0 0x1000>;
-- 
2.43.0


_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip

^ permalink raw reply related	[flat|nested] 5+ messages in thread

* Re: [RFC PATCH 2/3] media: rockchip: add VEPU510 H.264 encoder driver for RK3576
  2026-07-22  7:34 ` [RFC PATCH 2/3] media: rockchip: add VEPU510 H.264 encoder driver for RK3576 Jiaxing Hu
@ 2026-07-22 10:00   ` Heiko Stübner
  0 siblings, 0 replies; 5+ messages in thread
From: Heiko Stübner @ 2026-07-22 10:00 UTC (permalink / raw)
  To: mchehab, robh, krzk+dt, conor+dt, nicolas.dufresne, Jiaxing Hu
  Cc: ezequiel, linux-media, devicetree, linux-rockchip,
	linux-arm-kernel, linux-kernel, Jiaxing Hu

Am Mittwoch, 22. Juli 2026, 09:34:16 Mitteleuropäische Sommerzeit schrieb Jiaxing Hu:
> Add a from-scratch stateful V4L2 mem2mem driver for the Rockchip RK3576
> VEPU510 H.264 hardware video encoder (raw NV12 in, H.264 Annex-B out),
> modelled on the verisilicon/hantro device_run()/codec_ops split rather
> than the downstream MPP-service/task-queue model.
> 
> The two encoder cores (rkvenc0/rkvenc1) are exposed as two independent
> V4L2 M2M device nodes, each driving one physical core standalone; the
> downstream vendor CCU cross-core load-balancing is not implemented.
> 
> Register field semantics were worked out by trial-and-error against
> Rockchip's own open-source userspace codec library (rockchip-linux/mpp)
> and cross-checked against a real register-write trace of the downstream
> vendor stack running the same encode.
> 
> Intra (I-frame) encoding is confirmed working on real hardware (Radxa
> ROCK 4D): it produces valid H.264 that the reference decoders accept.
> Inter (P-frame) encoding still hits a hardware-watchdog stall in the
> reference-read datapath -- this is the main open question for this RFC;
> see the cover letter for the full symptom analysis.
> 
> Signed-off-by: Jiaxing Hu <gahing@gahingwoo.com>

[...]

> diff --git a/drivers/media/platform/rockchip/rkvenc/rkvenc.c b/drivers/media/platform/rockchip/rkvenc/rkvenc.c
> new file mode 100644
> index 000000000..d70bcc5fb
> --- /dev/null
> +++ b/drivers/media/platform/rockchip/rkvenc/rkvenc.c
> @@ -0,0 +1,892 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * Rockchip VEPU510 (RK3576) hardware video encoder driver.
> + *
> + * Copyright (C) 2026 Jiaxing Hu <gahing@gahingwoo.com>
> + *
> + * Architecture notes (see also rkvenc-regs.h):
> + *
> + *  - This is a *stateful* V4L2 mem2mem encoder (raw NV12 in on OUTPUT,
> + *    H.264 Annex-B out on CAPTURE) modelled on
> + *    drivers/media/platform/verisilicon's hantro_drv.c device_run()/
> + *    codec_ops{run,done} split, NOT on rkvdec's stateless request-API
> + *    decoder pattern — an encoder has no per-frame bitstream to parse,
> + *    so there is nothing analogous to rkvdec_run_preamble/postamble here.
> + *
> + *  - The vendor downstream driver (rockchip-linux/kernel,
> + *    drivers/video/rockchip/mpp/mpp_rkvenc2.c) groups rkvenc0/rkvenc1
> + *    under a "CCU" (rockchip,rkv-encoder-rk3576-ccu) that does pure
> + *    software task-queue load balancing across both cores, plus an
> + *    optional DCHS (dual-core-handshake) register protocol used only
> + *    when *deliberately* splitting one frame's rows across both cores.
> + *    There is no hardware descriptor/link-list engine behind it (unlike
> + *    the decoder's CCU). v1 of this driver does not implement either:
> + *    rkvenc0 and rkvenc1 are exposed as two independent V4L2 M2M device
> + *    nodes, each driving one physical core standalone — the same
> + *    simplification rkvdec itself makes for multi-core VDPU hardware
> + *    (see rkvdec_disable_multicore()).

At least the comment and from a casual look also the code get this
backwards. The idea is to explicitly _not_ expose multiple video devices.

See
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/media/platform/rockchip/rkvdec/rkvdec.c#n1613

Any (future) scheduling should happen inside the driver, and not get
offloaded onto _every_ userspace application individually.


Heiko



_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip

^ permalink raw reply	[flat|nested] 5+ messages in thread

end of thread, other threads:[~2026-07-22 10:00 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-22  7:34 [RFC PATCH 0/3] media: rockchip: VEPU510 H.264 encoder for RK3576 Jiaxing Hu
2026-07-22  7:34 ` [RFC PATCH 1/3] dt-bindings: media: add Rockchip RK3576 VEPU H.264 encoder Jiaxing Hu
2026-07-22  7:34 ` [RFC PATCH 2/3] media: rockchip: add VEPU510 H.264 encoder driver for RK3576 Jiaxing Hu
2026-07-22 10:00   ` Heiko Stübner
2026-07-22  7:34 ` [RFC PATCH 3/3] arm64: dts: rockchip: rk3576: add VEPU H.264 encoder nodes Jiaxing Hu

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox