Devicetree
 help / color / mirror / Atom feed
* [PATCH 00/11] media: amlogic: vdec: S4 decoding beyond H.264 Baseline
@ 2026-10-25 19:24 christian.koever-draxl
  2026-10-25 19:24 ` [PATCH 01/11] MAINTAINERS: fix the Amlogic VDEC binding path christian.koever-draxl
                   ` (11 more replies)
  0 siblings, 12 replies; 20+ messages in thread
From: christian.koever-draxl @ 2026-10-25 19:24 UTC (permalink / raw)
  To: zhentao.guo, mchehab, robh, krzk+dt, conor+dt
  Cc: linux-media, devicetree, linux-kernel,
	Christian Stefan Kövér-Draxl

From: Christian Stefan Kövér-Draxl <ckoever@outlook.de>

This builds on Zhentao Guo's "[PATCH v6 0/4] Add Amlogic V4L2 stateless
decoder" and makes the S4 decoder usable for real streams:

 1-2:  MAINTAINERS path fix, binding update for the second (HEVC) core;
 3:    microcode loading that the secure world accepts, HEVC core setup;
 4:    H.264 Main and High profile, bit-exact;
 5-6:  MPEG-1/2 and MJPEG on the VDEC core;
 7-9:  AV1, VP9 (profile 0 and 2) and HEVC (Main, Main 10) on the HEVC
       core, up to 4K;
 10:   MPEG-4 Part 2 SP/ASP;
 11:   regmap naming.

Bit-exactness was checked against ffmpeg/libavcodec frame by frame for
H.264, VP9, AV1 and HEVC conformance and real-world streams; MPEG-2
differs by the hardware's IDCT rounding. Tested with GStreamer
v4l2sl*dec and a VA-API backend (Firefox) on an S905W2.

Depends on Zhentao's v6 series (not resent).

Tool use (Documentation/process/generated-content.rst):
This work was done with the help of an AI coding assistant, Claude
(Anthropic), used through Claude Code over many sessions. It did a large
part of the hands-on work: it read the vendor kernel and the firmware
interfaces, probed registers on the board, drafted the code and the
changelogs, and ran the builds, checkpatch, dt_binding_check and the
tests on the hardware. I directed the work, reviewed every change and
the final patches, and take responsibility for them. There is no single
prompt to quote; the sessions were of the form "find out why X fails on
this board, fix it, test it on the box".

Tested on an Amediatech X98Q TV box (Amlogic S905W2, S4 family, 2 GiB),
vendor U-Boot and BL31/OP-TEE, Linux 7.3 plus this work.

Christian Stefan Kövér-Draxl (11):
  MAINTAINERS: fix the Amlogic VDEC binding path
  dt-bindings: media: amlogic,s4-vdec: describe both decoder cores
  media: amlogic: vdec: fix the microcode loading and prepare the HEVC
    core
  media: amlogic: vdec: h264: decode Main and High profile bit-exact
  media: amlogic: vdec: add MPEG-1/2 decoding
  media: amlogic: vdec: add MJPEG decoding
  media: amlogic: vdec: add AV1 decoding on the HEVC core
  media: amlogic: vdec: add VP9 decoding on the HEVC core
  media: amlogic: vdec: add HEVC decoding on the HEVC core
  media: amlogic: vdec: add MPEG-4 Part 2 decoding
  media: amlogic: vdec: give the regmaps their own names

 .../bindings/media/amlogic,s4-vdec.yaml       |   24 +-
 MAINTAINERS                                   |    2 +-
 drivers/media/platform/amlogic/vdec/Kconfig   |    1 +
 drivers/media/platform/amlogic/vdec/Makefile  |    2 +-
 .../media/platform/amlogic/vdec/aml_vdec.c    |  157 +-
 .../platform/amlogic/vdec/aml_vdec_drv.h      |   21 +
 .../media/platform/amlogic/vdec/aml_vdec_hw.c |  178 +-
 .../media/platform/amlogic/vdec/aml_vdec_hw.h |   64 +
 .../platform/amlogic/vdec/aml_vdec_platform.c |  129 +-
 .../platform/amlogic/vdec/aml_vdec_tee_fw.c   |  102 +-
 drivers/media/platform/amlogic/vdec/av1.c     | 3140 +++++++++++++++++
 drivers/media/platform/amlogic/vdec/av1.h     |  308 ++
 drivers/media/platform/amlogic/vdec/h264.c    |  477 ++-
 drivers/media/platform/amlogic/vdec/h264.h    |    7 +
 .../platform/amlogic/vdec/h264_headers.c      |  249 ++
 drivers/media/platform/amlogic/vdec/hevc.c    | 1728 +++++++++
 drivers/media/platform/amlogic/vdec/hevc.h    |  125 +
 drivers/media/platform/amlogic/vdec/mjpeg.c   |  471 +++
 drivers/media/platform/amlogic/vdec/mjpeg.h   |   33 +
 drivers/media/platform/amlogic/vdec/mpeg12.c  |  708 ++++
 drivers/media/platform/amlogic/vdec/mpeg12.h  |   51 +
 drivers/media/platform/amlogic/vdec/mpeg4.c   |  480 +++
 drivers/media/platform/amlogic/vdec/mpeg4.h   |   52 +
 .../media/platform/amlogic/vdec/reg_defines.h |   24 +
 drivers/media/platform/amlogic/vdec/vp9.c     | 2004 +++++++++++
 drivers/media/platform/amlogic/vdec/vp9.h     |  149 +
 .../media/platform/amlogic/vdec/vp9_prob.c    |  213 ++
 .../media/platform/amlogic/vdec/vp9_prob.h    |   32 +
 28 files changed, 10847 insertions(+), 84 deletions(-)
 create mode 100644 drivers/media/platform/amlogic/vdec/av1.c
 create mode 100644 drivers/media/platform/amlogic/vdec/av1.h
 create mode 100644 drivers/media/platform/amlogic/vdec/h264_headers.c
 create mode 100644 drivers/media/platform/amlogic/vdec/hevc.c
 create mode 100644 drivers/media/platform/amlogic/vdec/hevc.h
 create mode 100644 drivers/media/platform/amlogic/vdec/mjpeg.c
 create mode 100644 drivers/media/platform/amlogic/vdec/mjpeg.h
 create mode 100644 drivers/media/platform/amlogic/vdec/mpeg12.c
 create mode 100644 drivers/media/platform/amlogic/vdec/mpeg12.h
 create mode 100644 drivers/media/platform/amlogic/vdec/mpeg4.c
 create mode 100644 drivers/media/platform/amlogic/vdec/mpeg4.h
 create mode 100644 drivers/media/platform/amlogic/vdec/vp9.c
 create mode 100644 drivers/media/platform/amlogic/vdec/vp9.h
 create mode 100644 drivers/media/platform/amlogic/vdec/vp9_prob.c
 create mode 100644 drivers/media/platform/amlogic/vdec/vp9_prob.h


base-commit: 7be4bc52fd9e0e55dc9a2e0232b89427d2c4b614
prerequisite-patch-id: 106aac9a55fa72f68fe458d405a69a66678c7040
prerequisite-patch-id: ed3f15728a5c616aca82b5086dc924adba315993
prerequisite-patch-id: 6845fd22135330ee201405f38d5c2a72bbe9d452
prerequisite-patch-id: 626eaa3d309664901ba13acc331d327a4ddc64c1
-- 
2.56.0


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

* [PATCH 01/11] MAINTAINERS: fix the Amlogic VDEC binding path
  2026-10-25 19:24 [PATCH 00/11] media: amlogic: vdec: S4 decoding beyond H.264 Baseline christian.koever-draxl
@ 2026-10-25 19:24 ` christian.koever-draxl
  2026-10-25 19:24 ` [PATCH 02/11] dt-bindings: media: amlogic,s4-vdec: describe both decoder cores christian.koever-draxl
                   ` (10 subsequent siblings)
  11 siblings, 0 replies; 20+ messages in thread
From: christian.koever-draxl @ 2026-10-25 19:24 UTC (permalink / raw)
  To: zhentao.guo, mchehab, robh, krzk+dt, conor+dt
  Cc: linux-media, devicetree, linux-kernel,
	Christian Stefan Kövér-Draxl

From: Christian Stefan Kövér-Draxl <ckoever@outlook.de>

The binding was renamed to amlogic,s4-vdec.yaml, but the MAINTAINERS
entry still points to the old amlogic,s4-vcodec-dec.yaml, so the file
is not covered.

Signed-off-by: Christian Stefan Kövér-Draxl <ckoever@outlook.de>
---
 MAINTAINERS | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/MAINTAINERS b/MAINTAINERS
index 0e0d332ec39c..8c4baa252541 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1446,7 +1446,7 @@ AMLOGIC VDEC DRIVER
 M:	Zhentao Guo <zhentao.guo@amlogic.com>
 L:	linux-media@vger.kernel.org
 S:	Maintained
-F:	Documentation/devicetree/bindings/media/amlogic,s4-vcodec-dec.yaml
+F:	Documentation/devicetree/bindings/media/amlogic,s4-vdec.yaml
 F:	drivers/media/platform/amlogic/vdec/
 
 AMPHENOL CHIPCAP 2 DRIVER
-- 
2.56.0


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

* [PATCH 02/11] dt-bindings: media: amlogic,s4-vdec: describe both decoder cores
  2026-10-25 19:24 [PATCH 00/11] media: amlogic: vdec: S4 decoding beyond H.264 Baseline christian.koever-draxl
  2026-10-25 19:24 ` [PATCH 01/11] MAINTAINERS: fix the Amlogic VDEC binding path christian.koever-draxl
@ 2026-10-25 19:24 ` christian.koever-draxl
  2026-10-25 19:24 ` [PATCH 03/11] media: amlogic: vdec: fix the microcode loading and prepare the HEVC core christian.koever-draxl
                   ` (9 subsequent siblings)
  11 siblings, 0 replies; 20+ messages in thread
From: christian.koever-draxl @ 2026-10-25 19:24 UTC (permalink / raw)
  To: zhentao.guo, mchehab, robh, krzk+dt, conor+dt
  Cc: linux-media, devicetree, linux-kernel,
	Christian Stefan Kövér-Draxl

From: Christian Stefan Kövér-Draxl <ckoever@outlook.de>

The S4 video decoder has two cores: VDEC for H.264, MPEG-1/2 and MJPEG,
and the HEVC core for HEVC, VP9 and AV1. Update the description and
name the individual reg, interrupt, clock and power-domain entries.

Signed-off-by: Christian Stefan Kövér-Draxl <ckoever@outlook.de>
---
 .../bindings/media/amlogic,s4-vdec.yaml       | 24 ++++++++++++++-----
 1 file changed, 18 insertions(+), 6 deletions(-)

diff --git a/Documentation/devicetree/bindings/media/amlogic,s4-vdec.yaml b/Documentation/devicetree/bindings/media/amlogic,s4-vdec.yaml
index e3c2ab4000aa..6c98d7013029 100644
--- a/Documentation/devicetree/bindings/media/amlogic,s4-vdec.yaml
+++ b/Documentation/devicetree/bindings/media/amlogic,s4-vdec.yaml
@@ -11,15 +11,19 @@ maintainers:
   - Zhentao Guo <zhentao.guo@amlogic.com>
 
 description:
-  The Video Decoder Accelerator present on Amlogic SOCs.
-  It supports stateless h264 decoding.
+  The video decoder (DOS block) of the Amlogic S4 family. It has two cores,
+  VDEC for H.264, MPEG-1/2, MPEG-4 Part 2 and MJPEG, and the HEVC core for
+  HEVC, VP9 and AV1. Both run microcode that the secure world loads, and
+  are exposed as a stateless V4L2 decoder.
 
 properties:
   compatible:
     const: amlogic,s4-vdec
 
   reg:
-    maxItems: 2
+    items:
+      - description: DOS register block, both decoder cores
+      - description: DMC request control, gates the cores' memory requests
 
   reg-names:
     items:
@@ -27,10 +31,16 @@ properties:
       - const: dmc
 
   interrupts:
-    maxItems: 3
+    items:
+      - description: HEVC core
+      - description: VDEC core
+      - description: third DOS mailbox, not used by the decoder
 
   clocks:
-    maxItems: 3
+    items:
+      - description: DOS bus clock
+      - description: VDEC core clock
+      - description: HEVC core clock
 
   clock-names:
     items:
@@ -39,7 +49,9 @@ properties:
       - const: hevcf
 
   power-domains:
-    maxItems: 2
+    items:
+      - description: VDEC core
+      - description: HEVC core
 
   power-domain-names:
     items:
-- 
2.56.0


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

* [PATCH 03/11] media: amlogic: vdec: fix the microcode loading and prepare the HEVC core
  2026-10-25 19:24 [PATCH 00/11] media: amlogic: vdec: S4 decoding beyond H.264 Baseline christian.koever-draxl
  2026-10-25 19:24 ` [PATCH 01/11] MAINTAINERS: fix the Amlogic VDEC binding path christian.koever-draxl
  2026-10-25 19:24 ` [PATCH 02/11] dt-bindings: media: amlogic,s4-vdec: describe both decoder cores christian.koever-draxl
@ 2026-10-25 19:24 ` christian.koever-draxl
  2026-10-25 20:25   ` sashiko-bot
  2026-10-25 19:25 ` [PATCH 04/11] media: amlogic: vdec: h264: decode Main and High profile bit-exact christian.koever-draxl
                   ` (8 subsequent siblings)
  11 siblings, 1 reply; 20+ messages in thread
From: christian.koever-draxl @ 2026-10-25 19:24 UTC (permalink / raw)
  To: zhentao.guo, mchehab, robh, krzk+dt, conor+dt
  Cc: linux-media, devicetree, linux-kernel,
	Christian Stefan Kövér-Draxl

From: Christian Stefan Kövér-Draxl <ckoever@outlook.de>

The secure world rejected every microcode this driver handed over: the
images carry a 256-byte signature header that has to stay attached, and
the TEE only accepts loads once it has been told that boot is complete.
Pass the whole image with the signature size, issue SYS_BOOT_COMPLETE
before the first load, and keep probing when the preload fails because
the secure world may already hold the microcode.

Add what the HEVC core needs before codecs can use it: its power domain
and clock handling, the reset and memory-power sequence used when the
core switches between microcodes, and the per-codec hooks in the
request path.

Load a microcode into the HEVC core only when it is not already there.
A browser opens a new decoder context for every seek, quality switch or
advert; loading the image again for each of them makes the secure loader
answer TEEC_ERROR_BAD_STATE after a few hundred loads, and it stays that
way until the board is power cycled. The image survives in the core
until the core memories are cycled on a codec switch or after a timeout.

Signed-off-by: Christian Stefan Kövér-Draxl <ckoever@outlook.de>
---
 drivers/media/platform/amlogic/vdec/Makefile  |   2 +-
 .../media/platform/amlogic/vdec/aml_vdec.c    |  72 ++++++-
 .../platform/amlogic/vdec/aml_vdec_drv.h      |  21 +++
 .../media/platform/amlogic/vdec/aml_vdec_hw.c | 176 +++++++++++++++++-
 .../media/platform/amlogic/vdec/aml_vdec_hw.h |  64 +++++++
 .../platform/amlogic/vdec/aml_vdec_platform.c |  27 ++-
 .../platform/amlogic/vdec/aml_vdec_tee_fw.c   |  87 ++++++++-
 .../media/platform/amlogic/vdec/reg_defines.h |  24 +++
 8 files changed, 443 insertions(+), 30 deletions(-)

diff --git a/drivers/media/platform/amlogic/vdec/Makefile b/drivers/media/platform/amlogic/vdec/Makefile
index f752716cbf9e..f9c35e9ac902 100644
--- a/drivers/media/platform/amlogic/vdec/Makefile
+++ b/drivers/media/platform/amlogic/vdec/Makefile
@@ -1,4 +1,4 @@
 # SPDX-License-Identifier: GPL-2.0-only
-aml-vdec-drv-objs := aml_vdec.o aml_vdec_drv.o aml_vdec_hw.o aml_vdec_platform.o h264.o aml_vdec_tee_fw.o\
+aml-vdec-drv-objs := aml_vdec.o aml_vdec_drv.o aml_vdec_hw.o aml_vdec_platform.o h264.o aml_vdec_tee_fw.o
 
 obj-$(CONFIG_VIDEO_AMLOGIC_VDEC) += aml-vdec-drv.o
diff --git a/drivers/media/platform/amlogic/vdec/aml_vdec.c b/drivers/media/platform/amlogic/vdec/aml_vdec.c
index d4dcd0180d2d..2a5ff842fd39 100644
--- a/drivers/media/platform/amlogic/vdec/aml_vdec.c
+++ b/drivers/media/platform/amlogic/vdec/aml_vdec.c
@@ -66,8 +66,7 @@ static const struct aml_vdec_v4l2_ctrl controls[] = {
 			.max = V4L2_MPEG_VIDEO_H264_PROFILE_HIGH,
 			.def = V4L2_MPEG_VIDEO_H264_PROFILE_MAIN,
 		},
-	}
-};
+	},};
 
 static const struct aml_dec_type dec_type_name[] = {
 	{
@@ -130,23 +129,46 @@ static void m2mops_vdec_device_run(void *m2m_priv)
 	/* incase of bus hang in stop_streaming */
 	ctx->dos_clk_en = 1;
 
-	if (ctx->curr_dec_type == CODEC_TYPE_H264)
+	if (ctx->curr_dec_type == CODEC_TYPE_H264 ||
+	    ctx->curr_dec_type == CODEC_TYPE_MPEG2 ||
+	    ctx->curr_dec_type == CODEC_TYPE_MJPEG ||
+	    ctx->curr_dec_type == CODEC_TYPE_MPEG4)
 		aml_vdec_reset_core(dev->dec_hw);
 
 	if (load_firmware(dev->dec_hw, ctx->curr_dec_type) < 0)
 		goto err_cancel_job;
 
+	ctx->job_finished = false;
 	ret = ctx->codec_ops->run(ctx);
 
 	v4l2_m2m_buf_copy_metadata(src_buf, dst_buf);
-	if (src_req)
+	/*
+	 * AV1 completes the request itself, in its interrupt handler, before
+	 * the buffer is done - by the time run() returns here the job has
+	 * already finished and the request with it.
+	 */
+	if (src_req && ctx->curr_dec_type != CODEC_TYPE_AV1 &&
+	    ctx->curr_dec_type != CODEC_TYPE_VP9 &&
+	    ctx->curr_dec_type != CODEC_TYPE_HEVC &&
+	    ctx->curr_dec_type != CODEC_TYPE_MJPEG)
 		v4l2_ctrl_request_complete(src_req, &ctx->ctrl_handler);
-	if (ret < 0)
+	/*
+	 * Only cancel a job that is still running: a codec that finished it in
+	 * its own interrupt handler must not finish it twice.
+	 */
+	if (ret < 0 && !ctx->job_finished)
 		goto err_cancel_job;
 
 	return;
 
 err_cancel_job:
+	/*
+	 * Whatever went wrong, the request must not be left incomplete or the
+	 * client waits for it forever.  AV1 completes it in its own done path
+	 * on success, so this only covers the paths that never got there.
+	 */
+	if (src_req)
+		v4l2_ctrl_request_complete(src_req, &ctx->ctrl_handler);
 	v4l2_m2m_buf_done_and_job_finish(dev->m2m_dev_dec, ctx->m2m_ctx,
 					 VB2_BUF_STATE_ERROR);
 }
@@ -273,9 +295,6 @@ static int vdec_try_fmt_mp(struct aml_vdec_ctx *ctx, enum v4l2_buf_type type,
 	if (V4L2_TYPE_IS_OUTPUT(type)) {
 		pix_mp->num_planes = dec_fmt->num_planes;
 		pix_mp->pixelformat = dec_fmt->fourcc;
-		if (!pix_mp->plane_fmt[0].sizeimage)
-			pix_mp->plane_fmt[0].sizeimage =
-			    (pix_mp->height * pix_mp->width * 3) / 2;
 	}
 
 	align = ctx->dev->pvdec_data->dec_fmt->align;
@@ -284,6 +303,26 @@ static int vdec_try_fmt_mp(struct aml_vdec_ctx *ctx, enum v4l2_buf_type type,
 
 	v4l2_apply_frmsize_constraints(&pix_mp->width, &pix_mp->height,
 				       &dec_fmt->stepwise);
+
+	/*
+	 * The bitstream buffer has to be sized from the dimensions the driver
+	 * settled on, not the ones userspace asked for: a client that
+	 * negotiates before it knows the picture size passes zeroes, and
+	 * sizing from those gives a zero-length buffer that no pool can
+	 * allocate.  AV1 also gets a floor, because a single temporal unit -
+	 * a key frame in particular - can be larger than a raw frame of the
+	 * same nominal size once the picture is small.
+	 */
+	if (V4L2_TYPE_IS_OUTPUT(type) && !pix_mp->plane_fmt[0].sizeimage) {
+		u32 size = (pix_mp->height * pix_mp->width * 3) / 2;
+
+		if ((dec_fmt->fourcc == V4L2_PIX_FMT_AV1_FRAME ||
+		     dec_fmt->fourcc == V4L2_PIX_FMT_VP9_FRAME ||
+		     dec_fmt->fourcc == V4L2_PIX_FMT_HEVC_SLICE) &&
+		    size < SZ_1M)
+			size = SZ_1M;
+		pix_mp->plane_fmt[0].sizeimage = size;
+	}
 	dev_dbg(&ctx->dev->plat_dev->dev,
 		"%s type %d four_cc %d pix_mp->width %d pix_mp->height %d\n",
 		__func__, type, dec_fmt->fourcc, pix_mp->width, pix_mp->height);
@@ -600,8 +639,14 @@ static int vb2ops_vdec_buf_prepare(struct vb2_buffer *vb)
 
 	for (i = 0; i < pix_fmt->num_planes; i++) {
 		sizeimage = pix_fmt->plane_fmt[i].sizeimage;
-		if (vb2_plane_size(vb, i) < sizeimage)
+		if (vb2_plane_size(vb, i) < sizeimage) {
+			dev_err_ratelimited(&ctx->dev->plat_dev->dev,
+					    "buf_prepare: type %d plane %d is %lu bytes, format wants %u (%ux%u)\n",
+				vb->vb2_queue->type, i,
+				vb2_plane_size(vb, i), sizeimage,
+				pix_fmt->width, pix_fmt->height);
 			return -EINVAL;
+		}
 
 		if (V4L2_TYPE_IS_CAPTURE(vb->type)) {
 			vb2_set_plane_payload(vb, i,
@@ -709,6 +754,8 @@ int aml_vdec_queue_init(void *priv, struct vb2_queue *src_vq,
 	src_vq->lock = &ctx->v4l2_intf_lock;
 	src_vq->buf_struct_size = sizeof(struct v4l2_m2m_buffer);
 	src_vq->supports_requests = true;
+	/* MPEG-2 field pairs decode into one held capture buffer */
+	src_vq->subsystem_flags |= VB2_V4L2_FL_SUPPORTS_M2M_HOLD_CAPTURE_BUF;
 	src_vq->timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_COPY;
 	ret = vb2_queue_init(src_vq);
 	if (ret) {
@@ -725,6 +772,13 @@ int aml_vdec_queue_init(void *priv, struct vb2_queue *src_vq,
 	dst_vq->lock = &ctx->v4l2_intf_lock;
 	dst_vq->buf_struct_size = sizeof(struct v4l2_m2m_buffer);
 	dst_vq->timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_COPY;
+	/*
+	 * Cacheable capture buffers: a coherent allocation is mapped uncached
+	 * into userspace, and reading a decoded frame back (vaGetImage, ffmpeg
+	 * hwdownload) then crawls.  A client asks for cacheable buffers with
+	 * V4L2_MEMORY_FLAG_NON_COHERENT; vb2 cleans/invalidates around each job.
+	 */
+	dst_vq->allow_cache_hints = 1;
 	ret = vb2_queue_init(dst_vq);
 	if (ret) {
 		v4l2_info(&dev->v4l2_dev,
diff --git a/drivers/media/platform/amlogic/vdec/aml_vdec_drv.h b/drivers/media/platform/amlogic/vdec/aml_vdec_drv.h
index fa86233ea5b3..8ab2ee67a2a2 100644
--- a/drivers/media/platform/amlogic/vdec/aml_vdec_drv.h
+++ b/drivers/media/platform/amlogic/vdec/aml_vdec_drv.h
@@ -22,6 +22,12 @@
 #define AML_VDEC_MIN_H    64U
 #define AML_VDEC_1080P_MAX_H  1088U
 #define AML_VDEC_1080P_MAX_W  1920U
+/*
+ * The S905W2 decodes 4K.  AV1 goes up to the microcode's 4096x2304 working-set
+ * size; the other codecs keep the 1080p ceiling until they are tested there.
+ */
+#define AML_VDEC_4K_MAX_H     2304U
+#define AML_VDEC_4K_MAX_W     4096U
 
 struct aml_vdec_ctx;
 /**
@@ -37,6 +43,12 @@ enum aml_fmt_type {
  */
 enum aml_codec_type {
 	CODEC_TYPE_H264 = 0,
+	CODEC_TYPE_AV1,
+	CODEC_TYPE_VP9,
+	CODEC_TYPE_HEVC,
+	CODEC_TYPE_MPEG2,
+	CODEC_TYPE_MJPEG,
+	CODEC_TYPE_MPEG4,
 	CODEC_TYPE_FRAME,
 };
 
@@ -94,6 +106,7 @@ struct aml_vdec_dev {
 	struct aml_vdec_hw *dec_hw;
 	const struct aml_dev_platform_data *pvdec_data;
 
+	/* video_device lock: serialises ioctls, open and release */
 	struct mutex dev_mutex;
 	struct file *filp;
 };
@@ -125,6 +138,7 @@ struct dec_pic_info {
  * @v4l2_intf_lock: Mutex lock for v4l2 interface.
  * @codec_ops: Codec operation functions. See struct aml_codec_ops.
  * @int_cond: Variable used by the waitqueue.
+ * @job_finished: The m2m job was already finished by the codec's own path.
  * @queue: Waitqueue to wait for the current decode context finish.
  * @pix_fmt: To store the V4L2 pixel format.
  * @dec_fmt: To describe the decoding format supported by hardware platform.
@@ -140,10 +154,17 @@ struct aml_vdec_ctx {
 	struct v4l2_fh fh;
 	struct v4l2_m2m_ctx *m2m_ctx;
 	struct v4l2_ctrl_handler ctrl_handler;
+	/* vb2/m2m queue lock for this context */
 	struct mutex v4l2_intf_lock;
 
 	const struct aml_codec_ops *codec_ops;
 	int int_cond;
+	/*
+	 * Set by whatever path calls v4l2_m2m_buf_done_and_job_finish(), so
+	 * device_run() knows the buffers have already gone back to userspace
+	 * and must not be touched or finished a second time.
+	 */
+	bool job_finished;
 	wait_queue_head_t queue;
 	struct v4l2_pix_format_mplane pix_fmt[2];
 	struct aml_video_fmt dec_fmt[2];
diff --git a/drivers/media/platform/amlogic/vdec/aml_vdec_hw.c b/drivers/media/platform/amlogic/vdec/aml_vdec_hw.c
index 29a195eb6872..935018ea96c6 100644
--- a/drivers/media/platform/amlogic/vdec/aml_vdec_hw.c
+++ b/drivers/media/platform/amlogic/vdec/aml_vdec_hw.c
@@ -51,6 +51,20 @@ void dos_enable(struct aml_vdec_hw *hw)
 	regmap_update_bits(hw->map[DOS_BUS], MDEC_PIC_DC_CTRL, (1 << 31), 0);
 }
 
+/*
+ * dos_enable() opens DOS_GCLK_EN0 and GCLK_EN, which are the legacy VDEC-1
+ * gates; the HEVC/AV1 core has its own gate in DOS_GCLK_EN3 and nothing had
+ * ever opened it.  Measured on a live board: with the HEVC power domain up and
+ * hevcf running at 800 MHz, DOS_GCLK_EN3 still read 0, and asking secure world
+ * to write microcode into that core hung the board with no oops and no console
+ * output at all.  The register itself is plain read/write once the domain is
+ * powered, and so is HEVC_ASSIST_SCRATCH_0.
+ */
+void dos_enable_hevc(struct aml_vdec_hw *hw)
+{
+	regmap_write(hw->map[DOS_BUS], DOS_GCLK_EN3, 0xffffffff);
+}
+
 void aml_vdec_reset_core(struct aml_vdec_hw *hw)
 {
 	unsigned int mask = 0;
@@ -137,8 +151,8 @@ static int vdec_clock_gate_init(struct aml_vdec_hw *hw)
 	return devm_clk_bulk_get(hw->dev, CLK_MAX, hw->gates);
 }
 
-static struct clk_bulk_data *vdec_get_clk_by_name(struct aml_vdec_hw *hw,
-						  const char *name)
+struct clk_bulk_data *vdec_get_clk_by_name(struct aml_vdec_hw *hw,
+					   const char *name)
 {
 	int i;
 
@@ -249,6 +263,14 @@ static void dos_local_config(struct aml_vdec_hw *hw, bool is_on, int id)
 	dev_dbg(hw->dev, "%s end, id %d, is_on %d\n", __func__, id, is_on);
 }
 
+/*
+ * The core clock rates come from assigned-clock-rates in the device tree.
+ * hevcf_rate overrides the HEVC core's for testing.
+ */
+static ulong hevcf_rate;
+module_param(hevcf_rate, ulong, 0444);
+MODULE_PARM_DESC(hevcf_rate, "Override the HEVC core clock in Hz (0 = device tree)");
+
 static void pm_vdec_power_domain_power_on(struct aml_vdec_hw *hw, int id)
 {
 	const struct power_manager_s *pm = hw->pm;
@@ -262,10 +284,10 @@ static void pm_vdec_power_domain_power_on(struct aml_vdec_hw *hw, int id)
 
 	if (gate_node) {
 		clk_prepare_enable(gate_node->clk);
-		if (id == VDEC) {
-			clk_set_rate(gate_node->clk, 499999992);
-			dev_dbg(hw->dev, "after set, vdec clock is %lu Hz\n",
-				clk_get_rate(gate_node->clk));
+		if (id == HVDEC && hevcf_rate) {
+			clk_set_rate(gate_node->clk, hevcf_rate);
+			dev_info(hw->dev, "hevcf %lu Hz\n",
+				 clk_get_rate(gate_node->clk));
 		}
 		dev_dbg(hw->dev, "the %-15s clock on\n", gate_node->id);
 	} else {
@@ -364,6 +386,91 @@ int vdec_enable(struct aml_vdec_hw *hw)
 	return 0;
 }
 
+/*
+ * The HEVC core is a second, independent power domain with its own clock
+ * ("hevcf") and its own reset in DOS_SW_RESET3.  Nothing in this driver decodes
+ * on it yet; it is powered only so secure world has somewhere to put the
+ * microcode when the AV1/HEVC load is probed.
+ */
+int hvdec_enable(struct aml_vdec_hw *hw)
+{
+	vdec_poweron(hw, HVDEC);
+
+	return 0;
+}
+
+int hvdec_disable(struct aml_vdec_hw *hw)
+{
+	vdec_poweroff(hw, HVDEC);
+
+	return 0;
+}
+
+/*
+ * VP9 and AV1 share the HEVC core, and once one of them has run the other
+ * cannot: it loads, parses a header and never completes a picture.  Putting
+ * back every register the other codec changed does not help, reloading the
+ * module does - and what a reload does that nothing else here does is power
+ * the core's internal memories down and up.  hvdec_disable() cannot be used
+ * for that: hvdec_enable() runs for every frame and only counts, so the core
+ * never actually goes off.  Cycle the memories and reset the core directly,
+ * on a switch only, leaving the power domain, the clock and secure world
+ * alone.
+ */
+int hvdec_switch_codec(struct aml_vdec_hw *hw, int codec)
+{
+	if (hw->hevc_dirty || (hw->hevc_codec && hw->hevc_codec != codec)) {
+		dev_info(hw->dev, "HEVC core: %s, cycling core memories\n",
+			 hw->hevc_dirty ? "left dirty" : "codec switch");
+		mutex_lock(&hw->pm_mutex);
+		dos_local_config(hw, 0, HVDEC);
+		dos_local_config(hw, 1, HVDEC);
+		mutex_unlock(&hw->pm_mutex);
+		hw->hevc_fw = 0;
+	}
+	hw->hevc_codec = codec;
+	hw->hevc_dirty = false;
+	return 0;
+}
+
+/*
+ * Load a microcode into the HEVC core through secure world, unless it is
+ * already there. Every decoder context used to load it again, and a browser
+ * opens a new context for every seek, quality switch or advert: after a few
+ * hundred loads the secure loader answers with TEEC_ERROR_BAD_STATE and
+ * stays there until the next power cycle. The image survives in the core
+ * until hvdec_switch_codec() cycles the memories.
+ */
+int hevc_load_fw(struct aml_vdec_hw *hw, int codec, u32 format, u32 core,
+		 const char *name)
+{
+	int ret, sm_ret = 0;
+
+	/*
+	 * Halt the AMRISC in any case: the previous context may have left it
+	 * running, and the decoder start below expects it stopped.
+	 */
+	regmap_write(hw->map[DOS_BUS], HEVC_MPSR, 0);
+	regmap_write(hw->map[DOS_BUS], HEVC_CPSR, 0);
+
+	if (hw->hevc_fw == codec)
+		return 0;
+
+	regmap_write(hw->map[DOS_BUS], DOS_SW_RESET3, HEVC_LOAD_RESET_MASK);
+	regmap_write(hw->map[DOS_BUS], DOS_SW_RESET3, 0);
+
+	ret = meson_sm_call(hw->sec_fw, SM_LOAD_VIDEO_FW, &sm_ret, format, core,
+			    0, 0, 0);
+	if (ret < 0 || sm_ret) {
+		dev_err(hw->dev, "%s: microcode load failed %d/%d\n", name, ret,
+			sm_ret);
+		hw->hevc_fw = 0;
+		return -EIO;
+	}
+	hw->hevc_fw = codec;
+	return 0;
+}
+
 int vdec_disable(struct aml_vdec_hw *hw)
 {
 	vdec_poweroff(hw, VDEC);
@@ -382,6 +489,40 @@ static const struct power_manager_s pm[] = {
 	},
 };
 
+static irqreturn_t hevc_irq_handler(int irq, void *priv)
+{
+	struct aml_vdec_hw *hw = priv;
+	int n;
+
+	hw->hevc_irq_status = read_dos_reg(hw, HEVC_ASSIST_SCRATCH_0);
+	regmap_write(hw->map[DOS_BUS], HEVC_ASSIST_MBOX0_CLR_REG, 1);
+
+	n = atomic_inc_return(&hw->hevc_irq_count);
+	if (hw->hevc_irq_thread_fn)
+		return IRQ_WAKE_THREAD;
+
+	/*
+	 * Runaway guard for the probe path, which has no state machine to end
+	 * a decode.  It must not apply once a real handler is driving the
+	 * core: a stream produces a few interrupts per picture, so masking the
+	 * mailbox at a fixed count silently stops decoding part way through.
+	 */
+	if (n > 64)
+		regmap_write(hw->map[DOS_BUS], HEVC_ASSIST_MBOX0_MASK, 0);
+
+	return IRQ_HANDLED;
+}
+
+static irqreturn_t hevc_irq_thread(int irq, void *priv)
+{
+	struct aml_vdec_hw *hw = priv;
+
+	if (hw->hevc_irq_thread_fn)
+		return hw->hevc_irq_thread_fn(irq, hw->hevc_priv);
+
+	return IRQ_HANDLED;
+}
+
 static irqreturn_t vdec_irq_handler(int irq, void *priv)
 {
 	struct aml_vdec_dev *dev = (struct aml_vdec_dev *)priv;
@@ -445,6 +586,25 @@ int dev_request_hw_resources(void *priv)
 	hw = dev->dec_hw;
 	hw->dev = &pdev->dev;
 
+	/*
+	 * The HEVC/AV1 core signals on its own interrupt (IRQ_HVDEC, the first
+	 * entry in the DT, GIC SPI 91) through mailbox 0.  Nothing decodes on
+	 * that core yet, so this handler only acknowledges and counts - it is
+	 * how the microcode's replies are observed at all.  If it ever storms,
+	 * the mask is dropped rather than letting it wedge the board.
+	 */
+	hw->hevc_irq = platform_get_irq(pdev, IRQ_HVDEC);
+	if (hw->hevc_irq >= 0) {
+		atomic_set(&hw->hevc_irq_count, 0);
+		ret = devm_request_threaded_irq(&pdev->dev, hw->hevc_irq,
+						hevc_irq_handler,
+						hevc_irq_thread,
+						IRQF_ONESHOT, "aml-hevc", hw);
+		if (ret)
+			dev_warn(&pdev->dev, "hevc irq %d request failed (%d)\n",
+				 hw->hevc_irq, ret);
+	}
+
 	hw->dec_irq = platform_get_irq(pdev, IRQ_VDEC);
 	if (hw->dec_irq < 0) {
 		dev_err(&pdev->dev, "get irq failed\n");
@@ -514,7 +674,9 @@ int dev_request_hw_resources(void *priv)
 
 	ret = aml_tee_fw_preload(hw);
 	if (ret)
-		return ret;
+		dev_warn(&pdev->dev,
+			 "TEE firmware preload failed (%d), continuing in case secure world already holds the ucode\n",
+			 ret);
 
 	dev_dbg(&pdev->dev, "##Amlogic hw resource init OK##\n");
 
diff --git a/drivers/media/platform/amlogic/vdec/aml_vdec_hw.h b/drivers/media/platform/amlogic/vdec/aml_vdec_hw.h
index 9dc84d0d52bf..35645aad2375 100644
--- a/drivers/media/platform/amlogic/vdec/aml_vdec_hw.h
+++ b/drivers/media/platform/amlogic/vdec/aml_vdec_hw.h
@@ -7,6 +7,8 @@
 #define _AML_VDEC_HW_H_
 
 #include <linux/module.h>
+#include <linux/dma-mapping.h>
+#include <media/videobuf2-dma-contig.h>
 #include <linux/clk.h>
 #include <linux/platform_device.h>
 #include <linux/of.h>
@@ -115,6 +117,7 @@ struct aml_vdec_hw {
 	struct platform_device *pdev;
 	struct device *dev;
 	struct regmap *map[MAX_REG_BUS];
+	/* protects power-domain and clock state */
 	struct mutex pm_mutex;
 	struct meson_canvas *canvas;
 	struct meson_sm_firmware *sec_fw;
@@ -122,7 +125,35 @@ struct aml_vdec_hw {
 	struct aml_vdec_hw_ops hw_ops;
 	struct clk_bulk_data gates[CLK_MAX];
 	int dec_irq;
+	int hevc_irq;
+	atomic_t hevc_irq_count;
+	u32 hevc_irq_status;
+	/*
+	 * Optional threaded handler for the HEVC/AV1 core.  The AV1 state
+	 * machine allocates DMA memory while configuring a picture, so it
+	 * cannot run in hard interrupt context.  When this is NULL the hard
+	 * handler just counts interrupts, which is what the probe harness
+	 * polls for.
+	 */
+	irqreturn_t (*hevc_irq_thread_fn)(int irq, void *priv);
+	void *hevc_priv;
 	void *curr_ctx;
+	/* Codec whose microcode the HEVC core last ran; 0 when none yet. */
+	int hevc_codec;
+	/*
+	 * Codec whose microcode is loaded into the HEVC core right now; 0 when
+	 * none or when the core memories were cycled since.
+	 */
+	int hevc_fw;
+	/*
+	 * The core is in a state the next session cannot start from: a decode
+	 * timed out, or a 10-bit stream ran.  The next start cycles the core
+	 * memories, as a codec switch does.
+	 */
+	bool hevc_dirty;
+	void *ucode_vaddr;
+	dma_addr_t ucode_paddr;
+	u32 ucode_type;
 };
 
 /**
@@ -149,9 +180,42 @@ struct aml_vdec_hw *vdec_get_hw(void *priv);
 u32 read_dos_reg(struct aml_vdec_hw *hw, u32 reg_addr);
 int vdec_enable(struct aml_vdec_hw *hw);
 int vdec_disable(struct aml_vdec_hw *hw);
+struct clk_bulk_data *vdec_get_clk_by_name(struct aml_vdec_hw *hw,
+					   const char *name);
+int hvdec_enable(struct aml_vdec_hw *hw);
+int hvdec_disable(struct aml_vdec_hw *hw);
+int hvdec_switch_codec(struct aml_vdec_hw *hw, int codec);
+int hevc_load_fw(struct aml_vdec_hw *hw, int codec, u32 format, u32 core,
+		 const char *name);
 void dos_enable(struct aml_vdec_hw *hw);
+void dos_enable_hevc(struct aml_vdec_hw *hw);
 void aml_start_vdec_hw(struct aml_vdec_hw *hw);
 void aml_stop_vdec_hw(struct aml_vdec_hw *hw);
 void aml_vdec_reset_core(struct aml_vdec_hw *hw);
 
+/*
+ * The CPU wrote a decoded picture into a capture buffer.  A buffer the client
+ * asked to be cacheable (V4L2_MEMORY_FLAG_NON_COHERENT) still has it in dirty
+ * cache lines, which vb2 would invalidate on dequeue: write them back first.
+ */
+static inline void aml_vdec_dst_cpu_written(struct device *dev,
+					    struct vb2_buffer *vb)
+{
+	unsigned int i;
+
+	if (!vb->vb2_queue->non_coherent_mem)
+		return;
+	for (i = 0; i < vb->num_planes; i++)
+		dma_sync_single_for_device(dev,
+					   vb2_dma_contig_plane_dma_addr(vb, i),
+					   vb2_plane_size(vb, i), DMA_TO_DEVICE);
+}
+
+/*
+ * DOS_SW_RESET3 bits pulsed before a new microcode is handed to the HEVC
+ * core.  The two AMRISC CPU resets alone are not enough when the core was
+ * running a different codec's microcode.
+ */
+#define HEVC_LOAD_RESET_MASK	(BIT(12) | BIT(11))
+
 #endif
diff --git a/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c b/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c
index 60d20efb1d74..fa4f979b07ee 100644
--- a/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c
+++ b/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c
@@ -26,8 +26,8 @@ static struct aml_video_fmt aml_s4_video_formats[] = {
 		.align = 64,
 		.codec_type = CODEC_TYPE_FRAME,
 		.num_planes = 2,
-		.stepwise = {AML_VDEC_MIN_W, AML_VDEC_1080P_MAX_W, 2,
-			AML_VDEC_MIN_H, AML_VDEC_1080P_MAX_H, 2},
+		.stepwise = {AML_VDEC_MIN_W, AML_VDEC_4K_MAX_W, 2,
+			AML_VDEC_MIN_H, AML_VDEC_4K_MAX_H, 2},
 	},
 	{
 		.name = "NV21",
@@ -36,8 +36,8 @@ static struct aml_video_fmt aml_s4_video_formats[] = {
 		.align = 64,
 		.codec_type = CODEC_TYPE_FRAME,
 		.num_planes = 1,
-		.stepwise = {AML_VDEC_MIN_W, AML_VDEC_1080P_MAX_W, 2,
-			AML_VDEC_MIN_H, AML_VDEC_1080P_MAX_H, 2},
+		.stepwise = {AML_VDEC_MIN_W, AML_VDEC_4K_MAX_W, 2,
+			AML_VDEC_MIN_H, AML_VDEC_4K_MAX_H, 2},
 	},
 	{
 		.name = "NV12M",
@@ -46,10 +46,21 @@ static struct aml_video_fmt aml_s4_video_formats[] = {
 		.align = 64,
 		.codec_type = CODEC_TYPE_FRAME,
 		.num_planes = 2,
-		.stepwise = {AML_VDEC_MIN_W, AML_VDEC_1080P_MAX_W, 2,
-			AML_VDEC_MIN_H, AML_VDEC_1080P_MAX_H, 2},
+		.stepwise = {AML_VDEC_MIN_W, AML_VDEC_4K_MAX_W, 2,
+			AML_VDEC_MIN_H, AML_VDEC_4K_MAX_H, 2},
 
 	},
+	{
+		/* planar 4:2:0, only what the MJPEG microcode writes */
+		.name = "YUV420",
+		.fourcc = V4L2_PIX_FMT_YUV420,
+		.type = AML_FMT_FRAME,
+		.align = 64,
+		.codec_type = CODEC_TYPE_FRAME,
+		.num_planes = 1,
+		.stepwise = {AML_VDEC_MIN_W, AML_VDEC_4K_MAX_W, 2,
+			AML_VDEC_MIN_H, AML_VDEC_4K_MAX_H, 2},
+	},
 	{
 		.name = "NV12",
 		.fourcc = V4L2_PIX_FMT_NV12,
@@ -57,8 +68,8 @@ static struct aml_video_fmt aml_s4_video_formats[] = {
 		.align = 64,
 		.codec_type = CODEC_TYPE_FRAME,
 		.num_planes = 1,
-		.stepwise = {AML_VDEC_MIN_W, AML_VDEC_1080P_MAX_W, 2,
-			AML_VDEC_MIN_H, AML_VDEC_1080P_MAX_H, 2},
+		.stepwise = {AML_VDEC_MIN_W, AML_VDEC_4K_MAX_W, 2,
+			AML_VDEC_MIN_H, AML_VDEC_4K_MAX_H, 2},
 	},
 };
 
diff --git a/drivers/media/platform/amlogic/vdec/aml_vdec_tee_fw.c b/drivers/media/platform/amlogic/vdec/aml_vdec_tee_fw.c
index ad4156249f55..d7106ec47ba9 100644
--- a/drivers/media/platform/amlogic/vdec/aml_vdec_tee_fw.c
+++ b/drivers/media/platform/amlogic/vdec/aml_vdec_tee_fw.c
@@ -2,6 +2,7 @@
 /*
  * Copyright (C) 2025 Amlogic, Inc. All rights reserved
  */
+#include <linux/arm-smccc.h>
 #include <linux/tee_drv.h>
 #include <linux/firmware.h>
 
@@ -9,12 +10,40 @@
 #include "aml_vdec_drv.h"
 
 #define VIDEO_DEC_H264_MULTI	15
+#define VIDEO_DEC_MPEG12_MULTI	0x4d31324d	/* "M12M" */
+#define VIDEO_DEC_MJPEG_MULTI	6
+#define VIDEO_DEC_MPEG4_5_MULTI	0x4d34354d	/* "M45M" */
+
+/*
+ * Newer formats are not small enum values at all - Amlogic switched to
+ * four-character constants somewhere around G12.  The numbers below were read
+ * out of the vendor's own binaries rather than guessed:
+ *
+ *   - firmware.ko carries format_name[], an array of { u32 format;
+ *     const char *name; } at .rodata+0xb38.  It gives h264_multi = 15 (which
+ *     matches what this driver already uses, so the table is trustworthy) and
+ *     av1_mmu = 0x4156314d.
+ *   - decoder_common.ko's optee_load_fw() maps a microcode name onto the pair
+ *     that goes to secure world.  Its AV1 case is literally
+ *     tee_load_video_fw(0x4156314d, 2), so the AV1 microcode belongs to core 2,
+ *     not to the legacy core this driver drives today.
+ *
+ * Both modules come from this box's own stock image (fw_extract/vmods/).
+ */
+#define VIDEO_DEC_AV1_MMU	0x4156314d	/* 'M1VA' */
 
 #define CORE_VDEC_LEGENCY	0
+#define CORE_VDEC_HEVC		2
 
 #define FIRMWARE_PATH		"video_ucode.bin"
 #define ONCE_SENT_SIZE		(1024 * 128)
-#define UCODE_HEADER_SIZE	(1024 * 32)
+
+/*
+ * Size of the RSA signature the ucode container carries in front of its "KCAP"
+ * payload.  The TA is told about it at session open; the blob itself is sent
+ * whole, signature included.
+ */
+#define UCODE_SIGNATURE_SIZE	256
 
 #define TEEC_SUCCESS		0x0
 #define TEEC_ERROR_BUSY	0xffff000d
@@ -25,6 +54,23 @@
 	ARM_SMCCC_CALL_VAL(ARM_SMCCC_FAST_CALL, ARM_SMCCC_SMC_32, \
 			ARM_SMCCC_OWNER_TRUSTED_OS, TEE_SMC_FUNCID_LOAD_VIDEO_FW)
 
+/*
+ * Amlogic's secure OS refuses to load any TA from the REE filesystem until the
+ * normal world says it has finished booting - it answers every open with
+ * TEE_ERROR_ITEM_NOT_FOUND and prints
+ *
+ *   ta_open:202 ree system is not ready, ta can not be loaded.
+ *
+ * on the secure console.  The vendor kernel signals this from
+ * drivers/amlogic/tee/tee.c through the write-only sysfs attribute
+ * /sys/class/tee/sys_boot_complete; the SMC behind it is all it does.
+ */
+#define TEE_SMC_FUNCID_SYS_BOOT_COMPLETE	0xE060
+#define TEE_SMC_SYS_BOOT_COMPLETE \
+	ARM_SMCCC_CALL_VAL(ARM_SMCCC_FAST_CALL, ARM_SMCCC_SMC_32, \
+			ARM_SMCCC_OWNER_TRUSTED_OS, \
+			TEE_SMC_FUNCID_SYS_BOOT_COMPLETE)
+
 #define PTA_LOAD_FW UUID_INIT(0x526fc4fc, 0x7ee6, 0x4a12, \
 			0x96, 0xe3, 0x83, 0xda, 0x95, 0x65, 0xbc, 0xe8)
 
@@ -51,8 +97,11 @@ static void prepare_tee_grgs(size_t firmware_size, struct tee_param *param0,
 
 	param0[0].attr = TEE_IOCTL_PARAM_ATTR_TYPE_VALUE_INPUT;
 	param0[0].u.value.a = firmware_size;
+	param0[0].u.value.b = 0;
 
 	param0[1].attr = TEE_IOCTL_PARAM_ATTR_TYPE_VALUE_INPUT;
+	param0[1].u.value.a = 0;
+	param0[1].u.value.b = UCODE_SIGNATURE_SIZE;
 
 	param0[2].attr = TEE_IOCTL_PARAM_ATTR_TYPE_NONE;
 	param0[3].attr = TEE_IOCTL_PARAM_ATTR_TYPE_NONE;
@@ -152,6 +201,15 @@ int load_firmware(struct aml_vdec_hw *hw, u32 type)
 		dev_info(hw->dev, "codec type %d invalid\n", type);
 		return ret;
 	}
+	/*
+	 * AV1 loads its own microcode from aml_av1_dec_run(), because the load
+	 * must happen after the HEVC core is powered and clocked - issuing
+	 * SM_LOAD_VIDEO_FW at an unpowered core hangs the board hard enough
+	 * that SysRq cannot recover it, and device_run() gets here first.
+	 */
+	if (type == CODEC_TYPE_AV1 || type == CODEC_TYPE_VP9 ||
+	    type == CODEC_TYPE_HEVC)
+		return 0;
 	video_fw = &firmware[type];
 
 	meson_sm_call(hw->sec_fw, SM_LOAD_VIDEO_FW, &ret,
@@ -174,18 +232,21 @@ static int get_firmware(const char *path, void **data, size_t *size)
 	if (ret)
 		return ret;
 
-	/* get rid of the first 32K bytes plaintext */
-	buf = kzalloc((fw->size - UCODE_HEADER_SIZE), GFP_KERNEL);
+	/*
+	 * The whole container goes to the TA, signature first - that is what
+	 * the vendor's own loader (libtee_load_video_fw.so, load_sec_firmware)
+	 * does.  Stripping a header here makes the TA reject the blob.
+	 */
+	buf = kmemdup(fw->data, fw->size, GFP_KERNEL);
 	if (!buf) {
 		release_firmware(fw);
 		return -ENOMEM;
 	}
 
-	memcpy(buf, fw->data + UCODE_HEADER_SIZE, fw->size - UCODE_HEADER_SIZE);
+	*size = fw->size;
 	release_firmware(fw);
 
 	*data = buf;
-	*size = fw->size - UCODE_HEADER_SIZE;
 
 	return 0;
 }
@@ -228,10 +289,26 @@ static int pass_firmware_to_tee(struct aml_vdec_hw *hw)
 	return ret;
 }
 
+/*
+ * Open the gate described at TEE_SMC_SYS_BOOT_COMPLETE.  Harmless to repeat -
+ * secure world latches the flag on the first call.
+ */
+static void tee_sys_boot_complete(struct aml_vdec_hw *hw)
+{
+	struct arm_smccc_res res;
+
+	arm_smccc_smc(TEE_SMC_SYS_BOOT_COMPLETE, 0, 0, 0, 0, 0, 0, 0, &res);
+	if (res.a0)
+		dev_info(hw->dev, "TEE sys_boot_complete returned 0x%lx\n",
+			 res.a0);
+}
+
 int aml_tee_fw_preload(struct aml_vdec_hw *hw)
 {
 	int ret;
 
+	tee_sys_boot_complete(hw);
+
 	ret = pass_firmware_to_tee(hw);
 	if (ret)
 		dev_err(hw->dev, "Failed to preload firmware via TEE\n");
diff --git a/drivers/media/platform/amlogic/vdec/reg_defines.h b/drivers/media/platform/amlogic/vdec/reg_defines.h
index ea50018a078d..b3cbddfe530e 100644
--- a/drivers/media/platform/amlogic/vdec/reg_defines.h
+++ b/drivers/media/platform/amlogic/vdec/reg_defines.h
@@ -173,5 +173,29 @@
 #define DOS_GCLK_EN3                 REG_ALIGN(0x3f35)
 #define DOS_HEVC_INT_EN              REG_ALIGN(0x3f36)
 
+/*
+ * The HEVC/AV1 core mirrors the legacy VDEC-1 register layout 0x3000 words
+ * higher: MPSR 0x0301 -> HEVC_MPSR 0x3301, IMEM_DMA_CTRL 0x0340 ->
+ * HEVC_IMEM_DMA_CTRL 0x3340.  Its scratch file sits in the ASSIST block at
+ * 0x30c0 rather than in MDEC.  Nothing decodes on this core yet; these exist so
+ * its power/clock state can be measured before secure world is asked to write
+ * microcode into it.
+ */
+/*
+ * Mailbox 0 is the one the AV1 microcode signals on; mainline's older HEVC/VP9
+ * support uses mailbox 1 instead, so its hevc_regs.h only names that pair.  The
+ * spacing matches the legacy VDEC-1 assist block exactly (0x0075/0x0076 there),
+ * and mainline's HEVC_ASSIST_MBOX1_CLR_REG 0xc1d4 -> word 0x3075 confirms it.
+ */
+#define HEVC_ASSIST_MBOX0_IRQ_REG    REG_ALIGN(0x3070)
+#define HEVC_ASSIST_MBOX0_CLR_REG    REG_ALIGN(0x3071)
+#define HEVC_ASSIST_MBOX0_MASK       REG_ALIGN(0x3072)
+#define HEVC_ASSIST_SCRATCH_0        REG_ALIGN(0x30c0)
+#define HEVC_ASSIST_SCRATCH_1        REG_ALIGN(0x30c1)
+#define HEVC_MPC_E                   REG_ALIGN(0x3308)
+#define HEVC_MPSR                    REG_ALIGN(0x3301)
+#define HEVC_CPSR                    REG_ALIGN(0x3321)
+#define HEVC_IMEM_DMA_CTRL           REG_ALIGN(0x3340)
+
 #endif
 
-- 
2.56.0


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

* [PATCH 04/11] media: amlogic: vdec: h264: decode Main and High profile bit-exact
  2026-10-25 19:24 [PATCH 00/11] media: amlogic: vdec: S4 decoding beyond H.264 Baseline christian.koever-draxl
                   ` (2 preceding siblings ...)
  2026-10-25 19:24 ` [PATCH 03/11] media: amlogic: vdec: fix the microcode loading and prepare the HEVC core christian.koever-draxl
@ 2026-10-25 19:25 ` christian.koever-draxl
  2026-10-25 19:25 ` [PATCH 05/11] media: amlogic: vdec: add MPEG-1/2 decoding christian.koever-draxl
                   ` (7 subsequent siblings)
  11 siblings, 0 replies; 20+ messages in thread
From: christian.koever-draxl @ 2026-10-25 19:25 UTC (permalink / raw)
  To: zhentao.guo, mchehab, robh, krzk+dt, conor+dt
  Cc: linux-media, devicetree, linux-kernel,
	Christian Stefan Kövér-Draxl

From: Christian Stefan Kövér-Draxl <ckoever@outlook.de>

Fix the H.264 path so that Baseline, Main and High streams, CAVLC and
CABAC, I/P/B pictures, weighted prediction and the 8x8 transform decode
bit-exact against the reference decoder:

- the microcode parses the slice header itself and wants SPS/PPS NAL
  units in the bitstream, so rebuild them from the stateless controls
  and prepend them to every buffer
- write the PPS table with se(v) coding where the microcode expects it
- pass the scaling lists, without which High profile never starts
- take the reference-list length from the slice header unless the PPS
  default applies, and stop reordering once the list is full

Signed-off-by: Christian Stefan Kövér-Draxl <ckoever@outlook.de>
---
 drivers/media/platform/amlogic/vdec/Makefile  |   2 +-
 drivers/media/platform/amlogic/vdec/h264.c    | 477 ++++++++++++++++--
 drivers/media/platform/amlogic/vdec/h264.h    |   7 +
 .../platform/amlogic/vdec/h264_headers.c      | 249 +++++++++
 4 files changed, 685 insertions(+), 50 deletions(-)
 create mode 100644 drivers/media/platform/amlogic/vdec/h264_headers.c

diff --git a/drivers/media/platform/amlogic/vdec/Makefile b/drivers/media/platform/amlogic/vdec/Makefile
index f9c35e9ac902..5a2cf8e6814e 100644
--- a/drivers/media/platform/amlogic/vdec/Makefile
+++ b/drivers/media/platform/amlogic/vdec/Makefile
@@ -1,4 +1,4 @@
 # SPDX-License-Identifier: GPL-2.0-only
-aml-vdec-drv-objs := aml_vdec.o aml_vdec_drv.o aml_vdec_hw.o aml_vdec_platform.o h264.o aml_vdec_tee_fw.o
+aml-vdec-drv-objs := aml_vdec.o aml_vdec_drv.o aml_vdec_hw.o aml_vdec_platform.o h264.o h264_headers.o aml_vdec_tee_fw.o
 
 obj-$(CONFIG_VIDEO_AMLOGIC_VDEC) += aml-vdec-drv.o
diff --git a/drivers/media/platform/amlogic/vdec/h264.c b/drivers/media/platform/amlogic/vdec/h264.c
index 7584d5eb20ec..7f1ebd7588f5 100644
--- a/drivers/media/platform/amlogic/vdec/h264.c
+++ b/drivers/media/platform/amlogic/vdec/h264.c
@@ -11,8 +11,54 @@
 
 #define INVALID_POC 0xffffffff
 
+/*
+ * Every signed field in the SPS and PPS parameter tables is se(v)-coded in the
+ * bitstream, and the microcode decodes what the host writes the same way: it
+ * applies the se(v) mapping to the value it reads out of the table.  So a
+ * signed value has to be written back as its *code number*, not as two's
+ * complement.
+ *
+ * Measured, by writing known bytes and reading PIC_INIT_QP_MINUS26 back out of
+ * the microcode's LMEM: 0x00 -> 0, 0x01 -> +1, 0x10 -> -8, 0xf0 -> -120.  That
+ * is exactly k -> (k & 1) ? (k + 1) / 2 : -(k / 2).
+ *
+ * Writing two's complement instead - which is what this driver did, and what
+ * mainline's h264 patch does - turned pic_init_qp_minus26 = -16 into -120, so
+ * every slice decoded at a quantiser around 100 below the right one.  That is
+ * the whole of the "residual scaling is wrong and it tracks QP" defect: a
+ * QP-10 stream came out at 5.3 dB and now comes out at 25.9 dB.
+ */
+static u16 se_code(int v)
+{
+	return v > 0 ? (2 * v - 1) : (-2 * v);
+}
+
+static void write_scaling_lists(void *slot,
+				const struct v4l2_ctrl_h264_scaling_matrix *sm,
+				int num_8x8)
+{
+	u8 *b = slot;
+	int i, j;
+
+	if (!sm)
+		return;
+	for (i = 0; i < 6; i++)
+		for (j = 0; j < 16; j++)
+			*b++ = sm->scaling_list_4x4[i][j];
+	for (i = 0; i < num_8x8; i++)
+		for (j = 0; j < 64; j++)
+			*b++ = sm->scaling_list_8x8[i][j];
+}
+
 #define H264_SLICE_HEADER_DONE		0x1
 #define H264_SLICE_DATA_DONE		0x2
+/*
+ * The newer video_ucode packages (v0.3.31, from the 2025 vendor image) ask the
+ * host to acknowledge a configuration step before they will decode, and end a
+ * frame-based picture with BUFEMPTY rather than PIC_DATA_DONE.
+ */
+#define H264_CONFIG_REQUEST		0x11
+#define H264_DECODE_BUFEMPTY		0x20
 
 #define H264_MAX_COL_BUF			32
 #define H264_MAX_CANVAS_POS			26
@@ -29,7 +75,7 @@ struct vdec_h264_stateless_ctrl_ref {
 	const struct v4l2_ctrl_h264_pps *pps;
 };
 
-enum SliceType {
+enum aml_h264_slice_type {
 	P_SLICE = 0,
 	B_SLICE = 1,
 	I_SLICE = 2,
@@ -85,16 +131,16 @@ union param {
 		unsigned short num_ref_idx_l0_active_minus1;
 		unsigned short num_ref_idx_l1_active_minus1;
 
-		unsigned short PrevPicOrderCntLsb;
-		unsigned short PreviousFrameNum;
+		unsigned short prev_pic_order_cnt_lsb;
+		unsigned short previous_frame_num;
 
 		/* 32 bits variables */
 		unsigned short delta_pic_order_cnt_bottom[2];
 		unsigned short delta_pic_order_cnt_0[2];
 		unsigned short delta_pic_order_cnt_1[2];
 
-		unsigned short PrevPicOrderCntMsb[2];
-		unsigned short PrevFrameNumOffset[2];
+		unsigned short prev_pic_order_cnt_msb[2];
+		unsigned short prev_frame_num_offset[2];
 
 		unsigned short frame_pic_order_cnt[2];
 		unsigned short top_field_pic_order_cnt[2];
@@ -148,9 +194,10 @@ struct h264_decode_buf_spec {
 	u32 dpb_index;
 	int poc;
 	int col_buf_index;
-	u8 y_canvas_index;
-	u8 u_canvas_index;
-	u8 v_canvas_index;
+	/* signed: -1 means "no canvas held"; a u8 made that sentinel 255 */
+	s16 y_canvas_index;
+	s16 u_canvas_index;
+	s16 v_canvas_index;
 	u8 used;
 	u8 long_term_flag;
 	dma_addr_t y_dma_addr;
@@ -222,6 +269,15 @@ struct aml_h264_ctx {
 	u32 max_num_ref_frames;
 
 	struct vdec_h264_stateless_ctrl_ref ctrl_ref;
+
+	/*
+	 * Staging buffer for the in-band SPS/PPS path: the OUTPUT buffer is
+	 * normally handed to the stream FIFO directly, so prepending anything
+	 * needs a buffer of our own.
+	 */
+	void *stage_vaddr;
+	dma_addr_t stage_paddr;
+	size_t stage_size;
 };
 
 static inline int get_flag(u32 flag, u32 mask)
@@ -236,9 +292,9 @@ static inline void write_lmem(unsigned short *base, u32 offset, u32 value)
 
 static inline uint32_t spec2canvas(struct h264_decode_buf_spec *buf_spec)
 {
-	return (buf_spec->v_canvas_index << 16) |
-		(buf_spec->u_canvas_index << 8) |
-		(buf_spec->y_canvas_index << 0);
+	return ((buf_spec->v_canvas_index & 0xff) << 16) |
+		((buf_spec->u_canvas_index & 0xff) << 8) |
+		((buf_spec->y_canvas_index & 0xff) << 0);
 }
 
 static struct h264_decode_buf_spec *find_spec_by_dpb_index(struct aml_h264_ctx
@@ -262,14 +318,67 @@ static struct h264_decode_buf_spec *find_spec_by_dpb_index(struct aml_h264_ctx
 	return NULL;
 }
 
+/*
+ * Copy the buffer into the staging area behind freshly built SPS and PPS
+ * NALs.  Returns the new payload size, or a negative error to fall back to
+ * feeding the OUTPUT buffer as it stands.
+ */
+static int h264_stage_with_headers(struct aml_h264_ctx *h264_ctx,
+				   struct vb2_buffer *vb, u32 payload_size)
+{
+	struct aml_vdec_ctx *ctx = h264_ctx->v4l2_ctx;
+	struct aml_vdec_hw *hw = vdec_get_hw(ctx->dev);
+	struct vdec_h264_stateless_ctrl_ref *ctrls = &h264_ctx->ctrl_ref;
+	size_t want = ALIGN(payload_size + SZ_4K + VLD_PADDING_SIZE, SZ_64K);
+	void *src_v = vb2_plane_vaddr(vb, 0);
+	int n = 0, r;
+
+	if (!ctrls->sps || !ctrls->pps || !src_v)
+		return -EINVAL;
+
+	if (h264_ctx->stage_vaddr && h264_ctx->stage_size < want) {
+		dma_free_coherent(hw->dev, h264_ctx->stage_size,
+				  h264_ctx->stage_vaddr, h264_ctx->stage_paddr);
+		h264_ctx->stage_vaddr = NULL;
+	}
+	if (!h264_ctx->stage_vaddr) {
+		h264_ctx->stage_vaddr = dma_alloc_coherent(hw->dev, want,
+							   &h264_ctx->stage_paddr,
+							   GFP_KERNEL);
+		if (!h264_ctx->stage_vaddr)
+			return -ENOMEM;
+		h264_ctx->stage_size = want;
+	}
+
+	r = aml_h264_build_sps(h264_ctx->stage_vaddr, SZ_2K, ctrls->sps);
+	if (r < 0)
+		return r;
+	n = r;
+	r = aml_h264_build_pps(h264_ctx->stage_vaddr + n, SZ_2K, ctrls->pps,
+			       ctrls->scaling, ctrls->sps->chroma_format_idc);
+	if (r < 0)
+		return r;
+	n += r;
+
+	if (n + payload_size + VLD_PADDING_SIZE > h264_ctx->stage_size)
+		return -ENOSPC;
+	memcpy(h264_ctx->stage_vaddr + n, src_v, payload_size);
+	memset(h264_ctx->stage_vaddr + n + payload_size, 0, VLD_PADDING_SIZE);
+
+	dev_dbg(hw->dev, "h264: payload %u + %d bytes of SPS/PPS\n",
+		payload_size, n);
+	return n + payload_size;
+}
+
 static int h264_prepare_input(struct aml_vdec_ctx *ctx)
 {
 	struct aml_vdec_hw *hw = vdec_get_hw(ctx->dev);
+	struct aml_h264_ctx *h264_ctx = ctx->codec_priv;
 	struct vb2_v4l2_buffer *src;
 	struct vb2_buffer *vb;
 	dma_addr_t src_dma;
 	u32 payload_size;
-	int dummy;
+	int dummy, n;
 
 	src = v4l2_m2m_next_src_buf(ctx->fh.m2m_ctx);
 	if (!src) {
@@ -280,6 +389,12 @@ static int h264_prepare_input(struct aml_vdec_ctx *ctx)
 	payload_size = vb2_get_plane_payload(vb, 0);
 	src_dma = vb2_dma_contig_plane_dma_addr(vb, 0);
 
+	n = h264_stage_with_headers(h264_ctx, vb, payload_size);
+	if (n > 0) {
+		src_dma = h264_ctx->stage_paddr;
+		payload_size = n;
+	}
+
 	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_CONTROL, 0);
 	/* reset VLD fifo for all vdec */
 	regmap_write(hw->map[DOS_BUS], DOS_SW_RESET0,
@@ -288,8 +403,16 @@ static int h264_prepare_input(struct aml_vdec_ctx *ctx)
 	regmap_write(hw->map[DOS_BUS], POWER_CTL_VLD, 1 << 4);
 
 	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_START_PTR, src_dma);
+	/*
+	 * The end pointer bounds the FIFO itself, not this frame: the write
+	 * pointer below is payload_size + VLD_PADDING_SIZE past the start, so
+	 * ending the FIFO at the payload leaves WP outside it and the ucode
+	 * waits forever for data (decoder timeout, pc stuck around 0x796).
+	 */
 	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_END_PTR,
-		     (src_dma + payload_size));
+		     src_dma == h264_ctx->stage_paddr ?
+		     (src_dma + h264_ctx->stage_size - VDEC_FIFO_ALIGN) :
+		     (src_dma + vb2_plane_size(vb, 0) - VDEC_FIFO_ALIGN));
 	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_CURR_PTR,
 		     round_down(src_dma, VDEC_FIFO_ALIGN));
 
@@ -329,7 +452,17 @@ static void config_sps_params(struct aml_h264_ctx *h264_ctx,
 	unsigned short data_tmp[0x100];
 	int i, ii;
 
-	memset(sps_base, 0, 0x100);
+	/*
+	 * Both of these were wrong and the microcode read the difference.
+	 *
+	 * data_tmp is copied out in full - all 0x100 entries - but only the
+	 * first dozen or so are ever assigned, so every parameter past the
+	 * ones below was whatever the stack happened to hold.  And the
+	 * memset() cleared 0x100 *bytes* of a buffer that is 0x100 *entries*
+	 * wide, so it did not even cover the half of it that is written.
+	 */
+	memset(data_tmp, 0, sizeof(data_tmp));
+	memset(sps_base, 0, 0x100 * sizeof(*sps_base));
 
 	h264_ctx->frame_width = (sps->pic_width_in_mbs_minus1 + 1) << 4;
 	h264_ctx->frame_height = (sps->pic_height_in_map_units_minus1 + 1) << 4;
@@ -366,6 +499,13 @@ static void config_sps_params(struct aml_h264_ctx *h264_ctx,
 			GET_SPS_OFFSET_FOR_TOP_BOT_FIELD_LOW(sps->offset_for_top_to_bottom_field);
 		data_tmp[offset++] =
 			GET_SPS_OFFSET_FOR_TOP_BOT_FIELD_HIGH(sps->offset_for_top_to_bottom_field);
+		/*
+		 * offset_for_non_ref_pic and offset_for_top_to_bottom_field
+		 * are se(v) in the bitstream too, but they are written here as
+		 * a 32-bit value split over two entries, so whether the
+		 * microcode se(v)-decodes them is untested - pic_order_cnt_type
+		 * 1 is rare and no stream to hand uses it.
+		 */
 		data_tmp[offset++] = sps->num_ref_frames_in_pic_order_cnt_cycle;
 	}
 
@@ -390,6 +530,9 @@ static void config_sps_params(struct aml_h264_ctx *h264_ctx,
 		for (ii = 0; ii < 4; ii++)
 			sps_base[i + 3 - ii] = data_tmp[i + ii];
 	}
+	write_scaling_lists((u8 *)sps_base + 0x100,
+			    h264_ctx->ctrl_ref.scaling,
+				    sps->chroma_format_idc == 3 ? 6 : 2);
 
 	frame_size = (sps->pic_width_in_mbs_minus1 + 1) * (sps->pic_height_in_map_units_minus1 + 1);
 	cfg_tmp = (get_flag(sps->flags, V4L2_H264_SPS_FLAG_FRAME_MBS_ONLY) << 31) |
@@ -430,7 +573,9 @@ static void config_pps_params(struct aml_h264_ctx *h264_ctx,
 	u32 max_list_size;
 	int i, ii;
 
-	memset(pps_base, 0, 0x100);
+	/* Same two bugs as config_sps_params(); see the comment there. */
+	memset(data_tmp, 0, sizeof(data_tmp));
+	memset(pps_base, 0, 0x100 * sizeof(*pps_base));
 
 	data_tmp[offset++] = PARAM_BASE_VAL;
 
@@ -451,11 +596,12 @@ static void config_pps_params(struct aml_h264_ctx *h264_ctx,
 		GET_PPS_NUM_IDX_REF_L1_MINUS1(pps->num_ref_idx_l1_default_active_minus1) |
 		GET_PPS_NUM_IDX_REF_L0_MINUS1(pps->num_ref_idx_l0_default_active_minus1);
 
-	data_tmp[offset++] = GET_PPS_INIT_QS_MINUS26(pps->pic_init_qs_minus26) |
-	    GET_PPS_INIT_QP_MINUS26(pps->pic_init_qp_minus26);
+	data_tmp[offset++] =
+		GET_PPS_INIT_QS_MINUS26(se_code(pps->pic_init_qs_minus26)) |
+		GET_PPS_INIT_QP_MINUS26(se_code(pps->pic_init_qp_minus26));
 
 	data_tmp[offset] =
-	    GET_PPS_CHROMA_QP_INDEX_OFFSET(pps->chroma_qp_index_offset) |
+	    GET_PPS_CHROMA_QP_INDEX_OFFSET(se_code(pps->chroma_qp_index_offset)) |
 	    GET_PPS_DEBLOCK_FILTER_CTRL_PRESENT_FLAG
 				(get_flag(pps->flags,
 					  V4L2_H264_PPS_FLAG_DEBLOCKING_FILTER_CONTROL_PRESENT)) |
@@ -465,20 +611,29 @@ static void config_pps_params(struct aml_h264_ctx *h264_ctx,
 	    GET_PPS_REDUNDANT_PIC_CNT_PRESENT_FLAG
 	    (get_flag(pps->flags,
 		      V4L2_H264_PPS_FLAG_REDUNDANT_PIC_CNT_PRESENT));
+	/*
+	 * Bit 11 is "the PPS carries its trailing part" - transform_8x8_mode,
+	 * pic_scaling_matrix_present and second_chroma_qp_index_offset.  Only
+	 * set it when the stream really has one, or the microcode reads
+	 * entries this table has no business supplying.
+	 */
 	if (get_flag(pps->flags, V4L2_H264_PPS_FLAG_TRANSFORM_8X8_MODE |
 	     V4L2_H264_PPS_FLAG_SCALING_MATRIX_PRESENT))
 		data_tmp[offset] |= (1 << 11);
 	offset++;
 
-	data_tmp[offset++] =
-		GET_PPS_SCALING_MATRIX_PRESENT_FLAG(get_flag
-						    (pps->flags,
-						    V4L2_H264_PPS_FLAG_SCALING_MATRIX_PRESENT)) |
-		GET_PPS_TRANSFORM_8X8_FLAG(get_flag(pps->flags,
-						    V4L2_H264_PPS_FLAG_TRANSFORM_8X8_MODE));
+	{
+		bool t8x8 = get_flag(pps->flags,
+				     V4L2_H264_PPS_FLAG_TRANSFORM_8X8_MODE);
+		bool lists = get_flag(pps->flags,
+				      V4L2_H264_PPS_FLAG_SCALING_MATRIX_PRESENT);
+
+		data_tmp[offset++] = GET_PPS_SCALING_MATRIX_PRESENT_FLAG(lists) |
+				     GET_PPS_TRANSFORM_8X8_FLAG(t8x8);
+	}
 
 	data_tmp[offset++] =
-		GET_PPS_GET_SECOND_CHROMA_QP_OFFSET(pps->second_chroma_qp_index_offset);
+		GET_PPS_GET_SECOND_CHROMA_QP_OFFSET(se_code(pps->second_chroma_qp_index_offset));
 
 	max_list_size = (pps->num_ref_idx_l1_default_active_minus1 + 1) +
 	    (pps->num_ref_idx_l0_default_active_minus1 + 1);
@@ -494,6 +649,8 @@ static void config_pps_params(struct aml_h264_ctx *h264_ctx,
 		for (ii = 0; ii < 4; ii++)
 			pps_base[i + 3 - ii] = data_tmp[i + ii];
 	}
+	write_scaling_lists((u8 *)pps_base + 0x100,
+			    h264_ctx->ctrl_ref.scaling, 2);
 }
 
 static void h264_config_params(struct aml_vdec_ctx *ctx)
@@ -516,18 +673,41 @@ static void h264_config_params(struct aml_vdec_ctx *ctx)
 	config_pps_params(h264_ctx, p_pps_base, pps);
 }
 
+static void init_buf_spec_canvas(struct h264_decode_buf_spec *buf_spec)
+{
+	buf_spec->y_canvas_index = -1;
+	buf_spec->u_canvas_index = -1;
+	buf_spec->v_canvas_index = -1;
+}
+
 static void config_decode_canvas(struct aml_vdec_hw *hw,
 				 struct h264_decode_buf_spec *buf_spec,
 				 u32 mb_width, u32 mb_height)
 {
 	int canvas_alloc_result = 0;
 	int blkmode = 0x0;
+	u8 y_index, u_index;
+
+	/* already configured for this frame - do not leak another pair */
+	if (buf_spec->y_canvas_index >= 0 && buf_spec->u_canvas_index >= 0)
+		return;
 
-	canvas_alloc_result = meson_canvas_alloc(hw->canvas, &buf_spec->y_canvas_index);
-	canvas_alloc_result = meson_canvas_alloc(hw->canvas, &buf_spec->u_canvas_index);
-	buf_spec->v_canvas_index = buf_spec->u_canvas_index;
+	canvas_alloc_result = meson_canvas_alloc(hw->canvas, &y_index);
+	if (canvas_alloc_result) {
+		dev_err(hw->dev, "no canvas for y plane\n");
+		return;
+	}
+	canvas_alloc_result = meson_canvas_alloc(hw->canvas, &u_index);
+	if (canvas_alloc_result) {
+		dev_err(hw->dev, "no canvas for uv plane\n");
+		meson_canvas_free(hw->canvas, y_index);
+		return;
+	}
+	buf_spec->y_canvas_index = y_index;
+	buf_spec->u_canvas_index = u_index;
+	buf_spec->v_canvas_index = u_index;
 
-	if (!canvas_alloc_result) {
+	{
 		/* config y canvas */
 		meson_canvas_config(hw->canvas,
 				    buf_spec->y_canvas_index, buf_spec->y_dma_addr,
@@ -750,15 +930,26 @@ static int get_ref_list_size(struct aml_h264_ctx *h264_ctx, int cur_list)
 	unsigned short override_flag = h264_ctx->dpb_param.l.data[REF_IDC_OVERRIDE_FLAG];
 	int num_ref_idx_lx_active_minus1;
 
+	/*
+	 * The microcode parses the slice header, so its count already accounts
+	 * for num_ref_idx_active_override_flag and is the one to use.  Reading
+	 * the PPS default unless REF_IDC_OVERRIDE_FLAG happened to be set left
+	 * the list an entry short on every slice that overrides - x264's
+	 * weightp=2 with more than one reference does exactly that, and the
+	 * last reordering command was then dropped.
+	 */
 	if (cur_list == 0) {
 		num_ref_idx_lx_active_minus1 =
-			h264_ctx->ctrl_ref.pps->num_ref_idx_l0_default_active_minus1;
-		if (override_flag)
+			h264_ctx->dpb_param.dpb.num_ref_idx_l0_active_minus1;
+		if (!override_flag && !num_ref_idx_lx_active_minus1)
 			num_ref_idx_lx_active_minus1 =
-				h264_ctx->dpb_param.dpb.num_ref_idx_l0_active_minus1;
+				h264_ctx->ctrl_ref.pps->num_ref_idx_l0_default_active_minus1;
 	} else {
 		num_ref_idx_lx_active_minus1 =
-			h264_ctx->ctrl_ref.pps->num_ref_idx_l1_default_active_minus1;
+			h264_ctx->dpb_param.dpb.num_ref_idx_l1_active_minus1;
+		if (!override_flag && !num_ref_idx_lx_active_minus1)
+			num_ref_idx_lx_active_minus1 =
+				h264_ctx->ctrl_ref.pps->num_ref_idx_l1_default_active_minus1;
 	}
 	dev_dbg(&ctx->dev->plat_dev->dev, "%s get list %d size %d\n",
 		__func__, cur_list, num_ref_idx_lx_active_minus1 + 1);
@@ -848,6 +1039,14 @@ static void reorder_short_term(struct slice *curr_slice, int cur_list,
 		return;
 	}
 
+	/*
+	 * There used to be a "the picture is already at this index, nothing to
+	 * do" short cut here.  It is not equivalent: 8.2.4.3.1 always inserts
+	 * and then drops the *later* copy, which is what lets one picture
+	 * appear twice in a list - and that is exactly what x264's weightp=2
+	 * does with more than one reference frame.  Taking the short cut left
+	 * the list one entry short and every P frame after the third drifted.
+	 */
 	if (*ref_idx_lx == get_refidx_by_picnum(h264_ctx, pic_num_lx, cur_list)) {
 		dev_dbg(&ctx->dev->plat_dev->dev, "no need to move pic lx %d\n", *ref_idx_lx);
 		*ref_idx_lx = *ref_idx_lx + 1;
@@ -981,6 +1180,16 @@ static void reorder_pics(struct aml_h264_ctx *h264_ctx,
 
 	pic_num_lx_pred = curr_pic_num;
 	for (i = 0; i < REORDERING_COMMAND_MAX_SIZE && modification_of_pic_nums_idc[i] != 3; i++) {
+		/*
+		 * The microcode's command buffer is not always terminated with
+		 * a 3, and the zeros after the real commands parse as more
+		 * "insert the picture one below the last" commands.  A stream
+		 * can only reorder as many entries as the list has, so stop
+		 * there rather than trusting the terminator.
+		 */
+		if (ref_idx_lx >= get_ref_list_size(h264_ctx, cur_list))
+			break;
+
 		if (modification_of_pic_nums_idc[i] > 3) {
 			dev_info(&ctx->dev->plat_dev->dev, "error, Invalid modification_of_pic_nums_idc command\n");
 			break;
@@ -1008,13 +1217,35 @@ static void reorder_pics(struct aml_h264_ctx *h264_ctx,
 	}
 }
 
+static void dump_ref_list(struct aml_h264_ctx *h264_ctx, int list,
+			  const char *when)
+{
+	struct aml_vdec_ctx *ctx = h264_ctx->v4l2_ctx;
+	struct h264_decode_buf_spec *l = list ? h264_ctx->ref_list1 :
+					       h264_ctx->ref_list0;
+	char buf[128];
+	int i, n = 0;
+
+	for (i = 0; i < 16 && l[i].dpb; i++)
+		n += scnprintf(buf + n, sizeof(buf) - n, "%d(lt%d) ",
+			       l[i].dpb->pic_num, l[i].long_term_flag);
+	buf[n] = 0;
+	dev_dbg(&ctx->dev->plat_dev->dev,
+		"h264 reflist%d %s size %d frame_num %d: %s\n",
+		 list, when, h264_ctx->list_size[list],
+		 h264_ctx->mslice.frame_num, buf);
+}
+
 static void copy_ref_list(struct aml_h264_ctx *h264_ctx, int curr_list)
 {
 	if (curr_list == 0)
 		memcpy(h264_ctx->ref_list0_unreordered, h264_ctx->ref_list0,
 		       sizeof(h264_ctx->ref_list0));
 	else
-		memcpy(h264_ctx->ref_list1_unreordered, h264_ctx->ref_list0,
+		/* This copied ref_list0, so list 1 reordering never had its
+		 * own unreordered list to search.
+		 */
+		memcpy(h264_ctx->ref_list1_unreordered, h264_ctx->ref_list1,
 		       sizeof(h264_ctx->ref_list1));
 }
 
@@ -1048,8 +1279,10 @@ static void h264_reorder_reflists(struct aml_h264_ctx *h264_ctx)
 	if (curr_slice->slice_type != I_SLICE &&
 	    curr_slice->slice_type != SI_SLICE &&
 	    curr_slice->ref_pic_list_reordering_flag[0] != 0) {
+		dump_ref_list(h264_ctx, 0, "before");
 		copy_ref_list(h264_ctx, 0);
 		reorder_pics(h264_ctx, curr_slice, 0);
+		dump_ref_list(h264_ctx, 0, "after ");
 	}
 
 	if (curr_slice->slice_type == B_SLICE &&
@@ -1074,6 +1307,17 @@ static void h264_config_ref_lists(struct aml_vdec_ctx *ctx)
 	struct v4l2_h264_reference v4l2_b1_reflist[V4L2_H264_REF_LIST_LEN];
 	struct slice *curr_slice = &h264_ctx->mslice;
 
+	dev_dbg(&ctx->dev->plat_dev->dev,
+		"%s raw_slice_type %d mapped %d decode->flags 0x%x nal_ref_idc %d\n",
+		__func__, h264_ctx->dpb_param.l.data[SLICE_TYPE],
+		curr_slice->slice_type, decode->flags, decode->nal_ref_idc);
+	dev_dbg(&ctx->dev->plat_dev->dev,
+		"%s qp: ctrl pic_init_qp_minus26 %d packed 0x%04x ucode rpm 0x%04x chroma_off %d\n",
+		__func__, ctrls->pps->pic_init_qp_minus26,
+		GET_PPS_INIT_QP_MINUS26(ctrls->pps->pic_init_qp_minus26),
+		h264_ctx->dpb_param.l.data[PIC_INIT_QP_MINUS26],
+		ctrls->pps->chroma_qp_index_offset);
+
 	if (decode->flags == V4L2_H264_DECODE_PARAM_FLAG_IDR_PIC)
 		return;
 
@@ -1169,8 +1413,12 @@ static void clear_unused_col_buf(struct aml_h264_ctx *h264_ctx,
 
 	/* flush all col buffers when IDR */
 	if (decode->flags == V4L2_H264_DECODE_PARAM_FLAG_IDR_PIC) {
-		/* 32 : max index of co-locate buffer */
-		for (i = 0; i < 32; i++)
+		/*
+		 * Only the buffers that exist.  Sweeping all 32 slots made
+		 * release_colocate_buf() complain once per unused index on
+		 * every IDR - 56 dev_err lines for a 44-frame clip.
+		 */
+		for (i = 0; i < h264_ctx->colocated_buf_num; i++)
 			release_colocate_buf(h264_ctx, i);
 		for (i = 0; i < (V4L2_H264_NUM_DPB_ENTRIES + 1); i++)
 			release_canvas_pos(h264_ctx, i);
@@ -1236,8 +1484,15 @@ static void h264_config_decode_spec(struct aml_vdec_hw *hw, struct aml_vdec_ctx
 	for (i = 0; i < V4L2_H264_NUM_DPB_ENTRIES; i++) {
 		struct v4l2_h264_dpb_entry *dpb = &decode->dpb[i];
 
+		/*
+		 * Skip, do not stop.  The DPB is a flat array and nothing in
+		 * the uAPI says the active entries come first - GStreamer
+		 * happens to pack them that way, libva-v4l2 does not, and
+		 * breaking on the first inactive slot then finds no references
+		 * at all and decodes every P frame against nothing.
+		 */
 		if (!(dpb->flags & V4L2_H264_DPB_ENTRY_FLAG_ACTIVE))
-			break;
+			continue;
 
 		buf_spec_l0 = find_spec_by_dpb_index(h264_ctx, i, 0);
 		if (buf_spec_l0) {
@@ -1713,12 +1968,22 @@ static irqreturn_t h264_threaded_isr_func(int irq, void *priv)
 {
 	u32 dec_status;
 	struct aml_vdec_dev *dev = (struct aml_vdec_dev *)priv;
+	/*
+	 * The context whose job is on the hardware, not whichever was opened
+	 * last: dev->dec_ctx follows every open() of any codec, so with a
+	 * second client around it points at a context with no job at all.
+	 */
 	struct aml_h264_ctx *h264_ctx = (struct aml_h264_ctx *)dev->dec_hw->curr_ctx;
-	struct aml_vdec_ctx *ctx = (struct aml_vdec_ctx *)dev->dec_ctx;
-	struct aml_vdec_hw *hw = vdec_get_hw(ctx->dev);
-	unsigned short *p = (unsigned short *)h264_ctx->lmem_addr;
+	struct aml_vdec_ctx *ctx;
+	struct aml_vdec_hw *hw = dev->dec_hw;
+	unsigned short *p;
 	int i, ii;
 
+	if (!h264_ctx)
+		return IRQ_HANDLED;
+	ctx = h264_ctx->v4l2_ctx;
+	p = (unsigned short *)h264_ctx->lmem_addr;
+
 	regmap_read(hw->map[DOS_BUS], DPB_STATUS_REG, &dec_status);
 	h264_ctx->dec_status = dec_status;
 	dev_dbg
@@ -1744,9 +2009,23 @@ static irqreturn_t h264_threaded_isr_func(int irq, void *priv)
 		get_canvas_index(hw, ctx);
 
 		if (h264_config_decode_buf(hw, ctx) < 0) {
+			/*
+			 * Drop this picture's state and let the microcode
+			 * carry on - it usually does, and the slice-data
+			 * interrupt behind this one finishes the job.
+			 *
+			 * Deliberately no int_cond/wake here.  Waking
+			 * aml_h264_dec_run() makes it return success while the
+			 * job is still running, so if the microcode does *not*
+			 * come back the job is never finished at all: the next
+			 * VIDIOC_STREAMOFF then blocks for ever in
+			 * v4l2_m2m_cancel_job(), the RCU grace period stalls
+			 * behind it, and within a minute every task that
+			 * touches a V4L2 node or a cgroup is in D state and the
+			 * machine is unusable.  Staying asleep lets the decode
+			 * timeout fire and cancel the job properly.
+			 */
 			h264_release_decode_spec(hw, ctx);
-			ctx->int_cond = 1;
-			wake_up_interruptible(&ctx->queue);
 			goto irq_handled;
 		}
 		if (h264_ctx->new_pic_flag == 1) {
@@ -1758,20 +2037,70 @@ static irqreturn_t h264_threaded_isr_func(int irq, void *priv)
 			dev_dbg(&dev->plat_dev->dev, "action decode new slice\n");
 		}
 		break;
+	case H264_CONFIG_REQUEST:
+		/*
+		 * The ucode wants the picture configured before it will start:
+		 * same work as the slice-header path, acknowledged with
+		 * CONFIG_DONE instead of a decode action.
+		 */
+		for (i = 0; i < 0x400; i += 4)
+			for (ii = 0; ii < 4; ii++)
+				h264_ctx->dpb_param.l.data[i + ii] = p[i + 3 - ii];
+		save_reg_status(h264_ctx);
+		h264_get_slice_params(h264_ctx);
+		h264_config_decode_spec(hw, ctx);
+		h264_reorder_reflists(h264_ctx);
+		get_canvas_index(hw, ctx);
+		if (h264_config_decode_buf(hw, ctx) < 0) {
+			/*
+			 * Drop this picture's state and let the microcode
+			 * carry on - it usually does, and the slice-data
+			 * interrupt behind this one finishes the job.
+			 *
+			 * Deliberately no int_cond/wake here.  Waking
+			 * aml_h264_dec_run() makes it return success while the
+			 * job is still running, so if the microcode does *not*
+			 * come back the job is never finished at all: the next
+			 * VIDIOC_STREAMOFF then blocks for ever in
+			 * v4l2_m2m_cancel_job(), the RCU grace period stalls
+			 * behind it, and within a minute every task that
+			 * touches a V4L2 node or a cgroup is in D state and the
+			 * machine is unusable.  Staying asleep lets the decode
+			 * timeout fire and cancel the job properly.
+			 */
+			h264_release_decode_spec(hw, ctx);
+			goto irq_handled;
+		}
+		/*
+		 * The microcode raises this for profile_idc >= 100 with
+		 * TARGET_NUM_SCALING_LIST set, and will not go on to parse a
+		 * slice header until it is satisfied - so High profile never
+		 * decodes.  Acknowledging is all this driver knows how to do;
+		 * see the note in config_pps_params().
+		 */
+		dev_dbg(&dev->plat_dev->dev,
+			"config request, scaling lists %d\n",
+			h264_ctx->dpb_param.l.data[TARGET_NUM_SCALING_LIST]);
+		regmap_write(hw->map[DOS_BUS], DPB_STATUS_REG, H264_ACTION_CONFIG_DONE);
+		dev_dbg(&dev->plat_dev->dev, "action config done\n");
+		break;
 	case H264_SLICE_DATA_DONE:
+	case H264_DECODE_BUFEMPTY:
 		h264_release_decode_spec(hw, ctx);
 		h264_ctx->decode_pic_count++;
 		ctx->int_cond = 1;
+		ctx->job_finished = true;
 		v4l2_m2m_buf_done_and_job_finish(dev->m2m_dev_dec, ctx->m2m_ctx,
 						 VB2_BUF_STATE_DONE);
-		wake_up_interruptible(&ctx->queue);
+		wake_up(&ctx->queue);
 		break;
 	default:
 		h264_release_decode_spec(hw, ctx);
 		ctx->int_cond = 1;
+		ctx->job_finished = true;
 		v4l2_m2m_buf_done_and_job_finish(dev->m2m_dev_dec, ctx->m2m_ctx,
 						 VB2_BUF_STATE_ERROR);
-		wake_up_interruptible(&ctx->queue);
+		wake_up(&ctx->queue);
 		break;
 	}
 irq_handled:
@@ -1896,6 +2225,14 @@ int aml_h264_init(void *priv)
 		return -ENOMEM;
 
 	h264_ctx->v4l2_ctx = ctx;
+	init_buf_spec_canvas(&h264_ctx->curr_spec);
+	for (ret = 0; ret < V4L2_H264_NUM_DPB_ENTRIES + 1; ret++) {
+		init_buf_spec_canvas(&h264_ctx->ref_list0[ret]);
+		init_buf_spec_canvas(&h264_ctx->ref_list1[ret]);
+		init_buf_spec_canvas(&h264_ctx->ref_list0_unreordered[ret]);
+		init_buf_spec_canvas(&h264_ctx->ref_list1_unreordered[ret]);
+	}
+	ret = 0;
 	dec_hw = vdec_get_hw(ctx->dev);
 	if (!dec_hw)
 		return -1;
@@ -1923,8 +2260,8 @@ int aml_h264_init(void *priv)
 	h264_ctx->workspace_offset = h264_ctx->cma_alloc_addr + DCAC_READ_MARGIN;
 	h264_ctx->workspace_vaddr = h264_ctx->cma_alloc_vaddr + DCAC_READ_MARGIN;
 
+	/* curr_ctx is bound per job in aml_h264_dec_run(), not here. */
 	ctx->codec_priv = h264_ctx;
-	dec_hw->curr_ctx = h264_ctx;
 	h264_ctx->col_buf_alloc_size = 0;
 	h264_ctx->init_flag = 0;
 	h264_ctx->new_pic_flag = 0;
@@ -1960,6 +2297,12 @@ void aml_h264_exit(void *priv)
 	if (ctx->dos_clk_en)
 		aml_stop_vdec_hw(dec_hw);
 
+	if (h264_ctx->stage_vaddr) {
+		dma_free_coherent(dec_hw->dev, h264_ctx->stage_size,
+				  h264_ctx->stage_vaddr, h264_ctx->stage_paddr);
+		h264_ctx->stage_vaddr = NULL;
+	}
+
 	if (h264_ctx->collated_cma_vaddr) {
 		dma_free_coherent(dec_hw->dev, h264_ctx->col_buf_alloc_size,
 				  h264_ctx->collated_cma_vaddr,
@@ -1983,8 +2326,9 @@ void aml_h264_exit(void *priv)
 				  h264_ctx->cma_alloc_vaddr,
 				  h264_ctx->cma_alloc_addr);
 
+	if (dec_hw->curr_ctx == h264_ctx)
+		dec_hw->curr_ctx = NULL;
 	kfree(ctx->codec_priv);
-	dec_hw->curr_ctx = NULL;
 	ctx->codec_priv = NULL;
 }
 
@@ -1996,6 +2340,12 @@ static void config_decode_mode(struct aml_vdec_ctx *ctx)
 	regmap_write(hw->map[DOS_BUS], H264_DECODE_MODE, 0x1);	/*decode mode framebase */
 	regmap_write(hw->map[DOS_BUS], HEAD_PADDING_REG, 0);
 	regmap_write(hw->map[DOS_BUS], H264_DECODE_SEQINFO, h264_ctx->seq_info);
+	/*
+	 * Always 1.  The vendor writes 0 for the first run of an instance,
+	 * but that tells the microcode to initialise itself from SPS/PPS in
+	 * the bitstream - which the stateless API never delivers - and every
+	 * stream then decodes to nothing.
+	 */
 	regmap_write(hw->map[DOS_BUS], INIT_FLAG_REG, 1);
 }
 
@@ -2007,6 +2357,11 @@ int aml_h264_dec_run(void *priv)
 	int ret = -1;
 	int i;
 
+	dec_hw->curr_ctx = h264_ctx;
+	/* The legacy core's interrupt is shared with MPEG-2: claim it per job. */
+	dec_hw->hw_ops.irq_handler = h264_isr;
+	dec_hw->hw_ops.irq_threaded_func = h264_threaded_isr_func;
+
 	ret = aml_h264_get_stateless_ctrl_ref(h264_ctx);
 	if (ret < 0) {
 		dev_err(&ctx->dev->plat_dev->dev, "not ctrl ref for h264 decoder\n");
@@ -2042,13 +2397,37 @@ int aml_h264_dec_run(void *priv)
 
 	regmap_write(dec_hw->map[DOS_BUS], DPB_STATUS_REG, H264_ACTION_SEARCH_HEAD);
 
-	ret = wait_event_interruptible_timeout(ctx->queue, ctx->int_cond,
-					       msecs_to_jiffies(DECODER_TIMEOUT_MS));
+	/*
+	 * Uninterruptible on purpose: a signal must not abandon a decode the
+	 * hardware is still running - see the note in vp9.c.
+	 */
+	ret = wait_event_timeout(ctx->queue, ctx->int_cond,
+				 msecs_to_jiffies(DECODER_TIMEOUT_MS));
 	ctx->int_cond = 0;
 	if (!ret) {
 		ret = -1;
 		dev_err(&ctx->dev->plat_dev->dev, "dec timeout=%u\n", DECODER_TIMEOUT_MS);
-		for (i = 0; i < 16; i++) {	/* 16 : show ucode PC 16 times when timeout */
+		dev_err(&ctx->dev->plat_dev->dev,
+			"vififo start %08x curr %08x end %08x wp %08x rp %08x level %08x avail %08x ctrl %08x\n",
+			read_dos_reg(dec_hw, VLD_MEM_VIFIFO_START_PTR),
+			read_dos_reg(dec_hw, VLD_MEM_VIFIFO_CURR_PTR),
+			read_dos_reg(dec_hw, VLD_MEM_VIFIFO_END_PTR),
+			read_dos_reg(dec_hw, VLD_MEM_VIFIFO_WP),
+			read_dos_reg(dec_hw, VLD_MEM_VIFIFO_RP),
+			read_dos_reg(dec_hw, VLD_MEM_VIFIFO_LEVEL),
+			read_dos_reg(dec_hw, VLD_MEM_VIFIFO_BYTES_AVAIL),
+			read_dos_reg(dec_hw, VLD_MEM_VIFIFO_CONTROL));
+		dev_err(&ctx->dev->plat_dev->dev,
+			"dpb_status %08x bitcnt %08x shift %08x mby_mbx %08x scratch0 %08x scratch1 %08x scratchF %08x scratchG %08x\n",
+			read_dos_reg(dec_hw, DPB_STATUS_REG),
+			read_dos_reg(dec_hw, VIFF_BIT_CNT),
+			read_dos_reg(dec_hw, VLD_SHIFT_STATUS),
+			read_dos_reg(dec_hw, MBY_MBX),
+			read_dos_reg(dec_hw, AV_SCRATCH_0),
+			read_dos_reg(dec_hw, AV_SCRATCH_1),
+			read_dos_reg(dec_hw, AV_SCRATCH_F),
+			read_dos_reg(dec_hw, AV_SCRATCH_G));
+		for (i = 0; i < 4; i++) {	/* show ucode PC a few times when timeout */
 			dev_info(&ctx->dev->plat_dev->dev, "decoder timeout, pc 0x%x\n",
 				 read_dos_reg(dec_hw, MPC_E));
 			usleep_range(10, 20);
diff --git a/drivers/media/platform/amlogic/vdec/h264.h b/drivers/media/platform/amlogic/vdec/h264.h
index 830ab3241a1e..f12150f9f5b0 100644
--- a/drivers/media/platform/amlogic/vdec/h264.h
+++ b/drivers/media/platform/amlogic/vdec/h264.h
@@ -296,4 +296,11 @@ int aml_h264_init(void *priv);
 void aml_h264_exit(void *priv);
 int aml_h264_dec_run(void *priv);
 
+int aml_h264_build_sps(u8 *out, size_t out_size,
+		       const struct v4l2_ctrl_h264_sps *sps);
+int aml_h264_build_pps(u8 *out, size_t out_size,
+		       const struct v4l2_ctrl_h264_pps *pps,
+		       const struct v4l2_ctrl_h264_scaling_matrix *sm,
+		       u8 chroma_format_idc);
+
 #endif
diff --git a/drivers/media/platform/amlogic/vdec/h264_headers.c b/drivers/media/platform/amlogic/vdec/h264_headers.c
new file mode 100644
index 000000000000..a8450a672172
--- /dev/null
+++ b/drivers/media/platform/amlogic/vdec/h264_headers.c
@@ -0,0 +1,249 @@
+// SPDX-License-Identifier: (GPL-2.0-only OR MIT)
+/*
+ * Rebuild H.264 SPS and PPS NAL units from the V4L2 stateless controls.
+ *
+ * The stateless API hands the decoder slice NALs and puts the parameter sets
+ * in controls, so the microcode never sees an SPS or a PPS.  For most streams
+ * that is fine - the driver writes the parameters into the microcode's own
+ * tables - but for profile_idc >= 100 the microcode answers every buffer with
+ * H264_CONFIG_REQUEST, asking for scaling lists it expects to have parsed
+ * itself, and never gets as far as a slice header.  Handing it real headers is
+ * the way out: it is the mode the microcode was written for.
+ *
+ * The same shape as av1_build_sequence_obu() in av1.c.
+ */
+
+#include <linux/kernel.h>
+#include <linux/string.h>
+#include <media/v4l2-ctrls.h>
+
+#include "h264.h"
+
+struct h264_bw {
+	u8 *buf;
+	size_t size;
+	size_t bit;
+	bool overflow;
+};
+
+static void bw_bit(struct h264_bw *bw, u32 v)
+{
+	size_t byte = bw->bit >> 3;
+	int shift = 7 - (bw->bit & 7);
+
+	if (byte >= bw->size) {
+		bw->overflow = true;
+		return;
+	}
+	if (shift == 7)
+		bw->buf[byte] = 0;
+	if (v & 1)
+		bw->buf[byte] |= 1 << shift;
+	bw->bit++;
+}
+
+static void bw_bits(struct h264_bw *bw, u32 v, int n)
+{
+	while (n--)
+		bw_bit(bw, v >> n);
+}
+
+static void bw_ue(struct h264_bw *bw, u32 v)
+{
+	int bits = 0;
+	u32 t = v + 1;
+
+	while (t >> ++bits)
+		;
+	bw_bits(bw, 0, bits - 1);
+	bw_bits(bw, v + 1, bits);
+}
+
+static void bw_se(struct h264_bw *bw, int v)
+{
+	bw_ue(bw, v > 0 ? (2 * v - 1) : (-2 * v));
+}
+
+static void bw_trailing(struct h264_bw *bw)
+{
+	bw_bit(bw, 1);
+	while (bw->bit & 7)
+		bw_bit(bw, 0);
+}
+
+/*
+ * scaling_list(), 7.3.2.1.1.1.  The control carries the matrix already
+ * resolved by the fall-back rules, so every list is written out in full as a
+ * chain of delta_scale values that reproduces it exactly.
+ */
+static void bw_scaling_list(struct h264_bw *bw, const u8 *list, int n)
+{
+	int last = 8, j;
+
+	for (j = 0; j < n; j++) {
+		int delta = (int)list[j] - last;
+
+		while (delta > 127)
+			delta -= 256;
+		while (delta < -128)
+			delta += 256;
+		bw_se(bw, delta);
+		last = list[j];
+	}
+}
+
+/* Annex B: start code, NAL header, then the RBSP with 0x03 escaping. */
+static int emit_nal(u8 *out, size_t out_size, u8 nal_hdr,
+		    const u8 *rbsp, size_t rbsp_len)
+{
+	size_t n = 0, i;
+	int zeros = 0;
+
+	if (out_size < rbsp_len + 8)
+		return -ENOSPC;
+
+	out[n++] = 0;
+	out[n++] = 0;
+	out[n++] = 0;
+	out[n++] = 1;
+	out[n++] = nal_hdr;
+
+	for (i = 0; i < rbsp_len; i++) {
+		if (zeros == 2 && rbsp[i] <= 3) {
+			if (n + 2 > out_size)
+				return -ENOSPC;
+			out[n++] = 3;
+			zeros = 0;
+		}
+		if (n + 1 > out_size)
+			return -ENOSPC;
+		out[n++] = rbsp[i];
+		zeros = rbsp[i] ? 0 : zeros + 1;
+	}
+	return n;
+}
+
+int aml_h264_build_sps(u8 *out, size_t out_size,
+		       const struct v4l2_ctrl_h264_sps *sps)
+{
+	u8 rbsp[64];
+	struct h264_bw bw = { .buf = rbsp, .size = sizeof(rbsp) };
+	int i;
+
+	bw_bits(&bw, sps->profile_idc, 8);
+	bw_bits(&bw, sps->constraint_set_flags, 6);
+	bw_bits(&bw, 0, 2);			/* reserved_zero_2bits */
+	bw_bits(&bw, sps->level_idc, 8);
+	bw_ue(&bw, sps->seq_parameter_set_id);
+
+	if (sps->profile_idc >= 100) {
+		bw_ue(&bw, sps->chroma_format_idc);
+		if (sps->chroma_format_idc == 3)
+			bw_bit(&bw, !!(sps->flags &
+				       V4L2_H264_SPS_FLAG_SEPARATE_COLOUR_PLANE));
+		bw_ue(&bw, sps->bit_depth_luma_minus8);
+		bw_ue(&bw, sps->bit_depth_chroma_minus8);
+		bw_bit(&bw, !!(sps->flags &
+			       V4L2_H264_SPS_FLAG_QPPRIME_Y_ZERO_TRANSFORM_BYPASS));
+		/*
+		 * seq_scaling_matrix_present_flag = 0.  The resolved matrix
+		 * goes in the PPS instead, which overrides the sequence one,
+		 * so one place to write it is enough.
+		 */
+		bw_bit(&bw, 0);
+	}
+
+	bw_ue(&bw, sps->log2_max_frame_num_minus4);
+	bw_ue(&bw, sps->pic_order_cnt_type);
+	if (sps->pic_order_cnt_type == 0) {
+		bw_ue(&bw, sps->log2_max_pic_order_cnt_lsb_minus4);
+	} else if (sps->pic_order_cnt_type == 1) {
+		bw_bit(&bw, !!(sps->flags &
+			       V4L2_H264_SPS_FLAG_DELTA_PIC_ORDER_ALWAYS_ZERO));
+		bw_se(&bw, sps->offset_for_non_ref_pic);
+		bw_se(&bw, sps->offset_for_top_to_bottom_field);
+		bw_ue(&bw, sps->num_ref_frames_in_pic_order_cnt_cycle);
+		for (i = 0; i < sps->num_ref_frames_in_pic_order_cnt_cycle; i++)
+			bw_se(&bw, sps->offset_for_ref_frame[i]);
+	}
+
+	bw_ue(&bw, sps->max_num_ref_frames);
+	bw_bit(&bw, !!(sps->flags &
+		       V4L2_H264_SPS_FLAG_GAPS_IN_FRAME_NUM_VALUE_ALLOWED));
+	bw_ue(&bw, sps->pic_width_in_mbs_minus1);
+	bw_ue(&bw, sps->pic_height_in_map_units_minus1);
+	bw_bit(&bw, !!(sps->flags & V4L2_H264_SPS_FLAG_FRAME_MBS_ONLY));
+	if (!(sps->flags & V4L2_H264_SPS_FLAG_FRAME_MBS_ONLY))
+		bw_bit(&bw, !!(sps->flags &
+			       V4L2_H264_SPS_FLAG_MB_ADAPTIVE_FRAME_FIELD));
+	bw_bit(&bw, !!(sps->flags & V4L2_H264_SPS_FLAG_DIRECT_8X8_INFERENCE));
+	/*
+	 * No cropping and no VUI.  The control carries neither, and both are
+	 * the client's business: the driver sizes buffers from the picture
+	 * size it was given, not from what the microcode parses here.
+	 */
+	bw_bit(&bw, 0);			/* frame_cropping_flag */
+	bw_bit(&bw, 0);			/* vui_parameters_present_flag */
+	bw_trailing(&bw);
+
+	if (bw.overflow)
+		return -ENOSPC;
+	return emit_nal(out, out_size, 0x67, rbsp, bw.bit >> 3);
+}
+
+int aml_h264_build_pps(u8 *out, size_t out_size,
+		       const struct v4l2_ctrl_h264_pps *pps,
+		       const struct v4l2_ctrl_h264_scaling_matrix *sm,
+		       u8 chroma_format_idc)
+{
+	u8 rbsp[512];
+	struct h264_bw bw = { .buf = rbsp, .size = sizeof(rbsp) };
+	bool t8x8 = !!(pps->flags & V4L2_H264_PPS_FLAG_TRANSFORM_8X8_MODE);
+	int n8x8, i;
+
+	bw_ue(&bw, pps->pic_parameter_set_id);
+	bw_ue(&bw, pps->seq_parameter_set_id);
+	bw_bit(&bw, !!(pps->flags & V4L2_H264_PPS_FLAG_ENTROPY_CODING_MODE));
+	bw_bit(&bw, !!(pps->flags &
+		       V4L2_H264_PPS_FLAG_BOTTOM_FIELD_PIC_ORDER_IN_FRAME_PRESENT));
+	/*
+	 * num_slice_groups_minus1 is forced to 0: slice groups need a slice
+	 * group map the stateless API does not carry, and no encoder in use
+	 * emits them.
+	 */
+	bw_ue(&bw, 0);
+	bw_ue(&bw, pps->num_ref_idx_l0_default_active_minus1);
+	bw_ue(&bw, pps->num_ref_idx_l1_default_active_minus1);
+	bw_bit(&bw, !!(pps->flags & V4L2_H264_PPS_FLAG_WEIGHTED_PRED));
+	bw_bits(&bw, pps->weighted_bipred_idc, 2);
+	bw_se(&bw, pps->pic_init_qp_minus26);
+	bw_se(&bw, pps->pic_init_qs_minus26);
+	bw_se(&bw, pps->chroma_qp_index_offset);
+	bw_bit(&bw, !!(pps->flags &
+		       V4L2_H264_PPS_FLAG_DEBLOCKING_FILTER_CONTROL_PRESENT));
+	bw_bit(&bw, !!(pps->flags & V4L2_H264_PPS_FLAG_CONSTRAINED_INTRA_PRED));
+	bw_bit(&bw, !!(pps->flags & V4L2_H264_PPS_FLAG_REDUNDANT_PIC_CNT_PRESENT));
+
+	/* The trailing part, always present so the scaling lists can go in. */
+	bw_bit(&bw, t8x8);
+	if (sm) {
+		bw_bit(&bw, 1);		/* pic_scaling_matrix_present_flag */
+		n8x8 = t8x8 ? (chroma_format_idc == 3 ? 6 : 2) : 0;
+		for (i = 0; i < 6; i++) {
+			bw_bit(&bw, 1);	/* pic_scaling_list_present_flag */
+			bw_scaling_list(&bw, sm->scaling_list_4x4[i], 16);
+		}
+		for (i = 0; i < n8x8; i++) {
+			bw_bit(&bw, 1);
+			bw_scaling_list(&bw, sm->scaling_list_8x8[i], 64);
+		}
+	} else {
+		bw_bit(&bw, 0);
+	}
+	bw_se(&bw, pps->second_chroma_qp_index_offset);
+	bw_trailing(&bw);
+
+	if (bw.overflow)
+		return -ENOSPC;
+	return emit_nal(out, out_size, 0x68, rbsp, bw.bit >> 3);
+}
-- 
2.56.0


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

* [PATCH 05/11] media: amlogic: vdec: add MPEG-1/2 decoding
  2026-10-25 19:24 [PATCH 00/11] media: amlogic: vdec: S4 decoding beyond H.264 Baseline christian.koever-draxl
                   ` (3 preceding siblings ...)
  2026-10-25 19:25 ` [PATCH 04/11] media: amlogic: vdec: h264: decode Main and High profile bit-exact christian.koever-draxl
@ 2026-10-25 19:25 ` christian.koever-draxl
  2026-10-25 20:28   ` sashiko-bot
  2026-10-25 19:25 ` [PATCH 06/11] media: amlogic: vdec: add MJPEG decoding christian.koever-draxl
                   ` (6 subsequent siblings)
  11 siblings, 1 reply; 20+ messages in thread
From: christian.koever-draxl @ 2026-10-25 19:25 UTC (permalink / raw)
  To: zhentao.guo, mchehab, robh, krzk+dt, conor+dt
  Cc: linux-media, devicetree, linux-kernel,
	Christian Stefan Kövér-Draxl

From: Christian Stefan Kövér-Draxl <ckoever@outlook.de>

Decode MPEG-1 and MPEG-2 (V4L2_PIX_FMT_MPEG2_SLICE) on the VDEC core
with the mpeg12 multi-instance microcode. The microcode parses the
headers itself, so the sequence, picture and quantisation headers are
rebuilt from the stateless controls in front of the slice data.

Field pictures are decoded as pairs into one held CAPTURE buffer. The
output differs from ffmpeg by +-1..3 in a few blocks, which is the
hardware IDCT's rounding.

Signed-off-by: Christian Stefan Kövér-Draxl <ckoever@outlook.de>
---
 drivers/media/platform/amlogic/vdec/Makefile  |   2 +-
 .../media/platform/amlogic/vdec/aml_vdec.c    |  18 +
 .../platform/amlogic/vdec/aml_vdec_platform.c |  17 +
 .../platform/amlogic/vdec/aml_vdec_tee_fw.c   |   5 +
 drivers/media/platform/amlogic/vdec/mpeg12.c  | 708 ++++++++++++++++++
 drivers/media/platform/amlogic/vdec/mpeg12.h  |  51 ++
 6 files changed, 800 insertions(+), 1 deletion(-)
 create mode 100644 drivers/media/platform/amlogic/vdec/mpeg12.c
 create mode 100644 drivers/media/platform/amlogic/vdec/mpeg12.h

diff --git a/drivers/media/platform/amlogic/vdec/Makefile b/drivers/media/platform/amlogic/vdec/Makefile
index 5a2cf8e6814e..6d91750ec673 100644
--- a/drivers/media/platform/amlogic/vdec/Makefile
+++ b/drivers/media/platform/amlogic/vdec/Makefile
@@ -1,4 +1,4 @@
 # SPDX-License-Identifier: GPL-2.0-only
-aml-vdec-drv-objs := aml_vdec.o aml_vdec_drv.o aml_vdec_hw.o aml_vdec_platform.o h264.o h264_headers.o aml_vdec_tee_fw.o
+aml-vdec-drv-objs := aml_vdec.o aml_vdec_drv.o aml_vdec_hw.o aml_vdec_platform.o h264.o h264_headers.o mpeg12.o aml_vdec_tee_fw.o
 
 obj-$(CONFIG_VIDEO_AMLOGIC_VDEC) += aml-vdec-drv.o
diff --git a/drivers/media/platform/amlogic/vdec/aml_vdec.c b/drivers/media/platform/amlogic/vdec/aml_vdec.c
index 2a5ff842fd39..7a5098f3e116 100644
--- a/drivers/media/platform/amlogic/vdec/aml_vdec.c
+++ b/drivers/media/platform/amlogic/vdec/aml_vdec.c
@@ -66,12 +66,30 @@ static const struct aml_vdec_v4l2_ctrl controls[] = {
 			.max = V4L2_MPEG_VIDEO_H264_PROFILE_HIGH,
 			.def = V4L2_MPEG_VIDEO_H264_PROFILE_MAIN,
 		},
+	}, {
+		.codec_type = CODEC_TYPE_MPEG2,
+		.cfg = {
+			.id = V4L2_CID_STATELESS_MPEG2_SEQUENCE,
+		},
+	}, {
+		.codec_type = CODEC_TYPE_MPEG2,
+		.cfg = {
+			.id = V4L2_CID_STATELESS_MPEG2_PICTURE,
+		},
+	}, {
+		.codec_type = CODEC_TYPE_MPEG2,
+		.cfg = {
+			.id = V4L2_CID_STATELESS_MPEG2_QUANTISATION,
+		},
 	},};
 
 static const struct aml_dec_type dec_type_name[] = {
 	{
 		.codec_type = CODEC_TYPE_H264,
 		.name = "H264",
+	}, {
+		.codec_type = CODEC_TYPE_MPEG2,
+		.name = "MPEG2",
 	},
 };
 
diff --git a/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c b/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c
index fa4f979b07ee..83efadcb8aef 100644
--- a/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c
+++ b/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c
@@ -6,6 +6,7 @@
 #include "aml_vdec_platform.h"
 #include "aml_vdec_hw.h"
 #include "h264.h"
+#include "mpeg12.h"
 
 static struct aml_video_fmt aml_s4_video_formats[] = {
 	{
@@ -19,6 +20,17 @@ static struct aml_video_fmt aml_s4_video_formats[] = {
 		.stepwise = {AML_VDEC_MIN_W, AML_VDEC_1080P_MAX_W, 2,
 			AML_VDEC_MIN_H, AML_VDEC_1080P_MAX_H, 2},
 	},
+	{
+		.name = "MPEG-2",
+		.fourcc = V4L2_PIX_FMT_MPEG2_SLICE,
+		.type = AML_FMT_DEC,
+		.align = 64,
+		.is_10_bit_support = 0,
+		.codec_type = CODEC_TYPE_MPEG2,
+		.num_planes = 1,
+		.stepwise = {AML_VDEC_MIN_W, AML_VDEC_1080P_MAX_W, 2,
+			AML_VDEC_MIN_H, AML_VDEC_1080P_MAX_H, 2},
+	},
 	{
 		.name = "NV21M",
 		.fourcc = V4L2_PIX_FMT_NV21M,
@@ -79,6 +91,11 @@ const struct aml_codec_ops aml_S4_dec_ops[] = {
 		.exit = aml_h264_exit,
 		.run = aml_h264_dec_run,
 	},
+	[CODEC_TYPE_MPEG2] = {
+		.init = aml_mpeg12_init,
+		.exit = aml_mpeg12_exit,
+		.run = aml_mpeg12_dec_run,
+	},
 };
 
 const struct aml_dev_platform_data aml_vdec_s4_pdata = {
diff --git a/drivers/media/platform/amlogic/vdec/aml_vdec_tee_fw.c b/drivers/media/platform/amlogic/vdec/aml_vdec_tee_fw.c
index d7106ec47ba9..63da8b4dac28 100644
--- a/drivers/media/platform/amlogic/vdec/aml_vdec_tee_fw.c
+++ b/drivers/media/platform/amlogic/vdec/aml_vdec_tee_fw.c
@@ -82,6 +82,11 @@ static struct aml_tee_fw firmware[] = {
 		.core = CORE_VDEC_LEGENCY,
 		.is_swap = 1,
 	},
+	[CODEC_TYPE_MPEG2] = {
+		.fw_format = VIDEO_DEC_MPEG12_MULTI,
+		.core = CORE_VDEC_LEGENCY,
+		.is_swap = 0,
+	},
 };
 
 static int optee_ctx_match(struct tee_ioctl_version_data *ver, const void *data)
diff --git a/drivers/media/platform/amlogic/vdec/mpeg12.c b/drivers/media/platform/amlogic/vdec/mpeg12.c
new file mode 100644
index 000000000000..e09ff4d79902
--- /dev/null
+++ b/drivers/media/platform/amlogic/vdec/mpeg12.c
@@ -0,0 +1,708 @@
+// SPDX-License-Identifier: (GPL-2.0-only OR MIT)
+/*
+ * MPEG-1/2 decoding on the Amlogic legacy VDEC core, stateless.
+ *
+ * Ported from the reference's frame-based vmpeg12_multi.c.  That microcode
+ * parses the elementary stream itself - sequence header, extensions, picture
+ * header and the slices - so the parsed V4L2 controls are turned back into
+ * the headers they came from and staged in front of the slice data.  The
+ * microcode decodes into whatever canvas REC_CANVAS_ADDR names and predicts
+ * from MREG_REF0 (older) and MREG_REF1 (newer) anchor pictures; reordering
+ * and the reference bookkeeping are left to userspace, like every other
+ * stateless codec.
+ */
+
+#include <linux/delay.h>
+#include <linux/dma-mapping.h>
+#include <linux/module.h>
+#include <linux/sizes.h>
+#include <linux/slab.h>
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-mem2mem.h>
+#include <media/videobuf2-dma-contig.h>
+
+#include "aml_vdec_drv.h"
+#include "aml_vdec_hw.h"
+#include "mpeg12.h"
+
+#define MPEG12_TIMEOUT_MS	500
+#define MPEG12_HDR_MAX		1024
+#define MPEG12_TAIL		4
+
+struct mpeg12_canvas {
+	int y;
+	int uv;
+};
+
+struct aml_mpeg12_ctx {
+	struct aml_vdec_ctx *v4l2_ctx;
+
+	void *wks_vaddr;
+	dma_addr_t wks_paddr;
+	void *cc_vaddr;
+	dma_addr_t cc_paddr;
+
+	void *stage_vaddr;
+	dma_addr_t stage_paddr;
+	u32 stage_size;
+
+	/* rec, ref0, ref1 */
+	struct mpeg12_canvas canvas[3];
+
+	u32 pic_info;
+	u32 status;
+	u32 decoded;
+
+	/*
+	 * Field pictures come as two requests into one held capture buffer.
+	 * The first field is only stashed (headers + slices); the microcode
+	 * decodes both fields from one chunk when the second one arrives,
+	 * like the vendor's frame-based input, and interrupts once.
+	 */
+	u8 *fld_buf;
+	u32 fld_len;
+	u32 fld_cap;
+	u64 fld_ts;
+	bool fld_pending;
+	struct v4l2_ctrl_mpeg2_picture fld_pic;
+	/* the newest decoded I/P frame, for a field that predicts from its pair */
+	u64 anchor_ts;
+	bool anchor_valid;
+};
+
+struct bitw {
+	u8 *buf;
+	u32 bits;
+};
+
+static void put_bits(struct bitw *bw, u32 n, u32 v)
+{
+	while (n--) {
+		u32 byte = bw->bits >> 3;
+		u32 bit = 7 - (bw->bits & 7);
+
+		if (!(bw->bits & 7))
+			bw->buf[byte] = 0;
+		if ((v >> n) & 1)
+			bw->buf[byte] |= 1 << bit;
+		bw->bits++;
+	}
+}
+
+static void put_align(struct bitw *bw)
+{
+	while (bw->bits & 7)
+		put_bits(bw, 1, 0);
+}
+
+static void put_start(struct bitw *bw, u8 code)
+{
+	put_align(bw);
+	put_bits(bw, 24, 1);
+	put_bits(bw, 8, code);
+}
+
+static void *mpeg12_ctrl(struct aml_vdec_ctx *ctx, u32 id)
+{
+	struct v4l2_ctrl *ctrl = v4l2_ctrl_find(&ctx->ctrl_handler, id);
+
+	return ctrl ? ctrl->p_cur.p : NULL;
+}
+
+/*
+ * Rebuild sequence header + sequence extension + picture header + picture
+ * coding extension.  The quantiser matrices go in the sequence header
+ * (both are in zigzag order there and in the control); for 4:2:0 the chroma
+ * matrices are never used.
+ */
+static u32 mpeg12_write_headers(u8 *buf,
+				const struct v4l2_ctrl_mpeg2_sequence *seq,
+				const struct v4l2_ctrl_mpeg2_picture *pic,
+				const struct v4l2_ctrl_mpeg2_quantisation *quant,
+				bool with_seq)
+{
+	struct bitw bw = { .buf = buf };
+	u32 vbv = seq->vbv_buffer_size / 2048;
+	int i;
+
+	if (!vbv)
+		vbv = 112;
+
+	if (!with_seq)
+		goto picture;
+
+	put_start(&bw, 0xb3);
+	put_bits(&bw, 12, seq->horizontal_size & 0xfff);
+	put_bits(&bw, 12, seq->vertical_size & 0xfff);
+	put_bits(&bw, 4, 1);		/* aspect ratio: square */
+	put_bits(&bw, 4, 3);		/* 25 fps, informational */
+	put_bits(&bw, 18, 0x3ffff);	/* bit_rate: variable */
+	put_bits(&bw, 1, 1);
+	put_bits(&bw, 10, vbv & 0x3ff);
+	put_bits(&bw, 1, 0);		/* constrained_parameters_flag */
+	put_bits(&bw, 1, 1);
+	for (i = 0; i < 64; i++)
+		put_bits(&bw, 8, quant->intra_quantiser_matrix[i]);
+	put_bits(&bw, 1, 1);
+	for (i = 0; i < 64; i++)
+		put_bits(&bw, 8, quant->non_intra_quantiser_matrix[i]);
+
+	put_start(&bw, 0xb5);
+	put_bits(&bw, 4, 1);		/* sequence extension */
+	put_bits(&bw, 8, seq->profile_and_level_indication ?
+		 seq->profile_and_level_indication & 0xff : 0x48);
+	put_bits(&bw, 1, !!(seq->flags & V4L2_MPEG2_SEQ_FLAG_PROGRESSIVE));
+	put_bits(&bw, 2, seq->chroma_format ? seq->chroma_format : 1);
+	put_bits(&bw, 2, (seq->horizontal_size >> 12) & 3);
+	put_bits(&bw, 2, (seq->vertical_size >> 12) & 3);
+	put_bits(&bw, 12, 0);		/* bit_rate_extension */
+	put_bits(&bw, 1, 1);
+	put_bits(&bw, 8, (vbv >> 10) & 0xff);
+	put_bits(&bw, 1, 0);		/* low_delay */
+	put_bits(&bw, 2, 0);
+	put_bits(&bw, 5, 0);
+
+picture:
+	put_start(&bw, 0x00);
+	put_bits(&bw, 10, 0);		/* temporal_reference */
+	put_bits(&bw, 3, pic->picture_coding_type);
+	put_bits(&bw, 16, 0xffff);	/* vbv_delay */
+	if (pic->picture_coding_type == V4L2_MPEG2_PIC_CODING_TYPE_P ||
+	    pic->picture_coding_type == V4L2_MPEG2_PIC_CODING_TYPE_B) {
+		put_bits(&bw, 1, 0);
+		put_bits(&bw, 3, 7);
+	}
+	if (pic->picture_coding_type == V4L2_MPEG2_PIC_CODING_TYPE_B) {
+		put_bits(&bw, 1, 0);
+		put_bits(&bw, 3, 7);
+	}
+	put_bits(&bw, 1, 0);		/* extra_bit_picture */
+
+	put_start(&bw, 0xb5);
+	put_bits(&bw, 4, 8);		/* picture coding extension */
+	put_bits(&bw, 4, pic->f_code[0][0]);
+	put_bits(&bw, 4, pic->f_code[0][1]);
+	put_bits(&bw, 4, pic->f_code[1][0]);
+	put_bits(&bw, 4, pic->f_code[1][1]);
+	put_bits(&bw, 2, pic->intra_dc_precision);
+	put_bits(&bw, 2, pic->picture_structure);
+	put_bits(&bw, 1, !!(pic->flags & V4L2_MPEG2_PIC_FLAG_TOP_FIELD_FIRST));
+	put_bits(&bw, 1, !!(pic->flags & V4L2_MPEG2_PIC_FLAG_FRAME_PRED_DCT));
+	put_bits(&bw, 1, !!(pic->flags & V4L2_MPEG2_PIC_FLAG_CONCEALMENT_MV));
+	put_bits(&bw, 1, !!(pic->flags & V4L2_MPEG2_PIC_FLAG_Q_SCALE_TYPE));
+	put_bits(&bw, 1, !!(pic->flags & V4L2_MPEG2_PIC_FLAG_INTRA_VLC));
+	put_bits(&bw, 1, !!(pic->flags & V4L2_MPEG2_PIC_FLAG_ALT_SCAN));
+	put_bits(&bw, 1, !!(pic->flags & V4L2_MPEG2_PIC_FLAG_REPEAT_FIRST));
+	put_bits(&bw, 1, 1);		/* chroma_420_type */
+	put_bits(&bw, 1, !!(pic->flags & V4L2_MPEG2_PIC_FLAG_PROGRESSIVE));
+	put_bits(&bw, 1, 0);		/* composite_display_flag */
+	put_align(&bw);
+
+	return bw.bits >> 3;
+}
+
+/* Stash the first field of a pair: headers and slices, for the next job. */
+static int mpeg12_stash_field(struct aml_vdec_ctx *ctx,
+			      const struct v4l2_ctrl_mpeg2_sequence *seq,
+			      const struct v4l2_ctrl_mpeg2_picture *pic,
+			      const struct v4l2_ctrl_mpeg2_quantisation *quant,
+			      struct vb2_v4l2_buffer *src)
+{
+	struct aml_mpeg12_ctx *m = ctx->codec_priv;
+	u32 payload = vb2_get_plane_payload(&src->vb2_buf, 0);
+	u32 need = MPEG12_HDR_MAX + payload;
+	u32 hdr;
+
+	if (need > m->fld_cap) {
+		kvfree(m->fld_buf);
+		m->fld_cap = 0;
+		m->fld_buf = kvmalloc(need, GFP_KERNEL);
+		if (!m->fld_buf)
+			return -ENOMEM;
+		m->fld_cap = need;
+	}
+	hdr = mpeg12_write_headers(m->fld_buf, seq, pic, quant, true);
+	memcpy(m->fld_buf + hdr, vb2_plane_vaddr(&src->vb2_buf, 0), payload);
+	m->fld_len = hdr + payload;
+	m->fld_pic = *pic;
+	m->fld_ts = src->vb2_buf.timestamp;
+	m->fld_pending = true;
+	return 0;
+}
+
+static int mpeg12_prepare_input(struct aml_vdec_ctx *ctx,
+				const struct v4l2_ctrl_mpeg2_sequence *seq,
+				const struct v4l2_ctrl_mpeg2_picture *pic,
+				const struct v4l2_ctrl_mpeg2_quantisation *quant,
+				bool pair)
+{
+	struct aml_vdec_hw *hw = vdec_get_hw(ctx->dev);
+	struct aml_mpeg12_ctx *m = ctx->codec_priv;
+	struct vb2_v4l2_buffer *src;
+	struct vb2_buffer *vb;
+	u32 payload, hdr, need, len;
+	u8 *p;
+
+	src = v4l2_m2m_next_src_buf(ctx->fh.m2m_ctx);
+	if (!src)
+		return -EINVAL;
+	vb = &src->vb2_buf;
+	payload = vb2_get_plane_payload(vb, 0);
+
+	need = MPEG12_HDR_MAX + payload + MPEG12_TAIL + VLD_PADDING_SIZE + 64;
+	if (pair)
+		need += m->fld_len;
+	if (need > m->stage_size) {
+		if (m->stage_vaddr)
+			dma_free_coherent(hw->dev, m->stage_size,
+					  m->stage_vaddr, m->stage_paddr);
+		m->stage_size = PAGE_ALIGN(need + SZ_64K);
+		m->stage_vaddr = dma_alloc_coherent(hw->dev, m->stage_size,
+						    &m->stage_paddr, GFP_KERNEL);
+		if (!m->stage_vaddr) {
+			m->stage_size = 0;
+			return -ENOMEM;
+		}
+	}
+
+	p = m->stage_vaddr;
+	len = 0;
+	if (pair) {
+		memcpy(p, m->fld_buf, m->fld_len);
+		len = m->fld_len;
+	}
+	hdr = mpeg12_write_headers(p + len, seq, pic, quant, !pair);
+	memcpy(p + len + hdr, vb2_plane_vaddr(vb, 0), payload);
+	len += hdr + payload;
+	/* The microcode only ends the last slice on the next start code. */
+	p[len] = 0;
+	p[len + 1] = 0;
+	p[len + 2] = 1;
+	p[len + 3] = 0;
+	memset(p + len + MPEG12_TAIL, 0, VLD_PADDING_SIZE);
+
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_CONTROL, 0);
+	regmap_write(hw->map[DOS_BUS], DOS_SW_RESET0,
+		     (1 << 5) | (1 << 4) | (1 << 3));
+	regmap_write(hw->map[DOS_BUS], DOS_SW_RESET0, 0);
+	regmap_write(hw->map[DOS_BUS], POWER_CTL_VLD, 1 << 4);
+
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_START_PTR, m->stage_paddr);
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_END_PTR,
+		     m->stage_paddr + m->stage_size - VDEC_FIFO_ALIGN);
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_CURR_PTR, m->stage_paddr);
+
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_CONTROL, 1);
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_CONTROL, 0);
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_BUF_CNTL, 2);
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_RP, m->stage_paddr);
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_WP,
+		     round_down(m->stage_paddr + len + MPEG12_TAIL +
+				VLD_PADDING_SIZE, VDEC_FIFO_ALIGN));
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_BUF_CNTL, 3);
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_BUF_CNTL, 2);
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_CONTROL,
+		     (0x11 << 16) | (1 << 10) | (7 << 3));
+
+	regmap_write(hw->map[DOS_BUS], VIFF_BIT_CNT, len * 8);
+
+	return 0;
+}
+
+static void mpeg12_canvas_free(struct aml_vdec_hw *hw, struct aml_mpeg12_ctx *m)
+{
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(m->canvas); i++) {
+		if (m->canvas[i].y >= 0)
+			meson_canvas_free(hw->canvas, m->canvas[i].y);
+		if (m->canvas[i].uv >= 0)
+			meson_canvas_free(hw->canvas, m->canvas[i].uv);
+		m->canvas[i].y = -1;
+		m->canvas[i].uv = -1;
+	}
+}
+
+/* Returns the canvas spec (y | uv << 8 | uv << 16) for the buffer, or ~0. */
+static u32 mpeg12_canvas_setup(struct aml_vdec_ctx *ctx,
+			       struct mpeg12_canvas *c, struct vb2_buffer *vb)
+{
+	struct aml_vdec_hw *hw = vdec_get_hw(ctx->dev);
+	u32 w = ctx->pic_info.coded_width;
+	u32 h = ctx->pic_info.coded_height;
+	dma_addr_t y, uv;
+	u8 yi, ci;
+
+	y = vb2_dma_contig_plane_dma_addr(vb, 0);
+	if (ctx->pic_info.plane_num > 1)
+		uv = vb2_dma_contig_plane_dma_addr(vb, 1);
+	else
+		uv = y + ctx->pic_info.fb_size[0];
+
+	/* a slot reused within one job must not leak its old canvases */
+	if (c->y >= 0)
+		meson_canvas_free(hw->canvas, c->y);
+	if (c->uv >= 0)
+		meson_canvas_free(hw->canvas, c->uv);
+	c->y = -1;
+	c->uv = -1;
+
+	if (meson_canvas_alloc(hw->canvas, &yi))
+		return ~0u;
+	if (meson_canvas_alloc(hw->canvas, &ci)) {
+		meson_canvas_free(hw->canvas, yi);
+		return ~0u;
+	}
+	c->y = yi;
+	c->uv = ci;
+
+	meson_canvas_config(hw->canvas, yi, y, w, h, MESON_CANVAS_WRAP_NONE,
+			    MESON_CANVAS_BLKMODE_LINEAR, MESON_CANVAS_ENDIAN_SWAP64);
+	regmap_write(hw->map[DOS_BUS], VDEC_ASSIST_CANVAS_BLK32,
+		     (1 << 11) | (1 << 8) | yi);
+	meson_canvas_config(hw->canvas, ci, uv, w, h / 2, MESON_CANVAS_WRAP_NONE,
+			    MESON_CANVAS_BLKMODE_LINEAR, MESON_CANVAS_ENDIAN_SWAP64);
+	regmap_write(hw->map[DOS_BUS], VDEC_ASSIST_CANVAS_BLK32,
+		     (1 << 11) | (1 << 8) | ci);
+
+	return yi | (ci << 8) | (ci << 16);
+}
+
+static u32 mpeg12_ref_spec(struct aml_vdec_ctx *ctx, struct mpeg12_canvas *c,
+			   u64 ts)
+{
+	struct vb2_queue *vq = v4l2_m2m_get_dst_vq(ctx->fh.m2m_ctx);
+	struct vb2_buffer *vb = vb2_find_buffer(vq, ts);
+
+	if (!vb)
+		return 0xffffffff;
+	return mpeg12_canvas_setup(ctx, c, vb);
+}
+
+static void mpeg12_job_done(struct aml_vdec_ctx *ctx, enum vb2_buffer_state st)
+{
+	struct aml_vdec_dev *dev = ctx->dev;
+
+	mpeg12_canvas_free(dev->dec_hw, ctx->codec_priv);
+	ctx->int_cond = 1;
+	ctx->job_finished = true;
+	v4l2_m2m_buf_done_and_job_finish(dev->m2m_dev_dec, ctx->m2m_ctx, st);
+	wake_up(&ctx->queue);
+}
+
+static irqreturn_t mpeg12_isr(int irq, void *priv)
+{
+	struct aml_vdec_dev *dev = priv;
+
+	regmap_write(dev->dec_hw->map[DOS_BUS], VDEC_ASSIST_MBOX1_CLR_REG, 1);
+
+	return IRQ_WAKE_THREAD;
+}
+
+static irqreturn_t mpeg12_threaded_isr(int irq, void *priv)
+{
+	struct aml_vdec_dev *dev = priv;
+	struct aml_vdec_hw *hw = dev->dec_hw;
+	struct aml_mpeg12_ctx *m = hw->curr_ctx;
+	struct aml_vdec_ctx *ctx;
+	u32 reg;
+
+	if (!m)
+		return IRQ_HANDLED;
+	ctx = m->v4l2_ctx;
+	if (ctx->job_finished)
+		return IRQ_HANDLED;
+
+	if (read_dos_reg(hw, MREG_DEBUG)) {
+		regmap_write(hw->map[DOS_BUS], MREG_DEBUG, 0);
+		return IRQ_HANDLED;
+	}
+
+	/* Sequence info report: the parameters are known already, go on. */
+	if (read_dos_reg(hw, MREG_SEQ_REPORT) == 1) {
+		regmap_write(hw->map[DOS_BUS], MREG_SEQ_REPORT, 0);
+		return IRQ_HANDLED;
+	}
+
+	/* User data (closed captions) - nobody consumes it here. */
+	if (read_dos_reg(hw, MREG_USERDATA) & (1 << 16)) {
+		regmap_write(hw->map[DOS_BUS], MREG_USERDATA, 0);
+		return IRQ_HANDLED;
+	}
+
+	reg = read_dos_reg(hw, MREG_BUFFEROUT);
+	m->status = reg;
+	m->pic_info = read_dos_reg(hw, MREG_PIC_INFO);
+
+	dev_dbg(hw->dev,
+		"mpeg12 irq out %u info %08x rec %06x w %u h %u err %u\n",
+		 reg, m->pic_info, read_dos_reg(hw, REC_CANVAS_ADDR),
+		 read_dos_reg(hw, MREG_PIC_WIDTH),
+		 read_dos_reg(hw, MREG_PIC_HEIGHT),
+		 read_dos_reg(hw, MREG_ERROR_COUNT));
+
+	switch (reg) {
+	case MPEG12_PIC_DONE:
+	case MPEG12_SEQ_END:
+		m->decoded++;
+		mpeg12_job_done(ctx, (m->pic_info & MPEG12_PICINFO_ERROR) ?
+				VB2_BUF_STATE_ERROR : VB2_BUF_STATE_DONE);
+		break;
+	case MPEG12_DATA_EMPTY:
+	case MPEG12_DATA_REQUEST:
+		mpeg12_job_done(ctx, VB2_BUF_STATE_ERROR);
+		break;
+	default:
+		/* Not ours; leave the microcode waiting for the timeout. */
+		return IRQ_HANDLED;
+	}
+	regmap_write(hw->map[DOS_BUS], MREG_BUFFEROUT, 0);
+
+	return IRQ_HANDLED;
+}
+
+static void mpeg12_restore_hw(struct aml_vdec_ctx *ctx)
+{
+	struct aml_vdec_hw *hw = vdec_get_hw(ctx->dev);
+	struct aml_mpeg12_ctx *m = ctx->codec_priv;
+	u32 w = ctx->pic_info.coded_width;
+	u32 h = ctx->pic_info.coded_height;
+
+	regmap_write(hw->map[DOS_BUS], MREG_CO_MV_START, m->wks_paddr);
+	regmap_write(hw->map[DOS_BUS], MREG_CC_ADDR, m->cc_paddr);
+
+	/*
+	 * No context is carried between pictures: the headers are resent with
+	 * every one, so the microcode is always started as if fresh.
+	 */
+	regmap_write(hw->map[DOS_BUS], MPEG1_2_REG, 0);
+	regmap_write(hw->map[DOS_BUS], PSCALE_CTRL, 0);
+	regmap_write(hw->map[DOS_BUS], PIC_HEAD_INFO, 0x380);
+	regmap_write(hw->map[DOS_BUS], M4_CONTROL_REG, 0);
+	regmap_write(hw->map[DOS_BUS], VDEC_ASSIST_MBOX1_CLR_REG, 1);
+	regmap_write(hw->map[DOS_BUS], MREG_BUFFEROUT, 0);
+	regmap_write(hw->map[DOS_BUS], VDEC_ASSIST_MBOX1_MASK, 1);
+	regmap_write(hw->map[DOS_BUS], MREG_CMD, (w << 16) | h);
+	regmap_write(hw->map[DOS_BUS], MREG_PIC_WIDTH, 0);
+	regmap_write(hw->map[DOS_BUS], MREG_PIC_HEIGHT, 0);
+	regmap_write(hw->map[DOS_BUS], MREG_SEQ_INFO, 0);
+	regmap_write(hw->map[DOS_BUS], MREG_SIGNAL_TYPE, 0);
+	regmap_write(hw->map[DOS_BUS], MREG_ERROR_COUNT, 0);
+	/* bit1: report width/height, bit0: CC buffer from the host */
+	regmap_write(hw->map[DOS_BUS], MREG_FATAL_ERROR, 3);
+	regmap_write(hw->map[DOS_BUS], MREG_WAIT_BUFFER, 0);
+	regmap_write(hw->map[DOS_BUS], MREG_SEQ_REPORT, 0);
+	regmap_write(hw->map[DOS_BUS], MREG_DEBUG, 0);
+	regmap_write(hw->map[DOS_BUS], MREG_DECODE_STOP_POS, 0);
+
+	regmap_update_bits(hw->map[DOS_BUS], MDEC_PIC_DC_CTRL, 1 << 17, 1 << 17);
+	if (ctx->dec_fmt[AML_FMT_DST].fourcc == V4L2_PIX_FMT_NV21 ||
+	    ctx->dec_fmt[AML_FMT_DST].fourcc == V4L2_PIX_FMT_NV21M)
+		regmap_update_bits(hw->map[DOS_BUS], MDEC_PIC_DC_CTRL,
+				   1 << 16, 1 << 16);
+	else
+		regmap_update_bits(hw->map[DOS_BUS], MDEC_PIC_DC_CTRL, 1 << 16, 0);
+
+	regmap_write(hw->map[DOS_BUS], MREG_USERDATA, 0);
+	/* frame based input, no saved context */
+	regmap_write(hw->map[DOS_BUS], MREG_INPUT, 1 << 7);
+}
+
+int aml_mpeg12_init(void *priv)
+{
+	struct aml_vdec_ctx *ctx = priv;
+	struct aml_vdec_hw *hw = vdec_get_hw(ctx->dev);
+	struct aml_mpeg12_ctx *m;
+	int i;
+
+	if (!hw)
+		return -ENODEV;
+
+	m = kzalloc_obj(*m);
+	if (!m)
+		return -ENOMEM;
+	m->v4l2_ctx = ctx;
+	for (i = 0; i < ARRAY_SIZE(m->canvas); i++) {
+		m->canvas[i].y = -1;
+		m->canvas[i].uv = -1;
+	}
+
+	m->wks_vaddr = dma_alloc_coherent(hw->dev, MPEG12_WORKSPACE_SIZE,
+					  &m->wks_paddr, GFP_KERNEL);
+	m->cc_vaddr = dma_alloc_coherent(hw->dev, MPEG12_CCBUF_SIZE,
+					 &m->cc_paddr, GFP_KERNEL);
+	if (!m->wks_vaddr || !m->cc_vaddr) {
+		if (m->wks_vaddr)
+			dma_free_coherent(hw->dev, MPEG12_WORKSPACE_SIZE,
+					  m->wks_vaddr, m->wks_paddr);
+		if (m->cc_vaddr)
+			dma_free_coherent(hw->dev, MPEG12_CCBUF_SIZE,
+					  m->cc_vaddr, m->cc_paddr);
+		kfree(m);
+		return -ENOMEM;
+	}
+
+	ctx->codec_priv = m;
+	return 0;
+}
+
+void aml_mpeg12_exit(void *priv)
+{
+	struct aml_vdec_ctx *ctx = priv;
+	struct aml_mpeg12_ctx *m = ctx->codec_priv;
+	struct aml_vdec_hw *hw;
+
+	if (!m)
+		return;
+	hw = vdec_get_hw(ctx->dev);
+
+	if (ctx->dos_clk_en)
+		aml_stop_vdec_hw(hw);
+	mpeg12_canvas_free(hw, m);
+
+	if (m->stage_vaddr)
+		dma_free_coherent(hw->dev, m->stage_size,
+				  m->stage_vaddr, m->stage_paddr);
+	dma_free_coherent(hw->dev, MPEG12_WORKSPACE_SIZE,
+			  m->wks_vaddr, m->wks_paddr);
+	dma_free_coherent(hw->dev, MPEG12_CCBUF_SIZE,
+			  m->cc_vaddr, m->cc_paddr);
+	kvfree(m->fld_buf);
+
+	if (hw->curr_ctx == m)
+		hw->curr_ctx = NULL;
+	kfree(m);
+	ctx->codec_priv = NULL;
+}
+
+int aml_mpeg12_dec_run(void *priv)
+{
+	struct aml_vdec_ctx *ctx = priv;
+	struct aml_mpeg12_ctx *m = ctx->codec_priv;
+	struct aml_vdec_hw *hw = vdec_get_hw(ctx->dev);
+	const struct v4l2_ctrl_mpeg2_sequence *seq;
+	const struct v4l2_ctrl_mpeg2_picture *pic;
+	const struct v4l2_ctrl_mpeg2_quantisation *quant;
+	const struct v4l2_ctrl_mpeg2_picture *rpic;
+	struct vb2_v4l2_buffer *src, *dst;
+	u32 rec, ref0 = 0xffffffff, ref1 = 0xffffffff;
+	u64 fwd;
+	bool field, pair;
+	long ret;
+
+	seq = mpeg12_ctrl(ctx, V4L2_CID_STATELESS_MPEG2_SEQUENCE);
+	pic = mpeg12_ctrl(ctx, V4L2_CID_STATELESS_MPEG2_PICTURE);
+	quant = mpeg12_ctrl(ctx, V4L2_CID_STATELESS_MPEG2_QUANTISATION);
+	if (!seq || !pic || !quant)
+		return -EINVAL;
+
+	hw->curr_ctx = m;
+	hw->hw_ops.irq_handler = mpeg12_isr;
+	hw->hw_ops.irq_threaded_func = mpeg12_threaded_isr;
+
+	src = v4l2_m2m_next_src_buf(ctx->fh.m2m_ctx);
+	dst = v4l2_m2m_next_dst_buf(ctx->fh.m2m_ctx);
+	if (!src || !dst)
+		return -EINVAL;
+
+	field = pic->picture_structure != V4L2_MPEG2_PIC_FRAME;
+	pair = field && m->fld_pending &&
+	       m->fld_ts == src->vb2_buf.timestamp &&
+	       m->fld_pic.picture_structure != pic->picture_structure;
+	if (!pair)
+		m->fld_pending = false;
+
+	if (field && !pair &&
+	    (src->flags & V4L2_BUF_FLAG_M2M_HOLD_CAPTURE_BUF)) {
+		if (mpeg12_stash_field(ctx, seq, pic, quant, src) < 0)
+			return -ENOMEM;
+		dev_dbg(hw->dev, "mpeg12 stash field %u type %u\n",
+			pic->picture_structure,
+			 pic->picture_coding_type);
+		/* the capture buffer stays held for the second field */
+		ctx->int_cond = 1;
+		ctx->job_finished = true;
+		v4l2_m2m_buf_done_and_job_finish(ctx->dev->m2m_dev_dec,
+						 ctx->m2m_ctx,
+						 VB2_BUF_STATE_DONE);
+		return 0;
+	}
+	m->fld_pending = false;
+
+	if (mpeg12_prepare_input(ctx, seq, pic, quant, pair) < 0)
+		return -ENOMEM;
+
+	mpeg12_restore_hw(ctx);
+
+	rec = mpeg12_canvas_setup(ctx, &m->canvas[0], &dst->vb2_buf);
+	if (rec == ~0u)
+		goto fail;
+
+	/*
+	 * REF0 is the older anchor, REF1 the newer one.  A pair takes its
+	 * references from the first field unless that is an I field; the
+	 * second field of an I/P pair predicts from its own first field, which
+	 * the microcode knows, and from the previous anchor frame.
+	 */
+	rpic = pic;
+	if (pair && m->fld_pic.picture_coding_type !=
+		    V4L2_MPEG2_PIC_CODING_TYPE_I)
+		rpic = &m->fld_pic;
+	fwd = rpic->forward_ref_ts;
+	if (fwd == dst->vb2_buf.timestamp && m->anchor_valid)
+		fwd = m->anchor_ts;
+	if (rpic->picture_coding_type == V4L2_MPEG2_PIC_CODING_TYPE_P) {
+		ref1 = mpeg12_ref_spec(ctx, &m->canvas[2], fwd);
+		ref0 = ref1;
+	} else if (rpic->picture_coding_type == V4L2_MPEG2_PIC_CODING_TYPE_B) {
+		ref0 = mpeg12_ref_spec(ctx, &m->canvas[1], fwd);
+		ref1 = mpeg12_ref_spec(ctx, &m->canvas[2],
+				       rpic->backward_ref_ts);
+	}
+	if (rpic->picture_coding_type != V4L2_MPEG2_PIC_CODING_TYPE_B) {
+		m->anchor_ts = dst->vb2_buf.timestamp;
+		m->anchor_valid = true;
+	}
+	regmap_write(hw->map[DOS_BUS], MREG_REF0, ref0);
+	regmap_write(hw->map[DOS_BUS], MREG_REF1, ref1);
+	regmap_write(hw->map[DOS_BUS], REC_CANVAS_ADDR, rec);
+	regmap_write(hw->map[DOS_BUS], ANC2_CANVAS_ADDR, rec);
+
+	dev_dbg(hw->dev,
+		"mpeg12 run type %u struct %u%s %ux%u rec %06x ref0 %08x ref1 %08x\n",
+		 pic->picture_coding_type, pic->picture_structure,
+		 pair ? " pair" : "",
+		 seq->horizontal_size, seq->vertical_size,
+		 rec, ref0, ref1);
+
+	ctx->int_cond = 0;
+	regmap_update_bits(hw->map[DOS_BUS], VLD_MEM_VIFIFO_CONTROL, 0x6, 0x6);
+	aml_start_vdec_hw(hw);
+
+	/* Uninterruptible: see the note in vp9.c. */
+	ret = wait_event_timeout(ctx->queue, ctx->int_cond,
+				 msecs_to_jiffies(MPEG12_TIMEOUT_MS));
+	ctx->int_cond = 0;
+	if (!ret) {
+		dev_err(hw->dev,
+			"mpeg12 timeout: out %x info %x pc %x lvl %x rp %x wp %x bitcnt %x err %x\n",
+			read_dos_reg(hw, MREG_BUFFEROUT),
+			read_dos_reg(hw, MREG_PIC_INFO),
+			read_dos_reg(hw, MPC_E),
+			read_dos_reg(hw, VLD_MEM_VIFIFO_LEVEL),
+			read_dos_reg(hw, VLD_MEM_VIFIFO_RP),
+			read_dos_reg(hw, VLD_MEM_VIFIFO_WP),
+			read_dos_reg(hw, VIFF_BIT_CNT),
+			read_dos_reg(hw, MREG_ERROR_COUNT));
+		aml_stop_vdec_hw(hw);
+		mpeg12_canvas_free(hw, m);
+		return -ETIMEDOUT;
+	}
+
+	aml_stop_vdec_hw(hw);
+	return 0;
+
+fail:
+	mpeg12_canvas_free(hw, m);
+	return -ENOMEM;
+}
diff --git a/drivers/media/platform/amlogic/vdec/mpeg12.h b/drivers/media/platform/amlogic/vdec/mpeg12.h
new file mode 100644
index 000000000000..c5c9601623e1
--- /dev/null
+++ b/drivers/media/platform/amlogic/vdec/mpeg12.h
@@ -0,0 +1,51 @@
+/* SPDX-License-Identifier: (GPL-2.0-only OR MIT) */
+/*
+ * MPEG-1/2 decoding on the Amlogic legacy VDEC core.
+ */
+
+#ifndef _AML_MPEG12_H_
+#define _AML_MPEG12_H_
+
+/* Scratch register meanings for the mpeg12_multi microcode (vmpeg12_multi.c). */
+#define MREG_CC_ADDR		AV_SCRATCH_0
+#define MREG_REF0		AV_SCRATCH_2
+#define MREG_REF1		AV_SCRATCH_3
+#define MREG_SEQ_INFO		AV_SCRATCH_4
+#define MREG_PIC_INFO		AV_SCRATCH_5
+#define MREG_PIC_WIDTH		AV_SCRATCH_6
+#define MREG_PIC_HEIGHT		AV_SCRATCH_7
+#define MREG_INPUT		AV_SCRATCH_8
+#define MREG_BUFFEROUT		AV_SCRATCH_9
+#define MREG_CMD		AV_SCRATCH_A
+#define MREG_CO_MV_START	AV_SCRATCH_B
+#define MREG_ERROR_COUNT	AV_SCRATCH_C
+#define MREG_FRAME_OFFSET	AV_SCRATCH_D
+#define MREG_WAIT_BUFFER	AV_SCRATCH_E
+#define MREG_FATAL_ERROR	AV_SCRATCH_F
+#define MREG_SEQ_REPORT		AV_SCRATCH_G
+#define MREG_SIGNAL_TYPE	AV_SCRATCH_H
+#define MREG_USERDATA		AV_SCRATCH_J
+#define MREG_DECODE_STOP_POS	AV_SCRATCH_K
+#define MREG_DEBUG		AV_SCRATCH_M
+
+/* VLD block registers the microcode keeps its picture state in. */
+#define MPEG1_2_REG		REG_ALIGN(0x0c01)
+#define PIC_HEAD_INFO		REG_ALIGN(0x0c03)
+
+/* MREG_BUFFEROUT values. */
+#define MPEG12_PIC_DONE		1
+#define MPEG12_DATA_EMPTY	2
+#define MPEG12_SEQ_END		3
+#define MPEG12_DATA_REQUEST	4
+
+#define MPEG12_PICINFO_ERROR	0x80000000
+#define MPEG12_PICINFO_TYPE	0x00030000
+
+#define MPEG12_WORKSPACE_SIZE	(4 * SZ_64K)
+#define MPEG12_CCBUF_SIZE	(5 * 1024)
+
+int aml_mpeg12_init(void *priv);
+void aml_mpeg12_exit(void *priv);
+int aml_mpeg12_dec_run(void *priv);
+
+#endif
-- 
2.56.0


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

* [PATCH 06/11] media: amlogic: vdec: add MJPEG decoding
  2026-10-25 19:24 [PATCH 00/11] media: amlogic: vdec: S4 decoding beyond H.264 Baseline christian.koever-draxl
                   ` (4 preceding siblings ...)
  2026-10-25 19:25 ` [PATCH 05/11] media: amlogic: vdec: add MPEG-1/2 decoding christian.koever-draxl
@ 2026-10-25 19:25 ` christian.koever-draxl
  2026-10-25 20:20   ` sashiko-bot
  2026-10-25 19:25 ` [PATCH 07/11] media: amlogic: vdec: add AV1 decoding on the HEVC core christian.koever-draxl
                   ` (5 subsequent siblings)
  11 siblings, 1 reply; 20+ messages in thread
From: christian.koever-draxl @ 2026-10-25 19:25 UTC (permalink / raw)
  To: zhentao.guo, mchehab, robh, krzk+dt, conor+dt
  Cc: linux-media, devicetree, linux-kernel,
	Christian Stefan Kövér-Draxl

From: Christian Stefan Kövér-Draxl <ckoever@outlook.de>

Decode baseline 4:2:0 JPEG frames (V4L2_PIX_FMT_MJPEG) on the VDEC
core. The microcode skips the first marker segment after SOI, so the
frame is fed with that in mind, and the planar 4:2:0 picture it writes
is exposed as a CAPTURE format of its own.

Signed-off-by: Christian Stefan Kövér-Draxl <ckoever@outlook.de>
---
 drivers/media/platform/amlogic/vdec/Makefile  |   2 +-
 .../media/platform/amlogic/vdec/aml_vdec.c    |   3 +
 .../platform/amlogic/vdec/aml_vdec_platform.c |  17 +
 .../platform/amlogic/vdec/aml_vdec_tee_fw.c   |   5 +
 drivers/media/platform/amlogic/vdec/mjpeg.c   | 471 ++++++++++++++++++
 drivers/media/platform/amlogic/vdec/mjpeg.h   |  33 ++
 6 files changed, 530 insertions(+), 1 deletion(-)
 create mode 100644 drivers/media/platform/amlogic/vdec/mjpeg.c
 create mode 100644 drivers/media/platform/amlogic/vdec/mjpeg.h

diff --git a/drivers/media/platform/amlogic/vdec/Makefile b/drivers/media/platform/amlogic/vdec/Makefile
index 6d91750ec673..c5eda54598f6 100644
--- a/drivers/media/platform/amlogic/vdec/Makefile
+++ b/drivers/media/platform/amlogic/vdec/Makefile
@@ -1,4 +1,4 @@
 # SPDX-License-Identifier: GPL-2.0-only
-aml-vdec-drv-objs := aml_vdec.o aml_vdec_drv.o aml_vdec_hw.o aml_vdec_platform.o h264.o h264_headers.o mpeg12.o aml_vdec_tee_fw.o
+aml-vdec-drv-objs := aml_vdec.o aml_vdec_drv.o aml_vdec_hw.o aml_vdec_platform.o h264.o h264_headers.o mpeg12.o mjpeg.o aml_vdec_tee_fw.o
 
 obj-$(CONFIG_VIDEO_AMLOGIC_VDEC) += aml-vdec-drv.o
diff --git a/drivers/media/platform/amlogic/vdec/aml_vdec.c b/drivers/media/platform/amlogic/vdec/aml_vdec.c
index 7a5098f3e116..c383dd642586 100644
--- a/drivers/media/platform/amlogic/vdec/aml_vdec.c
+++ b/drivers/media/platform/amlogic/vdec/aml_vdec.c
@@ -90,6 +90,9 @@ static const struct aml_dec_type dec_type_name[] = {
 	}, {
 		.codec_type = CODEC_TYPE_MPEG2,
 		.name = "MPEG2",
+	}, {
+		.codec_type = CODEC_TYPE_MJPEG,
+		.name = "MJPEG",
 	},
 };
 
diff --git a/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c b/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c
index 83efadcb8aef..d54b09b6bbc0 100644
--- a/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c
+++ b/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c
@@ -7,6 +7,7 @@
 #include "aml_vdec_hw.h"
 #include "h264.h"
 #include "mpeg12.h"
+#include "mjpeg.h"
 
 static struct aml_video_fmt aml_s4_video_formats[] = {
 	{
@@ -31,6 +32,17 @@ static struct aml_video_fmt aml_s4_video_formats[] = {
 		.stepwise = {AML_VDEC_MIN_W, AML_VDEC_1080P_MAX_W, 2,
 			AML_VDEC_MIN_H, AML_VDEC_1080P_MAX_H, 2},
 	},
+	{
+		.name = "JPEG",
+		.fourcc = V4L2_PIX_FMT_JPEG,
+		.type = AML_FMT_DEC,
+		.align = 64,
+		.is_10_bit_support = 0,
+		.codec_type = CODEC_TYPE_MJPEG,
+		.num_planes = 1,
+		.stepwise = {AML_VDEC_MIN_W, AML_VDEC_1080P_MAX_W, 2,
+			AML_VDEC_MIN_H, AML_VDEC_1080P_MAX_H, 2},
+	},
 	{
 		.name = "NV21M",
 		.fourcc = V4L2_PIX_FMT_NV21M,
@@ -96,6 +108,11 @@ const struct aml_codec_ops aml_S4_dec_ops[] = {
 		.exit = aml_mpeg12_exit,
 		.run = aml_mpeg12_dec_run,
 	},
+	[CODEC_TYPE_MJPEG] = {
+		.init = aml_mjpeg_init,
+		.exit = aml_mjpeg_exit,
+		.run = aml_mjpeg_dec_run,
+	},
 };
 
 const struct aml_dev_platform_data aml_vdec_s4_pdata = {
diff --git a/drivers/media/platform/amlogic/vdec/aml_vdec_tee_fw.c b/drivers/media/platform/amlogic/vdec/aml_vdec_tee_fw.c
index 63da8b4dac28..a92405047c6e 100644
--- a/drivers/media/platform/amlogic/vdec/aml_vdec_tee_fw.c
+++ b/drivers/media/platform/amlogic/vdec/aml_vdec_tee_fw.c
@@ -87,6 +87,11 @@ static struct aml_tee_fw firmware[] = {
 		.core = CORE_VDEC_LEGENCY,
 		.is_swap = 0,
 	},
+	[CODEC_TYPE_MJPEG] = {
+		.fw_format = VIDEO_DEC_MJPEG_MULTI,
+		.core = CORE_VDEC_LEGENCY,
+		.is_swap = 0,
+	},
 };
 
 static int optee_ctx_match(struct tee_ioctl_version_data *ver, const void *data)
diff --git a/drivers/media/platform/amlogic/vdec/mjpeg.c b/drivers/media/platform/amlogic/vdec/mjpeg.c
new file mode 100644
index 000000000000..7989b5f42a56
--- /dev/null
+++ b/drivers/media/platform/amlogic/vdec/mjpeg.c
@@ -0,0 +1,471 @@
+// SPDX-License-Identifier: (GPL-2.0-only OR MIT)
+/*
+ * Motion-JPEG decoding on the Amlogic legacy VDEC core.
+ *
+ * Ported from the reference's vmjpeg_multi.c.  The microcode parses the whole
+ * JPEG itself - markers, tables, scan - so every OUTPUT buffer is simply one
+ * complete JPEG picture and needs no controls.  It writes planar 4:2:0 into
+ * three canvases (Y, U, V), so the only CAPTURE format is YUV420 (I420); a
+ * 4:2:2 or 4:4:4 source comes out downsampled to 4:2:0 by the microcode.
+ */
+
+#include <linux/delay.h>
+#include <linux/dma-mapping.h>
+#include <linux/module.h>
+#include <linux/sizes.h>
+#include <linux/slab.h>
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-mem2mem.h>
+#include <media/videobuf2-dma-contig.h>
+
+#include "aml_vdec_drv.h"
+#include "aml_vdec_hw.h"
+#include "mjpeg.h"
+
+#define MJPEG_TIMEOUT_MS	500
+
+struct aml_mjpeg_ctx {
+	struct aml_vdec_ctx *v4l2_ctx;
+
+	void *stage_vaddr;
+	dma_addr_t stage_paddr;
+	u32 stage_size;
+
+	int canvas[3];
+	u32 decoded;
+};
+
+static void mjpeg_canvas_free(struct aml_vdec_hw *hw, struct aml_mjpeg_ctx *m)
+{
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(m->canvas); i++) {
+		if (m->canvas[i] >= 0)
+			meson_canvas_free(hw->canvas, m->canvas[i]);
+		m->canvas[i] = -1;
+	}
+}
+
+/* Y, U, V canvases for the capture buffer; returns the spec or ~0. */
+static u32 mjpeg_canvas_setup(struct aml_vdec_ctx *ctx, struct vb2_buffer *vb)
+{
+	struct aml_vdec_hw *hw = vdec_get_hw(ctx->dev);
+	struct aml_mjpeg_ctx *m = ctx->codec_priv;
+	struct v4l2_pix_format_mplane *pf = &ctx->pix_fmt[AML_FMT_DST];
+	u32 w = pf->plane_fmt[0].bytesperline;
+	u32 h = pf->height;
+	dma_addr_t addr[3];
+	u32 cw[3] = { w, w / 2, w / 2 };
+	u32 ch[3] = { h, h / 2, h / 2 };
+	int i;
+
+	addr[0] = vb2_dma_contig_plane_dma_addr(vb, 0);
+	addr[1] = addr[0] + w * h;
+	addr[2] = addr[1] + (w / 2) * (h / 2);
+
+	for (i = 0; i < 3; i++) {
+		u8 idx;
+
+		if (meson_canvas_alloc(hw->canvas, &idx)) {
+			mjpeg_canvas_free(hw, m);
+			return ~0u;
+		}
+		m->canvas[i] = idx;
+		meson_canvas_config(hw->canvas, idx, addr[i], cw[i], ch[i],
+				    MESON_CANVAS_WRAP_NONE,
+				    MESON_CANVAS_BLKMODE_LINEAR,
+				    MESON_CANVAS_ENDIAN_SWAP64);
+		regmap_write(hw->map[DOS_BUS], VDEC_ASSIST_CANVAS_BLK32,
+			     (1 << 11) | (1 << 8) | idx);
+	}
+
+	return m->canvas[0] | (m->canvas[1] << 8) | (m->canvas[2] << 16);
+}
+
+/*
+ * The microcode only handles baseline (SOF0/SOF1) 4:2:0 Y/Cb/Cr: a 4:2:2,
+ * 4:4:4 or progressive picture decodes "successfully" into garbage, and a
+ * grey one fails.  Check the frame header and refuse the rest up front.
+ */
+static bool mjpeg_supported(struct device *dev, const u8 *p, u32 len)
+{
+	u32 i = 2;
+
+	if (len < 4 || p[0] != 0xff || p[1] != 0xd8)
+		return false;
+	while (i + 4 <= len) {
+		u8 marker;
+		u32 seg;
+
+		if (p[i] != 0xff) {
+			i++;
+			continue;
+		}
+		marker = p[i + 1];
+		if (marker == 0xff || marker == 0x01 ||
+		    (marker >= 0xd0 && marker <= 0xd7)) {
+			i += marker == 0xff ? 1 : 2;
+			continue;
+		}
+		seg = (p[i + 2] << 8) | p[i + 3];
+		if (marker == 0xc0 || marker == 0xc1) {
+			if (i + 2 + seg > len || seg < 17 || p[i + 4] != 8 ||
+			    p[i + 9] != 3)
+				break;
+			/* Y 2x2, Cb 1x1, Cr 1x1 */
+			if (p[i + 11] != 0x22 || p[i + 14] != 0x11 ||
+			    p[i + 17] != 0x11)
+				break;
+			return true;
+		}
+		if ((marker >= 0xc2 && marker <= 0xcf && marker != 0xc4 &&
+		     marker != 0xc8 && marker != 0xcc) || marker == 0xda)
+			break;
+		i += 2 + seg;
+	}
+	dev_dbg(dev, "mjpeg: not a baseline 4:2:0 JPEG\n");
+	return false;
+}
+
+static int mjpeg_prepare_input(struct aml_vdec_ctx *ctx,
+			       struct vb2_v4l2_buffer *src)
+{
+	struct aml_vdec_hw *hw = vdec_get_hw(ctx->dev);
+	struct aml_mjpeg_ctx *m = ctx->codec_priv;
+	struct vb2_buffer *vb = &src->vb2_buf;
+	const u8 *in = vb2_plane_vaddr(vb, 0);
+	u32 len = vb2_get_plane_payload(vb, 0);
+	u32 need = len + VLD_PADDING_SIZE + 64 + 8;
+	bool pad;
+	u8 *p;
+
+	/*
+	 * The microcode skips whatever segment follows SOI without looking at
+	 * it.  Encoders put APP0/COM there, but a webcam frame or a JPEG rebuilt
+	 * from VA-API buffers starts straight with DQT, which then goes missing
+	 * and the picture decodes with the wrong quantiser.  Give it a COM
+	 * segment to skip.
+	 */
+	pad = !((in[3] >= 0xe0 && in[3] <= 0xef) || in[3] == 0xfe);
+
+	if (need > m->stage_size) {
+		if (m->stage_vaddr)
+			dma_free_coherent(hw->dev, m->stage_size,
+					  m->stage_vaddr, m->stage_paddr);
+		m->stage_size = PAGE_ALIGN(need + SZ_64K);
+		m->stage_vaddr = dma_alloc_coherent(hw->dev, m->stage_size,
+						    &m->stage_paddr, GFP_KERNEL);
+		if (!m->stage_vaddr) {
+			m->stage_size = 0;
+			return -ENOMEM;
+		}
+	}
+
+	p = m->stage_vaddr;
+	if (pad) {
+		static const u8 com[] = { 0xff, 0xd8, 0xff, 0xfe, 0x00, 0x04, 0x00, 0x00 };
+
+		memcpy(p, com, sizeof(com));
+		memcpy(p + sizeof(com), in + 2, len - 2);
+		len += sizeof(com) - 2;
+	} else {
+		memcpy(p, in, len);
+	}
+	memset(p + len, 0, VLD_PADDING_SIZE);
+
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_CONTROL, 0);
+	regmap_write(hw->map[DOS_BUS], DOS_SW_RESET0,
+		     (1 << 5) | (1 << 4) | (1 << 3));
+	regmap_write(hw->map[DOS_BUS], DOS_SW_RESET0, 0);
+	regmap_write(hw->map[DOS_BUS], POWER_CTL_VLD, 1 << 4);
+
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_START_PTR, m->stage_paddr);
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_END_PTR,
+		     m->stage_paddr + m->stage_size - VDEC_FIFO_ALIGN);
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_CURR_PTR, m->stage_paddr);
+
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_CONTROL, 1);
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_CONTROL, 0);
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_BUF_CNTL, 2);
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_RP, m->stage_paddr);
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_WP,
+		     round_down(m->stage_paddr + len + VLD_PADDING_SIZE,
+				VDEC_FIFO_ALIGN));
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_BUF_CNTL, 3);
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_BUF_CNTL, 2);
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_CONTROL,
+		     (0x11 << 16) | (1 << 10) | (7 << 3));
+
+	regmap_write(hw->map[DOS_BUS], VIFF_BIT_CNT, len * 8);
+
+	return 0;
+}
+
+/* The reference's init_scaler(): a unity 4-point triangle in the pscaler. */
+static void mjpeg_init_scaler(struct aml_vdec_hw *hw)
+{
+	static const u32 filt_coef[] = {
+		0x20402000, 0x20402000, 0x1f3f2101, 0x1f3f2101,
+		0x1e3e2202, 0x1e3e2202, 0x1d3d2303, 0x1d3d2303,
+		0x1c3c2404, 0x1c3c2404, 0x1b3b2505, 0x1b3b2505,
+		0x1a3a2606, 0x1a3a2606, 0x19392707, 0x19392707,
+		0x18382808, 0x18382808, 0x17372909, 0x17372909,
+		0x16362a0a, 0x16362a0a, 0x15352b0b, 0x15352b0b,
+		0x14342c0c, 0x14342c0c, 0x13332d0d, 0x13332d0d,
+		0x12322e0e, 0x12322e0e, 0x11312f0f, 0x11312f0f,
+		0x10303010
+	};
+	/* horizontal/vertical initial info (Y, C) and phase steps */
+	static const u8 init_slot[] = { 37, 41, 39, 43 };
+	static const u8 step_slot[] = { 36, 40, 38, 42 };
+	struct regmap *map = hw->map[DOS_BUS];
+	int i;
+
+	regmap_write(map, PSCALE_CTRL, 0xc000);
+	regmap_write(map, PSCALE_BMEM_ADDR, 0);
+	for (i = 0; i < ARRAY_SIZE(filt_coef); i++) {
+		regmap_write(map, PSCALE_BMEM_DAT, 0);
+		regmap_write(map, PSCALE_BMEM_DAT, filt_coef[i]);
+	}
+	for (i = 0; i < ARRAY_SIZE(init_slot); i++) {
+		regmap_write(map, PSCALE_BMEM_ADDR, init_slot[i] * 2);
+		regmap_write(map, PSCALE_BMEM_DAT, 0x0008);
+		regmap_write(map, PSCALE_BMEM_DAT, 0x60000000);
+	}
+	for (i = 0; i < ARRAY_SIZE(step_slot); i++) {
+		regmap_write(map, PSCALE_BMEM_ADDR, step_slot[i] * 2 + 1);
+		regmap_write(map, PSCALE_BMEM_DAT, 0x10000);
+	}
+
+	regmap_write(map, DOS_SW_RESET0, 1 << 10);
+	regmap_write(map, DOS_SW_RESET0, 0);
+	regmap_write(map, PSCALE_RST, 0x7);
+	regmap_write(map, PSCALE_RST, 0x0);
+}
+
+static void mjpeg_job_done(struct aml_vdec_ctx *ctx, enum vb2_buffer_state st)
+{
+	struct aml_vdec_dev *dev = ctx->dev;
+	struct vb2_v4l2_buffer *dst = v4l2_m2m_next_dst_buf(ctx->fh.m2m_ctx);
+	struct vb2_v4l2_buffer *src = v4l2_m2m_next_src_buf(ctx->fh.m2m_ctx);
+
+	/*
+	 * A JPEG request carries no controls, so the buffer is its only
+	 * object: once that is done the request is complete, and completing
+	 * the control side afterwards (as device_run does for the other
+	 * legacy codecs) binds to a finished request and WARNs.  Do it first.
+	 */
+	if (src && src->vb2_buf.req_obj.req)
+		v4l2_ctrl_request_complete(src->vb2_buf.req_obj.req,
+					   &ctx->ctrl_handler);
+	mjpeg_canvas_free(dev->dec_hw, ctx->codec_priv);
+	if (dst)
+		vb2_set_plane_payload(&dst->vb2_buf, 0,
+				      ctx->pix_fmt[AML_FMT_DST].plane_fmt[0].sizeimage);
+	ctx->int_cond = 1;
+	ctx->job_finished = true;
+	v4l2_m2m_buf_done_and_job_finish(dev->m2m_dev_dec, ctx->m2m_ctx, st);
+	wake_up(&ctx->queue);
+}
+
+static irqreturn_t mjpeg_isr(int irq, void *priv)
+{
+	struct aml_vdec_dev *dev = priv;
+
+	regmap_write(dev->dec_hw->map[DOS_BUS], VDEC_ASSIST_MBOX1_CLR_REG, 1);
+
+	return IRQ_WAKE_THREAD;
+}
+
+static irqreturn_t mjpeg_threaded_isr(int irq, void *priv)
+{
+	struct aml_vdec_dev *dev = priv;
+	struct aml_vdec_hw *hw = dev->dec_hw;
+	struct aml_mjpeg_ctx *m = hw->curr_ctx;
+	struct aml_vdec_ctx *ctx;
+	u32 status;
+
+	if (!m)
+		return IRQ_HANDLED;
+	ctx = m->v4l2_ctx;
+	if (ctx->job_finished)
+		return IRQ_HANDLED;
+
+	if (read_dos_reg(hw, AV_SCRATCH_D)) {
+		regmap_write(hw->map[DOS_BUS], AV_SCRATCH_D, 0);
+		return IRQ_HANDLED;
+	}
+
+	status = read_dos_reg(hw, MJPEG_DEC_STATUS);
+	dev_dbg(hw->dev, "mjpeg irq status %x from %x w %u h %u\n",
+		status, read_dos_reg(hw, MJPEG_FROM_AMRISC),
+		 read_dos_reg(hw, MJPEG_PIC_WIDTH),
+		 read_dos_reg(hw, MJPEG_PIC_HEIGHT));
+
+	/* Picture size report: the buffer is already there, go on. */
+	if (status == MJPEG_STATUS_SIZE) {
+		u32 w = read_dos_reg(hw, MJPEG_PIC_WIDTH);
+		u32 h = read_dos_reg(hw, MJPEG_PIC_HEIGHT);
+		struct v4l2_pix_format_mplane *pf = &ctx->pix_fmt[AML_FMT_DST];
+
+		if (w > pf->plane_fmt[0].bytesperline || h > pf->height) {
+			dev_err(hw->dev, "mjpeg %ux%u does not fit %ux%u\n",
+				w, h, pf->plane_fmt[0].bytesperline, pf->height);
+			mjpeg_job_done(ctx, VB2_BUF_STATE_ERROR);
+			return IRQ_HANDLED;
+		}
+		regmap_write(hw->map[DOS_BUS], MJPEG_DECODE_PARAM,
+			     (h << 4) | 0x8000);
+		regmap_write(hw->map[DOS_BUS], MJPEG_DEC_STATUS, 0);
+		return IRQ_HANDLED;
+	}
+	if (status == MJPEG_STATUS_TIMEOUT) {
+		mjpeg_job_done(ctx, VB2_BUF_STATE_ERROR);
+		return IRQ_HANDLED;
+	}
+
+	/*
+	 * Anything else is the end of the picture - the reference does not
+	 * look at what FROM_AMRISC holds either (it is often 0).
+	 */
+	m->decoded++;
+	regmap_write(hw->map[DOS_BUS], MJPEG_FROM_AMRISC, 0);
+	mjpeg_job_done(ctx, VB2_BUF_STATE_DONE);
+
+	return IRQ_HANDLED;
+}
+
+static void mjpeg_restore_hw(struct aml_vdec_ctx *ctx, u32 spec)
+{
+	struct aml_vdec_hw *hw = vdec_get_hw(ctx->dev);
+	struct regmap *map = hw->map[DOS_BUS];
+
+	regmap_write(map, AV_SCRATCH_4, spec);
+	regmap_write(map, AV_SCRATCH_5, 0 | (1 << 24));
+
+	regmap_write(map, DOS_SW_RESET0, (1 << 7) | (1 << 6));
+	regmap_write(map, DOS_SW_RESET0, 0);
+	mjpeg_init_scaler(hw);
+
+	regmap_write(map, MJPEG_TO_AMRISC, 0);
+	regmap_write(map, MJPEG_FROM_AMRISC, 0);
+	regmap_write(map, MJPEG_DEC_STATUS, 0);
+	regmap_write(map, AV_SCRATCH_D, 0);
+	regmap_write(map, MJPEG_STOP_POS, 0);
+
+	regmap_write(map, MCPU_INTR_MSK, 0xffff);
+	regmap_write(map, MJPEG_DECODE_PARAM,
+		     (ctx->pix_fmt[AML_FMT_DST].height << 4) | 0x8000);
+
+	regmap_write(map, VDEC_ASSIST_MBOX1_CLR_REG, 1);
+	regmap_write(map, VDEC_ASSIST_MBOX1_MASK, 1);
+	regmap_write(map, VDEC_ASSIST_AMR1_INT8, 8);
+	regmap_update_bits(map, MDEC_PIC_DC_CTRL, 1 << 17, 0);
+}
+
+int aml_mjpeg_init(void *priv)
+{
+	struct aml_vdec_ctx *ctx = priv;
+	struct aml_mjpeg_ctx *m;
+	int i;
+
+	if (!vdec_get_hw(ctx->dev))
+		return -ENODEV;
+
+	m = kzalloc_obj(*m);
+	if (!m)
+		return -ENOMEM;
+	m->v4l2_ctx = ctx;
+	for (i = 0; i < ARRAY_SIZE(m->canvas); i++)
+		m->canvas[i] = -1;
+
+	ctx->codec_priv = m;
+	return 0;
+}
+
+void aml_mjpeg_exit(void *priv)
+{
+	struct aml_vdec_ctx *ctx = priv;
+	struct aml_mjpeg_ctx *m = ctx->codec_priv;
+	struct aml_vdec_hw *hw;
+
+	if (!m)
+		return;
+	hw = vdec_get_hw(ctx->dev);
+
+	if (ctx->dos_clk_en)
+		aml_stop_vdec_hw(hw);
+	mjpeg_canvas_free(hw, m);
+	if (m->stage_vaddr)
+		dma_free_coherent(hw->dev, m->stage_size,
+				  m->stage_vaddr, m->stage_paddr);
+
+	if (hw->curr_ctx == m)
+		hw->curr_ctx = NULL;
+	kfree(m);
+	ctx->codec_priv = NULL;
+}
+
+int aml_mjpeg_dec_run(void *priv)
+{
+	struct aml_vdec_ctx *ctx = priv;
+	struct aml_mjpeg_ctx *m = ctx->codec_priv;
+	struct aml_vdec_hw *hw = vdec_get_hw(ctx->dev);
+	struct vb2_v4l2_buffer *src, *dst;
+	u32 spec;
+	long ret;
+
+	if (ctx->pix_fmt[AML_FMT_DST].pixelformat != V4L2_PIX_FMT_YUV420) {
+		dev_err(hw->dev, "mjpeg: capture format must be YUV420\n");
+		return -EINVAL;
+	}
+
+	src = v4l2_m2m_next_src_buf(ctx->fh.m2m_ctx);
+	dst = v4l2_m2m_next_dst_buf(ctx->fh.m2m_ctx);
+	if (!src || !dst)
+		return -EINVAL;
+
+	if (!mjpeg_supported(hw->dev, vb2_plane_vaddr(&src->vb2_buf, 0),
+			     vb2_get_plane_payload(&src->vb2_buf, 0)))
+		return -EINVAL;
+
+	hw->curr_ctx = m;
+	hw->hw_ops.irq_handler = mjpeg_isr;
+	hw->hw_ops.irq_threaded_func = mjpeg_threaded_isr;
+
+	if (mjpeg_prepare_input(ctx, src) < 0)
+		return -ENOMEM;
+
+	spec = mjpeg_canvas_setup(ctx, &dst->vb2_buf);
+	if (spec == ~0u)
+		return -ENOMEM;
+	mjpeg_restore_hw(ctx, spec);
+
+	dev_dbg(hw->dev, "mjpeg run %lu bytes spec %06x\n",
+		vb2_get_plane_payload(&src->vb2_buf, 0), spec);
+
+	ctx->int_cond = 0;
+	regmap_update_bits(hw->map[DOS_BUS], VLD_MEM_VIFIFO_CONTROL, 0x6, 0x6);
+	aml_start_vdec_hw(hw);
+
+	ret = wait_event_timeout(ctx->queue, ctx->int_cond,
+				 msecs_to_jiffies(MJPEG_TIMEOUT_MS));
+	ctx->int_cond = 0;
+	if (!ret) {
+		dev_err(hw->dev,
+			"mjpeg timeout: status %x from %x pc %x lvl %x rp %x wp %x bitcnt %x\n",
+			read_dos_reg(hw, MJPEG_DEC_STATUS),
+			read_dos_reg(hw, MJPEG_FROM_AMRISC),
+			read_dos_reg(hw, MPC_E),
+			read_dos_reg(hw, VLD_MEM_VIFIFO_LEVEL),
+			read_dos_reg(hw, VLD_MEM_VIFIFO_RP),
+			read_dos_reg(hw, VLD_MEM_VIFIFO_WP),
+			read_dos_reg(hw, VIFF_BIT_CNT));
+		aml_stop_vdec_hw(hw);
+		mjpeg_canvas_free(hw, m);
+		return -ETIMEDOUT;
+	}
+
+	aml_stop_vdec_hw(hw);
+	return 0;
+}
diff --git a/drivers/media/platform/amlogic/vdec/mjpeg.h b/drivers/media/platform/amlogic/vdec/mjpeg.h
new file mode 100644
index 000000000000..153eb19ad79b
--- /dev/null
+++ b/drivers/media/platform/amlogic/vdec/mjpeg.h
@@ -0,0 +1,33 @@
+/* SPDX-License-Identifier: (GPL-2.0-only OR MIT) */
+/*
+ * Motion-JPEG decoding on the Amlogic legacy VDEC core.
+ */
+
+#ifndef _AML_MJPEG_H_
+#define _AML_MJPEG_H_
+
+/* Scratch register meanings for the mjpeg_multi microcode (vmjpeg_multi.c). */
+#define MJPEG_DECODE_PARAM	AV_SCRATCH_2	/* height << 4 | 0x8000 */
+#define MJPEG_TO_AMRISC		AV_SCRATCH_8
+#define MJPEG_FROM_AMRISC	AV_SCRATCH_9
+#define MJPEG_FRAME_OFFSET	AV_SCRATCH_A
+#define MJPEG_PIC_WIDTH		AV_SCRATCH_B
+#define MJPEG_PIC_HEIGHT	AV_SCRATCH_C
+#define MJPEG_DEC_STATUS	AV_SCRATCH_F
+#define MJPEG_STOP_POS		AV_SCRATCH_K
+
+#define MJPEG_STATUS_SIZE	1	/* picture size known, waits for an ack */
+#define MJPEG_STATUS_TIMEOUT	0x21
+
+/* Not in reg_defines.h; addresses from the staging meson vdec headers. */
+#define PSCALE_RST		REG_ALIGN(0x0910)
+#define PSCALE_BMEM_ADDR	REG_ALIGN(0x091f)
+#define PSCALE_BMEM_DAT		REG_ALIGN(0x0920)
+#define MCPU_INTR_MSK		REG_ALIGN(0x0304)
+#define VDEC_ASSIST_AMR1_INT8	REG_ALIGN(0x002d)
+
+int aml_mjpeg_init(void *priv);
+void aml_mjpeg_exit(void *priv);
+int aml_mjpeg_dec_run(void *priv);
+
+#endif
-- 
2.56.0


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

* [PATCH 07/11] media: amlogic: vdec: add AV1 decoding on the HEVC core
  2026-10-25 19:24 [PATCH 00/11] media: amlogic: vdec: S4 decoding beyond H.264 Baseline christian.koever-draxl
                   ` (5 preceding siblings ...)
  2026-10-25 19:25 ` [PATCH 06/11] media: amlogic: vdec: add MJPEG decoding christian.koever-draxl
@ 2026-10-25 19:25 ` christian.koever-draxl
  2026-10-25 20:27   ` sashiko-bot
  2026-10-25 19:25 ` [PATCH 08/11] media: amlogic: vdec: add VP9 " christian.koever-draxl
                   ` (4 subsequent siblings)
  11 siblings, 1 reply; 20+ messages in thread
From: christian.koever-draxl @ 2026-10-25 19:25 UTC (permalink / raw)
  To: zhentao.guo, mchehab, robh, krzk+dt, conor+dt
  Cc: linux-media, devicetree, linux-kernel,
	Christian Stefan Kövér-Draxl

From: Christian Stefan Kövér-Draxl <ckoever@outlook.de>

Decode AV1 main profile, 8- and 10-bit (10-bit output as rounded 8-bit
NV12), key and inter frames including temporal motion vectors, up to
4096x2304. The microcode expects whole OBUs behind a 20-byte AMLV
header, so the frame header OBU is rebuilt from the stateless controls;
reference slots are tracked from reference_frame_ts. The decoder writes
straight into the CAPTURE buffer through its double-write path, and the
compressed reference body sits behind the frame MMU.

Signed-off-by: Christian Stefan Kövér-Draxl <ckoever@outlook.de>
---
 drivers/media/platform/amlogic/vdec/Makefile  |    2 +-
 .../media/platform/amlogic/vdec/aml_vdec.c    |   24 +
 .../platform/amlogic/vdec/aml_vdec_platform.c |   17 +
 drivers/media/platform/amlogic/vdec/av1.c     | 3140 +++++++++++++++++
 drivers/media/platform/amlogic/vdec/av1.h     |  308 ++
 5 files changed, 3490 insertions(+), 1 deletion(-)
 create mode 100644 drivers/media/platform/amlogic/vdec/av1.c
 create mode 100644 drivers/media/platform/amlogic/vdec/av1.h

diff --git a/drivers/media/platform/amlogic/vdec/Makefile b/drivers/media/platform/amlogic/vdec/Makefile
index c5eda54598f6..4eb7601f61a3 100644
--- a/drivers/media/platform/amlogic/vdec/Makefile
+++ b/drivers/media/platform/amlogic/vdec/Makefile
@@ -1,4 +1,4 @@
 # SPDX-License-Identifier: GPL-2.0-only
-aml-vdec-drv-objs := aml_vdec.o aml_vdec_drv.o aml_vdec_hw.o aml_vdec_platform.o h264.o h264_headers.o mpeg12.o mjpeg.o aml_vdec_tee_fw.o
+aml-vdec-drv-objs := aml_vdec.o aml_vdec_drv.o aml_vdec_hw.o aml_vdec_platform.o h264.o h264_headers.o av1.o mpeg12.o mjpeg.o aml_vdec_tee_fw.o
 
 obj-$(CONFIG_VIDEO_AMLOGIC_VDEC) += aml-vdec-drv.o
diff --git a/drivers/media/platform/amlogic/vdec/aml_vdec.c b/drivers/media/platform/amlogic/vdec/aml_vdec.c
index c383dd642586..6892430c9569 100644
--- a/drivers/media/platform/amlogic/vdec/aml_vdec.c
+++ b/drivers/media/platform/amlogic/vdec/aml_vdec.c
@@ -66,6 +66,27 @@ static const struct aml_vdec_v4l2_ctrl controls[] = {
 			.max = V4L2_MPEG_VIDEO_H264_PROFILE_HIGH,
 			.def = V4L2_MPEG_VIDEO_H264_PROFILE_MAIN,
 		},
+	}, {
+		.codec_type = CODEC_TYPE_AV1,
+		.cfg = {
+			.id = V4L2_CID_STATELESS_AV1_SEQUENCE,
+		},
+	}, {
+		.codec_type = CODEC_TYPE_AV1,
+		.cfg = {
+			.id = V4L2_CID_STATELESS_AV1_FRAME,
+		},
+	}, {
+		.codec_type = CODEC_TYPE_AV1,
+		.cfg = {
+			.id = V4L2_CID_STATELESS_AV1_TILE_GROUP_ENTRY,
+			.dims = { V4L2_AV1_MAX_TILE_COUNT },
+		},
+	}, {
+		.codec_type = CODEC_TYPE_AV1,
+		.cfg = {
+			.id = V4L2_CID_STATELESS_AV1_FILM_GRAIN,
+		},
 	}, {
 		.codec_type = CODEC_TYPE_MPEG2,
 		.cfg = {
@@ -87,6 +108,9 @@ static const struct aml_dec_type dec_type_name[] = {
 	{
 		.codec_type = CODEC_TYPE_H264,
 		.name = "H264",
+	}, {
+		.codec_type = CODEC_TYPE_AV1,
+		.name = "AV1",
 	}, {
 		.codec_type = CODEC_TYPE_MPEG2,
 		.name = "MPEG2",
diff --git a/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c b/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c
index d54b09b6bbc0..1e8193821aaf 100644
--- a/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c
+++ b/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c
@@ -6,6 +6,7 @@
 #include "aml_vdec_platform.h"
 #include "aml_vdec_hw.h"
 #include "h264.h"
+#include "av1.h"
 #include "mpeg12.h"
 #include "mjpeg.h"
 
@@ -21,6 +22,17 @@ static struct aml_video_fmt aml_s4_video_formats[] = {
 		.stepwise = {AML_VDEC_MIN_W, AML_VDEC_1080P_MAX_W, 2,
 			AML_VDEC_MIN_H, AML_VDEC_1080P_MAX_H, 2},
 	},
+	{
+		.name = "AV1",
+		.fourcc = V4L2_PIX_FMT_AV1_FRAME,
+		.type = AML_FMT_DEC,
+		.align = 64,
+		.is_10_bit_support = 0,
+		.codec_type = CODEC_TYPE_AV1,
+		.num_planes = 1,
+		.stepwise = {AML_VDEC_MIN_W, AML_VDEC_4K_MAX_W, 2,
+			AML_VDEC_MIN_H, AML_VDEC_4K_MAX_H, 2},
+	},
 	{
 		.name = "MPEG-2",
 		.fourcc = V4L2_PIX_FMT_MPEG2_SLICE,
@@ -103,6 +115,11 @@ const struct aml_codec_ops aml_S4_dec_ops[] = {
 		.exit = aml_h264_exit,
 		.run = aml_h264_dec_run,
 	},
+	[CODEC_TYPE_AV1] = {
+		.init = aml_av1_init,
+		.exit = aml_av1_exit,
+		.run = aml_av1_dec_run,
+	},
 	[CODEC_TYPE_MPEG2] = {
 		.init = aml_mpeg12_init,
 		.exit = aml_mpeg12_exit,
diff --git a/drivers/media/platform/amlogic/vdec/av1.c b/drivers/media/platform/amlogic/vdec/av1.c
new file mode 100644
index 000000000000..ddadc83c8d86
--- /dev/null
+++ b/drivers/media/platform/amlogic/vdec/av1.c
@@ -0,0 +1,3140 @@
+// SPDX-License-Identifier: (GPL-2.0-only OR MIT)
+/*
+ * AV1 bring-up for the S4 HEVC/AV1 core.
+ *
+ * This is not a decoder yet.  It allocates the microcode's working buffers,
+ * programs them, configures the stream front end and releases the AMRISC -
+ * i.e. it is Amlogic's vav1_prot_init() followed by amhevc_start(), and nothing
+ * more.  The milestone it exists to reach is the first mailbox 0 interrupt:
+ * until the workspace is programmed the microcode has nothing to do but idle,
+ * so silence from the mailbox proves nothing either way.
+ *
+ * Reference: media_modules branch khadas-vim1s-r (VIM1S is S905Y4, the same S4
+ * family), drivers/frame_provider/decoder_v4l/vav1/vav1.c - aom_config_work_
+ * space_hw(), aom_init_decoder_hw(), vav1_prot_init().
+ *
+ * Build-time choices made here, matching how that reference is actually
+ * compiled: MMU on, double-write MMU off, MV_USE_FIXED_BUF off (so no mpred_mv
+ * in the workspace) and USE_SPEC_BUF_FOR_MMU_HEAD off (so cm_header is empty
+ * and the compressed headers come from the MMU allocator instead).
+ */
+
+#include <linux/crc32.h>
+#include <linux/dma-mapping.h>
+#include <linux/fs.h>
+#include <linux/firmware.h>
+#include <linux/module.h>
+#include <media/videobuf2-dma-contig.h>
+
+#include "aml_vdec_drv.h"
+#include "av1.h"
+
+/*
+ * Workspace layout, aom_workbuff_spec[1] of the reference - the 4096x2304
+ * entry - in the order init_buff_spec() lays them out.  sao_abv, sao_vb,
+ * sao_up and scalelut are zero-sized on AV1 and are simply absent here.
+ *
+ * The 1080p spec[0] is deliberately not used.  Several of these buffers are
+ * written by the hardware from the picture size it parsed, not from anything
+ * the host tells it, so a 4K stream arriving at a 1080p workspace overruns
+ * whatever follows it - and this is one allocation for the whole decoder, a
+ * few MB once, not per picture.  The 1080p values are kept in the comments
+ * for reference.
+ */
+#define AV1_IPP_SIZE            0x4000		/* 1080p: 0x1e00 */
+#define AV1_SHORT_TERM_RPS_SIZE 0x0800
+#define AV1_VPS_SIZE            0x0800
+#define AV1_SEG_MAP_SIZE        0x360000	/* 1080p: 0xbf400 */
+#define AV1_DAALA_TOP_SIZE      0x2000		/* 1080p: 0x0f00 */
+#define AV1_SWAP_BUF_SIZE       0x0800
+#define AV1_CDF_SIZE            0x80000
+#define AV1_CDF_SLOT_SIZE       0x8000
+#define AV1_GMC_SIZE            0x0800
+#define AV1_DBLK_PARA_SIZE      0x1a00		/* 1080p: 0x0d00 */
+#define AV1_DBLK_DATA_SIZE      0x52800		/* 1080p: 0x49000 */
+#define AV1_CDEF_DATA_SIZE      0x24a00		/* 1080p: 0x22400 */
+#define AV1_UPS_DATA_SIZE       0x6f000		/* 1080p: 0x36000 */
+/* 512 x 128 bits per frame buffer, and AV1 has REF_FRAMES + 1 + INTER_REFS. */
+#define AV1_FRAME_BUFFERS       16
+#define AV1_FGS_TABLE_SIZE      ((512 * 128 / 8) * AV1_FRAME_BUFFERS)
+#define AV1_MMU_VBH_SIZE        0x5000		/* 1080p: 0x3000 */
+#define AV1_MPRED_ABOVE_SIZE    0x5400		/* 1080p: 0x2800 */
+
+/* Separate small buffers the microcode DMAs to and from. */
+#define AV1_RPM_SIZE            (0x80 * 2)
+#define AV1_LMEM_SIZE           (0x400 * 2)
+#define AV1_PROB_SIZE           0x5000
+#define AV1_COUNT_SIZE          (0x300 * 4 * 4)
+/* One 32-bit page entry per 4 KiB page, MAX_FRAME_4K_NUM of them. */
+#define AV1_MMU_MAP_SIZE        (0x1200 * 4)
+/* Collocated motion vectors: MV_MEM_UNIT per superblock. */
+#define AV1_MV_MEM_UNIT         0x240
+#define AV1_MV_BUF_SIZE         0x26400
+/* Amlogic's own default: 16 KiB of prefix aux, no suffix. */
+#define AV1_AUX_PREFIX_SIZE     (16 * 1024)
+#define AV1_AUX_SUFFIX_SIZE     0
+
+/*
+ * The write pointer is deliberately parked this far past the payload, the same
+ * way the legacy VLD path uses VLD_PADDING_SIZE.  vdec.c calls it
+ * HEVC_PADDING_SIZE.
+ */
+#define HEVC_PADDING_SIZE       (1024 * 16)
+
+#define AV1_PARSER_CMD_NUMBER   37
+#define PARSER_CMD_SKIP_CFG_0   0x0000090b
+#define PARSER_CMD_SKIP_CFG_1   0x1b14140f
+#define PARSER_CMD_SKIP_CFG_2   0x001b1910
+
+static const u16 av1_parser_cmd[AV1_PARSER_CMD_NUMBER] = {
+	0x0401, 0x8401, 0x0800, 0x0402, 0x9002, 0x1423,
+	0x8CC3, 0x1423, 0x8804, 0x9825, 0x0800, 0x04FE,
+	0x8406, 0x8411, 0x1800, 0x8408, 0x8409, 0x8C2A,
+	0x9C2B, 0x1C00, 0x840F, 0x8407, 0x8000, 0x8408,
+	0x2000, 0xA800, 0x8410, 0x04DE, 0x840C, 0x840D,
+	0xAC00, 0xA000, 0x08C0, 0x08E0, 0xA40E, 0xFC00,
+	0x7C00
+};
+
+struct av1_workspace {
+	void *vaddr;
+	dma_addr_t paddr;
+	size_t size;
+
+	dma_addr_t ipp;
+	dma_addr_t short_term_rps;
+	dma_addr_t vps;
+	dma_addr_t seg_map;
+	dma_addr_t daala_top;
+	dma_addr_t swap_buf;
+	dma_addr_t cdf;
+	dma_addr_t gmc;
+	dma_addr_t dblk_para;
+	dma_addr_t dblk_data;
+	dma_addr_t cdef_data;
+	dma_addr_t ups_data;
+	dma_addr_t fgs_table;
+	dma_addr_t mmu_vbh;
+	dma_addr_t mpred_above;
+
+	void *side_vaddr;
+	dma_addr_t side_paddr;
+	size_t side_size;
+	dma_addr_t rpm;
+	dma_addr_t lmem;
+	dma_addr_t prob;
+	dma_addr_t count;
+	dma_addr_t mmu_map;
+	dma_addr_t aux;
+	dma_addr_t mv;
+	dma_addr_t cm_header[AV1_FRAME_BUFFERS];
+
+	void *body_vaddr;
+	dma_addr_t body_paddr;
+	size_t body_size;
+
+	/* Target of every frame-MMU entry past the current body. */
+	void *guard_vaddr;
+	dma_addr_t guard_paddr;
+
+	void *dw_vaddr;
+	dma_addr_t dw_paddr;
+	size_t dw_size;
+	u32 dw_width, dw_height;
+
+	void *stream_vaddr;
+	dma_addr_t stream_paddr;
+	size_t stream_size;
+	size_t payload;
+};
+
+/*
+ * Picture slots.  A key frame needs one buffer; anything inter needs the
+ * pictures it references to still be around, so each slot owns its own
+ * compressed body, its compressed-frame header and its collocated MV buffer.
+ * The canvas index the hardware uses to name a picture is simply the slot
+ * number - in MMU mode the reference sets mc_canvas_y = mc_canvas_u_v = index.
+ */
+/*
+ * AV1 keeps eight reference slots, and the picture being decoded needs a
+ * buffer of its own that none of them names - so eight is one too few and the
+ * slot search silently fails once all eight references are distinct.  That is
+ * exactly where a ten-frame stream started to go wrong at frame 8.
+ */
+#define AV1_SLOTS	10
+
+struct av1_slot {
+	/*
+	 * The compressed picture body, reached by the decoder only through
+	 * the page table av1_fill_mmu_map() builds.
+	 */
+	void *body_v;
+	dma_addr_t body_p;
+	void *hdr_v;
+	dma_addr_t hdr_p;
+	dma_addr_t mv_p;
+	void *mv_v;
+	/* The picture as the display sees it, one per slot so that a
+	 * show_existing_frame header can hand back an older one.
+	 */
+	void *dw_v;
+	dma_addr_t dw_p;
+	u32 order_hint;
+	/* State a later frame needs when it projects motion from this one. */
+	bool intra;
+	u8 frame_type;
+	u32 width, height;
+	u8 ref_order_hint[8];	/* of its own LAST..ALTREF, indexed by ref frame */
+	/* OUTPUT buffer timestamp of the frame decoded here; 0 when none. */
+	u64 ts;
+};
+
+static struct av1_slot av1_slots[AV1_SLOTS];
+static size_t av1_slot_body_size, av1_slot_mv_size, av1_slot_hdr_size;
+static size_t av1_slot_dw_size;
+static int av1_cur_slot;
+static u32 av1_last_width, av1_last_height;
+static int av1_prev_slot;
+/* False for a key frame and for any frame with PRIMARY_REF_NONE. */
+static bool av1_prev_valid;
+/* Slot a show_existing_frame header asked to be displayed again. */
+static int av1_shown_slot = -1;
+/* Slot that should be handed to the display next, -1 when there is none. */
+static int av1_out_slot = -1;
+static bool av1_cur_show;
+/*
+ * Set while userspace drives the decoder one frame per OUTPUT buffer, the way
+ * the V4L2 stateless API does.  Then the client - not this driver - owns
+ * display order: it keeps every decoded picture as a reference buffer and
+ * re-outputs one itself when a show_existing_frame header names it.  So every
+ * decoded picture has to be written to its CAPTURE buffer, hidden ones
+ * included.  The raw-OBU path is the opposite: it is handed whole temporal
+ * units and hands back pictures in display order, so a hidden frame produces
+ * no output buffer of its own.
+ */
+static bool av1_stateless;
+/*
+ * Where the double-write unit should lay the NV12 picture down.  Zero means
+ * "this picture's own slot buffer", which the raw-OBU path needs because a
+ * show_existing_frame header can ask for a picture decoded much earlier.  A
+ * stateless client gives every submitted frame - hidden ones included - its
+ * own CAPTURE buffer, so the hardware can write straight into it and the CPU
+ * never touches the picture at all.  That saves a 3 MB memcpy per frame and,
+ * at 4K, ten 12 MB slot buffers.
+ */
+static dma_addr_t av1_dw_target_y, av1_dw_target_c;
+
+/* Bytes the hardware may write at av1_dw_target_y, the CAPTURE buffer size. */
+static size_t av1_dw_target_size;
+/* AV1 keeps eight reference slots; each names one of our picture slots. */
+static int av1_ref_slot[8];
+static u32 av1_pending_refresh;
+/* OUTPUT timestamp of the frame being decoded, stateless path only. */
+static u64 av1_cur_ts;
+/* The picture slot each reference frame LAST..ALTREF resolves to, plus [0] = current. */
+static int av1_frame_slot[8];
+
+static struct av1_workspace av1_ws;
+
+/*
+ * Byte-order control for the stream shifter, HEVC_STREAM_CONTROL bits 6:4.
+ * vdec_enable_input() uses 7 for every frame-based target and that is the
+ * default here; it is exposed only because it is a three-bit documented field
+ * and the microcode's answer changes completely with it.
+ */
+/*
+ * HEVC_ASSIST_SCRATCH_I carries two names in Amlogic's headers, LMEM_DUMP_ADR
+ * and NAL_SEARCH_CTL, and the reference writes both: prot_init leaves 8 there
+ * ("check headers in ucode") and run() then overwrites it with the lmem
+ * address.
+ *
+ * That ambiguity was resolved the hard way: leaving a small constant there
+ * instead of the address oopsed the board, because for the AV1 microcode this
+ * register really is LMEM_DUMP_ADR and the hardware DMAs its local-memory dump
+ * to whatever address it holds - writing 0 aims that DMA at physical address 0.
+ * NAL_SEARCH_CTL is an HEVC-era leftover for this microcode.  Do not put a
+ * control value here.
+ */
+
+/* 32 bytes of compressed header per 128x64 block. */
+static u32 comp_header_size(u32 width, u32 height)
+{
+	return 32 * ((width + 127) >> 7) * ((height + 63) >> 6);
+}
+
+/* Main profile at 10 bit.  The double write still lays down rounded NV12. */
+static bool av1_ten_bit;
+
+/* 3200 bytes per 64x32 block at 8 bit (memory saving), 4096 at 10 bit. */
+static u32 comp_body_size(u32 width, u32 height)
+{
+	return (av1_ten_bit ? 4096 : 3200) * ((width + 63) >> 6) * ((height + 31) >> 5);
+}
+
+/*
+ * The compressed-header *allocation* is a fixed size per picture and has
+ * nothing to do with comp_header_size(), which only gives the length written
+ * to HEVC_CM_HEADER_LENGTH.  The reference allocates av1_get_header_size():
+ * 64 KiB up to 1080p, 288 KiB for 4K, 1152 KiB for 8K.  Allocating the
+ * computed length instead lets the hardware write off the end of the buffer -
+ * at 640x480 it filled 4 KiB where the formula says 1280 bytes, and the
+ * overrun landed in kernel page tables.
+ */
+#define AV1_HDR_SIZE_1080P	0x10000
+#define AV1_HDR_SIZE_4K		0x48000
+#define AV1_HDR_SIZE_8K		0x120000
+
+static size_t comp_hdr_alloc_size(u32 width, u32 height)
+{
+	if (width * height > 4096 * 2304)
+		return AV1_HDR_SIZE_8K;
+	if (width * height > 1920 * 1088)
+		return AV1_HDR_SIZE_4K;
+	return AV1_HDR_SIZE_1080P;
+}
+
+static void av1_free_slots(struct aml_vdec_hw *hw)
+{
+	int i;
+
+	for (i = 0; i < AV1_SLOTS; i++) {
+		if (av1_slots[i].body_v)
+			dma_free_coherent(hw->dev, av1_slot_body_size,
+					  av1_slots[i].body_v,
+					  av1_slots[i].body_p);
+		if (av1_slots[i].hdr_v)
+			dma_free_coherent(hw->dev, av1_slot_hdr_size,
+					  av1_slots[i].hdr_v,
+					  av1_slots[i].hdr_p);
+		if (av1_slots[i].mv_v)
+			dma_free_coherent(hw->dev, av1_slot_mv_size,
+					  av1_slots[i].mv_v, av1_slots[i].mv_p);
+		if (av1_slots[i].dw_v)
+			dma_free_noncoherent(hw->dev, av1_slot_dw_size,
+					     av1_slots[i].dw_v,
+					     av1_slots[i].dw_p,
+					     DMA_FROM_DEVICE);
+		memset(&av1_slots[i], 0, sizeof(av1_slots[i]));
+	}
+	av1_slot_body_size = 0;
+	av1_slot_mv_size = 0;
+	av1_slot_hdr_size = 0;
+	av1_slot_dw_size = 0;
+}
+
+static int av1_alloc_slots(struct aml_vdec_hw *hw, u32 width, u32 height)
+{
+	size_t body = ALIGN(comp_body_size(width, height), PAGE_SIZE);
+	/*
+	 * AV1_MV_MEM_UNIT is per 64x64 block whatever the superblock size.
+	 * Counting 128x128 superblocks gave a quarter of the buffer: 76 KiB
+	 * at 1080p where the reference allocates 0x20400, and the overrun
+	 * landed in whatever the allocator put next to it.
+	 */
+	size_t mv = ALIGN(DIV_ROUND_UP(width, 64) * DIV_ROUND_UP(height, 64) *
+			  AV1_MV_MEM_UNIT, PAGE_SIZE);
+	size_t hdr = comp_hdr_alloc_size(width, height);
+	size_t dw = av1_dw_target_y ? 0 : ALIGN(width * height * 3 / 2, SZ_64K);
+	int i;
+
+	if (av1_slots[0].body_v &&
+	    body == av1_slot_body_size &&
+	    mv == av1_slot_mv_size && hdr == av1_slot_hdr_size &&
+	    dw == av1_slot_dw_size)
+		return 0;
+
+	av1_free_slots(hw);
+	av1_slot_body_size = body;
+	av1_slot_mv_size = mv;
+	av1_slot_hdr_size = hdr;
+	av1_slot_dw_size = dw;
+
+	for (i = 0; i < AV1_SLOTS; i++) {
+		av1_slots[i].body_v = dma_alloc_coherent(hw->dev, body,
+							 &av1_slots[i].body_p,
+							 GFP_KERNEL);
+		if (!av1_slots[i].body_v) {
+			av1_free_slots(hw);
+			return -ENOMEM;
+		}
+		av1_slots[i].hdr_v = dma_alloc_coherent(hw->dev, hdr,
+							&av1_slots[i].hdr_p,
+							GFP_KERNEL);
+		av1_slots[i].mv_v = dma_alloc_coherent(hw->dev, mv,
+						       &av1_slots[i].mv_p,
+						       GFP_KERNEL);
+		/*
+		 * The double-write picture is the one buffer the CPU reads
+		 * in bulk, once per frame.  Coherent memory is uncached on
+		 * arm64, and a 3 MB uncached memcpy costs more than the
+		 * decode itself, so take cached memory and invalidate it by
+		 * hand before each read.
+		 */
+		if (dw)
+			av1_slots[i].dw_v =
+				dma_alloc_noncoherent(hw->dev, dw,
+						      &av1_slots[i].dw_p,
+						      DMA_FROM_DEVICE,
+						      GFP_KERNEL);
+		if (!av1_slots[i].hdr_v ||
+		    !av1_slots[i].mv_v || (dw && !av1_slots[i].dw_v)) {
+			av1_free_slots(hw);
+			return -ENOMEM;
+		}
+	}
+
+	dev_info(hw->dev,
+		 "av1: %d slots, body %zu KiB, hdr %zu KiB, mv %zu KiB, dw %zu KiB each\n",
+		 AV1_SLOTS, body / 1024, hdr / 1024, mv / 1024, dw / 1024);
+	return 0;
+}
+
+static void av1_free(struct aml_vdec_hw *hw)
+{
+	av1_free_slots(hw);
+	if (av1_ws.vaddr) {
+		dma_free_coherent(hw->dev, av1_ws.size, av1_ws.vaddr,
+				  av1_ws.paddr);
+		av1_ws.vaddr = NULL;
+	}
+	if (av1_ws.guard_vaddr) {
+		dma_free_coherent(hw->dev, PAGE_SIZE, av1_ws.guard_vaddr,
+				  av1_ws.guard_paddr);
+		av1_ws.guard_vaddr = NULL;
+	}
+	if (av1_ws.side_vaddr) {
+		dma_free_coherent(hw->dev, av1_ws.side_size, av1_ws.side_vaddr,
+				  av1_ws.side_paddr);
+		av1_ws.side_vaddr = NULL;
+	}
+	if (av1_ws.stream_vaddr) {
+		dma_free_coherent(hw->dev, av1_ws.stream_size,
+				  av1_ws.stream_vaddr, av1_ws.stream_paddr);
+		av1_ws.stream_vaddr = NULL;
+	}
+	if (av1_ws.dw_vaddr) {
+		dma_free_coherent(hw->dev, av1_ws.dw_size, av1_ws.dw_vaddr,
+				  av1_ws.dw_paddr);
+		av1_ws.dw_vaddr = NULL;
+	}
+	/* These alias the current slot; the slot pool owns the memory. */
+	av1_ws.body_vaddr = NULL;
+	av1_ws.body_paddr = 0;
+	av1_ws.body_size = 0;
+}
+
+static int av1_alloc(struct aml_vdec_hw *hw)
+{
+	dma_addr_t p;
+	size_t off;
+	int i;
+
+	if (av1_ws.vaddr)
+		return 0;
+
+	/*
+	 * One coherent allocation carved up in the reference's order.  Each
+	 * sub-buffer is page aligned, which the layout below happens to give
+	 * for free but is worth not relying on silently.
+	 */
+	av1_ws.size = ALIGN(AV1_IPP_SIZE + AV1_SHORT_TERM_RPS_SIZE +
+			    AV1_VPS_SIZE + AV1_SEG_MAP_SIZE +
+			    AV1_DAALA_TOP_SIZE + AV1_SWAP_BUF_SIZE +
+			    AV1_CDF_SIZE + AV1_GMC_SIZE + AV1_DBLK_PARA_SIZE +
+			    AV1_DBLK_DATA_SIZE + AV1_CDEF_DATA_SIZE +
+			    AV1_UPS_DATA_SIZE + AV1_FGS_TABLE_SIZE +
+			    AV1_MMU_VBH_SIZE + AV1_MPRED_ABOVE_SIZE,
+			    SZ_64K);
+
+	av1_ws.vaddr = dma_alloc_coherent(hw->dev, av1_ws.size, &av1_ws.paddr,
+					  GFP_KERNEL);
+	if (!av1_ws.vaddr)
+		return -ENOMEM;
+
+	p = av1_ws.paddr;
+	off = 0;
+#define CARVE(field, sz)	do {			\
+		av1_ws.field = p + off;			\
+		off += ALIGN(sz, SZ_4K);		\
+	} while (0)
+	CARVE(ipp, AV1_IPP_SIZE);
+	CARVE(short_term_rps, AV1_SHORT_TERM_RPS_SIZE);
+	CARVE(vps, AV1_VPS_SIZE);
+	CARVE(seg_map, AV1_SEG_MAP_SIZE);
+	CARVE(daala_top, AV1_DAALA_TOP_SIZE);
+	CARVE(swap_buf, AV1_SWAP_BUF_SIZE);
+	CARVE(cdf, AV1_CDF_SIZE);
+	CARVE(gmc, AV1_GMC_SIZE);
+	CARVE(dblk_para, AV1_DBLK_PARA_SIZE);
+	CARVE(dblk_data, AV1_DBLK_DATA_SIZE);
+	CARVE(cdef_data, AV1_CDEF_DATA_SIZE);
+	CARVE(ups_data, AV1_UPS_DATA_SIZE);
+	CARVE(fgs_table, AV1_FGS_TABLE_SIZE);
+	CARVE(mmu_vbh, AV1_MMU_VBH_SIZE);
+	CARVE(mpred_above, AV1_MPRED_ABOVE_SIZE);
+
+	av1_ws.side_size = ALIGN(AV1_RPM_SIZE + AV1_LMEM_SIZE + AV1_PROB_SIZE +
+				 AV1_COUNT_SIZE + AV1_MMU_MAP_SIZE +
+				 AV1_AUX_PREFIX_SIZE + AV1_AUX_SUFFIX_SIZE +
+				 AV1_MV_BUF_SIZE +
+				 AV1_FRAME_BUFFERS * SZ_4K, SZ_64K);
+	av1_ws.side_vaddr = dma_alloc_coherent(hw->dev, av1_ws.side_size,
+					       &av1_ws.side_paddr, GFP_KERNEL);
+	if (!av1_ws.side_vaddr) {
+		av1_free(hw);
+		return -ENOMEM;
+	}
+
+	av1_ws.guard_vaddr = dma_alloc_coherent(hw->dev, PAGE_SIZE,
+						&av1_ws.guard_paddr, GFP_KERNEL);
+	if (!av1_ws.guard_vaddr) {
+		av1_free(hw);
+		return -ENOMEM;
+	}
+
+	p = av1_ws.side_paddr;
+	off = 0;
+	CARVE(rpm, AV1_RPM_SIZE);
+	CARVE(lmem, AV1_LMEM_SIZE);
+	CARVE(prob, AV1_PROB_SIZE);
+	CARVE(count, AV1_COUNT_SIZE);
+	CARVE(mmu_map, AV1_MMU_MAP_SIZE);
+	CARVE(aux, AV1_AUX_PREFIX_SIZE + AV1_AUX_SUFFIX_SIZE);
+	CARVE(mv, AV1_MV_BUF_SIZE);
+	/*
+	 * One compressed-frame header per frame buffer.  At 320x240 each is
+	 * 384 bytes, but the address table wants them 32-byte granular and a
+	 * page each keeps the arithmetic obvious.
+	 */
+	for (i = 0; i < AV1_FRAME_BUFFERS; i++)
+		CARVE(cm_header[i], SZ_4K);
+#undef CARVE
+
+	dev_info(hw->dev,
+		 "av1: workspace %zu KiB at %pad, side %zu KiB at %pad\n",
+		 av1_ws.size / 1024, &av1_ws.paddr,
+		 av1_ws.side_size / 1024, &av1_ws.side_paddr);
+	return 0;
+}
+
+static void wr(struct aml_vdec_hw *hw, u32 reg, u32 val)
+{
+	regmap_write(hw->map[DOS_BUS], reg, val);
+}
+
+/* aom_config_work_space_hw(), front and back together. */
+static void av1_config_workspace(struct aml_vdec_hw *hw, u32 width, u32 height)
+{
+	u32 body = comp_body_size(width, height);
+	u32 header = comp_header_size(width, height);
+	u32 v;
+
+	wr(hw, HEVC_RPM_BUFFER, av1_ws.rpm);
+	wr(hw, LMEM_DUMP_ADR, av1_ws.lmem);
+
+	wr(hw, AOM_AV1_DAALA_TOP_BUFFER, av1_ws.daala_top);
+	wr(hw, AV1_GMC_PARAM_BUFF_ADDR, av1_ws.gmc);
+	wr(hw, HEVC_DBLK_CFG4, av1_ws.dblk_para);
+	wr(hw, HEVC_DBLK_CFG5, av1_ws.dblk_data);
+
+	wr(hw, HEVCD_IPP_LINEBUFF_BASE, av1_ws.ipp);
+	wr(hw, HEVC_DBLK_CFG4, av1_ws.dblk_para);
+	wr(hw, HEVC_DBLK_CFG5, av1_ws.dblk_data);
+	/* Default left-storage split; the 2x4k variant is for >4096x2304. */
+	wr(hw, HEVC_DBLK_CFG3, 0x404010);
+
+	/* bit 4: paged_mem_mode - the MMU path, which AV1 always uses. */
+	wr(hw, HEVCD_MPP_DECOMP_CTL1, BIT(4));
+	/*
+	 * CTL2 is the compressed body size in 32-byte units.  config_sao_hw()
+	 * writes it for every picture in both MMU and linear mode; leaving it
+	 * at zero is harmless while the block only writes, but the MC engine
+	 * reads references back through the same decompressor, so an inter
+	 * frame needs it.
+	 */
+	wr(hw, HEVCD_MPP_DECOMP_CTL2, body >> 5);
+	wr(hw, HEVC_CM_BODY_LENGTH, body);
+	wr(hw, HEVC_CM_HEADER_OFFSET, body);
+	wr(hw, HEVC_CM_HEADER_LENGTH, header);
+
+	wr(hw, HEVC_SAO_MMU_VH0_ADDR, av1_ws.mmu_vbh);
+	wr(hw, HEVC_SAO_MMU_VH1_ADDR, av1_ws.mmu_vbh + AV1_MMU_VBH_SIZE / 2);
+
+	v = read_dos_reg(hw, HEVC_SAO_CTRL5);
+	v |= BIT(10);		/* use HEVC_CM_HEADER_START_ADDR */
+	v &= ~BIT(15);		/* double-write MMU off */
+	wr(hw, HEVC_SAO_CTRL5, v);
+
+	wr(hw, LMEM_DUMP_ADR, av1_ws.lmem);
+	wr(hw, HEVC_SAO_MMU_DMA_CTRL, av1_ws.mmu_map);
+
+	/* Compressed collocated MV, T5D and later - S4 qualifies. */
+	v = read_dos_reg(hw, HEVC_MPRED_CTRL4);
+	v |= BIT(1);
+	wr(hw, HEVC_MPRED_CTRL4, v);
+}
+
+/*
+ * The frame MMU.  Compressed picture bodies are not written to a contiguous
+ * buffer: the decoder walks a page table whose entries are plain page frame
+ * numbers, one per 4 KiB of body, and HEVC_SAO_MMU_DMA_CTRL points at that
+ * table.  Mainline does exactly this in codec_hevc_fill_mmu_map() for VP9 and
+ * HEVC; the reference AV1 driver reaches the same result through its MMU box
+ * allocator, which hands out scattered pages.  Contiguous pages are used here
+ * because it makes the table trivially correct and one keyframe needs 125 KiB.
+ *
+ * Leaving this table zeroed - which is what the previous attempt did - gives
+ * the decoder page frame 0 for every page of the picture.  The front end still
+ * reads the stream, but nothing can be written back, and that is exactly the
+ * symptom that was observed: shift count advancing, RPM and LMEM staying empty.
+ */
+static int av1_fill_mmu_map(struct aml_vdec_hw *hw, u32 width, u32 height)
+{
+	u32 *map;
+	u32 pages, first, i;
+
+	/*
+	 * The picture being written is the current slot's body, not the old
+	 * single body buffer - that one went away when the slot pool landed.
+	 * Keep the workspace aliases pointing at it so the debug dump below
+	 * and this table cannot disagree again.
+	 */
+	struct av1_slot *slot = &av1_slots[av1_cur_slot];
+
+	av1_ws.body_vaddr = slot->body_v;
+	av1_ws.body_paddr = slot->body_p;
+	if (!slot->body_v)
+		return -EINVAL;
+	av1_ws.body_size = av1_slot_body_size;
+
+	pages = av1_ws.body_size / PAGE_SIZE;
+	if (!pages || pages * sizeof(u32) > AV1_MMU_MAP_SIZE)
+		return -EINVAL;
+
+	/*
+	 * The previous picture may have run past its body.  Whatever it wrote
+	 * there landed in the guard page, not in page frame 0 or in a body
+	 * freed by a resolution change.
+	 */
+	if (memchr_inv(av1_ws.guard_vaddr, 0, PAGE_SIZE)) {
+		dev_warn_ratelimited(hw->dev,
+				     "av1: frame MMU overflow past %zu KiB body\n",
+				     av1_ws.body_size / 1024);
+		memset(av1_ws.guard_vaddr, 0, PAGE_SIZE);
+	}
+
+	map = av1_ws.side_vaddr + (av1_ws.mmu_map - av1_ws.side_paddr);
+	first = av1_ws.body_paddr >> PAGE_SHIFT;
+	for (i = 0; i < pages; i++)
+		map[i] = first + i;
+	for (; i < AV1_MMU_MAP_SIZE / sizeof(u32); i++)
+		map[i] = av1_ws.guard_paddr >> PAGE_SHIFT;
+
+	dev_dbg(hw->dev,
+		"av1: mmu map %u pages from pfn %08x, body %zu KiB at %pad\n",
+		 pages, first, av1_ws.body_size / 1024, &av1_ws.body_paddr);
+	return 0;
+}
+
+/*
+ * The microcode keeps each picture's global motion in the GMC buffer, one
+ * 128-byte record per picture slot, and an inter frame reads its primary
+ * reference's record as PrevGmParams.  It only writes records for inter
+ * frames, though: after a key frame the slot still holds whatever was there
+ * - zeros on a fresh workspace - instead of the identity models the spec
+ * saves for an intra frame, and every global-motion parameter of the next
+ * frame is then decoded against the wrong reference.  Write the identity
+ * record here; the layout is what the microcode itself stores for a frame
+ * whose seven references all use identity, two identical 64-byte halves.
+ */
+#define AV1_GMC_RECORD		0x80
+
+static void av1_reset_gmc(int slot)
+{
+	static const u32 identity[16] = {
+		0, 0, 0, 0, 0, 0, 0, 0,
+		0x00000100, 0x00000001, 0, 0,
+		0x00010000, 0x00000100, 0x01000001, 0x00010000,
+	};
+	u8 *rec = av1_ws.vaddr + (av1_ws.gmc - av1_ws.paddr) +
+		  slot * AV1_GMC_RECORD;
+
+	if ((slot + 1) * AV1_GMC_RECORD > AV1_GMC_SIZE)
+		return;
+	memcpy(rec, identity, sizeof(identity));
+	memcpy(rec + sizeof(identity), identity, sizeof(identity));
+}
+
+/*
+ * init_pic_list_hw() plus config_aux_buf().  In MMU mode the address table
+ * holds each picture's compressed-header address shifted right by 5, and the
+ * canvas table is zeroed so it holds defined values.  Without this the
+ * microcode has nowhere to put a decoded picture.
+ */
+static void av1_config_pic_list(struct aml_vdec_hw *hw)
+{
+	int i;
+
+	/*
+	 * The table is written with AV1_FRAME_BUFFERS entries, the length the
+	 * reference uses and the length the verified key-frame configuration
+	 * used.  There are only AV1_SLOTS real pictures, so the tail repeats
+	 * slot 0; nothing addresses those indices.
+	 */
+	wr(hw, HEVCD_MPP_ANC2AXI_TBL_CONF_ADDR, BIT(1) | BIT(2));
+	for (i = 0; i < AV1_FRAME_BUFFERS; i++)
+		wr(hw, HEVCD_MPP_ANC2AXI_TBL_DATA,
+		   av1_slots[i < AV1_SLOTS ? i : 0].hdr_p >> 5);
+	wr(hw, HEVCD_MPP_ANC2AXI_TBL_CONF_ADDR, BIT(0));
+
+	wr(hw, HEVCD_MPP_ANC_CANVAS_ACCCONFIG_ADDR, (1 << 8) | BIT(0));
+	for (i = 0; i < 32; i++)
+		wr(hw, HEVCD_MPP_ANC_CANVAS_DATA_ADDR, 0);
+
+	wr(hw, HEVC_AUX_ADR, av1_ws.aux);
+	wr(hw, HEVC_AUX_DATA_SIZE,
+	   ((AV1_AUX_PREFIX_SIZE >> 4) << 16) | (AV1_AUX_SUFFIX_SIZE >> 4));
+}
+
+/* aom_init_decoder_hw(), front and back together. */
+static void av1_init_decoder(struct aml_vdec_hw *hw)
+{
+	u32 v;
+	int i;
+
+	v = read_dos_reg(hw, HEVC_PARSER_INT_CONTROL);
+	v &= 0x03ffffff;
+	v |= (3 << 29) |	/* stream_buffer_empty_int_ctl */
+	     (3 << 26) |	/* stream_fifo_empty_int_ctl */
+	     (1 << 24) |	/* stream_buffer_empty_int_amrisc_enable */
+	     (1 << 22) |	/* stream_fifo_empty_int_amrisc_enable */
+	     (1 << 7)  |	/* dec_done_int_cpu_enable */
+	     (1 << 4)  |	/* startcode_found_int_cpu_enable */
+	     (1 << 0);		/* parser_int_enable */
+	wr(hw, HEVC_PARSER_INT_CONTROL, v);
+
+	/* AV1 carries no emulation prevention, so only startcode check is on. */
+	v = read_dos_reg(hw, HEVC_SHIFT_STATUS);
+	v |= BIT(0);
+	wr(hw, HEVC_SHIFT_STATUS, v);
+
+	wr(hw, HEVC_SHIFT_CONTROL,
+	   (1 << 10) |	/* length_zero_startcode_en, AV1 */
+	   (1 << 9)  |	/* length_valid_startcode_en, AV1 */
+	   (3 << 6)  |	/* sft_valid_wr_position */
+	   (2 << 4)  |	/* emulate_code_length_sub_1 */
+	   (3 << 1)  |	/* start_code_length_sub_1: AV1 uses a 4-byte code */
+	   (1 << 0));	/* stream_shift_enable */
+
+	wr(hw, HEVC_CABAC_CONTROL, BIT(0));
+	wr(hw, HEVC_PARSER_CORE_CONTROL, BIT(0));
+	wr(hw, HEVC_DEC_STATUS_REG, 0);
+
+	/* Clear IQIT scale lookup so it holds defined values. */
+	wr(hw, HEVC_IQIT_SCALELUT_WR_ADDR, 0);
+	for (i = 0; i < 1024; i++)
+		wr(hw, HEVC_IQIT_SCALELUT_DATA, 0);
+
+	wr(hw, DECODE_MODE, DECODE_MODE_MULTI_FRAMEBASE);
+	wr(hw, HEVC_DECODE_SIZE, 0);
+	wr(hw, HEVC_DECODE_COUNT, 0);
+
+	wr(hw, HEVC_PARSER_CMD_WRITE, BIT(16));
+	for (i = 0; i < AV1_PARSER_CMD_NUMBER; i++)
+		wr(hw, HEVC_PARSER_CMD_WRITE, av1_parser_cmd[i]);
+	wr(hw, HEVC_PARSER_CMD_SKIP_0, PARSER_CMD_SKIP_CFG_0);
+	wr(hw, HEVC_PARSER_CMD_SKIP_1, PARSER_CMD_SKIP_CFG_1);
+	wr(hw, HEVC_PARSER_CMD_SKIP_2, PARSER_CMD_SKIP_CFG_2);
+
+	wr(hw, HEVC_PARSER_IF_CONTROL,
+	   (1 << 5) |	/* parser_sao_if_en */
+	   (1 << 2) |	/* parser_mpred_if_en */
+	   (1 << 0));	/* parser_scaler_if_en */
+
+	/* Reset IPP/MPP, then enable in AV1 mode (3 << 4). */
+	wr(hw, HEVCD_IPP_TOP_CNTL, BIT(0));
+	wr(hw, HEVCD_IPP_TOP_CNTL, (3 << 4) | BIT(1));
+}
+
+/* The tail of vav1_prot_init(): stream front end and mailbox. */
+static void av1_prot_init(struct aml_vdec_hw *hw)
+{
+	/*
+	 * Deliberately NOT enabling stream fetch here.  The reference reaches
+	 * it through vdec_enable_input(), which run() calls *after* the FIFO
+	 * pointers have been programmed.  Turning fetch on first lets the
+	 * shifter run against whatever pointers happen to be in the registers:
+	 * the read pointer then comes back a kilobyte into the buffer, the
+	 * microcode searches from there, finds no frame header in what is left
+	 * and answers AOM_NAL_DECODE_DONE - packet finished, nothing found.
+	 * aml_av1_feed() enables fetch, with the endian bits, once the pointers
+	 * are right.
+	 */
+	wr(hw, HEVC_SHIFT_STARTCODE, 0x00000001);
+	wr(hw, HEVC_SHIFT_EMULATECODE, 0x00000300);
+	wr(hw, HEVC_WAIT_FLAG, 1);
+
+	wr(hw, HEVC_ASSIST_MBOX0_CLR_REG, 1);
+	wr(hw, HEVC_ASSIST_MBOX0_MASK, 1);
+
+	wr(hw, HEVC_PSCALE_CTRL, 0);
+	wr(hw, DEBUG_REG1, 0);
+	/* Overwrites the lmem address written above - see the note in av1.h. */
+	wr(hw, NAL_SEARCH_CTL, 0x8);
+	wr(hw, DECODE_STOP_POS, 0);
+}
+
+/*
+ * Both codecs on the HEVC core keep their state in the same scratch
+ * registers, and they do not use the same ones: whatever the previous
+ * microcode left in a register this one does not program is read as a
+ * pointer or a command.  Decoding VP9 and then AV1 in one module lifetime
+ * timed out on every AV1 frame until these were cleared first.
+ *
+ * SCRATCH_0 (the status register) and SCRATCH_1 (the RPM buffer) are left
+ * alone - both are programmed unconditionally right after this.
+ */
+static void hevc_clear_scratch(struct aml_vdec_hw *hw)
+{
+	int i;
+
+	for (i = 2; i <= 0xf; i++)
+		regmap_write(hw->map[DOS_BUS], REG_ALIGN(0x30c0 + i), 0);
+}
+
+int aml_av1_bringup(struct aml_vdec_hw *hw, u32 width, u32 height)
+{
+	int ret;
+
+	hevc_clear_scratch(hw);
+	ret = av1_alloc(hw);
+	if (ret)
+		return ret;
+
+	/*
+	 * The MMU page table and the compressed body are deliberately NOT set
+	 * up here: this runs before anything has been parsed, so the size
+	 * passed in is only a guess.  They are sized from the microcode's own
+	 * parsed dimensions in aml_av1_config_picture() instead.  Sizing them
+	 * from a guess is how a 640x480 stream ends up with a page table built
+	 * for 320x240 and the decoder writing past the end of it.
+	 */
+	av1_config_workspace(hw, width, height);
+	av1_config_pic_list(hw);
+	av1_init_decoder(hw);
+	av1_prot_init(hw);
+
+	return 0;
+}
+
+/*
+ * The microcode does not eat a low-overhead OBU stream directly.  Amlogic's own
+ * V4L2 glue (vdec_av1_if.c, parser_frame()) rewrites it: every OBU gets a
+ * 20-byte header in front of it, and the OBU itself follows unchanged.
+ *
+ *   [0..3]   big endian (obu_size + 4)
+ *   [4..7]   ones complement of [0..3]
+ *   [8..11]  00 00 00 01      <- the start code HEVC_SHIFT_STARTCODE hunts for
+ *   [12..15] 'A' 'M' 'L' 'V'
+ *   [16..19] obu_size as a four-byte LEB128, continuation bits set
+ *
+ * where obu_size is the OBU's own header plus its size field plus its payload,
+ * plus four.  Without those start codes the shifter finds nothing and the
+ * microcode answers AOM_NAL_DECODE_DONE - packet consumed, no header seen -
+ * which is exactly what a raw OBU stream produced here.
+ */
+#define AV1_AMLV_HEADER_SIZE	20
+
+static int av1_read_leb128(const u8 *p, size_t avail, u64 *val, size_t *len)
+{
+	u64 v = 0;
+	size_t i;
+
+	for (i = 0; i < 8 && i < avail; i++) {
+		v |= (u64)(p[i] & 0x7f) << (i * 7);
+		if (!(p[i] & 0x80)) {
+			*val = v;
+			*len = i + 1;
+			return 0;
+		}
+	}
+	return -EINVAL;
+}
+
+static int av1_reframe(struct aml_vdec_hw *hw, const u8 *src, size_t len,
+		       u8 *dst, size_t dst_cap, size_t *out_len)
+{
+	size_t pos = 0, outp = 0, obus = 0;
+
+	while (pos < len) {
+		size_t hdr_len = 1, size_len = 0, consumed, total;
+		u64 payload;
+		u32 obu_size;
+		u8 h[AV1_AMLV_HEADER_SIZE] = { 0 };
+
+		bool synth = false;
+		u8 obu_hdr[2];
+		u8 leb[8];
+		size_t leb_len = 0;
+
+		if (src[pos] & 0x04)		/* obu_extension_flag */
+			hdr_len = 2;
+		if (pos + hdr_len > len)
+			return -EINVAL;
+
+		if (src[pos] & 0x02) {
+			if (pos + hdr_len >= len)
+				return -EINVAL;
+			if (av1_read_leb128(src + pos + hdr_len,
+					    len - pos - hdr_len,
+					    &payload, &size_len))
+				return -EINVAL;
+			consumed = hdr_len + size_len;
+			total = consumed + payload;
+			if (pos + total > len)
+				return -EINVAL;
+		} else {
+			/*
+			 * Low-overhead bitstream format: obu_has_size_field is
+			 * clear and the payload runs to the end of the buffer.
+			 * This is what the V4L2 stateless AV1 API delivers, and
+			 * what GStreamer's v4l2slav1dec sends.  Give the OBU an
+			 * explicit size so the microcode sees the same shape it
+			 * already handles.
+			 */
+			u64 v;
+
+			payload = len - pos - hdr_len;
+			obu_hdr[0] = src[pos] | 0x02;
+			if (hdr_len == 2)
+				obu_hdr[1] = src[pos + 1];
+			v = payload;
+			do {
+				leb[leb_len] = v & 0x7f;
+				v >>= 7;
+				if (v)
+					leb[leb_len] |= 0x80;
+				leb_len++;
+			} while (v && leb_len < sizeof(leb));
+			consumed = hdr_len + leb_len;
+			total = consumed + payload;
+			synth = true;
+		}
+
+		obu_size = total + 4;
+		if (outp + AV1_AMLV_HEADER_SIZE + total > dst_cap)
+			return -ENOSPC;
+
+		h[0] = (obu_size + 4) >> 24;
+		h[1] = (obu_size + 4) >> 16;
+		h[2] = (obu_size + 4) >> 8;
+		h[3] = (obu_size + 4);
+		h[4] = h[0] ^ 0xff;
+		h[5] = h[1] ^ 0xff;
+		h[6] = h[2] ^ 0xff;
+		h[7] = h[3] ^ 0xff;
+		h[11] = 1;
+		h[12] = 'A';
+		h[13] = 'M';
+		h[14] = 'L';
+		h[15] = 'V';
+		h[16] = (obu_size & 0x7f) | 0x80;
+		h[17] = ((obu_size >> 7) & 0x7f) | 0x80;
+		h[18] = ((obu_size >> 14) & 0x7f) | 0x80;
+		h[19] = (obu_size >> 21) & 0x7f;
+
+		dev_dbg(hw->dev,
+			"av1:   obu type %u at %zu, payload %llu%s\n",
+			 (src[pos] >> 3) & 0xf, pos, payload,
+			 synth ? " (size synthesised)" : "");
+
+		memcpy(dst + outp, h, AV1_AMLV_HEADER_SIZE);
+		outp += AV1_AMLV_HEADER_SIZE;
+		if (synth) {
+			memcpy(dst + outp, obu_hdr, hdr_len);
+			outp += hdr_len;
+			memcpy(dst + outp, leb, leb_len);
+			outp += leb_len;
+			memcpy(dst + outp, src + pos + hdr_len, payload);
+			outp += payload;
+			pos = len;
+		} else {
+			memcpy(dst + outp, src + pos, total);
+			outp += total;
+			pos += total;
+		}
+		obus++;
+	}
+
+	dev_dbg(hw->dev, "av1: reframed %zu OBUs, %zu -> %zu bytes\n",
+		obus, len, outp);
+	*out_len = outp;
+	return 0;
+}
+
+/*
+ * Hand the microcode one temporal unit and tell it to go.  This mirrors the
+ * frame-based branch of the reference's run() - stream FIFO, then
+ * HEVC_ACTION_DONE, shift count cleared, decode size, and only then the core is
+ * released by the caller.
+ *
+ * The FIFO end pointer bounds the *FIFO*, not the payload.  Ending it at the
+ * payload puts the write pointer outside the ring and the microcode waits
+ * forever; that exact mistake cost this driver weeks on the H.264 side.
+ */
+/*
+ * The sequence header, cached from the stream.
+ *
+ * The microcode is restarted for every OUTPUT buffer, and a restart loses
+ * everything it learned from the sequence header - a buffer carrying only a
+ * frame OBU then parses to a zero picture size.  Real streams repeat the
+ * sequence header rarely (often only once, before the first key frame), so
+ * remember it here and put it in front of every buffer that does not carry
+ * one of its own.
+ */
+static u8 av1_seq_obu[192];
+static size_t av1_seq_len;
+
+static void av1_cache_seq_header(const u8 *src, size_t len)
+{
+	size_t pos = 0;
+
+	while (pos < len) {
+		size_t hdr_len = 1, j;
+		u64 payload;
+		int type = (src[pos] >> 3) & 0xf;
+
+		if (src[pos] & 0x04)
+			hdr_len = 2;
+		if (!(src[pos] & 0x02)) {
+			/* no size field: this OBU runs to the end */
+			if (type == 1 && len - pos <= sizeof(av1_seq_obu)) {
+				av1_seq_len = len - pos;
+				memcpy(av1_seq_obu, src + pos, av1_seq_len);
+			}
+			return;
+		}
+		if (pos + hdr_len >= len)
+			return;
+		j = pos + hdr_len;
+		if (av1_read_leb128(src + j, len - j, &payload, &hdr_len))
+			return;
+		j += hdr_len;
+		if (j + payload > len)
+			return;
+		if (type == 1 && j + payload - pos <= sizeof(av1_seq_obu)) {
+			av1_seq_len = j + payload - pos;
+			memcpy(av1_seq_obu, src + pos, av1_seq_len);
+			return;
+		}
+		pos = j + payload;
+	}
+}
+
+static bool av1_has_seq_header(const u8 *src, size_t len)
+{
+	size_t pos = 0;
+
+	while (pos < len) {
+		size_t hdr_len = 1, j;
+		u64 payload;
+
+		if (((src[pos] >> 3) & 0xf) == 1)
+			return true;
+		if (src[pos] & 0x04)
+			hdr_len = 2;
+		if (!(src[pos] & 0x02) || pos + hdr_len >= len)
+			return false;
+		j = pos + hdr_len;
+		if (av1_read_leb128(src + j, len - j, &payload, &hdr_len))
+			return false;
+		pos = j + hdr_len + payload;
+	}
+	return false;
+}
+
+/*
+ * Build a sequence header OBU from V4L2_CID_STATELESS_AV1_SEQUENCE.
+ *
+ * The V4L2 stateless AV1 API hands the decoder a frame's OBU payload and keeps
+ * the sequence in a control, but this hardware parses the bitstream itself and
+ * cannot start without a real sequence header.  Writing one back out from the
+ * control is what bridges the two.  Follows AV1 spec 5.5.1; seq_level_idx and
+ * the operating point are not in the control, and nothing here depends on
+ * them, so they are written as zero.
+ */
+struct av1_bitwriter {
+	u8 *buf;
+	size_t cap;
+	size_t bit;
+};
+
+static void av1_wr_bits(struct av1_bitwriter *w, u32 val, int n)
+{
+	while (n--) {
+		size_t byte = w->bit >> 3;
+		int shift = 7 - (w->bit & 7);
+
+		if (byte >= w->cap)
+			return;
+		if ((val >> n) & 1)
+			w->buf[byte] |= 1 << shift;
+		else
+			w->buf[byte] &= ~(1 << shift);
+		w->bit++;
+	}
+}
+
+static int av1_bits_for(u32 v)
+{
+	int n = 1;
+
+	while (v >> n)
+		n++;
+	return n;
+}
+
+static size_t av1_build_sequence_obu(const struct v4l2_ctrl_av1_sequence *sq,
+				     u8 *out, size_t cap)
+{
+	struct av1_bitwriter w = { .buf = out + 2, .cap = cap - 2 };
+	u32 f = sq->flags;
+	bool reduced = false;
+	bool order_hint = f & V4L2_AV1_SEQUENCE_FLAG_ENABLE_ORDER_HINT;
+	bool mono = f & V4L2_AV1_SEQUENCE_FLAG_MONO_CHROME;
+	int wbits = av1_bits_for(sq->max_frame_width_minus_1);
+	int hbits = av1_bits_for(sq->max_frame_height_minus_1);
+	size_t payload;
+
+	memset(out, 0, cap);
+
+	av1_wr_bits(&w, sq->seq_profile, 3);
+	av1_wr_bits(&w, !!(f & V4L2_AV1_SEQUENCE_FLAG_STILL_PICTURE), 1);
+	av1_wr_bits(&w, reduced, 1);
+
+	av1_wr_bits(&w, 0, 1);		/* timing_info_present_flag */
+	av1_wr_bits(&w, 0, 1);		/* initial_display_delay_present */
+	av1_wr_bits(&w, 0, 5);		/* operating_points_cnt_minus_1 */
+	av1_wr_bits(&w, 0, 12);		/* operating_point_idc[0] */
+	av1_wr_bits(&w, 0, 5);		/* seq_level_idx[0], <= 7 so no tier */
+
+	av1_wr_bits(&w, wbits - 1, 4);
+	av1_wr_bits(&w, hbits - 1, 4);
+	av1_wr_bits(&w, sq->max_frame_width_minus_1, wbits);
+	av1_wr_bits(&w, sq->max_frame_height_minus_1, hbits);
+
+	av1_wr_bits(&w, 0, 1);		/* frame_id_numbers_present_flag */
+	av1_wr_bits(&w, !!(f & V4L2_AV1_SEQUENCE_FLAG_USE_128X128_SUPERBLOCK), 1);
+	av1_wr_bits(&w, !!(f & V4L2_AV1_SEQUENCE_FLAG_ENABLE_FILTER_INTRA), 1);
+	av1_wr_bits(&w, !!(f & V4L2_AV1_SEQUENCE_FLAG_ENABLE_INTRA_EDGE_FILTER), 1);
+
+	av1_wr_bits(&w, !!(f & V4L2_AV1_SEQUENCE_FLAG_ENABLE_INTERINTRA_COMPOUND), 1);
+	av1_wr_bits(&w, !!(f & V4L2_AV1_SEQUENCE_FLAG_ENABLE_MASKED_COMPOUND), 1);
+	av1_wr_bits(&w, !!(f & V4L2_AV1_SEQUENCE_FLAG_ENABLE_WARPED_MOTION), 1);
+	av1_wr_bits(&w, !!(f & V4L2_AV1_SEQUENCE_FLAG_ENABLE_DUAL_FILTER), 1);
+	av1_wr_bits(&w, order_hint, 1);
+	if (order_hint) {
+		av1_wr_bits(&w, !!(f & V4L2_AV1_SEQUENCE_FLAG_ENABLE_JNT_COMP), 1);
+		av1_wr_bits(&w, !!(f & V4L2_AV1_SEQUENCE_FLAG_ENABLE_REF_FRAME_MVS), 1);
+	}
+	av1_wr_bits(&w, 1, 1);		/* seq_choose_screen_content_tools */
+	av1_wr_bits(&w, 1, 1);		/* seq_choose_integer_mv */
+	if (order_hint)
+		av1_wr_bits(&w, sq->order_hint_bits - 1, 3);
+
+	av1_wr_bits(&w, !!(f & V4L2_AV1_SEQUENCE_FLAG_ENABLE_SUPERRES), 1);
+	av1_wr_bits(&w, !!(f & V4L2_AV1_SEQUENCE_FLAG_ENABLE_CDEF), 1);
+	av1_wr_bits(&w, !!(f & V4L2_AV1_SEQUENCE_FLAG_ENABLE_RESTORATION), 1);
+
+	/* color_config() */
+	av1_wr_bits(&w, sq->bit_depth > 8, 1);		/* high_bitdepth */
+	if (sq->seq_profile == 2 && sq->bit_depth > 8)
+		av1_wr_bits(&w, sq->bit_depth == 12, 1);
+	if (sq->seq_profile != 1)
+		av1_wr_bits(&w, mono, 1);
+	av1_wr_bits(&w, 0, 1);		/* color_description_present_flag */
+	av1_wr_bits(&w, !!(f & V4L2_AV1_SEQUENCE_FLAG_COLOR_RANGE), 1);
+	if (!mono) {
+		bool ssx = f & V4L2_AV1_SEQUENCE_FLAG_SUBSAMPLING_X;
+		bool ssy = f & V4L2_AV1_SEQUENCE_FLAG_SUBSAMPLING_Y;
+
+		if (sq->seq_profile == 2 && sq->bit_depth == 12) {
+			av1_wr_bits(&w, ssx, 1);
+			if (ssx)
+				av1_wr_bits(&w, ssy, 1);
+		}
+		if (ssx && ssy)
+			av1_wr_bits(&w, 0, 2);	/* chroma_sample_position */
+		av1_wr_bits(&w, !!(f & V4L2_AV1_SEQUENCE_FLAG_SEPARATE_UV_DELTA_Q), 1);
+	}
+
+	av1_wr_bits(&w, !!(f & V4L2_AV1_SEQUENCE_FLAG_FILM_GRAIN_PARAMS_PRESENT), 1);
+
+	av1_wr_bits(&w, 1, 1);		/* trailing one bit */
+	while (w.bit & 7)
+		av1_wr_bits(&w, 0, 1);
+
+	payload = w.bit >> 3;
+	if (payload > 127)
+		return 0;		/* would need a longer size field */
+
+	out[0] = 0x0a;			/* OBU_SEQUENCE_HEADER, has_size */
+	out[1] = payload;
+	return payload + 2;
+}
+
+static int av1_alloc_stream(struct aml_vdec_hw *hw, size_t size);
+static int av1_program_fifo(struct aml_vdec_hw *hw, u32 kick, size_t *unused);
+
+/*
+ * Emit one AMLV-framed OBU built from a header and a separate payload.  The
+ * V4L2 stateless API delivers the frame OBU's *payload* with the OBU header
+ * stripped, so the header has to be put back without copying the payload
+ * twice.
+ */
+static int av1_emit_obu(u8 *dst, size_t cap, const u8 *hdr, size_t hdr_len,
+			const u8 *payload, size_t payload_len, size_t *outp)
+{
+	u8 h[AV1_AMLV_HEADER_SIZE] = { 0 };
+	u32 obu_size = hdr_len + payload_len + 4;
+
+	if (*outp + AV1_AMLV_HEADER_SIZE + hdr_len + payload_len > cap)
+		return -ENOSPC;
+
+	h[0] = (obu_size + 4) >> 24;
+	h[1] = (obu_size + 4) >> 16;
+	h[2] = (obu_size + 4) >> 8;
+	h[3] = (obu_size + 4);
+	h[4] = h[0] ^ 0xff;
+	h[5] = h[1] ^ 0xff;
+	h[6] = h[2] ^ 0xff;
+	h[7] = h[3] ^ 0xff;
+	h[11] = 1;
+	h[12] = 'A';
+	h[13] = 'M';
+	h[14] = 'L';
+	h[15] = 'V';
+	h[16] = (obu_size & 0x7f) | 0x80;
+	h[17] = ((obu_size >> 7) & 0x7f) | 0x80;
+	h[18] = ((obu_size >> 14) & 0x7f) | 0x80;
+	h[19] = (obu_size >> 21) & 0x7f;
+
+	memcpy(dst + *outp, h, AV1_AMLV_HEADER_SIZE);
+	*outp += AV1_AMLV_HEADER_SIZE;
+	memcpy(dst + *outp, hdr, hdr_len);
+	*outp += hdr_len;
+	memcpy(dst + *outp, payload, payload_len);
+	*outp += payload_len;
+	return 0;
+}
+
+/*
+ * Feed one frame the way the V4L2 stateless API delivers it: the sequence
+ * header rebuilt from the control, then the frame OBU reassembled from its
+ * payload.
+ */
+int aml_av1_feed_frame(struct aml_vdec_hw *hw, const void *data, size_t size)
+{
+	u8 fh[8];
+	size_t hdr_len = 0, outp = 0, dummy;
+	u64 v = size;
+	int ret;
+
+	av1_stateless = true;
+
+	ret = av1_alloc_stream(hw, size);
+	if (ret)
+		return ret;
+
+	memset(av1_ws.stream_vaddr, 0, av1_ws.stream_size);
+
+	if (av1_seq_len) {
+		ret = av1_reframe(hw, av1_seq_obu, av1_seq_len,
+				  av1_ws.stream_vaddr,
+				  av1_ws.stream_size - HEVC_PADDING_SIZE,
+				  &outp);
+		if (ret)
+			return ret;
+	}
+
+	fh[hdr_len++] = 0x32;		/* OBU_FRAME, has_size_field */
+	do {
+		fh[hdr_len] = v & 0x7f;
+		v >>= 7;
+		if (v)
+			fh[hdr_len] |= 0x80;
+		hdr_len++;
+	} while (v && hdr_len < sizeof(fh));
+
+	ret = av1_emit_obu(av1_ws.stream_vaddr,
+			   av1_ws.stream_size - HEVC_PADDING_SIZE,
+			   fh, hdr_len, data, size, &outp);
+	if (ret)
+		return ret;
+
+	av1_ws.payload = outp;
+	dummy = 0;
+	return av1_program_fifo(hw, AOM_AV1_SEARCH_HEAD, &dummy);
+}
+
+bool aml_av1_have_sequence(void)
+{
+	return av1_seq_len != 0;
+}
+
+/* Rebuild the cached sequence header from the V4L2 control. */
+bool aml_av1_set_sequence(const void *ctrl)
+{
+	const struct v4l2_ctrl_av1_sequence *sq = ctrl;
+	size_t n;
+
+	if (!sq || !sq->max_frame_width_minus_1 || !sq->max_frame_height_minus_1)
+		return false;
+
+	n = av1_build_sequence_obu(sq, av1_seq_obu, sizeof(av1_seq_obu));
+	if (!n)
+		return false;
+	av1_seq_len = n;
+	return true;
+}
+
+bool av1_buf_has_sequence(const void *data, size_t size)
+{
+	return data && av1_has_seq_header(data, size);
+}
+
+static int av1_alloc_stream(struct aml_vdec_hw *hw, size_t size)
+{
+	/*
+	 * The reframed stream is 20 bytes per OBU larger than the input and
+	 * needs the FIFO padding after it, so size the buffer from the input
+	 * rather than fixing it: a large temporal unit would otherwise be
+	 * rejected outright.
+	 */
+	size_t need = ALIGN(size + size / 8 + HEVC_PADDING_SIZE +
+			    VLD_PADDING_SIZE, SZ_1M);
+
+	if (av1_ws.stream_vaddr && av1_ws.stream_size < need) {
+		dma_free_coherent(hw->dev, av1_ws.stream_size,
+				  av1_ws.stream_vaddr, av1_ws.stream_paddr);
+		av1_ws.stream_vaddr = NULL;
+	}
+	if (!av1_ws.stream_vaddr) {
+		av1_ws.stream_size = need;
+		av1_ws.stream_vaddr = dma_alloc_coherent(hw->dev,
+							 av1_ws.stream_size,
+							 &av1_ws.stream_paddr,
+							 GFP_KERNEL);
+		if (!av1_ws.stream_vaddr)
+			return -ENOMEM;
+	}
+	return 0;
+}
+
+static int av1_program_fifo(struct aml_vdec_hw *hw, u32 kick, size_t *unused)
+{
+	size_t dummy;
+	u32 v;
+
+	/*
+	 * Exactly what vdec.c does for a frame-based HEVC target: the FIFO
+	 * spans the whole block, the read pointer sits at the chunk and the
+	 * write pointer is parked a padding length past its end.
+	 */
+	dummy = av1_ws.payload + HEVC_PADDING_SIZE;
+	if (dummy >= av1_ws.stream_size)
+		dummy -= av1_ws.stream_size;
+
+	wr(hw, HEVC_STREAM_START_ADDR, av1_ws.stream_paddr);
+	wr(hw, HEVC_STREAM_END_ADDR,
+	   av1_ws.stream_paddr + av1_ws.stream_size);
+	wr(hw, HEVC_STREAM_RD_PTR, av1_ws.stream_paddr);
+	wr(hw, HEVC_STREAM_WR_PTR,
+	   round_down(av1_ws.stream_paddr + dummy, VDEC_FIFO_ALIGN));
+
+	/*
+	 * Endian control for the non-parser path.  Without these three bits the
+	 * shifter reads the buffer in the wrong byte order: it still consumes
+	 * bytes, so the shift count advances and everything looks alive, but
+	 * nothing the microcode sees parses and it reports HEVC_ACTION_DONE
+	 * within ten microseconds.  vdec_enable_input() sets them for every
+	 * frame-based HEVC target.
+	 */
+	v = read_dos_reg(hw, HEVC_STREAM_CONTROL);
+	v &= ~(7 << 4);
+	v |= (7 << 4) | BIT(0);
+	wr(hw, HEVC_STREAM_CONTROL, v);
+	v = read_dos_reg(hw, HEVC_STREAM_FIFO_CTL);
+	v |= BIT(29);
+	wr(hw, HEVC_STREAM_FIFO_CTL, v);
+
+	wr(hw, HEVC_DEC_STATUS_REG, kick);
+	wr(hw, HEVC_SHIFT_BYTE_COUNT, 0);
+	wr(hw, HEVC_DECODE_SIZE, av1_ws.payload);
+	wr(hw, HEVC_DECODE_COUNT, 0);
+	wr(hw, LMEM_DUMP_ADR, av1_ws.lmem);
+
+	dev_dbg(hw->dev, "av1: fed %zu bytes at %pad, fifo %zu KiB, kick %02x, endian %d\n",
+		av1_ws.payload, &av1_ws.stream_paddr,
+		 av1_ws.stream_size / 1024, kick, 7);
+	return 0;
+}
+
+int aml_av1_feed_buf(struct aml_vdec_hw *hw, const void *data, size_t size,
+		     u32 kick)
+{
+	size_t dummy = 0;
+	int ret;
+
+	av1_stateless = false;
+
+	/*
+	 * The reframed stream is 20 bytes per OBU larger than the input and
+	 * needs the FIFO padding after it, so size the buffer from the input
+	 * rather than fixing it: a large temporal unit would otherwise be
+	 * rejected outright.
+	 */
+	ret = av1_alloc_stream(hw, size);
+	if (ret)
+		return ret;
+
+	memset(av1_ws.stream_vaddr, 0, av1_ws.stream_size);
+
+	dev_dbg(hw->dev, "av1: feed %zu bytes, first 24: %*ph\n",
+		size, 24, (const u8 *)data);
+
+	if (av1_has_seq_header(data, size)) {
+		av1_cache_seq_header(data, size);
+		av1_ws.payload = 0;
+	} else if (av1_seq_len) {
+		ret = av1_reframe(hw, av1_seq_obu, av1_seq_len,
+				  av1_ws.stream_vaddr,
+				  av1_ws.stream_size - HEVC_PADDING_SIZE,
+				  &av1_ws.payload);
+		if (ret)
+			return ret;
+	} else {
+		av1_ws.payload = 0;
+	}
+
+	{
+		size_t n = 0;
+
+		ret = av1_reframe(hw, data, size,
+				  av1_ws.stream_vaddr + av1_ws.payload,
+				  av1_ws.stream_size - HEVC_PADDING_SIZE -
+					  av1_ws.payload, &n);
+		if (ret)
+			return ret;
+		av1_ws.payload += n;
+	}
+
+	return av1_program_fifo(hw, kick, &dummy);
+}
+
+/* The probe harness feeds a file; the V4L2 path feeds an OUTPUT buffer. */
+int aml_av1_feed(struct aml_vdec_hw *hw, const char *fw_name, u32 kick)
+{
+	const struct firmware *fw;
+	int ret;
+
+	ret = request_firmware(&fw, fw_name, hw->dev);
+	if (ret) {
+		dev_err(hw->dev, "av1: test stream %s not found (%d)\n",
+			fw_name, ret);
+		return ret;
+	}
+	ret = aml_av1_feed_buf(hw, fw->data, fw->size, kick);
+	release_firmware(fw);
+	return ret;
+}
+
+void aml_av1_dump_stream_state(struct aml_vdec_hw *hw)
+{
+	dev_info(hw->dev,
+		 "av1: fifo start %08x end %08x rd %08x wr %08x level %08x shifted %08x\n",
+		 read_dos_reg(hw, HEVC_STREAM_START_ADDR),
+		 read_dos_reg(hw, HEVC_STREAM_END_ADDR),
+		 read_dos_reg(hw, HEVC_STREAM_RD_PTR),
+		 read_dos_reg(hw, HEVC_STREAM_WR_PTR),
+		 read_dos_reg(hw, HEVC_STREAM_LEVEL),
+		 read_dos_reg(hw, HEVC_SHIFT_BYTE_COUNT));
+}
+
+/*
+ * The microcode's own output.  RPM is the parsed-parameter block it DMAs back
+ * after reading a frame header, LMEM its local memory dump.  Both are zero
+ * until it has actually parsed something, so this distinguishes "read the bytes
+ * and understood them" from "read the bytes and gave up".
+ */
+void aml_av1_dump_ucode_output(struct aml_vdec_hw *hw)
+{
+	const u32 *rpm, *lmem;
+	int i;
+
+	if (!av1_ws.side_vaddr)
+		return;
+
+	rpm = av1_ws.side_vaddr + (av1_ws.rpm - av1_ws.side_paddr);
+	lmem = av1_ws.side_vaddr + (av1_ws.lmem - av1_ws.side_paddr);
+
+	for (i = 0; i < 8; i++)
+		dev_info(hw->dev, "av1: rpm[%02d] %08x %08x %08x %08x\n",
+			 i * 4, rpm[i * 4], rpm[i * 4 + 1],
+			 rpm[i * 4 + 2], rpm[i * 4 + 3]);
+	for (i = 0; i < 4; i++)
+		dev_info(hw->dev, "av1: lmem[%02d] %08x %08x %08x %08x\n",
+			 i * 4, lmem[i * 4], lmem[i * 4 + 1],
+			 lmem[i * 4 + 2], lmem[i * 4 + 3]);
+}
+
+/*
+ * The microcode DMAs its parsed parameters into the RPM buffer with the entries
+ * reversed inside every group of four u16s - load_param() in the reference does
+ * params->l.data[i+ii] = rpm_ptr[i+3-ii].  Undo that and the block lines up with
+ * union param_u.
+ */
+/* frame_type from the last parsed header: 0 KEY, 1 INTER, 2 INTRA_ONLY, 3 S. */
+u32 aml_av1_frame_type(struct aml_vdec_hw *hw)
+{
+	const u16 *raw;
+
+	if (!av1_ws.side_vaddr)
+		return 0xffff;
+	raw = av1_ws.side_vaddr + (av1_ws.rpm - av1_ws.side_paddr);
+	/* index 14 unscrambles to raw[(14 & ~3) + 3 - (14 & 3)] = raw[13] */
+	return raw[13];
+}
+
+void aml_av1_dump_params(struct aml_vdec_hw *hw)
+{
+	static const struct { const char *name; int idx; } fields[] = {
+		{ "profile",          AV1_RPM_PROFILE },
+		{ "max_frame_width",  AV1_RPM_MAX_FRAME_WIDTH },
+		{ "max_frame_height", AV1_RPM_MAX_FRAME_HEIGHT },
+		{ "frame_type",       AV1_RPM_FRAME_TYPE },
+		{ "show_frame",       AV1_RPM_SHOW_FRAME },
+		{ "frame_width",      AV1_RPM_FRAME_WIDTH },
+		{ "frame_height",     AV1_RPM_FRAME_HEIGHT },
+		{ "bit_depth",        AV1_RPM_BIT_DEPTH },
+		{ "seq_flags",        AV1_RPM_SEQ_FLAGS },
+		{ "lf_sharpness",     AV1_RPM_LF_SHARPNESS },
+		{ "lf_mode_ref_delta", AV1_RPM_LF_MODE_REF_DELTA },
+		{ "lf_level_0",       AV1_RPM_LF_LEVEL_0 },
+		{ "lf_level_1",       AV1_RPM_LF_LEVEL_1 },
+		{ "lf_level_u",       AV1_RPM_LF_LEVEL_U },
+		{ "lf_level_v",       AV1_RPM_LF_LEVEL_V },
+		{ "segmentation_en",  AV1_RPM_SEGMENTATION_EN },
+	};
+	const u16 *raw;
+	u16 p[AV1_RPM_COUNT];
+	int i, j;
+
+	if (!av1_ws.side_vaddr)
+		return;
+
+	raw = av1_ws.side_vaddr + (av1_ws.rpm - av1_ws.side_paddr);
+	for (i = 0; i < AV1_RPM_COUNT; i += 4)
+		for (j = 0; j < 4; j++)
+			p[i + j] = raw[i + 3 - j];
+
+	for (i = 0; i < ARRAY_SIZE(fields); i++)
+		dev_info(hw->dev, "av1: param %-18s = %u (0x%x)\n",
+			 fields[i].name, p[fields[i].idx], p[fields[i].idx]);
+}
+
+/*
+ * Per-picture configuration, i.e. the reference's config_sao_hw() and the
+ * hardware half of config_dblk_hw(), specialised for what this bring-up
+ * actually produces: a single key frame, MMU on, double write off, NV12,
+ * linear memory map, no segmentation and no reference pictures.
+ *
+ * The loop filter is the only part that needs real arithmetic.  Its software
+ * side in the reference is long, but only twelve registers ever reach the
+ * hardware, and for a key frame the per-segment level table collapses onto the
+ * intra entry.
+ */
+
+#define AV1_MAX_LOOP_FILTER	63
+
+/* Superres upscaler fixed point, from av1_upscale_frame_init(). */
+#define RS_SUBPEL_BITS		6
+#define RS_SCALE_SUBPEL_BITS	14
+#define RS_SCALE_SUBPEL_MASK	((1 << RS_SCALE_SUBPEL_BITS) - 1)
+#define RS_SCALE_EXTRA_BITS	(RS_SCALE_SUBPEL_BITS - RS_SUBPEL_BITS)
+#define RS_SCALE_EXTRA_OFF	(1 << (RS_SCALE_EXTRA_BITS - 1))
+
+struct av1_lfthr { u8 mblim; u8 lim; };
+
+/*
+ * av1_loop_filter_init()/update_sharpness().  With sharpness 0 the inside limit
+ * is just the level, floored at 1, and the macroblock limit is 2*(lvl+2) on top
+ * of it.
+ */
+static void av1_build_lfthr(struct av1_lfthr *thr, u32 sharpness)
+{
+	int lvl;
+
+	for (lvl = 0; lvl <= AV1_MAX_LOOP_FILTER; lvl++) {
+		int inside = lvl >> ((sharpness > 0) + (sharpness > 4));
+
+		if (sharpness > 0 && inside > 9 - (int)sharpness)
+			inside = 9 - sharpness;
+		if (inside < 1)
+			inside = 1;
+
+		thr[lvl].lim = inside;
+		thr[lvl].mblim = 2 * (lvl + 2) + inside;
+	}
+}
+
+static int av1_clamp_lvl(int v)
+{
+	if (v < 0)
+		return 0;
+	if (v > AV1_MAX_LOOP_FILTER)
+		return AV1_MAX_LOOP_FILTER;
+	return v;
+}
+
+/*
+ * config_mc_buffer().  Entry 0 names the picture being decoded, entries 1..7
+ * are LAST_FRAME..ALTREF_FRAME.  For an intra frame every entry is the current
+ * picture, which is what the reference does too.  The whole table is written
+ * twice, at canvas offset 0 and 16, then the reference-info table gets each
+ * picture's dimensions and scale factors.
+ */
+/*
+ * Reference frame numbering, as the spec and the reference code use it.
+ */
+#define AV1_LAST_FRAME		1
+#define AV1_LAST2_FRAME		2
+#define AV1_GOLDEN_FRAME	4
+#define AV1_BWDREF_FRAME	5
+#define AV1_ALTREF2_FRAME	6
+#define AV1_ALTREF_FRAME	7
+#define AV1_MFMV_STACK_SIZE	3
+
+/* get_relative_dist(): signed distance between two wrapped order hints. */
+static int av1_rel_dist(u32 a, u32 b, u32 bits)
+{
+	int diff, m;
+
+	if (!bits)
+		return 0;
+
+	diff = (int)a - (int)b;
+	m = 1 << (bits - 1);
+	return (diff & (m - 1)) - (diff & m);
+}
+
+/*
+ * av1_setup_motion_field() and motion_field_projection(), reduced to what the
+ * hardware actually consumes.  The projection itself runs in the decoder; the
+ * host only has to say which reference frames to project from and, for each,
+ * the distance from that frame to each of its own references.
+ */
+struct av1_mfmv {
+	int id[8];		/* reference frame each entry projects from */
+	s8 offset[8][8];	/* [entry][ref frame] order-hint distance */
+	bool valid[8];		/* frame was inter and the same size */
+	int count;
+};
+
+static bool av1_mf_project(struct av1_mfmv *mf, int rf, u32 bits,
+			   u32 width, u32 height)
+{
+	int idx = mf->count++;
+	const struct av1_slot *b = &av1_slots[av1_frame_slot[rf]];
+	int j;
+
+	mf->id[idx] = rf;
+	memset(mf->offset[idx], 0, sizeof(mf->offset[idx]));
+	mf->valid[idx] = false;
+
+	/* An intra frame has no motion to project, nor has a resized one. */
+	if (b->intra || b->width != width || b->height != height)
+		return false;
+
+	for (j = AV1_LAST_FRAME; j <= AV1_ALTREF_FRAME; j++)
+		mf->offset[idx][j] = av1_rel_dist(b->order_hint,
+						  b->ref_order_hint[j], bits);
+	mf->valid[idx] = true;
+	return true;
+}
+
+static void av1_setup_motion_field(struct av1_mfmv *mf, u32 cur_hint, u32 bits)
+{
+	u32 width = av1_slots[av1_cur_slot].width;
+	u32 height = av1_slots[av1_cur_slot].height;
+	int ref_stamp = AV1_MFMV_STACK_SIZE - 1;
+	u32 oh[8];
+	int i;
+
+	memset(mf, 0, sizeof(*mf));
+	if (!bits)
+		return;
+
+	for (i = AV1_LAST_FRAME; i <= AV1_ALTREF_FRAME; i++)
+		oh[i] = av1_slots[av1_frame_slot[i]].order_hint;
+
+	/*
+	 * LAST is skipped when it is an overlay of GOLDEN - its altref hint
+	 * matching GOLDEN's own hint means it carries no new motion.
+	 */
+	if (av1_slots[av1_frame_slot[AV1_LAST_FRAME]].ref_order_hint[AV1_ALTREF_FRAME] !=
+	    oh[AV1_GOLDEN_FRAME])
+		av1_mf_project(mf, AV1_LAST_FRAME, bits, width, height);
+	ref_stamp--;
+
+	if (av1_rel_dist(oh[AV1_BWDREF_FRAME], cur_hint, bits) > 0 &&
+	    av1_mf_project(mf, AV1_BWDREF_FRAME, bits, width, height))
+		ref_stamp--;
+
+	if (av1_rel_dist(oh[AV1_ALTREF2_FRAME], cur_hint, bits) > 0 &&
+	    av1_mf_project(mf, AV1_ALTREF2_FRAME, bits, width, height))
+		ref_stamp--;
+
+	if (av1_rel_dist(oh[AV1_ALTREF_FRAME], cur_hint, bits) > 0 &&
+	    ref_stamp >= 0 &&
+	    av1_mf_project(mf, AV1_ALTREF_FRAME, bits, width, height))
+		ref_stamp--;
+
+	if (ref_stamp >= 0)
+		av1_mf_project(mf, AV1_LAST2_FRAME, bits, width, height);
+}
+
+/*
+ * av1_setup_skip_mode_allowed().  Skip mode needs two references that bracket
+ * the current picture, or failing that the two nearest forward ones.  It can
+ * only ever apply when the stream signals REFERENCE_MODE_SELECT, which in
+ * practice means alt-ref style encoding - a plain forward-only IPPP stream
+ * never reaches it, which is why leaving these bits zero went unnoticed.
+ */
+static void av1_skip_mode(const u16 *pr, u32 cur_hint, u32 oh_bits, bool inter,
+			  u32 *allowed, u32 *idx0, u32 *idx1)
+{
+	int hint[2] = { 0, 0 };
+	int idx[2] = { -1, -1 };
+	int i, h, d;
+
+	*allowed = 0;
+	*idx0 = 0;
+	*idx1 = 0;
+	if (!oh_bits || !inter || !(pr[AV1_RPM_REFERENCE_MODE] & 1))
+		return;
+
+	/* Nearest reference on each side of the current picture. */
+	for (i = 0; i < 7; i++) {
+		h = av1_slots[av1_frame_slot[AV1_LAST_FRAME + i]].order_hint;
+		d = av1_rel_dist(h, cur_hint, oh_bits);
+		if (d < 0) {
+			if (idx[0] < 0 ||
+			    av1_rel_dist(h, hint[0], oh_bits) > 0) {
+				hint[0] = h;
+				idx[0] = i;
+			}
+		} else if (d > 0) {
+			if (idx[1] < 0 ||
+			    av1_rel_dist(h, hint[1], oh_bits) < 0) {
+				hint[1] = h;
+				idx[1] = i;
+			}
+		}
+	}
+
+	if (idx[0] < 0)
+		return;
+
+	if (idx[1] < 0) {
+		/* Forward only: find the second nearest forward reference. */
+		int best = -1;
+
+		for (i = 0; i < 7; i++) {
+			h = av1_slots[av1_frame_slot[AV1_LAST_FRAME + i]].order_hint;
+			if (av1_rel_dist(h, hint[0], oh_bits) >= 0)
+				continue;
+			if (best < 0 || av1_rel_dist(h, hint[1], oh_bits) > 0) {
+				hint[1] = h;
+				best = i;
+			}
+		}
+		if (best < 0)
+			return;
+		idx[1] = best;
+	}
+
+	*allowed = 1;
+	*idx0 = min(idx[0], idx[1]);
+	*idx1 = max(idx[0], idx[1]);
+}
+
+static void av1_config_mc_buffer(struct aml_vdec_hw *hw, bool inter,
+				 const u16 *pr, u32 width, u32 height)
+{
+	int *slots = av1_frame_slot;
+	int i;
+
+	wr(hw, HEVCD_MPP_ANC_CANVAS_ACCCONFIG_ADDR, (0 << 8) | BIT(0));
+	for (i = 0; i < 8; i++)
+		wr(hw, HEVCD_MPP_ANC_CANVAS_DATA_ADDR,
+		   (av1_slots[slots[i]].order_hint << 24) |
+		   (slots[i] << 16) | (slots[i] << 8) | slots[i]);
+
+	wr(hw, HEVCD_MPP_ANC_CANVAS_ACCCONFIG_ADDR, (16 << 8) | BIT(0));
+	for (i = 0; i < 8; i++)
+		wr(hw, HEVCD_MPP_ANC_CANVAS_DATA_ADDR,
+		   (av1_slots[slots[i]].order_hint << 24) |
+		   (slots[i] << 16) | (slots[i] << 8) | slots[i]);
+
+	/*
+	 * The motion-compensation reference cache.  Nothing in this driver had
+	 * ever written it, so it kept whatever state the previously loaded
+	 * codec left behind - and a stale-enabled cache stalls the MC engine,
+	 * which is what the microcode was spinning on.  Mainline's VP9 codec
+	 * resets it and disables it outright on a key frame; do the same for
+	 * both frame types for now.  Enabling it needs the reference canvas
+	 * entries and only buys bandwidth, not correctness.
+	 */
+	wr(hw, HEVCD_MCRCC_CTL1, 0x2);
+	wr(hw, HEVCD_MCRCC_CTL1, 0x0);
+
+	/*
+	 * config_mpred_hw() packs the reference slot indices and order hints
+	 * into REF03..REF05.  Indices are 5 bits each, ALTREF(7) down to
+	 * ref 2 at bit positions 25, 20, 15, 10, 5, 0 of REF03; LAST(1)'s
+	 * index lands at bit 24 of REF04, followed by the order hints a byte
+	 * apart.  Only meaningful for an inter frame.
+	 */
+	{
+		u32 r3 = 0, r4 = 0, r5 = 0;
+
+		/*
+		 * config_mpred_hw() writes these for every frame, not only
+		 * inter ones - a key frame gets zeroes rather than whatever
+		 * the last picture left behind.
+		 */
+		if (inter) {
+			for (i = 7; i >= 2; i--)
+				r3 |= (slots[i] & 0x1f) << (25 - (7 - i) * 5);
+
+			r4 = (slots[1] & 0x1f) << 24;
+			r4 |= (av1_slots[slots[7]].order_hint & 0xff) << 16;
+			r4 |= (av1_slots[slots[6]].order_hint & 0xff) << 8;
+			r4 |= (av1_slots[slots[5]].order_hint & 0xff);
+
+			r5 = (av1_slots[slots[4]].order_hint & 0xff) << 24;
+			r5 |= (av1_slots[slots[3]].order_hint & 0xff) << 16;
+			r5 |= (av1_slots[slots[2]].order_hint & 0xff) << 8;
+			r5 |= (av1_slots[slots[1]].order_hint & 0xff);
+		}
+		wr(hw, HEVC_MPRED_L0_REF03_POC, r3);
+		wr(hw, HEVC_MPRED_L0_REF04_POC, r4);
+		wr(hw, HEVC_MPRED_L0_REF05_POC, r5);
+	}
+
+	/* auto-increment from index 0 */
+	wr(hw, AV1D_MPP_REFINFO_TBL_ACCCONFIG, BIT(2));
+	for (i = 0; i < 8; i++) {
+		wr(hw, AV1D_MPP_REFINFO_DATA, width);
+		wr(hw, AV1D_MPP_REFINFO_DATA, height);
+		wr(hw, AV1D_MPP_REFINFO_DATA, AV1_REF_NO_SCALE);
+		wr(hw, AV1D_MPP_REFINFO_DATA, AV1_REF_NO_SCALE);
+		wr(hw, AV1D_MPP_REFINFO_DATA, 0);
+	}
+
+	/*
+	 * No reference is ever scaled here - superres is off and every picture
+	 * in the stream is the same size - but both registers must be written
+	 * rather than left holding whatever the last codec put there.
+	 */
+	wr(hw, AV1D_MPP_REF_SCALE_ENBL, 0);
+	wr(hw, PARSER_REF_SCALE_ENBL, 0);
+}
+
+/*
+ * AV1 reference management, the part of av1_bufmgr_process() a plain IPPP
+ * stream actually needs: after a picture is decoded, every reference slot named
+ * in refresh_frame_flags starts pointing at it.
+ */
+/*
+ * config_next_ref_info_hw().  The microcode does not learn the reference map
+ * from any register - the host writes it into the parser's internal memory,
+ * twelve words at 0x11a0 on SC2 and later (0x1000 before that).  The layout
+ * comes from av1_get_next_used_ref_info():
+ *
+ *   word 0     order hints of reference slots 0..3, one byte each
+ *   word 1     order hints of reference slots 4..7
+ *   words 2..9 (width << 16) | height of reference slot (word - 2)
+ *   word 10    picture indices of reference slots 0..3, one byte each
+ *   word 11    picture indices of reference slots 4..7
+ *
+ * Without it an inter frame has no resolvable references and the microcode
+ * spins in its own wait loop instead of ever starting on an LCU.
+ */
+static void av1_config_next_ref_info(struct aml_vdec_hw *hw,
+				     u32 width, u32 height)
+{
+	u32 info;
+	int i, j;
+
+	wr(hw, HEVC_PARSER_MEM_WR_ADDR, 0x11a0);
+	for (i = 0; i < 12; i++) {
+		info = 0;
+		if (i < 2) {
+			for (j = 0; j < 4; j++)
+				info |= (av1_slots[av1_ref_slot[i * 4 + j]].order_hint & 0xff)
+					<< (j * 8);
+		} else if (i < 10) {
+			/* Every picture in the stream is the same size. */
+			info = (width << 16) | (height & 0xffff);
+		} else {
+			for (j = 0; j < 4; j++)
+				info |= (av1_ref_slot[(i - 10) * 4 + j] & 0xff)
+					<< (j * 8);
+		}
+		wr(hw, HEVC_PARSER_MEM_RW_DATA, info);
+	}
+}
+
+/*
+ * Re-apply the reference map.  The map lives in the parser's internal memory,
+ * which does not survive a microcode restart, so it has to be written again
+ * whenever the core is restarted for a new buffer.
+ */
+/*
+ * The stateless API states the reference map outright: reference_frame_ts[]
+ * names, for each of the eight slots, the OUTPUT buffer whose frame it holds.
+ * Take the map from there rather than from the refresh_frame_flags the
+ * microcode parsed.  A client that rebuilds the frame header - VA-API, which
+ * never carries refresh_frame_flags - cannot know that field when it has to
+ * submit the frame, and the map is exact either way.  Slots whose frame is
+ * not one of ours keep what they had.
+ */
+static void av1_apply_reference_ts(const struct v4l2_ctrl_av1_frame *f)
+{
+	int s, k;
+
+	for (s = 0; s < 8; s++) {
+		u64 ts = f->reference_frame_ts[s];
+
+		if (!ts)
+			continue;
+		for (k = 0; k < AV1_SLOTS; k++) {
+			if (av1_slots[k].ts == ts) {
+				av1_ref_slot[s] = k;
+				break;
+			}
+		}
+	}
+}
+
+void aml_av1_restore_refs(struct aml_vdec_hw *hw)
+{
+	if (av1_last_width && av1_last_height)
+		av1_config_next_ref_info(hw, av1_last_width, av1_last_height);
+}
+
+void aml_av1_frame_done(struct aml_vdec_hw *hw)
+{
+	int i;
+
+	for (i = 0; i < 8; i++)
+		if (av1_pending_refresh & (1 << i))
+			av1_ref_slot[i] = av1_cur_slot;
+
+	/*
+	 * A picture with show_frame clear is decoded now and displayed later,
+	 * when a show_existing_frame header names its slot.
+	 */
+	if (av1_cur_show || av1_stateless)
+		av1_out_slot = av1_cur_slot;
+
+	/* The map the next frame will resolve its references against. */
+	av1_config_next_ref_info(hw, av1_last_width, av1_last_height);
+}
+
+int aml_av1_config_picture(struct aml_vdec_hw *hw)
+{
+	struct av1_lfthr thr[AV1_MAX_LOOP_FILTER + 1];
+	const u16 *raw;
+	u16 pr[AV1_RPM_COUNT];
+	u32 width, height, lcu_size, lcu_total, uv_size, uv_h;
+	u32 data32, base[3], frame_type;
+	dma_addr_t dw_y, dw_c;
+	s8 ref_deltas[8], mode_deltas[2];
+	u32 oh_bits;
+	bool inter, mv_read = false;
+	int i, j, plane, ret;
+
+	if (!av1_ws.side_vaddr)
+		return -EINVAL;
+
+	raw = av1_ws.side_vaddr + (av1_ws.rpm - av1_ws.side_paddr);
+	for (i = 0; i < AV1_RPM_COUNT; i += 4)
+		for (j = 0; j < 4; j++)
+			pr[i + j] = raw[i + 3 - j];
+
+	width = pr[AV1_RPM_MAX_FRAME_WIDTH];
+	height = pr[AV1_RPM_MAX_FRAME_HEIGHT];
+	/*
+	 * Everything downstream sizes DMA from these: the MMU page table, the
+	 * compressed body, the picture slots.  If the microcode never saw a
+	 * sequence header they are whatever was left in the RPM, and building
+	 * a page table from that points the hardware at memory it does not
+	 * own - which resets the board outright, with no oops to read
+	 * afterwards.  Refuse instead.
+	 */
+	if (width < AV1_MIN_DIM || height < AV1_MIN_DIM ||
+	    width > AV1_MAX_DIM || height > AV1_MAX_DIM) {
+		dev_err(hw->dev, "av1: implausible picture size %ux%u, refusing\n",
+			width, height);
+		return -EINVAL;
+	}
+
+	/*
+	 * Unlike VP9, AV1 codes the bit depth in every sequence header, at
+	 * every profile.  12 bit is professional profile only.
+	 */
+	if (pr[AV1_RPM_BIT_DEPTH] != 8 && pr[AV1_RPM_BIT_DEPTH] != 10) {
+		dev_err_once(hw->dev, "av1: bit depth %u not supported (8 or 10 only)\n",
+			     pr[AV1_RPM_BIT_DEPTH]);
+		return -EINVAL;
+	}
+	av1_ten_bit = pr[AV1_RPM_BIT_DEPTH] == 10;
+
+	/* seq_flags bit 6 picks the superblock size. */
+	lcu_size = (pr[AV1_RPM_SEQ_FLAGS] >> 6) & 1 ? 128 : 64;
+	lcu_total = DIV_ROUND_UP(width, lcu_size) *
+		    DIV_ROUND_UP(height, lcu_size);
+	uv_size = lcu_total * lcu_size * lcu_size / 2;
+	uv_h = (uv_size + 0xffff) >> 16;
+
+	/*
+	 * show_existing_frame carries no picture data at all: it asks for a
+	 * reference slot to be output again.  Decoding it as if it were a real
+	 * frame writes over a live slot and corrupts the stream from there on.
+	 * The one piece of state it does carry is that re-showing a key frame
+	 * resets the whole reference map.
+	 */
+	if (pr[AV1_RPM_SHOW_EXISTING_FRAME] & 1) {
+		u32 idx = pr[AV1_RPM_EXISTING_FRAME_IDX] & 7;
+
+		av1_shown_slot = av1_ref_slot[idx];
+		av1_out_slot = av1_shown_slot;
+		if (av1_slots[av1_shown_slot].intra)
+			for (i = 0; i < 8; i++)
+				av1_ref_slot[i] = av1_shown_slot;
+		av1_pending_refresh = 0;
+		dev_dbg(hw->dev, "av1: show existing frame, slot %d\n",
+			av1_shown_slot);
+		return -EAGAIN;
+	}
+
+	frame_type = pr[AV1_RPM_FRAME_TYPE];
+	inter = (frame_type == 1);
+	av1_pending_refresh = pr[AV1_RPM_REFRESH_FRAME_FLAGS];
+	oh_bits = (pr[AV1_RPM_ENABLE_ORDER_HINT] & 0x1) ?
+		  (pr[AV1_RPM_ORDER_HINT_BITS_M1] & 0x7) + 1 : 0;
+	av1_last_width = width;
+	av1_last_height = height;
+
+	ret = av1_alloc_slots(hw, width, height);
+	if (ret)
+		return ret;
+
+	/*
+	 * config_pic_size(): av1_config_workspace() set the body length from
+	 * the pre-parse guess, at 8 bit.  The real size and depth are only
+	 * known now.
+	 */
+	data32 = comp_body_size(width, height);
+	wr(hw, HEVCD_MPP_DECOMP_CTL2, data32 >> 5);
+	wr(hw, HEVC_CM_BODY_LENGTH, data32);
+	wr(hw, HEVC_CM_HEADER_OFFSET, data32);
+	data32 = read_dos_reg(hw, HEVC_SAO_CTRL5);
+	if (av1_ten_bit)
+		data32 &= ~BIT(9);
+	else
+		data32 |= BIT(9);	/* 8 bit, memory saving */
+	wr(hw, HEVC_SAO_CTRL5, data32);
+
+	/*
+	 * The picture address table has to be written after the slots exist.
+	 * It used to be filled once during bring-up, when every hdr_p was
+	 * still zero, so every entry named physical address 0.  A key frame
+	 * never noticed because it reads no reference; an inter frame read its
+	 * references from address 0 and got a residual with no prediction
+	 * added to it.
+	 */
+	av1_config_pic_list(hw);
+
+	/*
+	 * A key frame resets the reference state and starts from slot 0.
+	 * Otherwise take a slot that no reference currently names, so decoding
+	 * into it cannot destroy a picture this frame still needs.
+	 */
+	if (frame_type == 0 && (pr[AV1_RPM_SHOW_FRAME] & 1)) {
+		/*
+		 * Only a *shown* key frame empties the reference map.  A hidden
+		 * key frame, or an intra-only one, keeps every reference alive
+		 * and must not decode over one of them.
+		 */
+		av1_cur_slot = 0;
+		for (i = 0; i < 8; i++)
+			av1_ref_slot[i] = 0;
+	} else {
+		for (i = 0; i < AV1_SLOTS; i++) {
+			int cand = (av1_cur_slot + 1 + i) % AV1_SLOTS;
+			int j;
+
+			for (j = 0; j < 8; j++)
+				if (av1_ref_slot[j] == cand)
+					break;
+			if (j == 8) {
+				av1_cur_slot = cand;
+				break;
+			}
+		}
+		if (i == AV1_SLOTS) {
+			/*
+			 * Never reached with AV1_SLOTS > 8, but decoding into
+			 * a picture something still references corrupts both,
+			 * so say so rather than do it quietly.
+			 */
+			dev_err(hw->dev, "av1: no free picture slot\n");
+			return -ENOBUFS;
+		}
+	}
+	av1_slots[av1_cur_slot].order_hint = pr[AV1_RPM_ORDER_HINT];
+	av1_slots[av1_cur_slot].ts = av1_cur_ts;
+
+	/*
+	 * The entropy context this picture starts from is the one saved by the
+	 * frame named by primary_ref_frame - av1_get_primary_ref_frame_buf(),
+	 * cm->prev_frame in the reference - not simply the previously decoded
+	 * picture.  Those are the same frame for as long as the encoder keeps
+	 * choosing LAST_FRAME, which is why using the last decoded picture
+	 * stays bit-exact for a while and then quietly drifts.  A key frame,
+	 * and any frame with PRIMARY_REF_NONE, starts from the defaults.
+	 */
+	if (inter && (pr[AV1_RPM_PRIMARY_REF_FRAME] & 7) != AV1_PRIMARY_REF_NONE) {
+		u32 prim = pr[AV1_RPM_PRIMARY_REF_FRAME] & 7;
+
+		av1_prev_slot =
+			av1_ref_slot[pr[AV1_RPM_REMAPPED_REF_IDX0 + prim] & 7];
+		av1_prev_valid = true;
+	} else {
+		av1_prev_slot = 0;
+		av1_prev_valid = false;
+	}
+
+	/* Resolve each reference frame to a picture slot once, for everything. */
+	av1_frame_slot[0] = av1_cur_slot;
+	for (i = 1; i < 8; i++)
+		av1_frame_slot[i] = inter ?
+			av1_ref_slot[pr[AV1_RPM_REMAPPED_REF_IDX0 + i - 1] & 7] :
+			av1_cur_slot;
+
+	/* What a later frame needs in order to project motion from this one. */
+	av1_slots[av1_cur_slot].intra = !inter;
+	if (!inter)
+		av1_reset_gmc(av1_cur_slot);
+	av1_slots[av1_cur_slot].frame_type = frame_type;
+	av1_cur_show = pr[AV1_RPM_SHOW_FRAME] & 1;
+	av1_slots[av1_cur_slot].width = width;
+	av1_slots[av1_cur_slot].height = height;
+	for (i = AV1_LAST_FRAME; i <= AV1_ALTREF_FRAME; i++)
+		av1_slots[av1_cur_slot].ref_order_hint[i] =
+			av1_slots[av1_frame_slot[i]].order_hint;
+
+	/* Now that the real picture size is known, size the MMU and body. */
+	ret = av1_fill_mmu_map(hw, width, height);
+	if (ret)
+		return ret;
+	wr(hw, HEVC_SAO_MMU_DMA_CTRL, av1_ws.mmu_map);
+
+	/* ---- output / SAO ---- */
+	data32 = read_dos_reg(hw, HEVC_SAO_CTRL9) | BIT(1);
+	wr(hw, HEVC_SAO_CTRL9, data32);
+
+	data32 = read_dos_reg(hw, HEVC_SAO_CTRL5);
+	data32 |= BIT(14);		/* av1 mode */
+	/* Double-write scaler (bits 23:16): 0 is 1:1. */
+	data32 &= ~(0xff << 16);
+	wr(hw, HEVC_SAO_CTRL5, data32);
+
+	wr(hw, HEVC_SAO_CTRL0, lcu_size == 128 ? 0x7 : 0x6);
+
+	wr(hw, HEVC_CM_BODY_START_ADDR, av1_slots[av1_cur_slot].body_p);
+	wr(hw, HEVC_CM_HEADER_START_ADDR, av1_slots[av1_cur_slot].hdr_p);
+	wr(hw, HEVC_FGS_TABLE_START, av1_ws.fgs_table);
+	wr(hw, HEVC_FGS_TABLE_LENGTH, (512 * 128 / 8) * 8);
+
+	wr(hw, HEVC_SAO_Y_LENGTH, (uv_h << 16) << 1);
+	wr(hw, HEVC_SAO_C_LENGTH, uv_h << 16);
+
+	/*
+	 * Double-write target: plain NV12, luma plane followed by interleaved
+	 * chroma, linear.  Allocated on first use now that the real picture
+	 * size is known.
+	 */
+	/*
+	 * The double-write output goes to this picture's own slot.  It used to
+	 * be one buffer shared by every picture, which only worked because the
+	 * caller copied each frame out as soon as it was decoded - a
+	 * show_existing_frame header asking for an older picture had nothing
+	 * left to hand back.
+	 */
+	av1_ws.dw_width = width;
+	av1_ws.dw_height = height;
+	if (av1_dw_target_y) {
+		/*
+		 * Writing straight into a CAPTURE buffer means the picture has
+		 * to fit it exactly: the double-write unit lays down a tightly
+		 * packed width x height NV12 frame and knows nothing about the
+		 * buffer behind the address.  A stream larger than the
+		 * negotiated format would run off the end.
+		 */
+		if ((size_t)width * height * 3 / 2 > av1_dw_target_size) {
+			dev_err(hw->dev,
+				"av1: %ux%u does not fit the %zu byte capture buffer\n",
+				width, height, av1_dw_target_size);
+			return -EINVAL;
+		}
+		dw_y = av1_dw_target_y;
+		dw_c = av1_dw_target_c;
+	} else {
+		dw_y = av1_slots[av1_cur_slot].dw_p;
+		dw_c = dw_y + width * height;
+	}
+	wr(hw, HEVC_SAO_Y_START_ADDR, dw_y);
+	wr(hw, HEVC_SAO_Y_WPTR, dw_y);
+	wr(hw, HEVC_SAO_C_START_ADDR, dw_c);
+	wr(hw, HEVC_SAO_C_WPTR, dw_c);
+
+	data32 = read_dos_reg(hw, HEVC_SAO_CTRL1);
+	data32 &= ~(3 << 12);				/* linear memory map */
+	data32 &= ~0xff0;
+	data32 |= (HEVC_CONFIG_LITTLE_ENDIAN >> 8) & 0xfff;
+	/*
+	 * Both writes on: the compressed picture still goes through the MMU,
+	 * and the hardware additionally lays down a plain NV12 copy at the
+	 * double-write addresses below.  bit0 disables compression, bit1
+	 * disables double write; neither is wanted here.
+	 */
+	data32 &= ~0x3;
+	data32 |= BIT(8);				/* NV12 */
+	data32 &= ~(3 << 14);
+	data32 |= (2 << 14);
+	wr(hw, HEVC_SAO_CTRL1, data32);
+
+	data32 = read_dos_reg(hw, HEVCD_IPP_AXIIF_CONFIG);
+	data32 &= ~0x30;				/* linear */
+	data32 &= ~0xf;
+	data32 |= HEVC_CONFIG_LITTLE_ENDIAN & 0xf;
+	data32 &= ~BIT(12);				/* NV12 */
+	data32 &= ~(3 << 8);
+	data32 |= (2 << 8);
+	wr(hw, HEVCD_IPP_AXIIF_CONFIG, data32);
+
+	/* bit 8 first (compressed) write, bit 9 double (uncompressed) write */
+	data32 = read_dos_reg(hw, HEVC_DBLK_CFGB);
+	data32 &= ~0x300;
+	data32 |= BIT(8) | BIT(9);
+	wr(hw, HEVC_DBLK_CFGB, data32);
+
+	/* ---- loop filter ---- */
+	av1_build_lfthr(thr, pr[AV1_RPM_LF_SHARPNESS]);
+	for (i = 0; i < 32; i++) {
+		u32 v = ((thr[i * 2 + 1].lim & 0x3f) << 8) |
+			 (thr[i * 2 + 1].mblim & 0xff);
+
+		v = (v << 16) | ((thr[i * 2].lim & 0x3f) << 8) |
+		    (thr[i * 2].mblim & 0xff);
+		wr(hw, HEVC_DBLK_CFG9, v);
+	}
+
+	/* 1st/2nd write both enable, AV1 video format */
+	wr(hw, HEVC_DBLK_CFGB, (0x57 << 8) | (0x4 << 0));
+
+	base[0] = pr[AV1_RPM_LF_LEVEL_0];
+	base[1] = pr[AV1_RPM_LF_LEVEL_U];
+	base[2] = pr[AV1_RPM_LF_LEVEL_V];
+
+	for (i = 0; i < 8; i++)
+		ref_deltas[i] = (s8)(i == 0 ? 1 : (i == 4 || i >= 6 ? -1 : 0));
+	mode_deltas[0] = 0;
+	mode_deltas[1] = 0;
+
+	/*
+	 * lfi->lvl[plane][seg][dir][ref][mode], flattened the way the hardware
+	 * wants it: 192 words, each packing the two directions and two modes for
+	 * one (plane, segment, ref).  Every block in a key frame is intra, so
+	 * only the INTRA_FRAME reference delta contributes.
+	 */
+	for (i = 0; i < 192; i++) {
+		u32 lvl;
+		int seg;
+		int ref = i & 7;
+		int scale, v;
+
+		plane = (i >> 6) & 3;
+		seg = (i >> 3) & 7;
+		(void)seg;
+
+		scale = 1 << (base[plane] >> 5);
+		v = av1_clamp_lvl(base[plane] + ref_deltas[ref] * scale);
+		lvl = ((v & 0x3f) << 24) | ((v & 0x3f) << 16) |
+		      ((v & 0x3f) << 8) | (v & 0x3f);
+		if (!pr[AV1_RPM_LF_LEVEL_0] && !pr[AV1_RPM_LF_LEVEL_1])
+			lvl = 0;
+		wr(hw, HEVC_DBLK_CFGA, lvl);
+	}
+
+	wr(hw, HEVC_DBLK_DBLK0,
+	   pr[AV1_RPM_LF_LEVEL_0] | (pr[AV1_RPM_LF_LEVEL_1] << 6) |
+	   (pr[AV1_RPM_LF_LEVEL_U] << 12) | (pr[AV1_RPM_LF_LEVEL_V] << 18));
+
+	for (i = 0; i < 10; i++)
+		wr(hw, HEVC_DBLK_DBLK1,
+		   i < 2 ? (u32)(u8)mode_deltas[i & 1]
+			 : (u32)(u8)ref_deltas[(i - 2) & 7]);
+
+	/* No segmentation here, so the per-segment filter info is all zero. */
+	for (i = 0; i < 8; i++)
+		wr(hw, HEVC_DBLK_DBLK2, 0);
+
+	data32 = read_dos_reg(hw, HEVC_DBLK_CFGB);
+	if (pr[AV1_RPM_LF_MODE_REF_DELTA] & 1)
+		data32 |= BIT(28);
+	else
+		data32 &= ~BIT(28);
+	data32 &= ~BIT(29);		/* segmentation off */
+	if (width >= 1280)
+		data32 |= BIT(4);
+	else
+		data32 &= ~(3 << 4);
+	wr(hw, HEVC_DBLK_CFGB, data32);
+
+	/*
+	 * Horizontal upscaler, av1_upscale_frame_init().  AV1's superres codes a
+	 * frame at reduced width and upscales on the way out; with superres off
+	 * the step is exactly one pixel and the origin is RS_SCALE_EXTRA_OFF.
+	 *
+	 * Leaving these registers alone is not harmless.  The upscaler runs
+	 * regardless, so an unprogrammed step smears the picture horizontally
+	 * and an unprogrammed origin displaces it - which is exactly the
+	 * horizontal-only detail loss and the eight pixel offset measured
+	 * against a software decode.  Both symptoms, one cause.
+	 */
+	{
+		u32 in_l = width, out_l = width;
+		u32 in_c = (width + 1) >> 1, out_c = (width + 1) >> 1;
+		u32 step_l = ((in_l << RS_SCALE_SUBPEL_BITS) + out_l / 2) / out_l;
+		u32 step_c = ((in_c << RS_SCALE_SUBPEL_BITS) + out_c / 2) / out_c;
+		s32 err_l = out_l * step_l - (in_l << RS_SCALE_SUBPEL_BITS);
+		s32 err_c = out_c * step_c - (in_c << RS_SCALE_SUBPEL_BITS);
+		u32 x0_l = (u32)((-(s32)((out_l - in_l) << (RS_SCALE_SUBPEL_BITS - 1)) +
+				  (s32)(out_l / 2)) / (s32)out_l +
+				 RS_SCALE_EXTRA_OFF - err_l / 2) &
+			   RS_SCALE_SUBPEL_MASK;
+		u32 x0_c = (u32)((-(s32)((out_c - in_c) << (RS_SCALE_SUBPEL_BITS - 1)) +
+				  (s32)(out_c / 2)) / (s32)out_c +
+				 RS_SCALE_EXTRA_OFF - err_c / 2) &
+			   RS_SCALE_SUBPEL_MASK;
+
+		wr(hw, HEVC_DBLK_UPS1, av1_ws.ups_data);
+		wr(hw, HEVC_DBLK_UPS2, x0_l);
+		wr(hw, HEVC_DBLK_UPS3, x0_c);
+		wr(hw, HEVC_DBLK_UPS4, step_l);
+		wr(hw, HEVC_DBLK_UPS5, step_c);
+		wr(hw, AV1_UPSCALE_X0_QN, (x0_c << 16) | x0_l);
+		wr(hw, AV1_UPSCALE_STEP_QN, (step_c << 16) | step_l);
+		dev_dbg(hw->dev,
+			"av1: upscale step %u/%u x0 %u/%u\n",
+			 step_l, step_c, x0_l, x0_c);
+	}
+
+	wr(hw, HEVC_DBLK_CDEF0, av1_ws.cdef_data);
+	data32 = read_dos_reg(hw, HEVC_DBLK_CDEF1) & 0xffffff00;
+	data32 |= 17;
+	wr(hw, HEVC_DBLK_CDEF1, data32);
+
+	/*
+	 * Motion prediction.  A key frame has no references and no previous
+	 * frame MVs, so this reduces to pointing the block at its above-row and
+	 * collocated-MV buffers and clearing use_prev_frame_mvs.  CTRL3 carries
+	 * the AV1 constants Amlogic uses from T5D onwards, which is what S4 is.
+	 */
+	wr(hw, HEVC_MPRED_CTRL3, 0x10151015);
+
+	/*
+	 * CTRL4 bits 19:12 are ref_frame_sign_bias, one per reference frame,
+	 * set when that reference lies *after* the current picture in output
+	 * order.  Nothing had ever written them, so they held whatever the
+	 * previously loaded codec left there.  A key frame and a forward-only
+	 * stream both want zeroes, which is why leaving them alone looked
+	 * harmless - but the temporal MV projection uses sign bias to decide
+	 * which way to scale a borrowed vector, so stale bits there are worse
+	 * than not projecting at all.
+	 */
+	data32 = read_dos_reg(hw, HEVC_MPRED_CTRL4);
+	data32 &= ~BIT(6);		/* use_prev_frame_mvs = 0 */
+	data32 &= ~(0xff << 12);
+	if (inter) {
+		for (i = AV1_LAST_FRAME; i <= AV1_ALTREF_FRAME; i++)
+			if (av1_rel_dist(av1_slots[av1_frame_slot[i]].order_hint,
+					 pr[AV1_RPM_ORDER_HINT], oh_bits) > 0)
+				data32 |= BIT(12 + i);
+	}
+	wr(hw, HEVC_MPRED_CTRL4, data32);
+	dev_dbg(hw->dev, "av1: mpred ctrl4 %08x\n", data32);
+
+	wr(hw, HEVC_MPRED_ABV_START_ADDR, av1_ws.mpred_above);
+	wr(hw, HEVC_MPRED_MV_WR_START_ADDR, av1_slots[av1_cur_slot].mv_p);
+	wr(hw, HEVC_MPRED_MV_WPTR, av1_slots[av1_cur_slot].mv_p);
+	/* One tile spanning the picture, so no row jump. */
+	wr(hw, HEVC_MPRED_MV_WR_ROW_JUMP, 0);
+	wr(hw, HEVC_MPRED_MV_RD_ROW_JUMP, 0);
+
+	/*
+	 * REF00 carries the current picture's order hint and the sequence-level
+	 * order-hint info.  config_mpred_hw() writes it for every frame, not
+	 * just inter ones.  The 0x10 pair in the low half is the compressed-MV
+	 * constant Amlogic uses from T5D onwards, matching CTRL3 above.
+	 */
+	data32 = (pr[AV1_RPM_ENABLE_ORDER_HINT] & 0x1) << 27;
+	data32 |= (pr[AV1_RPM_ORDER_HINT_BITS_M1] & 0x7) << 24;
+	data32 |= (pr[AV1_RPM_ORDER_HINT] & 0xff) << 16;
+	data32 |= (0x10 << 8) | 0x10;
+	wr(hw, HEVC_MPRED_L0_REF00_POC, data32);
+
+	/*
+	 * MV write is always on; MV read stays off until the temporal-MV
+	 * buffers (MV_RD_START_ADDR and the REF06..11 offsets) are programmed.
+	 * A stream that sets allow_ref_frame_mvs will decode without temporal
+	 * MV prediction rather than reading an unprogrammed address.
+	 */
+	/*
+	 * Temporal motion vectors.  When the frame allows reference-frame MVs
+	 * the decoder projects motion from up to three earlier frames; the host
+	 * names those frames, hands over their collocated-MV buffers and gives
+	 * the order-hint distance from each to each of its own references.
+	 */
+	{
+		static const u32 off_reg[6] = {
+			HEVC_MPRED_L0_REF06_POC, HEVC_MPRED_L0_REF07_POC,
+			HEVC_MPRED_L0_REF08_POC, HEVC_MPRED_L0_REF09_POC,
+			HEVC_MPRED_L0_REF10_POC, HEVC_MPRED_L0_REF11_POC,
+		};
+		u32 off_val[6] = {};
+		struct av1_mfmv mf;
+		int used[AV1_MFMV_STACK_SIZE] = {};
+		int nused = 0, pos = 24, reg = 0;
+
+		if (inter && (pr[AV1_RPM_ALLOW_REF_FRAME_MVS] & 0x1) &&
+		    oh_bits)
+			av1_setup_motion_field(&mf, pr[AV1_RPM_ORDER_HINT],
+					       oh_bits);
+		else
+			memset(&mf, 0, sizeof(mf));
+
+		data32 = 0;
+		for (i = 0; i < mf.count; i++) {
+			if (!mf.valid[i])
+				continue;
+			if (nused < AV1_MFMV_STACK_SIZE)
+				used[nused] = mf.id[i];
+			nused++;
+			for (j = AV1_LAST_FRAME; j <= AV1_ALTREF_FRAME; j++) {
+				data32 |= (mf.offset[i][j] & 0xff) << pos;
+				if (pos == 0) {
+					if (reg < 6)
+						off_val[reg++] = data32;
+					data32 = 0;
+					pos = 24;
+				} else {
+					pos -= 8;
+				}
+			}
+		}
+		if (pos != 24 && reg < 6)
+			off_val[reg] = data32;
+
+		off_val[5] |= nused | (used[0] << 2) | (used[1] << 5) |
+			      (used[2] << 8);
+		for (i = 0; i < 6; i++)
+			wr(hw, off_reg[i], off_val[i]);
+
+		/*
+		 * Collocated MV buffers, one per projected frame.  Start from
+		 * a real buffer rather than whatever address was left over, so
+		 * that a frame which allows reference MVs but has nothing
+		 * valid to project from still names mapped memory.
+		 */
+		wr(hw, HEVC_MPRED_MV_RD_START_ADDR,
+		   av1_slots[av1_frame_slot[AV1_LAST_FRAME]].mv_p);
+		wr(hw, HEVC_MPRED_MV_RPTR,
+		   av1_slots[av1_frame_slot[AV1_LAST_FRAME]].mv_p);
+		for (i = 0, j = 0; i < mf.count && j < AV1_MFMV_STACK_SIZE; i++) {
+			dma_addr_t mv;
+
+			if (!mf.valid[i])
+				continue;
+			mv = av1_slots[av1_frame_slot[mf.id[i]]].mv_p;
+			if (j == 0) {
+				wr(hw, HEVC_MPRED_MV_RD_START_ADDR, mv);
+				wr(hw, HEVC_MPRED_MV_RPTR, mv);
+			} else if (j == 1) {
+				wr(hw, HEVC_MPRED_L0_REF01_POC, mv);
+				wr(hw, HEVC_MPRED_MV_RPTR_1, mv);
+			} else {
+				wr(hw, HEVC_MPRED_L0_REF02_POC, mv);
+				wr(hw, HEVC_MPRED_MV_RPTR_2, mv);
+			}
+			j++;
+		}
+		/*
+		 * The reference enables the MV read path on allow_ref_frame_mvs
+		 * alone, not on whether any projection turned out usable.  A
+		 * frame whose only reference is a key frame projects nothing
+		 * and still needs the path on, because the temporal candidate
+		 * list is part of how its motion vectors are coded.
+		 */
+		mv_read = inter && (pr[AV1_RPM_ALLOW_REF_FRAME_MVS] & 0x1);
+	}
+
+	data32 = read_dos_reg(hw, HEVC_MPRED_CTRL0);
+	data32 &= ~(MPRED_CTRL0_MV_WR_EN | MPRED_CTRL0_MV_RD_EN);
+	data32 |= MPRED_CTRL0_MV_WR_EN;
+	if (mv_read)
+		data32 |= MPRED_CTRL0_MV_RD_EN;
+	wr(hw, HEVC_MPRED_CTRL0, data32);
+
+	/*
+	 * Only touched when inter support is enabled.  The verified key-frame
+	 * configuration never wrote the canvas or reference-info tables at all,
+	 * and writing them turned out to be enough on its own to wedge the
+	 * board - so the default path is left exactly as it was measured.
+	 */
+	av1_config_mc_buffer(hw, inter, pr, width, height);
+
+	/*
+	 * ---- CDF, segment map, segment features ----
+	 *
+	 * Both are per picture, not per stream: the workspace carves 0x8000 of
+	 * CDF and a sixteenth of the segment map for each of the 16 picture
+	 * slots.  A frame writes its own slot and reads the slot the previous
+	 * frame wrote, which is how the entropy context is carried forward.
+	 * Pointing read and write at the same base works for a key frame,
+	 * whose context is the default table, and is wrong for anything else.
+	 */
+	wr(hw, AOM_AV1_CDF_BUFFER_W, av1_ws.cdf + AV1_CDF_SLOT_SIZE * av1_cur_slot);
+	wr(hw, AOM_AV1_CDF_BUFFER_R, av1_ws.cdf + AV1_CDF_SLOT_SIZE * av1_prev_slot);
+	wr(hw, AV1_SEG_W_ADDR,
+	   av1_ws.seg_map + (AV1_SEG_MAP_SIZE / 16) * av1_cur_slot);
+	wr(hw, AV1_SEG_R_ADDR,
+	   av1_ws.seg_map + (AV1_SEG_MAP_SIZE / 16) * av1_prev_slot);
+	for (i = 0; i < 8; i++)
+		wr(hw, AOM_AV1_SEGMENT_FEATURE, 0x80000000 | (i << 22));
+
+	/*
+	 * Previous-picture state the microcode needs for skip mode.  Skip mode
+	 * itself is not signalled in the RPM, so it stays off; what matters
+	 * here is that the previous and current picture indices are named.
+	 * 0x1f is the reference's "no such picture" value.
+	 */
+	{
+		u32 sm_allowed, sm_idx0, sm_idx1;
+
+		av1_skip_mode(pr, pr[AV1_RPM_ORDER_HINT], oh_bits, inter,
+			      &sm_allowed, &sm_idx0, &sm_idx1);
+
+		/*
+		 * Bit 31 is the previous picture's segmentation state, which
+		 * is only meaningful once segmentation is supported; it stays
+		 * zero.  Bit 30 is prev_frame->intra_only, which is the
+		 * INTRA_ONLY frame type (2), not a key frame.
+		 */
+		/*
+		 * 0x1f says there is no previous frame, as the reference does
+		 * for PRIMARY_REF_NONE.  Naming slot 0 there instead made the
+		 * microcode load that slot's global-motion models as
+		 * PrevGmParams, and the frame decoded against them.
+		 */
+		data32 = (av1_prev_valid &&
+			  av1_slots[av1_prev_slot].frame_type == 2) << 30;
+		data32 |= (av1_prev_valid ? av1_prev_slot : 0x1f) << 24;
+		data32 |= (av1_cur_slot & 0x1f) << 16;
+		data32 |= sm_allowed << 8;
+		data32 |= (sm_idx1 & 0xf) << 4;
+		data32 |= sm_idx0 & 0xf;
+		wr(hw, AV1_SKIP_MODE_INFO, data32);
+	}
+
+	/* Cleared for each new compressed frame. */
+	wr(hw, PIC_END_LCU_COUNT, 0);
+
+	/*
+	 * Immediately before AOM_AV1_DECODE_SLICE the reference stores the
+	 * segment feature word for this picture through the parser's memory
+	 * port, at 0x11b0 plus the picture index on SC2 and later.
+	 * Segmentation is off here, so the value is zero.
+	 */
+	wr(hw, HEVC_PARSER_MEM_WR_ADDR, 0x11b0 + av1_cur_slot);
+	wr(hw, HEVC_PARSER_MEM_RW_DATA, 0);
+
+	dev_dbg(hw->dev,
+		"av1: pic %ux%u %s show %u slot %d refresh %02x refs %d%d%d%d%d%d%d lf %u/%u/%u/%u sharp %u\n",
+		 width, height, inter ? "inter" : "key", av1_cur_show, av1_cur_slot,
+		 av1_pending_refresh,
+		 av1_ref_slot[0], av1_ref_slot[1], av1_ref_slot[2],
+		 av1_ref_slot[3], av1_ref_slot[4], av1_ref_slot[5],
+		 av1_ref_slot[6],
+		 pr[AV1_RPM_LF_LEVEL_0], pr[AV1_RPM_LF_LEVEL_1],
+		 pr[AV1_RPM_LF_LEVEL_U], pr[AV1_RPM_LF_LEVEL_V],
+		 pr[AV1_RPM_LF_SHARPNESS]);
+	return 0;
+}
+
+void aml_av1_dump_output(struct aml_vdec_hw *hw)
+{
+	const u8 *body = av1_ws.body_vaddr;
+	const u8 *hdr;
+	size_t i, nz = 0, hdr_nz = 0;
+
+	if (!body || !av1_ws.side_vaddr)
+		return;
+
+	for (i = 0; i < av1_ws.body_size; i++)
+		if (body[i])
+			nz++;
+
+	/*
+	 * The compressed header goes to the current slot, not to the workspace
+	 * cm_header[] carve-out - that one is left over from before the slot
+	 * pool and nothing programs it into the hardware any more.
+	 */
+	hdr = av1_slots[av1_cur_slot].hdr_v;
+	if (!hdr)
+		return;
+	for (i = 0; i < av1_slot_hdr_size; i++)
+		if (hdr[i])
+			hdr_nz++;
+
+	dev_info(hw->dev,
+		 "av1: body %zu/%zu bytes non-zero, header %zu/%zu non-zero\n",
+		 nz, av1_ws.body_size, hdr_nz, av1_slot_hdr_size);
+	dev_info(hw->dev, "av1: body[0..15] %*ph\n", 16, body);
+	dev_info(hw->dev, "av1: hdr [0..15] %*ph\n", 16, hdr);
+}
+
+int aml_av1_save_nv12(struct aml_vdec_hw *hw, const char *path, bool append)
+{
+	const u8 *y;
+	size_t i, nz = 0;
+	struct file *f;
+	loff_t pos = 0;
+	size_t len;
+
+	/* Only write a frame the stream actually asked to be displayed. */
+	if (av1_out_slot < 0)
+		return -EAGAIN;
+	y = av1_slots[av1_out_slot].dw_v;
+	dma_sync_single_for_cpu(hw->dev, av1_slots[av1_out_slot].dw_p,
+				av1_slot_dw_size, DMA_FROM_DEVICE);
+	av1_out_slot = -1;
+	if (!y)
+		return -ENODEV;
+
+	len = av1_ws.dw_width * av1_ws.dw_height * 3 / 2;
+	for (i = 0; i < len; i++)
+		if (y[i])
+			nz++;
+	dev_info(hw->dev, "av1: nv12 %zu/%zu non-zero, Y[0..15] %*ph\n",
+		 nz, len, 16, y);
+
+	f = filp_open(path, O_WRONLY | O_CREAT |
+		      (append ? O_APPEND : O_TRUNC), 0644);
+	if (IS_ERR(f))
+		return PTR_ERR(f);
+	if (append)
+		pos = i_size_read(file_inode(f));
+	kernel_write(f, y, len, &pos);
+	filp_close(f, NULL);
+	dev_info(hw->dev, "av1: wrote %zu bytes of NV12 to %s\n", len, path);
+	return 0;
+}
+
+void aml_av1_teardown(struct aml_vdec_hw *hw)
+{
+	wr(hw, HEVC_ASSIST_MBOX0_MASK, 0);
+	av1_free(hw);
+}
+
+/* ------------------------------------------------------------------------
+ * V4L2 stateless decoder
+ *
+ * The microcode parses the frame header itself and reports what it found
+ * through the RPM buffer, so this layer does not have to translate the
+ * V4L2 AV1 controls into hardware state - it hands the core one temporal
+ * unit per OUTPUT buffer and runs the same status ping-pong the probe
+ * harness established.  The controls are still accepted, because the
+ * stateless API requires them and userspace sets them.
+ * ------------------------------------------------------------------------
+ */
+
+struct aml_av1_ctx {
+	struct aml_vdec_ctx *v4l2_ctx;
+	struct aml_vdec_hw *hw;
+	bool ucode_loaded;
+	bool started;
+	bool have_picture;
+	u32 rounds;
+};
+
+/* Copy one decoded picture into the CAPTURE buffer. */
+static void av1_copy_out(struct aml_av1_ctx *av1, struct vb2_v4l2_buffer *dst)
+{
+	struct v4l2_pix_format_mplane *pf =
+		&av1->v4l2_ctx->pix_fmt[AML_FMT_DST];
+	struct vb2_buffer *vb = &dst->vb2_buf;
+	u32 w = av1_ws.dw_width, h = av1_ws.dw_height;
+	u32 dst_stride = pf->plane_fmt[0].bytesperline ? : w;
+	u32 dst_h = pf->height ? : h;
+	const u8 *src;
+	u8 *y, *c;
+	u32 row;
+	int slot = av1_out_slot;
+
+	if (slot < 0)
+		return;
+	src = av1_slots[slot].dw_v;
+	av1_out_slot = -1;
+
+	/*
+	 * Zero copy: the hardware already wrote this picture into the CAPTURE
+	 * buffer.  Nothing to move, only the payload to declare.
+	 */
+	if (av1_dw_target_y) {
+		if (vb->num_planes >= 2) {
+			vb2_set_plane_payload(vb, 0, dst_stride * dst_h);
+			vb2_set_plane_payload(vb, 1, dst_stride * dst_h / 2);
+		} else {
+			vb2_set_plane_payload(vb, 0, dst_stride * dst_h * 3 / 2);
+		}
+		av1->have_picture = true;
+		return;
+	}
+
+	dma_sync_single_for_cpu(av1->hw->dev, av1_slots[slot].dw_p,
+				av1_slot_dw_size, DMA_FROM_DEVICE);
+	if (!src)
+		return;
+
+	/*
+	 * The decoder writes a tightly packed w x h picture; the capture
+	 * buffer may be wider and taller, because the format is aligned up.
+	 * Copy row by row rather than assuming the two agree - they do not:
+	 * a 320x240 stream gets a 320x256 capture buffer, so chroma starts a
+	 * stride times 16 rows further in than the source has it.
+	 */
+	y = vb2_plane_vaddr(vb, 0);
+	c = vb->num_planes >= 2 ? vb2_plane_vaddr(vb, 1)
+				: y + dst_stride * dst_h;
+	if (!y || !c)
+		return;
+
+	for (row = 0; row < h; row++)
+		memcpy(y + row * dst_stride, src + row * w, w);
+	for (row = 0; row < h / 2; row++)
+		memcpy(c + row * dst_stride, src + w * h + row * w, w);
+	aml_vdec_dst_cpu_written(av1->hw->dev, vb);
+
+	if (vb->num_planes >= 2) {
+		vb2_set_plane_payload(vb, 0, dst_stride * dst_h);
+		vb2_set_plane_payload(vb, 1, dst_stride * dst_h / 2);
+	} else {
+		vb2_set_plane_payload(vb, 0, dst_stride * dst_h * 3 / 2);
+	}
+	av1->have_picture = true;
+}
+
+static void av1_finish(struct aml_av1_ctx *av1, enum vb2_buffer_state state)
+{
+	struct aml_vdec_ctx *ctx = av1->v4l2_ctx;
+	struct vb2_v4l2_buffer *dst;
+	unsigned int i;
+
+	/*
+	 * A buffer holding only a hidden frame decodes fine and produces
+	 * nothing to show.  Say so with a zero payload - otherwise the
+	 * capture buffer goes back to userspace still describing whatever it
+	 * held last time, which reads as a repeat of an older picture.
+	 */
+	if (!av1->have_picture) {
+		dst = v4l2_m2m_next_dst_buf(ctx->m2m_ctx);
+		if (dst)
+			for (i = 0; i < dst->vb2_buf.num_planes; i++)
+				vb2_set_plane_payload(&dst->vb2_buf, i, 0);
+	}
+
+	/*
+	 * Complete the request before the buffer is handed back.  Doing it
+	 * afterwards - which is what device_run() would do, since run() only
+	 * returns once this has already finished the job - binds the control
+	 * objects to a request that has already completed, and the media
+	 * controller warns about it.
+	 */
+	dst = v4l2_m2m_next_src_buf(ctx->m2m_ctx);
+	if (dst && dst->vb2_buf.req_obj.req)
+		v4l2_ctrl_request_complete(dst->vb2_buf.req_obj.req,
+					   &ctx->ctrl_handler);
+
+	ctx->int_cond = 1;
+	ctx->job_finished = true;
+	v4l2_m2m_buf_done_and_job_finish(ctx->dev->m2m_dev_dec, ctx->m2m_ctx,
+					 state);
+	wake_up(&ctx->queue);
+}
+
+/*
+ * The decode state machine, driven by the mailbox.  This is the loop the
+ * probe harness ran by polling, moved into the threaded interrupt: the
+ * microcode posts a status, the host does what that status asks for and
+ * hands back the next command.
+ */
+static irqreturn_t av1_irq_thread(int irq, void *priv)
+{
+	struct aml_av1_ctx *av1 = priv;
+	struct aml_vdec_ctx *ctx = av1->v4l2_ctx;
+	struct aml_vdec_hw *hw = av1->hw;
+	u32 st = hw->hevc_irq_status & 0xff;
+	u32 next = AOM_AV1_SEARCH_HEAD;
+
+	if (++av1->rounds > 256) {
+		dev_err(hw->dev, "av1: runaway decode, giving up\n");
+		av1_finish(av1, VB2_BUF_STATE_ERROR);
+		return IRQ_HANDLED;
+	}
+
+	switch (st) {
+	case AOM_AV1_SEQ_HEAD_PARSER_DONE:
+		/* Sequence header only: keep looking for a frame. */
+		break;
+	case AOM_AV1_FRAME_HEAD_PARSER_DONE:
+	case AOM_AV1_FRAME_PARSER_DONE: {
+		int cfg = aml_av1_config_picture(hw);
+
+		if (cfg == 0) {
+			next = AOM_AV1_DECODE_SLICE;
+		} else if (cfg == -EAGAIN) {
+			/* show_existing_frame: output an earlier picture. */
+			struct vb2_v4l2_buffer *dst =
+				v4l2_m2m_next_dst_buf(ctx->m2m_ctx);
+
+			if (dst)
+				av1_copy_out(av1, dst);
+		} else {
+			dev_err(hw->dev, "av1: picture config failed (%d)\n",
+				cfg);
+			/*
+			 * A failed slot allocation has freed the picture
+			 * slots. Halt the microcode and bring the core up from
+			 * scratch on the next buffer: a plain restart would
+			 * point the core at the freed slots again, and its
+			 * DMA then lands in memory the kernel has handed out.
+			 */
+			regmap_write(hw->map[DOS_BUS], HEVC_MPSR, 0);
+			regmap_write(hw->map[DOS_BUS], HEVC_CPSR, 0);
+			aml_av1_teardown(hw);
+			av1->started = false;
+			av1_finish(av1, VB2_BUF_STATE_ERROR);
+			return IRQ_HANDLED;
+		}
+		break;
+	}
+	case AOM_AV1_DEC_PIC_END: {
+		struct vb2_v4l2_buffer *dst =
+			v4l2_m2m_next_dst_buf(ctx->m2m_ctx);
+
+		aml_av1_frame_done(hw);
+		if (dst)
+			av1_copy_out(av1, dst);
+		break;
+	}
+	case AOM_NAL_DECODE_DONE:
+	case HEVC_ACTION_DONE:
+	case AOM_DECODE_BUFEMPTY:
+	case AOM_SEARCH_BUFEMPTY:
+	case AOM_EOS:
+		/*
+		 * End of this buffer.  With one frame per OUTPUT buffer the
+		 * usual ending is BUFEMPTY - the microcode has consumed the
+		 * frame and wants more data - rather than NAL_DECODE_DONE.
+		 * A buffer that decoded a hidden frame produces no picture of
+		 * its own and still completes.
+		 */
+		av1_finish(av1, VB2_BUF_STATE_DONE);
+		return IRQ_HANDLED;
+	default:
+		dev_dbg(hw->dev, "av1: unhandled status %02x\n", st);
+		break;
+	}
+
+	regmap_write(hw->map[DOS_BUS], HEVC_ASSIST_MBOX0_CLR_REG, 1);
+	regmap_write(hw->map[DOS_BUS], HEVC_DEC_STATUS_REG, next);
+	return IRQ_HANDLED;
+}
+
+int aml_av1_init(void *priv)
+{
+	struct aml_vdec_ctx *ctx = priv;
+	struct aml_vdec_hw *hw = vdec_get_hw(ctx->dev);
+	struct aml_av1_ctx *av1;
+
+	if (!hw)
+		return -ENODEV;
+
+	av1 = kzalloc_obj(*av1);
+	if (!av1)
+		return -ENOMEM;
+
+	av1->v4l2_ctx = ctx;
+	av1->hw = hw;
+
+	/* One client on the HEVC core at a time - see the note in vp9.c. */
+	if (cmpxchg(&hw->hevc_priv, NULL, av1)) {
+		dev_info(&ctx->dev->plat_dev->dev,
+			 "av1: HEVC core already in use, refusing second instance\n");
+		kfree(av1);
+		return -EBUSY;
+	}
+
+	ctx->codec_priv = av1;
+	hw->hevc_irq_thread_fn = av1_irq_thread;
+	return 0;
+}
+
+void aml_av1_exit(void *priv)
+{
+	struct aml_vdec_ctx *ctx = priv;
+	struct aml_av1_ctx *av1 = ctx->codec_priv;
+
+	if (!av1)
+		return;
+
+	av1->hw->hevc_irq_thread_fn = NULL;
+	av1->hw->hevc_priv = NULL;
+	if (av1->started) {
+		regmap_write(av1->hw->map[DOS_BUS], HEVC_MPSR, 0);
+		aml_av1_teardown(av1->hw);
+		/*
+		 * Deliberately not hvdec_disable(): powering the HEVC core
+		 * down here leaves it in a state the next context's bring-up
+		 * does not recover from, and every frame of the *second*
+		 * decode session on this core then times out - whichever
+		 * codec it is.  The domain is left up until the module is
+		 * unloaded.
+		 */
+	}
+	kfree(av1);
+	ctx->codec_priv = NULL;
+}
+
+/*
+ * Point the double-write unit at the CAPTURE buffer this frame will be handed
+ * back in.  Only safe when the buffer is laid out exactly the way the hardware
+ * writes: stride equal to the picture width, chroma immediately after luma.
+ * Anything else falls back to the slot buffer and the row-by-row copy.
+ */
+static void av1_set_dw_target(struct aml_vdec_ctx *ctx)
+{
+	struct v4l2_pix_format_mplane *pf = &ctx->pix_fmt[AML_FMT_DST];
+	struct vb2_v4l2_buffer *dst = v4l2_m2m_next_dst_buf(ctx->m2m_ctx);
+	struct vb2_buffer *vb;
+
+	av1_dw_target_y = 0;
+	av1_dw_target_c = 0;
+	av1_dw_target_size = 0;
+	if (!dst)
+		return;
+	vb = &dst->vb2_buf;
+
+	if (pf->plane_fmt[0].bytesperline != pf->width)
+		return;
+
+	av1_dw_target_y = vb2_dma_contig_plane_dma_addr(vb, 0);
+	if (!av1_dw_target_y)
+		return;
+	if (vb->num_planes >= 2) {
+		av1_dw_target_c = vb2_dma_contig_plane_dma_addr(vb, 1);
+		av1_dw_target_size = vb2_plane_size(vb, 0) +
+				     vb2_plane_size(vb, 1);
+		if (!av1_dw_target_c) {
+			av1_dw_target_y = 0;
+			return;
+		}
+	} else {
+		av1_dw_target_c = av1_dw_target_y +
+				  (dma_addr_t)pf->width * pf->height;
+		av1_dw_target_size = vb2_plane_size(vb, 0);
+	}
+}
+
+int aml_av1_dec_run(void *priv)
+{
+	struct aml_vdec_ctx *ctx = priv;
+	struct aml_av1_ctx *av1 = ctx->codec_priv;
+	struct aml_vdec_hw *hw = av1->hw;
+	struct vb2_v4l2_buffer *src;
+	struct vb2_buffer *vb;
+	struct v4l2_ctrl *seq_ctrl;
+	int ret;
+
+	src = v4l2_m2m_next_src_buf(ctx->m2m_ctx);
+	if (!src)
+		return -EINVAL;
+	vb = &src->vb2_buf;
+
+	ret = hvdec_enable(hw);
+	if (ret)
+		return ret;
+	dos_enable_hevc(hw);
+
+	if (!av1->ucode_loaded) {
+		/*
+		 * As for VP9: after a 10-bit stream the core decodes the next
+		 * 8-bit one to garbage until its memories are cycled.
+		 * av1_ten_bit still holds the last stream's depth here.
+		 */
+		if (av1_ten_bit)
+			hw->hevc_dirty = true;
+		av1_ten_bit = false;
+		hvdec_switch_codec(hw, CODEC_TYPE_AV1);
+
+		/*
+		 * Halts the AMRISC first: the other codec on this core may
+		 * still be running - VP9 and AV1 share it - and loading over a
+		 * running microcode leaves the core timing out every frame.
+		 */
+		ret = hevc_load_fw(hw, CODEC_TYPE_AV1, AV1_FW_FORMAT_MMU, AV1_FW_CORE_HEVC, "av1");
+		if (ret) {
+			hvdec_disable(hw);
+			return ret;
+		}
+		av1->ucode_loaded = true;
+	}
+
+	if (!av1->started) {
+		ret = aml_av1_bringup(hw, ctx->pix_fmt[AML_FMT_SRC].width,
+				      ctx->pix_fmt[AML_FMT_SRC].height);
+		if (ret) {
+			hvdec_disable(hw);
+			return ret;
+		}
+	}
+
+	/*
+	 * Two shapes of OUTPUT buffer have to work.
+	 *
+	 * The stateless API keeps the sequence in a control and hands over the
+	 * frame OBU's *payload*, header stripped - that is what GStreamer's
+	 * v4l2slav1dec sends.  Rebuild the sequence header from the control
+	 * and put the frame's OBU header back.
+	 *
+	 * Without that control the buffer is taken to be a plain OBU stream,
+	 * carrying its own sequence header, which is what a simple client
+	 * feeding temporal units does.  The microcode parses the bitstream
+	 * either way, so it has to end up with real OBUs.
+	 */
+	seq_ctrl = v4l2_ctrl_find(&ctx->ctrl_handler,
+				  V4L2_CID_STATELESS_AV1_SEQUENCE);
+	if (seq_ctrl && aml_av1_set_sequence(seq_ctrl->p_cur.p)) {
+		struct v4l2_ctrl *frame_ctrl =
+			v4l2_ctrl_find(&ctx->ctrl_handler,
+				       V4L2_CID_STATELESS_AV1_FRAME);
+
+		av1_cur_ts = vb->timestamp;
+		if (frame_ctrl)
+			av1_apply_reference_ts(frame_ctrl->p_cur.p);
+		av1_set_dw_target(ctx);
+		ret = aml_av1_feed_frame(hw, vb2_plane_vaddr(vb, 0),
+					 vb2_get_plane_payload(vb, 0));
+	} else {
+		av1_dw_target_y = 0;
+		av1_dw_target_c = 0;
+		av1_dw_target_size = 0;
+		if (!aml_av1_have_sequence() &&
+		    !av1_buf_has_sequence(vb2_plane_vaddr(vb, 0),
+					  vb2_get_plane_payload(vb, 0))) {
+			dev_err_once(hw->dev,
+				     "av1: no sequence header, and no usable sequence control\n");
+			return -EINVAL;
+		}
+		ret = aml_av1_feed_buf(hw, vb2_plane_vaddr(vb, 0),
+				       vb2_get_plane_payload(vb, 0),
+				       AOM_AV1_SEARCH_HEAD);
+	}
+	if (ret)
+		return ret;
+
+	av1->rounds = 0;
+	av1->have_picture = false;
+	ctx->int_cond = 0;
+	atomic_set(&hw->hevc_irq_count, 0);
+
+	/*
+	 * The microcode runs for the life of the stream: it is started once
+	 * and afterwards simply told to look for the next frame header.
+	 * Restarting it per buffer - or powering the core down between
+	 * buffers - loses its state, and every buffer after the first then
+	 * times out with the program counter parked at 1 or 2.
+	 */
+	if (av1->started) {
+		/*
+		 * The microcode reports NAL_DECODE_DONE when it runs out of
+		 * data and parks in its search loop; handing it another
+		 * SEARCH_HEAD there does not make it look at a refilled FIFO.
+		 * Restart it instead.  Everything that has to persist between
+		 * buffers - the picture slots, their compressed data, the CDF
+		 * contexts - lives in DRAM and is re-pointed for every
+		 * picture, so a restart costs only the parser's reference map.
+		 */
+		regmap_write(hw->map[DOS_BUS], HEVC_MPSR, 0);
+		regmap_write(hw->map[DOS_BUS], HEVC_CPSR, 0);
+	}
+	regmap_write(hw->map[DOS_BUS], DOS_SW_RESET3, BIT(12) | BIT(11));
+	regmap_write(hw->map[DOS_BUS], DOS_SW_RESET3, 0);
+	if (av1->started)
+		aml_av1_restore_refs(hw);
+	regmap_write(hw->map[DOS_BUS], HEVC_ASSIST_MBOX0_CLR_REG, 1);
+	regmap_write(hw->map[DOS_BUS], HEVC_DEC_STATUS_REG, AOM_AV1_SEARCH_HEAD);
+	regmap_write(hw->map[DOS_BUS], HEVC_MPSR, 1);
+	av1->started = true;
+
+	/*
+	 * Uninterruptible on purpose: a signal must not abandon a decode the
+	 * hardware is still running - see the note in vp9.c.
+	 */
+	ret = wait_event_timeout(ctx->queue, ctx->int_cond,
+				 msecs_to_jiffies(AV1_TIMEOUT_MS));
+	if (!ret) {
+		/*
+		 * Report the failure and let device_run() cancel the job.
+		 * Finishing it here as well was a double completion: the
+		 * caller runs its own error path on a negative return, and
+		 * the media core warns about the second one.
+		 */
+		dev_err(hw->dev, "av1: decode timeout, status %02x pc %08x\n",
+			hw->hevc_irq_status,
+			read_dos_reg(hw, HEVC_MPC_E));
+		hw->hevc_dirty = true;
+		ret = -ETIMEDOUT;
+	} else {
+		ret = 0;
+	}
+	ctx->int_cond = 0;
+	return ret;
+}
diff --git a/drivers/media/platform/amlogic/vdec/av1.h b/drivers/media/platform/amlogic/vdec/av1.h
new file mode 100644
index 000000000000..97ddbbe61f21
--- /dev/null
+++ b/drivers/media/platform/amlogic/vdec/av1.h
@@ -0,0 +1,308 @@
+/* SPDX-License-Identifier: (GPL-2.0-only OR MIT) */
+/*
+ * AV1 bring-up for the S4 HEVC/AV1 core.
+ *
+ * Register addresses are taken from Amlogic's own headers - khadas/linux
+ * branch khadas-vim1s-r, include/linux/amlogic/media/registers/regs/hevc_regs.h
+ * (VIM1S is S905Y4, the same S4 family) - and cross-checked against mainline
+ * drivers/staging/media/meson/vdec/hevc_regs.h, which byte-addresses the same
+ * block (HEVC_ASSIST_SCRATCH_0 0xc300 there == word 0x30c0 here).
+ *
+ * The sequence this file implements is Amlogic's vav1_prot_init(), from
+ * media_modules branch khadas-vim1s-r,
+ * drivers/frame_provider/decoder_v4l/vav1/vav1.c.
+ */
+
+#ifndef AML_VDEC_AV1_H_
+#define AML_VDEC_AV1_H_
+
+#include "aml_vdec_hw.h"
+
+/* Assist block */
+#define HEVC_ASSIST_MMU_MAP_ADDR     REG_ALIGN(0x3009)
+#define HEVC_ASSIST_SCRATCH_2        REG_ALIGN(0x30c2)
+#define HEVC_ASSIST_SCRATCH_5        REG_ALIGN(0x30c5)
+#define HEVC_ASSIST_SCRATCH_8        REG_ALIGN(0x30c8)
+#define HEVC_ASSIST_SCRATCH_9        REG_ALIGN(0x30c9)
+#define HEVC_ASSIST_SCRATCH_7        REG_ALIGN(0x30c7)
+#define HEVC_ASSIST_SCRATCH_L        REG_ALIGN(0x30d5)
+#define HEVC_ASSIST_SCRATCH_E        REG_ALIGN(0x30ce)
+#define HEVC_ASSIST_SCRATCH_G        REG_ALIGN(0x30d0)
+#define HEVC_ASSIST_SCRATCH_H        REG_ALIGN(0x30d1)
+#define HEVC_ASSIST_SCRATCH_I        REG_ALIGN(0x30d2)
+#define HEVC_ASSIST_SCRATCH_J        REG_ALIGN(0x30d3)
+#define HEVC_ASSIST_SCRATCH_K        REG_ALIGN(0x30d4)
+#define HEVC_ASSIST_SCRATCH_M        REG_ALIGN(0x30d6)
+#define HEVC_ASSIST_SCRATCH_N        REG_ALIGN(0x30d7)
+
+/*
+ * Amlogic overloads the scratch file rather than giving these registers real
+ * names, and the aliases collide: LMEM_DUMP_ADR and NAL_SEARCH_CTL are both
+ * scratch I.  vav1_prot_init() writes the constant 8 there and run() then
+ * overwrites it with the lmem address, so the address is what the microcode
+ * actually sees.
+ *
+ * For the AV1 microcode scratch I is LMEM_DUMP_ADR and nothing else: it DMAs
+ * its local-memory dump to whatever address the register holds.  Leaving a
+ * small control value there aims that DMA at low physical memory and oopses the
+ * board - measured.  NAL_SEARCH_CTL is a leftover name from the HEVC microcode.
+ */
+#define HEVC_DEC_STATUS_REG          HEVC_ASSIST_SCRATCH_0
+/* PIC_END_LCU_COUNT aliases scratch 2 in the reference. */
+#define PIC_END_LCU_COUNT            HEVC_ASSIST_SCRATCH_2
+#define HEVC_RPM_BUFFER              HEVC_ASSIST_SCRATCH_1
+#define AOM_AV1_DAALA_TOP_BUFFER     HEVC_ASSIST_SCRATCH_5
+#define HEVC_WAIT_FLAG               HEVC_ASSIST_SCRATCH_E
+#ifndef DEBUG_REG1
+#define DEBUG_REG1                   HEVC_ASSIST_SCRATCH_G
+#endif
+#ifndef LMEM_DUMP_ADR
+#define LMEM_DUMP_ADR                HEVC_ASSIST_SCRATCH_I
+#endif
+#ifndef NAL_SEARCH_CTL
+#define NAL_SEARCH_CTL               HEVC_ASSIST_SCRATCH_I
+#endif
+#define DECODE_MODE                  HEVC_ASSIST_SCRATCH_J
+#define DECODE_STOP_POS              HEVC_ASSIST_SCRATCH_K
+#define HEVC_DECODE_COUNT            HEVC_ASSIST_SCRATCH_M
+#define HEVC_DECODE_SIZE             HEVC_ASSIST_SCRATCH_N
+#define HEVC_AUX_ADR                 HEVC_ASSIST_SCRATCH_L
+#define HEVC_AUX_DATA_SIZE           HEVC_ASSIST_SCRATCH_7
+
+/*
+ * Commands the host leaves in HEVC_DEC_STATUS_REG, and the status values the
+ * microcode writes back into the same register.
+ */
+#define AOM_AV1_DEC_IDLE             0
+#define AOM_AV1_DECODE_SLICE         5
+#define AOM_AV1_SEARCH_HEAD          6
+#define AOM_AV1_HEAD_PARSER_DONE     0xf0
+#define AOM_AV1_SEQ_HEAD_PARSER_DONE 0xf2
+#define AOM_AV1_FRAME_HEAD_PARSER_DONE 0xf3
+#define AOM_AV1_FRAME_PARSER_DONE    0xf4
+#define AOM_AV1_DEC_PIC_END          0xe0
+
+/* Microcode status values written into HEVC_DEC_STATUS_REG. */
+#define HEVC_ACTION_DONE             0xff
+#define AOM_DECODE_BUFEMPTY          0x20
+#define AOM_DECODE_TIMEOUT           0x21
+#define AOM_SEARCH_BUFEMPTY          0x22
+#define AOM_DECODE_OVER_SIZE         0x23
+#define AOM_EOS                      0x24
+#define AOM_NAL_DECODE_DONE          0x25
+
+#define DECODE_MODE_MULTI_FRAMEBASE  ((0x80 << 24) | 2)
+
+/* Parser / stream front end */
+#define HEVC_STREAM_CONTROL          REG_ALIGN(0x3101)
+#define HEVC_STREAM_FIFO_CTL         REG_ALIGN(0x3107)
+#define HEVC_SHIFT_CONTROL           REG_ALIGN(0x3108)
+#define HEVC_SHIFT_STARTCODE         REG_ALIGN(0x3109)
+#define HEVC_SHIFT_EMULATECODE       REG_ALIGN(0x310a)
+#define HEVC_SHIFT_STATUS            REG_ALIGN(0x310b)
+#define HEVC_CABAC_CONTROL           REG_ALIGN(0x3110)
+#define HEVC_PARSER_CMD_WRITE        REG_ALIGN(0x3112)
+#define HEVC_PARSER_CORE_CONTROL     REG_ALIGN(0x3113)
+#define HEVC_PARSER_INT_CONTROL      REG_ALIGN(0x3120)
+#define HEVC_PARSER_IF_CONTROL       REG_ALIGN(0x3122)
+#define HEVC_PARSER_MEM_WR_ADDR      REG_ALIGN(0x312d)
+#define HEVC_PARSER_MEM_RW_DATA      REG_ALIGN(0x312e)
+#define HEVC_PARSER_CMD_SKIP_0       REG_ALIGN(0x3128)
+#define HEVC_PARSER_CMD_SKIP_1       REG_ALIGN(0x3129)
+#define HEVC_PARSER_CMD_SKIP_2       REG_ALIGN(0x312a)
+
+/* MPRED, IPP, deblock, SAO, compression */
+#define HEVC_MPRED_CTRL0             REG_ALIGN(0x3201)
+#define HEVC_MPRED_ABV_START_ADDR    REG_ALIGN(0x3212)
+#define HEVC_MPRED_MV_WR_START_ADDR  REG_ALIGN(0x3213)
+#define HEVC_MPRED_MV_WPTR           REG_ALIGN(0x3215)
+#define HEVC_MPRED_MV_WR_ROW_JUMP    REG_ALIGN(0x3217)
+#define HEVC_MPRED_MV_RD_ROW_JUMP    REG_ALIGN(0x3218)
+#define HEVC_MPRED_MV_RD_START_ADDR  REG_ALIGN(0x3214)
+#define HEVC_MPRED_MV_RPTR           REG_ALIGN(0x3216)
+#define HEVC_MPRED_CURR_LCU          REG_ALIGN(0x3219)
+#define HEVC_MPRED_CTRL3             REG_ALIGN(0x321d)
+/*
+ * L0_REF00..0F occupy 0x3220..0x322f: mainline's hevc_regs.h has
+ * HEVC_MPRED_L0_REF00_POC at byte 0xc880 and L1_REF00_POC at 0xc8c0, so the
+ * L0 bank is 16 registers long.  config_mpred_hw() uses REF00 for the current
+ * order hint and REF03..05 for the reference slot indices and their hints.
+ */
+#define HEVC_MPRED_L0_REF00_POC      REG_ALIGN(0x3220)
+#define HEVC_MPRED_L0_REF01_POC      REG_ALIGN(0x3221)
+#define HEVC_MPRED_L0_REF02_POC      REG_ALIGN(0x3222)
+#define HEVC_MPRED_L0_REF03_POC      REG_ALIGN(0x3223)
+#define HEVC_MPRED_L0_REF04_POC      REG_ALIGN(0x3224)
+#define HEVC_MPRED_L0_REF05_POC      REG_ALIGN(0x3225)
+#define HEVC_MPRED_L0_REF06_POC      REG_ALIGN(0x3226)
+#define HEVC_MPRED_L0_REF07_POC      REG_ALIGN(0x3227)
+#define HEVC_MPRED_L0_REF08_POC      REG_ALIGN(0x3228)
+#define HEVC_MPRED_L0_REF09_POC      REG_ALIGN(0x3229)
+#define HEVC_MPRED_L0_REF10_POC      REG_ALIGN(0x322a)
+#define HEVC_MPRED_L0_REF11_POC      REG_ALIGN(0x322b)
+#define HEVC_MPRED_MV_RPTR_1         REG_ALIGN(0x3263)
+#define HEVC_MPRED_MV_RPTR_2         REG_ALIGN(0x3264)
+
+#define MPRED_CTRL0_MV_WR_EN         BIT(10)
+#define MPRED_CTRL0_MV_RD_EN         BIT(11)
+#define HEVC_MPRED_CTRL4             REG_ALIGN(0x324c)
+#define HEVCD_IPP_TOP_CNTL           REG_ALIGN(0x3400)
+#define HEVCD_IPP_LINEBUFF_BASE      REG_ALIGN(0x3409)
+/*
+ * Frame-buffer address table.  TBL_DATA is not in the vendor header shipped
+ * with this branch; mainline's hevc_regs.h names it 0xd190, i.e. word 0x3464,
+ * one past the RDATA port.
+ */
+#define HEVCD_MPP_ANC2AXI_TBL_CONF_ADDR   REG_ALIGN(0x3460)
+#define HEVCD_MPP_ANC2AXI_TBL_DATA        REG_ALIGN(0x3464)
+#define HEVCD_MPP_ANC_CANVAS_ACCCONFIG_ADDR REG_ALIGN(0x34c0)
+#define HEVCD_MPP_ANC_CANVAS_DATA_ADDR    REG_ALIGN(0x34c1)
+#define AV1_SKIP_MODE_INFO           REG_ALIGN(0x316c)
+#define PARSER_REF_SCALE_ENBL        REG_ALIGN(0x316b)
+#define AV1D_MPP_REF_SCALE_ENBL      REG_ALIGN(0x3441)
+/* Motion-compensation reference cache: mainline hevc_regs.h 0xd3c0..0xd3c8. */
+#define HEVCD_MCRCC_CTL1             REG_ALIGN(0x34f0)
+#define HEVCD_MCRCC_CTL2             REG_ALIGN(0x34f1)
+#define HEVCD_MCRCC_CTL3             REG_ALIGN(0x34f2)
+#define HEVCD_MPP_DECOMP_CTL1        REG_ALIGN(0x34c2)
+#define HEVCD_MPP_DECOMP_CTL2        REG_ALIGN(0x34c3)
+#define HEVC_DBLK_CFG3               REG_ALIGN(0x3503)
+#define HEVC_DBLK_CFG4               REG_ALIGN(0x3504)
+#define HEVC_DBLK_CFG5               REG_ALIGN(0x3505)
+#define HEVC_SAO_CTRL5               REG_ALIGN(0x3623)
+#define HEVC_CM_BODY_LENGTH          REG_ALIGN(0x3627)
+#define HEVC_CM_HEADER_LENGTH        REG_ALIGN(0x3629)
+#define HEVC_CM_HEADER_OFFSET        REG_ALIGN(0x362b)
+#define HEVC_SAO_MMU_VH0_ADDR        REG_ALIGN(0x363a)
+#define HEVC_SAO_MMU_VH1_ADDR        REG_ALIGN(0x363b)
+#define HEVC_SAO_MMU_DMA_CTRL        REG_ALIGN(0x363e)
+#define HEVC_IQIT_SCALELUT_WR_ADDR   REG_ALIGN(0x3702)
+#define HEVC_IQIT_SCALELUT_DATA      REG_ALIGN(0x3704)
+#define HEVC_PSCALE_CTRL             REG_ALIGN(0x3911)
+
+/* Stream FIFO the microcode fetches from */
+#define HEVC_STREAM_START_ADDR       REG_ALIGN(0x3102)
+#define HEVC_STREAM_END_ADDR         REG_ALIGN(0x3103)
+#define HEVC_STREAM_WR_PTR           REG_ALIGN(0x3104)
+#define HEVC_STREAM_RD_PTR           REG_ALIGN(0x3105)
+#define HEVC_STREAM_LEVEL            REG_ALIGN(0x3106)
+#define HEVC_SHIFT_BYTE_COUNT        REG_ALIGN(0x310d)
+
+/* SAO / output */
+#define HEVC_SAO_CTRL0               REG_ALIGN(0x3601)
+#define HEVC_SAO_CTRL1               REG_ALIGN(0x3602)
+#define HEVC_SAO_CTRL9               REG_ALIGN(0x362d)
+#define HEVC_SAO_Y_START_ADDR        REG_ALIGN(0x360b)
+#define HEVC_SAO_Y_LENGTH            REG_ALIGN(0x360c)
+#define HEVC_SAO_C_START_ADDR        REG_ALIGN(0x360d)
+#define HEVC_SAO_C_LENGTH            REG_ALIGN(0x360e)
+#define HEVC_SAO_Y_WPTR              REG_ALIGN(0x360f)
+#define HEVC_SAO_C_WPTR              REG_ALIGN(0x3610)
+#define HEVC_CM_BODY_START_ADDR      REG_ALIGN(0x3626)
+#define HEVC_CM_HEADER_START_ADDR    REG_ALIGN(0x3628)
+#define HEVCD_IPP_AXIIF_CONFIG       REG_ALIGN(0x340b)
+#define HEVC_FGS_TABLE_START         REG_ALIGN(0x3666)
+#define HEVC_FGS_TABLE_LENGTH        REG_ALIGN(0x3667)
+
+/* Deblock / CDEF */
+#define HEVC_DBLK_CFG9               REG_ALIGN(0x3509)
+#define HEVC_DBLK_CFGA               REG_ALIGN(0x350a)
+#define HEVC_DBLK_CFGB               REG_ALIGN(0x350b)
+#define HEVC_DBLK_CDEF0              REG_ALIGN(0x3515)
+#define HEVC_DBLK_CDEF1              REG_ALIGN(0x3516)
+#define HEVC_DBLK_UPS1               REG_ALIGN(0x351c)
+#define HEVC_DBLK_UPS2               REG_ALIGN(0x351d)
+#define HEVC_DBLK_UPS3               REG_ALIGN(0x351e)
+#define HEVC_DBLK_UPS4               REG_ALIGN(0x351f)
+#define HEVC_DBLK_UPS5               REG_ALIGN(0x3520)
+#define AV1_UPSCALE_X0_QN            REG_ALIGN(0x316e)
+#define AV1_UPSCALE_STEP_QN          REG_ALIGN(0x316f)
+#define HEVC_DBLK_DBLK0              REG_ALIGN(0x3523)
+#define HEVC_DBLK_DBLK1              REG_ALIGN(0x3524)
+#define HEVC_DBLK_DBLK2              REG_ALIGN(0x3525)
+
+/* AV1-specific */
+#define AV1_GMC_PARAM_BUFF_ADDR      REG_ALIGN(0x316d)
+#define AV1_QUANT_WR                 REG_ALIGN(0x3146)
+#define AV1_SEG_W_ADDR               REG_ALIGN(0x3165)
+#define AV1_SEG_R_ADDR               REG_ALIGN(0x3166)
+#define AOM_AV1_CDF_BUFFER_W         HEVC_ASSIST_SCRATCH_8
+#define AOM_AV1_CDF_BUFFER_R         HEVC_ASSIST_SCRATCH_9
+#define AOM_AV1_SEGMENT_FEATURE      AV1_QUANT_WR
+#define AV1D_MPP_REFINFO_TBL_ACCCONFIG REG_ALIGN(0x3442)
+#define AV1D_MPP_REFINFO_DATA          REG_ALIGN(0x3443)
+#define AV1_REF_NO_SCALE             BIT(14)
+
+/* Amlogic's endian presets, from vav1.c */
+#define HEVC_CONFIG_LITTLE_ENDIAN    ((0xff0 << 8) | 0xf)
+
+int aml_av1_bringup(struct aml_vdec_hw *hw, u32 width, u32 height);
+int aml_av1_feed(struct aml_vdec_hw *hw, const char *fw_name, u32 kick);
+int aml_av1_feed_buf(struct aml_vdec_hw *hw, const void *data, size_t size,
+		     u32 kick);
+int aml_av1_config_picture(struct aml_vdec_hw *hw);
+void aml_av1_dump_stream_state(struct aml_vdec_hw *hw);
+void aml_av1_dump_ucode_output(struct aml_vdec_hw *hw);
+void aml_av1_dump_params(struct aml_vdec_hw *hw);
+u32 aml_av1_frame_type(struct aml_vdec_hw *hw);
+void aml_av1_frame_done(struct aml_vdec_hw *hw);
+void aml_av1_restore_refs(struct aml_vdec_hw *hw);
+void aml_av1_dump_output(struct aml_vdec_hw *hw);
+int aml_av1_save_nv12(struct aml_vdec_hw *hw, const char *path, bool append);
+
+/*
+ * Indices into the unscrambled RPM block, counted from union param_u in
+ * av1_global.h (INTER_REFS_PER_FRAME 7, REF_FRAMES 8).
+ */
+#define AV1_RPM_PROFILE              0
+#define AV1_RPM_ORDER_HINT_BITS_M1   9
+#define AV1_RPM_ENABLE_ORDER_HINT   10
+#define AV1_RPM_ALLOW_REF_FRAME_MVS 29
+#define AV1_RPM_REF_ORDER_HINT0     48
+#define AV1_RPM_MAX_FRAME_WIDTH      4
+#define AV1_RPM_MAX_FRAME_HEIGHT     5
+#define AV1_RPM_FRAME_TYPE          14
+#define AV1_RPM_REFRESH_FRAME_FLAGS 17
+#define AV1_RPM_ORDER_HINT          21
+#define AV1_RPM_PRIMARY_REF_FRAME   22
+#define AV1_PRIMARY_REF_NONE         7
+#define AV1_RPM_REMAPPED_REF_IDX0   34
+#define AV1_RPM_SHOW_FRAME          15
+#define AV1_RPM_REFERENCE_MODE      28
+#define AV1_RPM_SHOW_EXISTING_FRAME 13
+#define AV1_RPM_EXISTING_FRAME_IDX  33
+#define AV1_RPM_FRAME_WIDTH         24
+#define AV1_RPM_FRAME_HEIGHT        27
+#define AV1_RPM_BIT_DEPTH           56
+#define AV1_RPM_SEQ_FLAGS           57
+#define AV1_RPM_LF_SHARPNESS        60
+#define AV1_RPM_LF_MODE_REF_DELTA   61
+#define AV1_RPM_LF_REF_DELTAS_0     62
+#define AV1_RPM_LF_MODE_DELTAS_0    70
+#define AV1_RPM_LF_LEVEL_0          72
+#define AV1_RPM_LF_LEVEL_1          73
+#define AV1_RPM_LF_LEVEL_U          74
+#define AV1_RPM_LF_LEVEL_V          75
+#define AV1_RPM_SEGMENTATION_EN     76
+#define AV1_RPM_COUNT              128
+void aml_av1_teardown(struct aml_vdec_hw *hw);
+
+#define AV1_TIMEOUT_MS 1000
+
+/* Sanity bounds on a parsed picture size, see aml_av1_config_picture(). */
+#define AV1_MIN_DIM 16
+#define AV1_MAX_DIM 4096
+
+/* Secure microcode load: AV1 in MMU mode, on the HEVC core. */
+#define AV1_FW_FORMAT_MMU  0x4156314d	/* 'M1VA' */
+#define AV1_FW_CORE_HEVC   2
+
+int aml_av1_init(void *priv);
+void aml_av1_exit(void *priv);
+int aml_av1_dec_run(void *priv);
+bool aml_av1_have_sequence(void);
+int aml_av1_feed_frame(struct aml_vdec_hw *hw, const void *data, size_t size);
+bool aml_av1_set_sequence(const void *ctrl);
+bool av1_buf_has_sequence(const void *data, size_t size);
+
+#endif
-- 
2.56.0


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

* [PATCH 08/11] media: amlogic: vdec: add VP9 decoding on the HEVC core
  2026-10-25 19:24 [PATCH 00/11] media: amlogic: vdec: S4 decoding beyond H.264 Baseline christian.koever-draxl
                   ` (6 preceding siblings ...)
  2026-10-25 19:25 ` [PATCH 07/11] media: amlogic: vdec: add AV1 decoding on the HEVC core christian.koever-draxl
@ 2026-10-25 19:25 ` christian.koever-draxl
  2026-10-25 20:23   ` sashiko-bot
  2026-10-25 19:25 ` [PATCH 09/11] media: amlogic: vdec: add HEVC " christian.koever-draxl
                   ` (3 subsequent siblings)
  11 siblings, 1 reply; 20+ messages in thread
From: christian.koever-draxl @ 2026-10-25 19:25 UTC (permalink / raw)
  To: zhentao.guo, mchehab, robh, krzk+dt, conor+dt
  Cc: linux-media, devicetree, linux-kernel,
	Christian Stefan Kövér-Draxl

From: Christian Stefan Kövér-Draxl <ckoever@outlook.de>

Decode VP9 profile 0 and profile 2 (10-bit 4:2:0, output as rounded
8-bit NV12) up to 4K on the HEVC core. The microcode parses the
uncompressed and compressed headers itself; the host places each frame
behind the 16-byte AMLV prefix, owns the reference slots, and writes
the four frame contexts into the probability buffer before every frame
because the microcode only ever reads them.

The microcode hands backward adaptation to the host and then wedges
the core on this platform, so streams that need it (refresh_frame_context
without frame-parallel or error-resilient mode) are refused and left to
a software decoder. Frame-parallel and error-resilient streams, which is
what players mostly meet, decode bit-exact including alt-ref frames.

After a timeout or a 10-bit session the core memories are power-cycled
and the microcode reloaded before the next frame.

Signed-off-by: Christian Stefan Kövér-Draxl <ckoever@outlook.de>
---
 drivers/media/platform/amlogic/vdec/Kconfig   |    1 +
 drivers/media/platform/amlogic/vdec/Makefile  |    2 +-
 .../media/platform/amlogic/vdec/aml_vdec.c    |   13 +
 .../platform/amlogic/vdec/aml_vdec_platform.c |   17 +
 drivers/media/platform/amlogic/vdec/vp9.c     | 2004 +++++++++++++++++
 drivers/media/platform/amlogic/vdec/vp9.h     |  149 ++
 .../media/platform/amlogic/vdec/vp9_prob.c    |  213 ++
 .../media/platform/amlogic/vdec/vp9_prob.h    |   32 +
 8 files changed, 2430 insertions(+), 1 deletion(-)
 create mode 100644 drivers/media/platform/amlogic/vdec/vp9.c
 create mode 100644 drivers/media/platform/amlogic/vdec/vp9.h
 create mode 100644 drivers/media/platform/amlogic/vdec/vp9_prob.c
 create mode 100644 drivers/media/platform/amlogic/vdec/vp9_prob.h

diff --git a/drivers/media/platform/amlogic/vdec/Kconfig b/drivers/media/platform/amlogic/vdec/Kconfig
index d392967c7743..5e5dd892b15e 100644
--- a/drivers/media/platform/amlogic/vdec/Kconfig
+++ b/drivers/media/platform/amlogic/vdec/Kconfig
@@ -8,6 +8,7 @@ config VIDEO_AMLOGIC_VDEC
     depends on TEE
     select VIDEOBUF2_DMA_CONTIG
     select V4L2_H264
+    select V4L2_VP9
     select V4L2_MEM2MEM_DEV
     select MESON_CANVAS
     select MESON_SM
diff --git a/drivers/media/platform/amlogic/vdec/Makefile b/drivers/media/platform/amlogic/vdec/Makefile
index 4eb7601f61a3..e32cac07a4a7 100644
--- a/drivers/media/platform/amlogic/vdec/Makefile
+++ b/drivers/media/platform/amlogic/vdec/Makefile
@@ -1,4 +1,4 @@
 # SPDX-License-Identifier: GPL-2.0-only
-aml-vdec-drv-objs := aml_vdec.o aml_vdec_drv.o aml_vdec_hw.o aml_vdec_platform.o h264.o h264_headers.o av1.o mpeg12.o mjpeg.o aml_vdec_tee_fw.o
+aml-vdec-drv-objs := aml_vdec.o aml_vdec_drv.o aml_vdec_hw.o aml_vdec_platform.o h264.o h264_headers.o av1.o vp9.o vp9_prob.o mpeg12.o mjpeg.o aml_vdec_tee_fw.o
 
 obj-$(CONFIG_VIDEO_AMLOGIC_VDEC) += aml-vdec-drv.o
diff --git a/drivers/media/platform/amlogic/vdec/aml_vdec.c b/drivers/media/platform/amlogic/vdec/aml_vdec.c
index 6892430c9569..f1696aa2fac5 100644
--- a/drivers/media/platform/amlogic/vdec/aml_vdec.c
+++ b/drivers/media/platform/amlogic/vdec/aml_vdec.c
@@ -87,6 +87,16 @@ static const struct aml_vdec_v4l2_ctrl controls[] = {
 		.cfg = {
 			.id = V4L2_CID_STATELESS_AV1_FILM_GRAIN,
 		},
+	}, {
+		.codec_type = CODEC_TYPE_VP9,
+		.cfg = {
+			.id = V4L2_CID_STATELESS_VP9_FRAME,
+		},
+	}, {
+		.codec_type = CODEC_TYPE_VP9,
+		.cfg = {
+			.id = V4L2_CID_STATELESS_VP9_COMPRESSED_HDR,
+		},
 	}, {
 		.codec_type = CODEC_TYPE_MPEG2,
 		.cfg = {
@@ -111,6 +121,9 @@ static const struct aml_dec_type dec_type_name[] = {
 	}, {
 		.codec_type = CODEC_TYPE_AV1,
 		.name = "AV1",
+	}, {
+		.codec_type = CODEC_TYPE_VP9,
+		.name = "VP9",
 	}, {
 		.codec_type = CODEC_TYPE_MPEG2,
 		.name = "MPEG2",
diff --git a/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c b/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c
index 1e8193821aaf..1620e2764f47 100644
--- a/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c
+++ b/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c
@@ -7,6 +7,7 @@
 #include "aml_vdec_hw.h"
 #include "h264.h"
 #include "av1.h"
+#include "vp9.h"
 #include "mpeg12.h"
 #include "mjpeg.h"
 
@@ -33,6 +34,17 @@ static struct aml_video_fmt aml_s4_video_formats[] = {
 		.stepwise = {AML_VDEC_MIN_W, AML_VDEC_4K_MAX_W, 2,
 			AML_VDEC_MIN_H, AML_VDEC_4K_MAX_H, 2},
 	},
+	{
+		.name = "VP9",
+		.fourcc = V4L2_PIX_FMT_VP9_FRAME,
+		.type = AML_FMT_DEC,
+		.align = 64,
+		.is_10_bit_support = 0,
+		.codec_type = CODEC_TYPE_VP9,
+		.num_planes = 1,
+		.stepwise = {AML_VDEC_MIN_W, AML_VDEC_4K_MAX_W, 2,
+			AML_VDEC_MIN_H, AML_VDEC_4K_MAX_H, 2},
+	},
 	{
 		.name = "MPEG-2",
 		.fourcc = V4L2_PIX_FMT_MPEG2_SLICE,
@@ -120,6 +132,11 @@ const struct aml_codec_ops aml_S4_dec_ops[] = {
 		.exit = aml_av1_exit,
 		.run = aml_av1_dec_run,
 	},
+	[CODEC_TYPE_VP9] = {
+		.init = aml_vp9_init,
+		.exit = aml_vp9_exit,
+		.run = aml_vp9_dec_run,
+	},
 	[CODEC_TYPE_MPEG2] = {
 		.init = aml_mpeg12_init,
 		.exit = aml_mpeg12_exit,
diff --git a/drivers/media/platform/amlogic/vdec/vp9.c b/drivers/media/platform/amlogic/vdec/vp9.c
new file mode 100644
index 000000000000..b67d503b9351
--- /dev/null
+++ b/drivers/media/platform/amlogic/vdec/vp9.c
@@ -0,0 +1,2004 @@
+// SPDX-License-Identifier: (GPL-2.0-only OR MIT)
+/*
+ * VP9 decoding on the Amlogic S4 HEVC core.
+ *
+ * This is the same hardware the AV1 path drives, running different microcode,
+ * and the two agree on almost everything structurally: one mailbox register
+ * carries the status and the next command, the picture is written compressed
+ * through an MMU page table plus a per-picture header buffer, and a
+ * double-write unit lays down a plain NV12 copy alongside it.
+ *
+ * What VP9 does differently, and what this file therefore has to get right:
+ *
+ *   - No sequence header and no AMLV reframing.  A VP9 frame is fed to the
+ *     shifter as it stands; the microcode's own start-code search finds it.
+ *   - The workspace has a different shape - short_term_rps/vps/sps/pps and a
+ *     second stream swap buffer, none of which AV1 uses, and no CDF, GMC,
+ *     CDEF, upscale or film-grain areas, which it does.
+ *   - Entropy state is a probability buffer plus a count buffer that the host
+ *     adapts between frames, where AV1 keeps CDFs per slot.
+ *
+ * Sizes are taken from the reference's amvvp9_workbuff_spec[1], the 4096x2304
+ * entry, for the same reason the AV1 path takes spec[1]: several of these
+ * buffers are written from the size the hardware parsed rather than anything
+ * the host tells it, so sizing them for 1080p and then meeting a 4K stream
+ * writes past the end.  It is one allocation for the decoder, not per picture.
+ */
+
+#include <linux/dma-mapping.h>
+#include <linux/delay.h>
+#include <linux/firmware.h>
+#include <linux/fs.h>
+#include <linux/unaligned.h>
+#include <linux/module.h>
+#include "aml_vdec_drv.h"
+#include "aml_vdec_hw.h"
+#include "vp9.h"
+#include "vp9_prob.h"
+
+#define VP9_IPP_SIZE		0x4000
+#define VP9_SHORT_TERM_RPS_SIZE	0x0800
+#define VP9_VPS_SIZE		0x0800
+#define VP9_SPS_SIZE		0x0800
+#define VP9_PPS_SIZE		0x2000
+#define VP9_SWAP_BUF_SIZE	0x0800
+#define VP9_SWAP_BUF2_SIZE	0x0800
+/*
+ * Deblocking parameter and data storage, and the motion-prediction "above"
+ * context.  These were all smaller than the reference driver's smallest
+ * (1920x1088) work-buffer spec.  Undersized, the picture keeps its structure
+ * but goes soft part way down the frame - the deblocking filter runs off the
+ * end of its parameters - and only at resolutions large enough to reach it,
+ * which is why 320x240 and 640x480 were bit-exact while 1080p was not.
+ */
+#define VP9_DBLK_PARA_SIZE	0x80000
+#define VP9_DBLK_DATA_SIZE	0x80000
+#define VP9_SEG_MAP_SIZE	0xd800
+#define VP9_MMU_VBH_SIZE	0x5000
+#define VP9_MPRED_ABOVE_SIZE	0x10000
+
+/* Small buffers the microcode DMAs to and from, same shapes as AV1. */
+#define VP9_RPM_SIZE		(0x80 * 2)
+#define VP9_LMEM_SIZE		(0x600 * 2)	/* LMEM_BUF_SIZE in the reference */
+#define VP9_PROB_SIZE		0x5000
+#define VP9_COUNT_SIZE		(0x300 * 4 * 4)
+/* One 32-bit page entry per 4 KiB page, MAX_FRAME_4K_NUM of them. */
+#define VP9_MMU_MAP_SIZE	(0x1200 * 4)
+
+#define VP9_PADDING_SIZE	(1024 * 16)
+#define VP9_AMLV_HEADER_SIZE	16
+
+/* Same fixed parser skip configuration the AV1 path uses. */
+#define PARSER_CMD_SKIP_CFG_0   0x0000090b
+#define PARSER_CMD_SKIP_CFG_1   0x1b14140f
+#define PARSER_CMD_SKIP_CFG_2   0x001b1910
+
+struct vp9_workspace {
+	void *vaddr;
+	dma_addr_t paddr;
+	size_t size;
+
+	dma_addr_t ipp, short_term_rps, vps, sps, pps;
+	dma_addr_t swap_buf, swap_buf2;
+	dma_addr_t dblk_para, dblk_data, seg_map, mmu_vbh, mpred_above;
+
+	/* Separately allocated, because the host reads these back. */
+	void *side_vaddr;
+	dma_addr_t side_paddr;
+	size_t side_size;
+	dma_addr_t rpm, lmem, prob, count, mmu_map;
+
+	void *stream_vaddr;
+	dma_addr_t stream_paddr;
+	size_t stream_size;
+	size_t payload;
+};
+
+static struct vp9_workspace vp9_ws;
+static u32 vp9_decode_count;
+
+/*
+ * Secure-world VP9 entropy helpers exist in the reference driver
+ * (tee_vp9_prob_malloc / tee_vp9_prob_process, SMC 0xb200e070..72): on G12A
+ * and later it can hand the whole backward update to the secure OS, which
+ * would explain a microcode that writes its symbol counts somewhere the host
+ * cannot see.  This board's secure OS does not implement them - the allocate
+ * call returns 0xffffffff, "unknown function" - so that route is closed here.
+ */
+
+/* DOS_SW_RESET3 bits aml_vp9_reset_core() pulses between frames. */
+#define VP9_RESET_MASK	(BIT(3) | BIT(4) | BIT(8) | BIT(10) | BIT(11) | \
+			 BIT(12) | BIT(14) | BIT(15) | BIT(17) | BIT(18) | \
+			 BIT(19))
+
+/* Defined with the picture-configuration code further down. */
+static void vp9_free_slots(struct aml_vdec_hw *hw);
+static void vp9_reset_refs(void);
+
+static void wr(struct aml_vdec_hw *hw, u32 reg, u32 val)
+{
+	regmap_write(hw->map[DOS_BUS], reg, val);
+}
+
+void aml_vp9_teardown(struct aml_vdec_hw *hw)
+{
+	vp9_free_slots(hw);
+	wr(hw, HEVC_ASSIST_MBOX0_MASK, 0);
+	if (vp9_ws.stream_vaddr) {
+		dma_free_coherent(hw->dev, vp9_ws.stream_size,
+				  vp9_ws.stream_vaddr, vp9_ws.stream_paddr);
+		vp9_ws.stream_vaddr = NULL;
+	}
+	if (vp9_ws.side_vaddr) {
+		dma_free_coherent(hw->dev, vp9_ws.side_size,
+				  vp9_ws.side_vaddr, vp9_ws.side_paddr);
+		vp9_ws.side_vaddr = NULL;
+	}
+	if (vp9_ws.vaddr) {
+		dma_free_coherent(hw->dev, vp9_ws.size, vp9_ws.vaddr,
+				  vp9_ws.paddr);
+		vp9_ws.vaddr = NULL;
+	}
+}
+
+static int vp9_alloc(struct aml_vdec_hw *hw)
+{
+	dma_addr_t p;
+
+	if (vp9_ws.vaddr)
+		return 0;
+
+	vp9_ws.size = VP9_IPP_SIZE + VP9_SHORT_TERM_RPS_SIZE + VP9_VPS_SIZE +
+		      VP9_SPS_SIZE + VP9_PPS_SIZE + VP9_SWAP_BUF_SIZE +
+		      VP9_SWAP_BUF2_SIZE + VP9_DBLK_PARA_SIZE +
+		      VP9_DBLK_DATA_SIZE + VP9_SEG_MAP_SIZE + VP9_MMU_VBH_SIZE +
+		      VP9_MPRED_ABOVE_SIZE;
+	vp9_ws.size = ALIGN(vp9_ws.size, SZ_64K);
+
+	vp9_ws.vaddr = dma_alloc_coherent(hw->dev, vp9_ws.size, &vp9_ws.paddr,
+					  GFP_KERNEL);
+	if (!vp9_ws.vaddr)
+		return -ENOMEM;
+
+	/* Carve in the order the reference's init_buff_spec() lays them out. */
+	p = vp9_ws.paddr;
+#define CARVE(field, sz)	do { vp9_ws.field = p; p += (sz); } while (0)
+	CARVE(ipp, VP9_IPP_SIZE);
+	CARVE(short_term_rps, VP9_SHORT_TERM_RPS_SIZE);
+	CARVE(vps, VP9_VPS_SIZE);
+	CARVE(sps, VP9_SPS_SIZE);
+	CARVE(pps, VP9_PPS_SIZE);
+	CARVE(swap_buf, VP9_SWAP_BUF_SIZE);
+	CARVE(swap_buf2, VP9_SWAP_BUF2_SIZE);
+	CARVE(dblk_para, VP9_DBLK_PARA_SIZE);
+	CARVE(dblk_data, VP9_DBLK_DATA_SIZE);
+	CARVE(seg_map, VP9_SEG_MAP_SIZE);
+	CARVE(mmu_vbh, VP9_MMU_VBH_SIZE);
+	CARVE(mpred_above, VP9_MPRED_ABOVE_SIZE);
+#undef CARVE
+
+	vp9_ws.side_size = ALIGN(SZ_4K + VP9_PROB_SIZE + SZ_4K +
+				 VP9_COUNT_SIZE + SZ_4K + VP9_MMU_MAP_SIZE,
+				 SZ_64K);
+	vp9_ws.side_vaddr = dma_alloc_coherent(hw->dev, vp9_ws.side_size,
+					       &vp9_ws.side_paddr, GFP_KERNEL);
+	if (!vp9_ws.side_vaddr) {
+		aml_vp9_teardown(hw);
+		return -ENOMEM;
+	}
+	p = vp9_ws.side_paddr;
+#define CARVE(field, sz)	do { vp9_ws.field = p; p += (sz); } while (0)
+	CARVE(rpm, VP9_RPM_SIZE);
+	CARVE(lmem, VP9_LMEM_SIZE);
+	/*
+	 * The probability buffer holds four frame contexts at a 0x1000 stride
+	 * and the microcode addresses them by that stride, so it has to start
+	 * on a 0x1000 boundary; the count buffer it DMAs the symbol counts to
+	 * gets the same treatment.  Carved out of the side buffer behind the
+	 * RPM and LMEM areas they landed at +0x900, and the microcode then
+	 * wrote neither of them - the whole entropy exchange was silently
+	 * dead, which is what left every non-error-resilient stream decoding
+	 * garbage from its second frame on.
+	 */
+	p = ALIGN(p, SZ_4K);
+	CARVE(prob, VP9_PROB_SIZE);
+	p = ALIGN(p, SZ_4K);
+	CARVE(count, VP9_COUNT_SIZE);
+	p = ALIGN(p, SZ_4K);
+	CARVE(mmu_map, VP9_MMU_MAP_SIZE);
+#undef CARVE
+
+	dev_info(hw->dev,
+		 "vp9: workspace %zu KiB at %pad, side %zu KiB at %pad\n",
+		 vp9_ws.size / 1024, &vp9_ws.paddr,
+		 vp9_ws.side_size / 1024, &vp9_ws.side_paddr);
+	return 0;
+}
+
+/* vp9_config_work_space_hw(), front and back together. */
+static void vp9_config_workspace(struct aml_vdec_hw *hw)
+{
+	wr(hw, HEVC_RPM_BUFFER, vp9_ws.rpm);
+	wr(hw, HEVC_SHORT_TERM_RPS, vp9_ws.short_term_rps);
+	wr(hw, HEVC_PPS_BUFFER, vp9_ws.pps);
+	wr(hw, HEVC_STREAM_SWAP_BUFFER, vp9_ws.swap_buf);
+	wr(hw, HEVC_STREAM_SWAP_BUFFER2, vp9_ws.swap_buf2);
+	wr(hw, VP9_PROB_SWAP_BUFFER, vp9_ws.prob);
+	wr(hw, VP9_COUNT_SWAP_BUFFER, vp9_ws.count);
+	wr(hw, VP9_SEG_MAP_BUFFER, vp9_ws.seg_map);
+	wr(hw, VP9_LMEM_DUMP_ADR, vp9_ws.lmem);
+
+	wr(hw, HEVCD_IPP_LINEBUFF_BASE, vp9_ws.ipp);
+	wr(hw, HEVC_DBLK_CFGE, vp9_ws.dblk_para);
+	wr(hw, HEVC_DBLK_CFG4, vp9_ws.dblk_para);
+	wr(hw, HEVC_DBLK_CFG5, vp9_ws.dblk_data);
+	/* 4096x2304 and below take the default left-storage split. */
+	wr(hw, HEVC_DBLK_CFG3, 0x404010);
+
+	/* paged_mem_mode: the picture goes through the MMU. */
+	wr(hw, HEVCD_MPP_DECOMP_CTL1, BIT(4));
+
+	wr(hw, HEVC_SAO_MMU_VH0_ADDR, vp9_ws.mmu_vbh);
+	wr(hw, HEVC_SAO_MMU_VH1_ADDR, vp9_ws.mmu_vbh + VP9_MMU_VBH_SIZE / 2);
+	/*
+	 * From G12A onwards the page table address lives in
+	 * HEVC_ASSIST_MMU_MAP_ADDR, not in the scratch register the older
+	 * microcode read; S4 is well past that.  HEVC_SAO_MMU_DMA_CTRL is what
+	 * the SAO block itself follows, and the AV1 path programs it, so
+	 * program both rather than guess which one this microcode honours.
+	 */
+	wr(hw, HEVC_ASSIST_MMU_MAP_ADDR, vp9_ws.mmu_map);
+	wr(hw, HEVC_SAO_MMU_DMA_CTRL, vp9_ws.mmu_map);
+}
+
+/*
+ * vp9_init_decoder_hw().  Identical in shape to the AV1 one - the parser
+ * command table is the difference that matters, and VP9 enables the IPP in
+ * plain mode rather than AV1 mode.
+ */
+#define VP9_PARSER_CMD_NUMBER	37
+static const u16 vp9_parser_cmd[VP9_PARSER_CMD_NUMBER] = {
+	0x0401, 0x8401, 0x0800, 0x0402, 0x9002, 0x1423,
+	0x8CC3, 0x1423, 0x8804, 0x9825, 0x0800, 0x04FE,
+	0x8406, 0x8411, 0x1800, 0x8408, 0x8409, 0x8C2A,
+	0x9C2B, 0x1C00, 0x840F, 0x8407, 0x8000, 0x8408,
+	0x2000, 0xA800, 0x8410, 0x04DE, 0x840C, 0x840D,
+	0xAC00, 0xA000, 0x08C0, 0x08E0, 0xA40E, 0xFC00,
+	0x7C00
+};
+
+static void vp9_init_decoder(struct aml_vdec_hw *hw)
+{
+	u32 v;
+	int i;
+
+	v = read_dos_reg(hw, HEVC_PARSER_INT_CONTROL);
+	v &= ~(7 << 29);
+	v |= (3 << 29) |
+	     (1 << 24) |	/* stream_buffer_empty_int_amrisc_enable */
+	     (1 << 22) |	/* stream_fifo_empty_int_amrisc_enable */
+	     (1 << 7)  |	/* dec_done_int_cpu_enable */
+	     (1 << 4)  |	/* startcode_found_int_cpu_enable */
+	     (1 << 0);		/* parser_int_enable */
+	wr(hw, HEVC_PARSER_INT_CONTROL, v);
+
+	/* VP9 has no emulation prevention, so only the startcode check is on. */
+	v = read_dos_reg(hw, HEVC_SHIFT_STATUS);
+	v |= BIT(0);
+	wr(hw, HEVC_SHIFT_STATUS, v);
+
+	wr(hw, HEVC_SHIFT_CONTROL,
+	   (0 << 14) |	/* disable_start_code_protect */
+	   (1 << 10) |	/* length_zero_startcode_en, VP9 */
+	   (1 << 9)  |	/* length_valid_startcode_en, VP9 */
+	   (3 << 6)  |	/* sft_valid_wr_position */
+	   (2 << 4)  |	/* emulate_code_length_sub_1 */
+	   (3 << 1)  |	/* start_code_length_sub_1, 4-byte code */
+	   (1 << 0));	/* stream_shift_enable */
+
+	wr(hw, HEVC_CABAC_CONTROL, BIT(0));
+	wr(hw, HEVC_PARSER_CORE_CONTROL, BIT(0));
+	wr(hw, HEVC_DEC_STATUS_REG, 0);
+
+	wr(hw, HEVC_IQIT_SCALELUT_WR_ADDR, 0);
+	for (i = 0; i < 1024; i++)
+		wr(hw, HEVC_IQIT_SCALELUT_DATA, 0);
+
+	wr(hw, HEVC_STREAM_SWAP_TEST, 0);
+	wr(hw, DECODE_MODE, (0x80 << 24) | 2);
+	wr(hw, HEVC_DECODE_SIZE, 0);
+	wr(hw, HEVC_DECODE_COUNT, 0);
+
+	wr(hw, HEVC_PARSER_CMD_WRITE, BIT(16));
+	for (i = 0; i < VP9_PARSER_CMD_NUMBER; i++)
+		wr(hw, HEVC_PARSER_CMD_WRITE, vp9_parser_cmd[i]);
+	wr(hw, HEVC_PARSER_CMD_SKIP_0, PARSER_CMD_SKIP_CFG_0);
+	wr(hw, HEVC_PARSER_CMD_SKIP_1, PARSER_CMD_SKIP_CFG_1);
+	wr(hw, HEVC_PARSER_CMD_SKIP_2, PARSER_CMD_SKIP_CFG_2);
+
+	wr(hw, HEVC_PARSER_IF_CONTROL,
+	   (1 << 5) |	/* parser_sao_if_en */
+	   (1 << 2) |	/* parser_mpred_if_en */
+	   (1 << 0));	/* parser_scaler_if_en */
+
+	wr(hw, HEVCD_IPP_TOP_CNTL, BIT(0));	/* reset ipp/mpp */
+	wr(hw, HEVCD_IPP_TOP_CNTL, BIT(1));	/* enable ipp */
+}
+
+static void vp9_prot_init(struct aml_vdec_hw *hw)
+{
+	/*
+	 * Stream fetch stays off here for the same reason as AV1: turning it
+	 * on before the FIFO pointers are programmed lets the shifter run
+	 * against stale pointers, and the microcode then reports the packet
+	 * finished without having found a frame.  aml_vp9_feed() enables it.
+	 */
+	wr(hw, HEVC_SHIFT_STARTCODE, 0x00000001);
+	wr(hw, HEVC_SHIFT_EMULATECODE, 0x00000300);
+	wr(hw, HEVC_WAIT_FLAG, 1);
+
+	wr(hw, HEVC_ASSIST_MBOX0_CLR_REG, 1);
+	wr(hw, HEVC_ASSIST_MBOX0_MASK, 1);
+
+	wr(hw, HEVC_PSCALE_CTRL, 0);
+	wr(hw, DEBUG_REG1, 0);
+	wr(hw, NAL_SEARCH_CTL, 0x8);
+	wr(hw, DECODE_STOP_POS, 0);
+}
+
+/*
+ * hevc_reset_core() from the reference's vdec.c, which is what runs between
+ * chunks - and is a great deal more than the two CPU resets the AV1 path gets
+ * away with.  It also resets the parser (3), the parser state machine (4), the
+ * deblocker (8), lmem write response (10), ddr (13), iqit (14), ipp (15),
+ * qdct (17), mpred (18), sao (19), the HEVC async FIFO (24) and the MMU (26).
+ *
+ * The reason this matters here: after frame 0 the stream shifter would not
+ * fetch again.  HEVC_STREAM_RD_PTR stayed exactly where the host put it while
+ * the FIFO held a full frame, so the microcode had nothing to parse and never
+ * posted a status.  Resetting only the two CPUs leaves the parser and the
+ * async FIFO holding state from the finished frame.
+ */
+void aml_vp9_reset_core(struct aml_vdec_hw *hw)
+{
+	int i;
+
+	wr(hw, HEVC_STREAM_CONTROL, 0);
+
+	/*
+	 * Deliberately NOT the reference's full mask.  Adding its bus-facing
+	 * bits - 13 (ddr), 24 (hevc_afifo) and 26 (rst_mmu_n) - to this,
+	 * together with the HEVC_SAO_MMU_RESET_CTRL pulse around it, hung the
+	 * whole SoC instantly: no console output, no ping, serial alive but
+	 * silent, recoverable only by pulling power.  The reference can do it
+	 * because vdec.c gates the DMC ports off first (dec_dmc_port_ctrl());
+	 * nothing here does, so resetting the memory path underneath
+	 * outstanding DMA wedges the bus and takes the CPUs with it.
+	 *
+	 * What is left is the parser (3), its state machine (4), and the
+	 * per-block resets: dblk (8), lmem write response (10), the two CPUs
+	 * (11, 12), iqit (14), ipp (15), qdct (17), mpred (18), sao (19).
+	 */
+	wr(hw, DOS_SW_RESET3, VP9_RESET_MASK);
+	wr(hw, DOS_SW_RESET3, 0);
+
+	/* Wait for outstanding lmem writes to retire, bounded. */
+	for (i = 0; i < 10000; i++) {
+		if (!(read_dos_reg(hw, HEVC_WRRSP_LMEM) & 0xfff))
+			break;
+		udelay(1);
+	}
+	if (i == 10000)
+		dev_warn(hw->dev, "vp9: lmem write response never drained\n");
+}
+
+/*
+ * Put the hardware back the way it was without touching anything that has to
+ * survive between frames.  The reference does exactly this before every chunk
+ * - vp9_hw_ctx_restore() calls vvp9_prot_init() again - because it stops the
+ * core between frames and a stopped core forgets its register state.  What it
+ * must not forget is the side buffer: the probability and count buffers carry
+ * the entropy context from one frame to the next, and the segment map carries
+ * segment ids.
+ */
+void aml_vp9_prot_reinit(struct aml_vdec_hw *hw)
+{
+	vp9_prot_init(hw);
+}
+
+void aml_vp9_restart(struct aml_vdec_hw *hw)
+{
+	vp9_config_workspace(hw);
+	vp9_init_decoder(hw);
+	vp9_prot_init(hw);
+}
+
+/*
+ * Fill all four frame contexts with the VP9 defaults.  This is what a key
+ * frame's setup_past_independence() leaves behind, and the microcode reloads
+ * its contexts from here every time it is started - which is once per frame.
+ */
+static void vp9_seed_default_probs(struct aml_vdec_hw *hw)
+{
+	u8 *prob;
+	int i;
+
+	if (!vp9_ws.side_vaddr)
+		return;
+	prob = vp9_ws.side_vaddr + (vp9_ws.prob - vp9_ws.side_paddr);
+	for (i = 0; i < 4; i++)
+		aml_vp9_pack_probs(prob + i * 0x1000, &v4l2_vp9_default_probs);
+	dev_dbg(hw->dev, "vp9: seeded four frame contexts with the defaults\n");
+}
+
+int aml_vp9_bringup(struct aml_vdec_hw *hw, u32 width, u32 height)
+{
+	int ret;
+	int i;
+
+	/*
+	 * Clear the scratch registers the other microcode on this core may
+	 * have left pointing at its own buffers - see the note on
+	 * hevc_clear_scratch() in av1.c.  SCRATCH_0 and _1 are programmed
+	 * immediately afterwards.
+	 */
+	for (i = 2; i <= 0xf; i++)
+		wr(hw, REG_ALIGN(0x30c0 + i), 0);
+
+	ret = vp9_alloc(hw);
+	if (ret)
+		return ret;
+
+	memset(vp9_ws.side_vaddr, 0, vp9_ws.side_size);
+	vp9_seed_default_probs(hw);
+	vp9_reset_refs();
+	aml_vp9_restart(hw);
+	return 0;
+}
+
+/*
+ * Put one VP9 frame in front of the shifter.  Unlike AV1 there is no
+ * reframing: the microcode's start-code search works on the frame as it
+ * stands, so this only has to place it and point the FIFO at it.
+ */
+int aml_vp9_feed(struct aml_vdec_hw *hw, const void *data, size_t size)
+{
+	size_t want = ALIGN(size + VP9_PADDING_SIZE, SZ_64K);
+	u32 end;
+
+	if (!data || !size)
+		return -EINVAL;
+
+	if (vp9_ws.stream_vaddr && vp9_ws.stream_size < want) {
+		dma_free_coherent(hw->dev, vp9_ws.stream_size,
+				  vp9_ws.stream_vaddr, vp9_ws.stream_paddr);
+		vp9_ws.stream_vaddr = NULL;
+	}
+	if (!vp9_ws.stream_vaddr) {
+		vp9_ws.stream_vaddr = dma_alloc_coherent(hw->dev, want,
+							 &vp9_ws.stream_paddr,
+							 GFP_KERNEL);
+		if (!vp9_ws.stream_vaddr)
+			return -ENOMEM;
+		vp9_ws.stream_size = want;
+	}
+
+	/*
+	 * Two ways to put a frame in front of the shifter.
+	 *
+	 * The plain one rewinds the FIFO for every frame, which is only safe
+	 * when the core is restarted with it.  The ring one appends after the
+	 * previous frame and only advances the write pointer, the way the
+	 * reference's parser feeds a running decoder - that is what lets the
+	 * microcode keep running, and keep its entropy contexts, between
+	 * frames.
+	 */
+
+	memset(vp9_ws.stream_vaddr, 0, vp9_ws.stream_size);
+	/*
+	 * Frame-base *with* a head, which is what the vendor's V4L2
+	 * path asks for (`no_head:0` in vdec_vp9_if.c) and what the
+	 * AV1 microcode on this same core also wants.  VP9's prefix is
+	 * 16 bytes and is built by add_prefix_data() there:
+	 *
+	 *   [0..3]   big endian (frame_size + 16)
+	 *   [4..7]   ones complement of [0..3]
+	 *   [8..11]  00 00 00 01   <- the start code the shifter hunts
+	 *   [12..15] 'A' 'M' 'L' 'V'
+	 *
+	 * Note this is *not* AV1's 20-byte header: no LEB128 tail, and
+	 * the length counts the prefix rather than adding four.
+	 */
+	u32 psize = size + VP9_AMLV_HEADER_SIZE;
+	u8 *h = vp9_ws.stream_vaddr;
+
+	memset(h, 0, VP9_AMLV_HEADER_SIZE);
+	h[0] = psize >> 24;
+	h[1] = psize >> 16;
+	h[2] = psize >> 8;
+	h[3] = psize;
+	h[4] = h[0] ^ 0xff;
+	h[5] = h[1] ^ 0xff;
+	h[6] = h[2] ^ 0xff;
+	h[7] = h[3] ^ 0xff;
+	h[11] = 1;
+	h[12] = 'A';
+	h[13] = 'M';
+	h[14] = 'L';
+	h[15] = 'V';
+	memcpy(vp9_ws.stream_vaddr + VP9_AMLV_HEADER_SIZE, data, size);
+	size += VP9_AMLV_HEADER_SIZE;
+	vp9_ws.payload = size;
+
+	/*
+	 * The end pointer bounds the FIFO, not the payload - ending it at the
+	 * payload puts the write pointer outside the ring and the microcode
+	 * waits for data that can never arrive.  This is the bug that cost the
+	 * AV1 path a long time; keep the whole buffer in the ring.
+	 */
+	end = vp9_ws.stream_paddr + vp9_ws.stream_size - 1;
+
+	wr(hw, HEVC_STREAM_START_ADDR, vp9_ws.stream_paddr);
+	wr(hw, HEVC_STREAM_END_ADDR, end);
+	wr(hw, HEVC_STREAM_RD_PTR, vp9_ws.stream_paddr);
+	wr(hw, HEVC_STREAM_WR_PTR,
+	   vp9_ws.stream_paddr + vp9_ws.payload + VP9_PADDING_SIZE);
+
+	/*
+	 * Reset the shift byte count.  The microcode compares it against
+	 * HEVC_DECODE_SIZE to know where the frame ends, so leaving the
+	 * previous frame's count in place makes the second frame look already
+	 * finished before it starts - which is exactly how frame 1 came back
+	 * with no answer at all while frame 0 decoded perfectly.  The
+	 * reference writes it for every frame-based chunk.
+	 */
+	wr(hw, HEVC_SHIFT_BYTE_COUNT, 0);
+	wr(hw, HEVC_DECODE_SIZE, vp9_ws.payload);
+	wr(hw, HEVC_DECODE_COUNT, vp9_decode_count++);
+	wr(hw, VP9_LMEM_DUMP_ADR, vp9_ws.lmem);
+
+	/* Enable stream fetch now that the pointers are right. */
+	wr(hw, HEVC_STREAM_CONTROL,
+	   (read_dos_reg(hw, HEVC_STREAM_CONTROL) & ~0xfff0) |
+	   (7 << 4) | BIT(0));
+	/* stream_fifo_hole, G12A and later. */
+	wr(hw, HEVC_STREAM_FIFO_CTL,
+	   read_dos_reg(hw, HEVC_STREAM_FIFO_CTL) | BIT(29));
+
+	/*
+	 * Deliberately not writing the status register or starting the core
+	 * here.  Doing either from feed() lets the microcode run against a
+	 * half-configured decoder: the first attempt did exactly that, the
+	 * shifter drained the whole FIFO before the caller had reset the core,
+	 * and the only answer left in the mailbox was HEVC_ACTION_DONE.  The
+	 * caller kicks it once everything is in place.
+	 */
+	dev_dbg(hw->dev, "vp9: fed %zu bytes at %pad, fifo %zu KiB\n",
+		vp9_ws.payload, &vp9_ws.stream_paddr,
+		 vp9_ws.stream_size / 1024);
+	return 0;
+}
+
+/* Load a test frame from /lib/firmware, the way the AV1 probe does. */
+int aml_vp9_feed_file(struct aml_vdec_hw *hw, const char *name)
+{
+	const struct firmware *fw;
+	int ret;
+
+	ret = request_firmware(&fw, name, hw->dev);
+	if (ret) {
+		dev_err(hw->dev, "vp9: test stream %s not found (%d)\n",
+			name, ret);
+		return ret;
+	}
+	ret = aml_vp9_feed(hw, fw->data, fw->size);
+	release_firmware(fw);
+	return ret;
+}
+
+void aml_vp9_dump_state(struct aml_vdec_hw *hw)
+{
+	dev_info(hw->dev,
+		 "vp9: fifo start %08x end %08x rd %08x wr %08x level %08x\n",
+		 read_dos_reg(hw, HEVC_STREAM_START_ADDR),
+		 read_dos_reg(hw, HEVC_STREAM_END_ADDR),
+		 read_dos_reg(hw, HEVC_STREAM_RD_PTR),
+		 read_dos_reg(hw, HEVC_STREAM_WR_PTR),
+		 read_dos_reg(hw, HEVC_STREAM_LEVEL));
+	dev_info(hw->dev, "vp9: status %08x mpc %08x mpsr %08x\n",
+		 read_dos_reg(hw, HEVC_DEC_STATUS_REG),
+		 read_dos_reg(hw, HEVC_MPC_E),
+		 read_dos_reg(hw, HEVC_MPSR));
+}
+
+/*
+ * Read back what the microcode parsed.  The RPM comes over scrambled the same
+ * way AV1's does - entries reversed within groups of four - so unscramble it
+ * here rather than making every caller remember.
+ */
+void aml_vp9_dump_params(struct aml_vdec_hw *hw)
+{
+	const u16 *raw;
+	u16 pr[0x80];
+	int i, j;
+
+	if (!vp9_ws.side_vaddr)
+		return;
+	raw = vp9_ws.side_vaddr + (vp9_ws.rpm - vp9_ws.side_paddr);
+	for (i = 0; i < 0x80; i += 4)
+		for (j = 0; j < 4; j++)
+			pr[i + j] = raw[i + 3 - j];
+
+	dev_info(hw->dev, "vp9: rpm[0..15] %*ph\n", 32, pr);
+	dev_info(hw->dev, "vp9: rpm[16..31] %*ph\n", 32, pr + 16);
+}
+
+/* ------------------------------------------------------------------------
+ * Picture configuration
+ *
+ * Everything above this line gets the microcode as far as parsing a frame
+ * header.  From here the host has to give it somewhere to put a picture: a
+ * compressed body reached through the MMU page table, a compressed header,
+ * a collocated-MV buffer, and - for the plain NV12 copy the display wants -
+ * a double-write target.  The reference splits this across config_pic_size(),
+ * config_mc_buffer(), config_mpred_hw(), config_sao_hw() and
+ * vp9_loop_filter_frame_init(); it is one function here because the order
+ * between them matters and splitting it only hides that.
+ * ------------------------------------------------------------------------
+ */
+
+/*
+ * VP9 keeps eight reference slots and the picture being decoded needs one no
+ * reference names, so eight is one too few.  Ten, for the same reason the AV1
+ * path uses ten.
+ */
+#define VP9_SLOTS		10
+#define VP9_REF_FRAMES		8
+#define VP9_REFS_PER_FRAME	3
+#define VP9_MV_MEM_UNIT		0x240
+
+struct vp9_slot {
+	void *body_v;
+	dma_addr_t body_p;
+	void *hdr_v;
+	dma_addr_t hdr_p;
+	void *mv_v;
+	dma_addr_t mv_p;
+	void *dw_v;
+	dma_addr_t dw_p;
+	u32 width, height;
+	bool intra;
+	bool show;
+};
+
+static struct vp9_slot vp9_slots[VP9_SLOTS];
+static size_t vp9_slot_body_size, vp9_slot_hdr_size, vp9_slot_mv_size;
+static size_t vp9_slot_dw_size;
+static int vp9_cur_slot;
+static int vp9_prev_slot;
+static int vp9_out_slot = -1;
+static bool vp9_cur_show;
+static u32 vp9_pending_refresh;
+static int vp9_ref_slot[VP9_REF_FRAMES];
+/* The picture slot each of LAST/GOLDEN/ALTREF resolves to. */
+static int vp9_frame_slot[VP9_REFS_PER_FRAME];
+static u32 vp9_last_width, vp9_last_height;
+static bool vp9_have_key;
+/*
+ * Entropy adaptation needs to know whether this frame and the one before it
+ * were key frames - the update factors differ - and the reference keeps a
+ * picture counter purely for its own tracing.
+ */
+static bool vp9_cur_kf;
+
+/* Start a stream from nothing: no key frame seen, no slot referenced. */
+static void vp9_reset_refs(void)
+{
+	vp9_have_key = false;
+	vp9_out_slot = -1;
+	vp9_cur_slot = 0;
+	vp9_prev_slot = 0;
+	vp9_cur_show = false;
+	vp9_pending_refresh = 0;
+	vp9_decode_count = 0;
+	vp9_cur_kf = false;
+	memset(vp9_ref_slot, 0, sizeof(vp9_ref_slot));
+	memset(vp9_frame_slot, 0, sizeof(vp9_frame_slot));
+}
+
+/* Where the double-write unit should lay the NV12 picture down, when the
+ * caller owns the buffer.  Zero means "this picture's own slot buffer".
+ */
+static dma_addr_t vp9_dw_target_y, vp9_dw_target_c;
+static size_t vp9_dw_target_size;
+
+/* Profile 2: 10-bit.  The double write still lays down rounded NV12. */
+static bool vp9_ten_bit;
+
+/*
+ * Per 64x32 block: 3200 bytes at 8 bit (memory saving), 4096 at 10 bit,
+ * which cannot use it.  Same formula as AV1 and HEVC.
+ */
+static u32 vp9_comp_body_size(u32 width, u32 height)
+{
+	return (vp9_ten_bit ? 4096 : 3200) * ((width + 63) >> 6) * ((height + 31) >> 5);
+}
+
+static u32 vp9_comp_header_size(u32 width, u32 height)
+{
+	return 32 * ((width + 127) >> 7) * ((height + 63) >> 6);
+}
+
+/*
+ * As with AV1 the compressed-header *allocation* is a fixed size per picture
+ * and has nothing to do with the length written to HEVC_CM_HEADER_LENGTH.
+ * Allocating the computed length lets the hardware write off the end.
+ */
+static size_t vp9_hdr_alloc_size(u32 width, u32 height)
+{
+	if (width * height > 4096 * 2304)
+		return 0x120000;
+	if (width * height > 1920 * 1088)
+		return 0x48000;
+	return 0x10000;
+}
+
+/* cal_mv_buf_size() in the reference. */
+static size_t vp9_mv_buf_size(u32 width, u32 height)
+{
+	u32 w_lcu = DIV_ROUND_UP(ALIGN(width, 64), 64);
+	u32 h_lcu = DIV_ROUND_UP(ALIGN(height, 32), 64);
+	u32 size_a = w_lcu * h_lcu * 36 * 16;
+	u32 size_b = w_lcu * 16 * ((h_lcu >> 3) + (h_lcu & 7));
+
+	return ALIGN(size_a + size_b, SZ_64K);
+}
+
+static void vp9_free_slots(struct aml_vdec_hw *hw)
+{
+	int i;
+
+	for (i = 0; i < VP9_SLOTS; i++) {
+		if (vp9_slots[i].body_v)
+			dma_free_coherent(hw->dev, vp9_slot_body_size,
+					  vp9_slots[i].body_v,
+					  vp9_slots[i].body_p);
+		if (vp9_slots[i].hdr_v)
+			dma_free_coherent(hw->dev, vp9_slot_hdr_size,
+					  vp9_slots[i].hdr_v,
+					  vp9_slots[i].hdr_p);
+		if (vp9_slots[i].mv_v)
+			dma_free_coherent(hw->dev, vp9_slot_mv_size,
+					  vp9_slots[i].mv_v, vp9_slots[i].mv_p);
+		if (vp9_slots[i].dw_v)
+			dma_free_noncoherent(hw->dev, vp9_slot_dw_size,
+					     vp9_slots[i].dw_v,
+					     vp9_slots[i].dw_p,
+					     DMA_FROM_DEVICE);
+		memset(&vp9_slots[i], 0, sizeof(vp9_slots[i]));
+	}
+	vp9_slot_body_size = 0;
+	vp9_slot_hdr_size = 0;
+	vp9_slot_mv_size = 0;
+	vp9_slot_dw_size = 0;
+}
+
+static int vp9_alloc_slots(struct aml_vdec_hw *hw, u32 width, u32 height)
+{
+	size_t body = ALIGN(vp9_comp_body_size(width, height), PAGE_SIZE);
+	size_t hdr = vp9_hdr_alloc_size(width, height);
+	size_t mv = vp9_mv_buf_size(width, height);
+	size_t dw = vp9_dw_target_y ? 0 : ALIGN(width * height * 3 / 2, SZ_64K);
+	int i;
+
+	if (vp9_slots[0].body_v && body == vp9_slot_body_size &&
+	    hdr == vp9_slot_hdr_size && mv == vp9_slot_mv_size &&
+	    dw == vp9_slot_dw_size)
+		return 0;
+
+	vp9_free_slots(hw);
+	vp9_slot_body_size = body;
+	vp9_slot_hdr_size = hdr;
+	vp9_slot_mv_size = mv;
+	vp9_slot_dw_size = dw;
+
+	for (i = 0; i < VP9_SLOTS; i++) {
+		vp9_slots[i].body_v = dma_alloc_coherent(hw->dev, body,
+							 &vp9_slots[i].body_p,
+							 GFP_KERNEL);
+		vp9_slots[i].hdr_v = dma_alloc_coherent(hw->dev, hdr,
+							&vp9_slots[i].hdr_p,
+							GFP_KERNEL);
+		vp9_slots[i].mv_v = dma_alloc_coherent(hw->dev, mv,
+						       &vp9_slots[i].mv_p,
+						       GFP_KERNEL);
+		/*
+		 * The picture the CPU reads in bulk once per frame.  Coherent
+		 * memory is uncached on arm64 and a 3 MB uncached read costs
+		 * more than the decode, so take cached memory and invalidate
+		 * it by hand - the same trade the AV1 path makes.
+		 */
+		if (dw)
+			vp9_slots[i].dw_v =
+				dma_alloc_noncoherent(hw->dev, dw,
+						      &vp9_slots[i].dw_p,
+						      DMA_FROM_DEVICE,
+						      GFP_KERNEL);
+		if (!vp9_slots[i].body_v || !vp9_slots[i].hdr_v ||
+		    !vp9_slots[i].mv_v || (dw && !vp9_slots[i].dw_v)) {
+			vp9_free_slots(hw);
+			return -ENOMEM;
+		}
+	}
+
+	dev_info(hw->dev,
+		 "vp9: %d slots, body %zu KiB, hdr %zu KiB, mv %zu KiB, dw %zu KiB each\n",
+		 VP9_SLOTS, body / 1024, hdr / 1024, mv / 1024, dw / 1024);
+	return 0;
+}
+
+/*
+ * The MMU page table: one page frame number per 4 KiB of compressed body.
+ * The hardware pulls pages from this list while writing the picture and
+ * records which it took in that picture's compressed header, so the order is
+ * free but every entry has to name a page this picture owns.
+ */
+static int vp9_fill_mmu_map(struct aml_vdec_hw *hw)
+{
+	struct vp9_slot *slot = &vp9_slots[vp9_cur_slot];
+	u32 *map, pages, first, i;
+
+	pages = vp9_slot_body_size / PAGE_SIZE;
+	if (!pages || pages * sizeof(u32) > VP9_MMU_MAP_SIZE)
+		return -EINVAL;
+
+	map = vp9_ws.side_vaddr + (vp9_ws.mmu_map - vp9_ws.side_paddr);
+	first = slot->body_p >> PAGE_SHIFT;
+	for (i = 0; i < pages; i++)
+		map[i] = first + i;
+
+	dev_dbg(hw->dev, "vp9: mmu map %u pages from pfn %08x\n", pages, first);
+	return 0;
+}
+
+/* init_pic_list_hw().  In MMU mode the table holds each picture's
+ * compressed-header address shifted right by 5.
+ */
+static void vp9_config_pic_list(struct aml_vdec_hw *hw)
+{
+	int i;
+
+	wr(hw, HEVCD_MPP_ANC2AXI_TBL_CONF_ADDR, BIT(1) | BIT(2));
+	for (i = 0; i < VP9_SLOTS; i++)
+		wr(hw, HEVCD_MPP_ANC2AXI_TBL_DATA, vp9_slots[i].hdr_p >> 5);
+	wr(hw, HEVCD_MPP_ANC2AXI_TBL_CONF_ADDR, BIT(0));
+
+	wr(hw, HEVCD_MPP_ANC_CANVAS_ACCCONFIG_ADDR, (0 << 8) | BIT(0));
+	for (i = 0; i < 32; i++)
+		wr(hw, HEVCD_MPP_ANC_CANVAS_DATA_ADDR, 0);
+}
+
+/*
+ * config_mc_buffer().  Names the three reference pictures twice - once for
+ * each of the two canvas tables the motion compensation block reads - and then
+ * hands over their sizes so it can scale a reference coded at a different
+ * resolution.  In MMU mode the "canvas" is just the picture index.
+ */
+static void vp9_config_mc_buffer(struct aml_vdec_hw *hw, u32 width, u32 height)
+{
+	u8 scale_enable = 0;
+	int i;
+
+	wr(hw, HEVCD_MPP_ANC_CANVAS_ACCCONFIG_ADDR, (0 << 8) | BIT(0));
+	for (i = 0; i < VP9_REFS_PER_FRAME; i++) {
+		u32 idx = vp9_frame_slot[i];
+
+		wr(hw, HEVCD_MPP_ANC_CANVAS_DATA_ADDR,
+		   (idx << 16) | (idx << 8) | idx);
+	}
+	wr(hw, HEVCD_MPP_ANC_CANVAS_ACCCONFIG_ADDR, (16 << 8) | BIT(0));
+	for (i = 0; i < VP9_REFS_PER_FRAME; i++) {
+		u32 idx = vp9_frame_slot[i];
+
+		wr(hw, HEVCD_MPP_ANC_CANVAS_DATA_ADDR,
+		   (idx << 16) | (idx << 8) | idx);
+	}
+
+	wr(hw, VP9D_MPP_REFINFO_TBL_ACCCONFIG, BIT(2));	/* auto increment */
+	for (i = 0; i < VP9_REFS_PER_FRAME; i++) {
+		struct vp9_slot *ref = &vp9_slots[vp9_frame_slot[i]];
+		u32 rw = ref->width ? ref->width : width;
+		u32 rh = ref->height ? ref->height : height;
+
+		wr(hw, VP9D_MPP_REFINFO_DATA, rw);
+		wr(hw, VP9D_MPP_REFINFO_DATA, rh);
+		if (rw != width || rh != height)
+			scale_enable |= 1 << i;
+		wr(hw, VP9D_MPP_REFINFO_DATA, (rw << 14) / width);
+		wr(hw, VP9D_MPP_REFINFO_DATA, (rh << 14) / height);
+		/* MMU mode: the body offset is carried by the page table. */
+		wr(hw, VP9D_MPP_REFINFO_DATA, 0);
+	}
+	wr(hw, VP9D_MPP_REF_SCALE_ENBL, scale_enable);
+}
+
+/*
+ * vp9_loop_filter_frame_init().  VP9's filter table is 16 words, packing the
+ * four reference classes for one (segment, mode) pair per word - a great deal
+ * simpler than AV1's 192.
+ */
+#define VP9_MAX_LOOP_FILTER	63
+#define VP9_MAX_SEGMENTS	8
+
+static int vp9_clamp_lvl(int v)
+{
+	return clamp(v, 0, VP9_MAX_LOOP_FILTER);
+}
+
+static void vp9_config_loop_filter(struct aml_vdec_hw *hw, const u16 *pr)
+{
+	u8 lvl[VP9_MAX_SEGMENTS][4][2];
+	int filt = pr[VP9_RPM_FILTER_LEVEL] & 0x3f;
+	int scale = 1 << (filt >> 5);
+	bool delta_en = pr[VP9_RPM_MODE_REF_DELTA_ENABLED] & 1;
+	bool seg_en = pr[VP9_RPM_SEG_ENABLED] & 1;
+	bool seg_abs = pr[VP9_RPM_SEG_ABS_DELTA] & 1;
+	s8 ref_deltas[4], mode_deltas[2];
+	int seg, ref, mode, i;
+
+	for (i = 0; i < 4; i++)
+		ref_deltas[i] = (s8)(u8)pr[VP9_RPM_REF_DELTAS + i];
+	for (i = 0; i < 2; i++)
+		mode_deltas[i] = (s8)(u8)pr[VP9_RPM_MODE_DELTAS + i];
+
+	for (seg = 0; seg < VP9_MAX_SEGMENTS; seg++) {
+		int lvl_seg = filt;
+		u16 info = pr[VP9_RPM_SEG_LF_INFO + seg];
+
+		/* bit 15 feature enabled, bit 8 sign, bits 5:0 magnitude */
+		if (seg_en && (info & 0x8000)) {
+			int data = (info & 0x100) ? -(int)(info & 0x3f)
+						  : (int)(info & 0x3f);
+
+			lvl_seg = vp9_clamp_lvl(seg_abs ? data : filt + data);
+		}
+
+		if (!delta_en) {
+			memset(lvl[seg], lvl_seg, sizeof(lvl[seg]));
+			continue;
+		}
+		/* INTRA_FRAME has no mode delta. */
+		lvl[seg][0][0] = vp9_clamp_lvl(lvl_seg + ref_deltas[0] * scale);
+		lvl[seg][0][1] = lvl[seg][0][0];
+		for (ref = 1; ref < 4; ref++)
+			for (mode = 0; mode < 2; mode++)
+				lvl[seg][ref][mode] =
+					vp9_clamp_lvl(lvl_seg +
+						      ref_deltas[ref] * scale +
+						      mode_deltas[mode] * scale);
+	}
+
+	for (i = 0; i < 16; i++) {
+		u32 level;
+		int s = i >> 1, m = i & 1;
+
+		level = ((lvl[s][3][m] & 0x3f) << 24) |
+			((lvl[s][2][m] & 0x3f) << 16) |
+			((lvl[s][1][m] & 0x3f) << 8) |
+			 (lvl[s][0][m] & 0x3f);
+		if (!filt)
+			level = 0;
+		wr(hw, HEVC_DBLK_CFGA, level);
+	}
+}
+
+/* vp9_loop_filter_init(): the sharpness-derived threshold table, written once
+ * per stream unless sharpness changes.  Kept per frame here because it is 32
+ * register writes and the sharpness is only known after the parse.
+ */
+static void vp9_config_lf_thresholds(struct aml_vdec_hw *hw, u32 sharpness)
+{
+	u8 mblim[VP9_MAX_LOOP_FILTER + 1], lim[VP9_MAX_LOOP_FILTER + 1];
+	int lvl, i;
+
+	for (lvl = 0; lvl <= VP9_MAX_LOOP_FILTER; lvl++) {
+		int inside = lvl >> ((sharpness > 0) + (sharpness > 4));
+
+		if (sharpness > 0 && inside > 9 - (int)sharpness)
+			inside = 9 - sharpness;
+		if (inside < 1)
+			inside = 1;
+		lim[lvl] = inside;
+		mblim[lvl] = 2 * (lvl + 2) + inside;
+	}
+
+	for (i = 0; i < 32; i++) {
+		u32 thr = ((lim[i * 2 + 1] & 0x3f) << 8) | mblim[i * 2 + 1];
+
+		thr = (thr << 16) | ((lim[i * 2] & 0x3f) << 8) | mblim[i * 2];
+		wr(hw, HEVC_DBLK_CFG9, thr);
+	}
+}
+
+int aml_vp9_config_picture(struct aml_vdec_hw *hw)
+{
+	const u16 *raw;
+	u16 pr[VP9_RPM_COUNT];
+	u32 width, height, lcu_total, uv_size, uv_h;
+	u32 data32, body, header;
+	dma_addr_t dw_y, dw_c;
+	bool inter, intra_only, use_prev_mvs;
+	int i, j, ret;
+
+	if (!vp9_ws.side_vaddr)
+		return -EINVAL;
+
+	raw = vp9_ws.side_vaddr + (vp9_ws.rpm - vp9_ws.side_paddr);
+	for (i = 0; i < VP9_RPM_COUNT; i += 4)
+		for (j = 0; j < 4; j++)
+			pr[i + j] = raw[i + 3 - j];
+
+	width = pr[VP9_RPM_WIDTH];
+	height = pr[VP9_RPM_HEIGHT];
+
+	/*
+	 * The MMU page table, the compressed body and the picture slots are
+	 * all sized from these.  If the microcode never actually parsed a
+	 * header they hold whatever was left in the RPM, and building a page
+	 * table from that points the hardware at memory it does not own -
+	 * which resets the board outright with no oops to read afterwards.
+	 */
+	if (width < VP9_MIN_DIM || height < VP9_MIN_DIM ||
+	    width > VP9_MAX_DIM || height > VP9_MAX_DIM) {
+		dev_err(hw->dev, "vp9: implausible picture size %ux%u, refusing\n",
+			width, height);
+		return -EINVAL;
+	}
+	/*
+	 * Profiles 0 and 1 code no bit depth - they are 8 bit - and the
+	 * microcode leaves the RPM word alone for them, so it still says 10
+	 * after a profile 2 stream.  Only trust it for profiles 2 and 3.
+	 */
+	vp9_ten_bit = false;
+	if (pr[VP9_RPM_PROFILE] >= 2) {
+		if (pr[VP9_RPM_BIT_DEPTH] != 10) {
+			dev_err(hw->dev, "vp9: bit depth %u not supported\n",
+				pr[VP9_RPM_BIT_DEPTH]);
+			return -EINVAL;
+		}
+		vp9_ten_bit = true;
+	}
+	/*
+	 * Only profiles 0 and 2 - 4:2:0, 8 or 10 bit.  Anything else parses far enough to
+	 * look plausible and then decodes to nothing, which reads like a
+	 * driver bug; say so instead.  ffmpeg picks profile 1 for a source
+	 * that is not already 4:2:0, so this is easy to hit by leaving
+	 * -pix_fmt yuv420p off an encode.
+	 */
+	if (pr[VP9_RPM_PROFILE] != 0 && pr[VP9_RPM_PROFILE] != 2) {
+		dev_err_once(hw->dev,
+			     "vp9: profile %u is not supported, only 0 and 2 (4:2:0)\n",
+			     pr[VP9_RPM_PROFILE]);
+		return -EINVAL;
+	}
+
+	/*
+	 * show_existing_frame carries no picture data: it asks for a reference
+	 * slot to be output again.  Decoding it as a real frame would write
+	 * over a live slot.
+	 */
+	if (pr[VP9_RPM_SHOW_EXISTING_FRAME] & 1) {
+		u32 idx = pr[VP9_RPM_FRAME_TO_SHOW_IDX] & 7;
+
+		vp9_out_slot = vp9_ref_slot[idx];
+		vp9_pending_refresh = 0;
+		dev_dbg(hw->dev, "vp9: show existing frame, slot %d\n",
+			vp9_out_slot);
+		return -EAGAIN;
+	}
+
+	intra_only = (pr[VP9_RPM_SHOW_FRAME] & 1) ? false :
+		     (pr[VP9_RPM_INTRA_ONLY] & 1);
+	inter = pr[VP9_RPM_FRAME_TYPE] != VP9_KEY_FRAME && !intra_only;
+
+	/*
+	 * A stream that starts on an inter frame has nothing to reference.
+	 * The reference refuses those outright rather than decoding against
+	 * uninitialised slots, and so does this.
+	 */
+	vp9_cur_kf = pr[VP9_RPM_FRAME_TYPE] == VP9_KEY_FRAME;
+	if (vp9_cur_kf) {
+		vp9_have_key = true;
+	} else if (!vp9_have_key) {
+		dev_dbg(hw->dev, "vp9: no key frame yet, skipping\n");
+		return -EAGAIN;
+	}
+
+	lcu_total = DIV_ROUND_UP(width, 64) * DIV_ROUND_UP(height, 64);
+	uv_size = lcu_total * 64 * 64 / 2;
+	uv_h = (uv_size + 0xffff) >> 16;
+
+	vp9_last_width = width;
+	vp9_last_height = height;
+	vp9_cur_show = pr[VP9_RPM_SHOW_FRAME] & 1;
+	vp9_pending_refresh = pr[VP9_RPM_REFRESH_FRAME_FLAGS] & 0xff;
+	if (!inter && pr[VP9_RPM_FRAME_TYPE] == VP9_KEY_FRAME)
+		vp9_pending_refresh = 0xff;
+
+	ret = vp9_alloc_slots(hw, width, height);
+	if (ret)
+		return ret;
+
+	/*
+	 * The picture address table has to be written after the slots exist:
+	 * filled during bring-up it would name physical address 0 for every
+	 * entry, which a key frame never notices and an inter frame reads its
+	 * references from.
+	 */
+	vp9_config_pic_list(hw);
+
+	/* A key frame resets the reference state; otherwise take a slot no
+	 * reference names, so decoding cannot destroy a picture still needed.
+	 */
+	if (!inter) {
+		vp9_cur_slot = 0;
+		for (i = 0; i < VP9_REF_FRAMES; i++)
+			vp9_ref_slot[i] = 0;
+	} else {
+		for (i = 0; i < VP9_SLOTS; i++) {
+			int cand = (vp9_cur_slot + 1 + i) % VP9_SLOTS;
+
+			for (j = 0; j < VP9_REF_FRAMES; j++)
+				if (vp9_ref_slot[j] == cand)
+					break;
+			if (j == VP9_REF_FRAMES) {
+				vp9_prev_slot = vp9_cur_slot;
+				vp9_cur_slot = cand;
+				break;
+			}
+		}
+		if (i == VP9_SLOTS) {
+			dev_err(hw->dev, "vp9: no free picture slot\n");
+			return -ENOBUFS;
+		}
+	}
+
+	/*
+	 * ref_info packs the three references most-significant first, four
+	 * bits each: the top three are the reference-map index, the bottom one
+	 * is that reference's sign bias.
+	 */
+	for (i = 0; i < VP9_REFS_PER_FRAME; i++) {
+		u32 shift = (VP9_REFS_PER_FRAME - i - 1) * 4;
+		u32 ref = (pr[VP9_RPM_REF_INFO] >> (shift + 1)) & 7;
+
+		vp9_frame_slot[i] = inter ? vp9_ref_slot[ref] : vp9_cur_slot;
+	}
+
+	vp9_slots[vp9_cur_slot].width = width;
+	vp9_slots[vp9_cur_slot].height = height;
+	vp9_slots[vp9_cur_slot].intra = !inter;
+	vp9_slots[vp9_cur_slot].show = vp9_cur_show;
+
+	ret = vp9_fill_mmu_map(hw);
+	if (ret)
+		return ret;
+	wr(hw, HEVC_ASSIST_MMU_MAP_ADDR, vp9_ws.mmu_map);
+	wr(hw, HEVC_SAO_MMU_DMA_CTRL, vp9_ws.mmu_map);
+
+	wr(hw, HEVC_PARSER_PICTURE_SIZE, (height << 16) | width);
+
+	/* ---- config_pic_size() ---- */
+	body = vp9_comp_body_size(width, height);
+	header = vp9_comp_header_size(width, height);
+	data32 = read_dos_reg(hw, HEVC_SAO_CTRL5);
+	if (vp9_ten_bit)
+		data32 &= ~BIT(9);
+	else
+		data32 |= BIT(9);	/* 8 bit, memory saving */
+	data32 |= BIT(10);		/* use HEVC_CM_HEADER_START_ADDR */
+	data32 &= ~(0xff << 16);	/* no down-scale on the double write */
+	wr(hw, HEVC_SAO_CTRL5, data32);
+	wr(hw, HEVCD_MPP_DECOMP_CTL1, BIT(4));	/* paged_mem_mode */
+	wr(hw, HEVC_CM_BODY_LENGTH, body);
+	wr(hw, HEVC_CM_HEADER_OFFSET, body);
+	wr(hw, HEVC_CM_HEADER_LENGTH, header);
+	wr(hw, HEVC_CM_BODY_START_ADDR, vp9_slots[vp9_cur_slot].body_p);
+	wr(hw, HEVC_CM_HEADER_START_ADDR, vp9_slots[vp9_cur_slot].hdr_p);
+
+	/* ---- motion compensation and prediction ---- */
+	wr(hw, HEVCD_MCRCC_CTL1, 0x2);		/* reset mcrcc */
+	if (inter) {
+		vp9_config_mc_buffer(hw, width, height);
+		wr(hw, HEVCD_MCRCC_CTL1, 0xff0);	/* progressive mode */
+	} else {
+		wr(hw, HEVCD_MCRCC_CTL1, 0x0);
+	}
+
+	wr(hw, HEVC_MPRED_CTRL3, 0x24122412);
+	wr(hw, HEVC_MPRED_ABV_START_ADDR, vp9_ws.mpred_above);
+	/*
+	 * The reference only programs the MV write address for an inter
+	 * frame, leaving whatever the previous stream left there for a key
+	 * frame.  Program it always: a key frame writes no motion vectors, but
+	 * naming a buffer it owns is strictly safer than naming a stale one.
+	 */
+	wr(hw, HEVC_MPRED_MV_WR_START_ADDR, vp9_slots[vp9_cur_slot].mv_p);
+	wr(hw, HEVC_MPRED_MV_WPTR, vp9_slots[vp9_cur_slot].mv_p);
+
+	data32 = read_dos_reg(hw, HEVC_MPRED_CTRL4);
+	data32 &= ~BIT(6);			/* use_prev_frame_mvs */
+	use_prev_mvs = inter && vp9_slots[vp9_prev_slot].width == width &&
+		       vp9_slots[vp9_prev_slot].height == height &&
+		       !vp9_slots[vp9_prev_slot].intra &&
+		       vp9_slots[vp9_prev_slot].show &&
+		       !(pr[VP9_RPM_ERROR_RESILIENT] & 1);
+	if (use_prev_mvs) {
+		data32 |= BIT(6);
+		wr(hw, HEVC_MPRED_MV_RD_START_ADDR,
+		   vp9_slots[vp9_prev_slot].mv_p);
+		wr(hw, HEVC_MPRED_MV_RPTR, vp9_slots[vp9_prev_slot].mv_p);
+		wr(hw, HEVC_MPRED_MV_RD_END_ADDR,
+		   vp9_slots[vp9_prev_slot].mv_p + vp9_slot_mv_size);
+	}
+	wr(hw, HEVC_MPRED_CTRL4, data32);
+
+	/* ---- config_sao_hw() ---- */
+	if (vp9_dw_target_y) {
+		if ((size_t)width * height * 3 / 2 > vp9_dw_target_size) {
+			dev_err(hw->dev,
+				"vp9: %ux%u does not fit the %zu byte capture buffer\n",
+				width, height, vp9_dw_target_size);
+			return -EINVAL;
+		}
+		dw_y = vp9_dw_target_y;
+		dw_c = vp9_dw_target_c;
+	} else {
+		dw_y = vp9_slots[vp9_cur_slot].dw_p;
+		dw_c = dw_y + width * height;
+	}
+	wr(hw, HEVC_SAO_Y_START_ADDR, dw_y);
+	wr(hw, HEVC_SAO_Y_WPTR, dw_y);
+	wr(hw, HEVC_SAO_C_START_ADDR, dw_c);
+	wr(hw, HEVC_SAO_C_WPTR, dw_c);
+	wr(hw, HEVC_SAO_Y_LENGTH, (uv_h << 16) << 1);
+	wr(hw, HEVC_SAO_C_LENGTH, uv_h << 16);
+
+	data32 = read_dos_reg(hw, HEVC_SAO_CTRL1);
+	data32 &= ~(3 << 14);
+	data32 |= (2 << 14);		/* line align 64 */
+	data32 &= ~(3 << 12);		/* linear memory map */
+	data32 &= ~0xff0;
+	data32 |= (HEVC_CONFIG_LITTLE_ENDIAN >> 8) & 0xfff;
+	/* bit0 disables compression, bit1 disables double write; want both. */
+	data32 &= ~0x3;
+	data32 |= BIT(8);		/* NV12 */
+	wr(hw, HEVC_SAO_CTRL1, data32);
+
+	data32 = read_dos_reg(hw, HEVCD_IPP_AXIIF_CONFIG);
+	data32 &= ~0x30;		/* linear */
+	data32 &= ~0xf;
+	data32 |= HEVC_CONFIG_LITTLE_ENDIAN & 0xf;
+	data32 &= ~BIT(12);		/* NV12 */
+	data32 &= ~(3 << 8);
+	data32 |= (2 << 8);		/* line align 64 */
+	wr(hw, HEVCD_IPP_AXIIF_CONFIG, data32);
+
+	/* ---- loop filter ---- */
+	vp9_config_lf_thresholds(hw, pr[VP9_RPM_SHARPNESS_LEVEL] & 7);
+	/* 1st and 2nd write both enabled, VP9 video format. */
+	data32 = (0x3 << 14) | (0x3 << 12) | (0x3 << 10) | (0x3 << 8) | BIT(0);
+	wr(hw, HEVC_DBLK_CFGB, data32);
+	vp9_config_loop_filter(hw, pr);
+	data32 = read_dos_reg(hw, HEVC_DBLK_CFGB);
+	if (width >= 1280)
+		data32 |= BIT(4);	/* dblk pipeline mode */
+	else
+		data32 &= ~(3 << 4);
+	wr(hw, HEVC_DBLK_CFGB, data32);
+
+	dev_dbg(hw->dev,
+		"vp9: pic %ux%u %s show %u slot %d refresh %02x refs %d/%d/%d filt %u sharp %u\n",
+		width, height, inter ? "inter" : (intra_only ? "intra" : "key"),
+		vp9_cur_show, vp9_cur_slot, vp9_pending_refresh,
+		vp9_frame_slot[0], vp9_frame_slot[1], vp9_frame_slot[2],
+		pr[VP9_RPM_FILTER_LEVEL], pr[VP9_RPM_SHARPNESS_LEVEL]);
+	return 0;
+}
+
+/* The picture is finished: rotate the reference slots the way
+ * refresh_frame_flags asks and hand the picture to the display.
+ */
+void aml_vp9_frame_done(struct aml_vdec_hw *hw)
+{
+	int i;
+
+	for (i = 0; i < VP9_REF_FRAMES; i++)
+		if (vp9_pending_refresh & (1 << i))
+			vp9_ref_slot[i] = vp9_cur_slot;
+
+	if (vp9_cur_show)
+		vp9_out_slot = vp9_cur_slot;
+	vp9_prev_slot = vp9_cur_slot;
+	/* The next frame's adaptation needs this frame's type. */
+}
+
+void aml_vp9_dump_output(struct aml_vdec_hw *hw)
+{
+	const u8 *body = vp9_slots[vp9_cur_slot].body_v;
+	const u8 *hdr = vp9_slots[vp9_cur_slot].hdr_v;
+	size_t i, nz = 0, hdr_nz = 0;
+
+	if (!body || !hdr)
+		return;
+	for (i = 0; i < vp9_slot_body_size; i++)
+		if (body[i])
+			nz++;
+	for (i = 0; i < vp9_slot_hdr_size; i++)
+		if (hdr[i])
+			hdr_nz++;
+
+	dev_info(hw->dev,
+		 "vp9: body %zu/%zu bytes non-zero, header %zu/%zu non-zero\n",
+		 nz, vp9_slot_body_size, hdr_nz, vp9_slot_hdr_size);
+	dev_info(hw->dev, "vp9: body[0..15] %*ph\n", 16, body);
+	dev_info(hw->dev, "vp9: hdr [0..15] %*ph\n", 16, hdr);
+}
+
+int aml_vp9_save_nv12(struct aml_vdec_hw *hw, const char *path, bool append)
+{
+	const u8 *y;
+	size_t i, nz = 0, len;
+	struct file *f;
+	loff_t pos = 0;
+	int slot = vp9_out_slot;
+
+	if (slot < 0)
+		return -EAGAIN;
+	vp9_out_slot = -1;
+	y = vp9_slots[slot].dw_v;
+	if (!y)
+		return -ENODEV;
+	dma_sync_single_for_cpu(hw->dev, vp9_slots[slot].dw_p,
+				vp9_slot_dw_size, DMA_FROM_DEVICE);
+
+	len = (size_t)vp9_slots[slot].width * vp9_slots[slot].height * 3 / 2;
+	for (i = 0; i < len; i++)
+		if (y[i])
+			nz++;
+	dev_info(hw->dev, "vp9: nv12 %zu/%zu non-zero, Y[0..15] %*ph\n",
+		 nz, len, 16, y);
+
+	f = filp_open(path, O_WRONLY | O_CREAT |
+		      (append ? O_APPEND : O_TRUNC), 0644);
+	if (IS_ERR(f))
+		return PTR_ERR(f);
+	if (append)
+		pos = i_size_read(file_inode(f));
+	kernel_write(f, y, len, &pos);
+	filp_close(f, NULL);
+	dev_info(hw->dev, "vp9: wrote %zu bytes of NV12 to %s\n", len, path);
+	return 0;
+}
+
+/* ------------------------------------------------------------------------
+ * IVF test harness
+ *
+ * The decoder is frame based and start-code free, so it wants exactly one
+ * frame in front of the shifter at a time.  An IVF file is the easiest way to
+ * carry a sequence of them: a 32-byte file header, then a 12-byte length and
+ * timestamp in front of each frame.
+ * ------------------------------------------------------------------------
+ */
+
+static u8 *vp9_ivf;
+static size_t vp9_ivf_size;
+static size_t vp9_ivf_pos;
+
+void aml_vp9_ivf_release(void)
+{
+	kfree(vp9_ivf);
+	vp9_ivf = NULL;
+	vp9_ivf_size = 0;
+	vp9_ivf_pos = 0;
+}
+
+int aml_vp9_ivf_load(struct aml_vdec_hw *hw, const char *name)
+{
+	const struct firmware *fw;
+	int ret;
+
+	aml_vp9_ivf_release();
+	ret = request_firmware(&fw, name, hw->dev);
+	if (ret) {
+		dev_err(hw->dev, "vp9: %s not found (%d)\n", name, ret);
+		return ret;
+	}
+	vp9_ivf = kmemdup(fw->data, fw->size, GFP_KERNEL);
+	vp9_ivf_size = fw->size;
+	release_firmware(fw);
+	if (!vp9_ivf)
+		return -ENOMEM;
+
+	if (vp9_ivf_size > 32 && !memcmp(vp9_ivf, "DKIF", 4)) {
+		vp9_ivf_pos = get_unaligned_le16(vp9_ivf + 6);
+		if (vp9_ivf_pos < 32 || vp9_ivf_pos > vp9_ivf_size)
+			vp9_ivf_pos = 32;
+	} else {
+		/* A bare frame, which is what the first bring-up used. */
+		vp9_ivf_pos = 0;
+	}
+	dev_info(hw->dev, "vp9: loaded %s, %zu bytes, first frame at %zu\n",
+		 name, vp9_ivf_size, vp9_ivf_pos);
+	return 0;
+}
+
+/* Feed the next frame.  -ENODATA once the file is exhausted. */
+int aml_vp9_ivf_feed_next(struct aml_vdec_hw *hw)
+{
+	u32 len;
+
+	if (!vp9_ivf)
+		return -ENODEV;
+	if (!vp9_ivf_pos && vp9_ivf_size) {
+		/* Bare frame: the whole file is one frame. */
+		vp9_ivf_pos = vp9_ivf_size;
+		return aml_vp9_feed(hw, vp9_ivf, vp9_ivf_size);
+	}
+	if (vp9_ivf_pos + 12 > vp9_ivf_size)
+		return -ENODATA;
+	len = get_unaligned_le32(vp9_ivf + vp9_ivf_pos);
+	vp9_ivf_pos += 12;
+	if (!len || vp9_ivf_pos + len > vp9_ivf_size)
+		return -ENODATA;
+	vp9_ivf_pos += len;
+	return aml_vp9_feed(hw, vp9_ivf + vp9_ivf_pos - len, len);
+}
+
+/*
+ * The microcode asks the host to adapt the entropy context by putting 0xfd in
+ * the low byte of VP9_ADAPT_PROB_REG, with the frame context it used in the
+ * next byte up.  Fold the counts it collected into that context, leave the
+ * result where the microcode will read it, and clear the request.
+ *
+ * Skipping this is not cosmetic: the entropy context then diverges from the
+ * encoder's after the first adapting frame and every later picture decodes
+ * wrong.  A stream encoded -error-resilient 1 -frame-parallel 1 never asks.
+ */
+bool aml_vp9_check_adapt(struct aml_vdec_hw *hw)
+{
+	u32 v = read_dos_reg(hw, VP9_ADAPT_PROB_REG);
+
+	if ((v & 0xff) != 0xfd)
+		return false;
+
+	/*
+	 * Backward adaptation is refused at request time, so the microcode
+	 * should never ask.  Release it rather than letting it time out.
+	 */
+	dev_warn_once(hw->dev, "vp9: unexpected probability adaptation request\n");
+	wr(hw, VP9_ADAPT_PROB_REG, 0);
+	return true;
+}
+
+/* ------------------------------------------------------------------------
+ * V4L2 stateless decoder
+ *
+ * The microcode parses the frame header itself, so the frame control is only
+ * consulted for the things the *host* has to own: which frame context the
+ * microcode is about to load, and whether it has to be saved afterwards.
+ *
+ * That ownership is the whole reason this glue is more than a copy of the AV1
+ * one.  The microcode never writes the probability buffer - it only reads it,
+ * once per frame, because it halts itself when a frame is done - so the driver
+ * keeps the four frame contexts, applies the forward updates userspace parsed
+ * out of the compressed header, and writes the result back where the microcode
+ * will look.
+ * ------------------------------------------------------------------------
+ */
+
+struct aml_vp9_ctx {
+	struct aml_vdec_ctx *v4l2_ctx;
+	struct aml_vdec_hw *hw;
+	bool ucode_loaded;
+	bool started;
+	bool have_picture;
+	u32 rounds;
+
+	struct v4l2_vp9_frame_context fctx[4];
+	struct v4l2_vp9_frame_context cur_probs;
+	u8 fctx_idx;
+	bool refresh_ctx;
+};
+
+static void *vp9_get_ctrl(struct aml_vdec_ctx *ctx, u32 id)
+{
+	struct v4l2_ctrl *c = v4l2_ctrl_find(&ctx->ctrl_handler, id);
+
+	return c ? c->p_cur.p : NULL;
+}
+
+/*
+ * Put the context the microcode is about to load where it will look, and
+ * remember what it will be afterwards.  The microcode applies the compressed
+ * header's forward updates itself, so the buffer must hold the context as it
+ * was *before* them; the updated copy is only needed if this frame refreshes
+ * its slot.
+ */
+static void vp9_prepare_frame_ctx(struct aml_vp9_ctx *vp9,
+				  const struct v4l2_ctrl_vp9_frame *dec,
+				  const struct v4l2_ctrl_vp9_compressed_hdr *hdr)
+{
+	u8 *prob;
+	int i;
+
+	if (!vp9_ws.side_vaddr)
+		return;
+	prob = vp9_ws.side_vaddr + (vp9_ws.prob - vp9_ws.side_paddr);
+
+	vp9->fctx_idx = v4l2_vp9_reset_frame_ctx(dec, vp9->fctx);
+	vp9->refresh_ctx = !!(dec->flags & V4L2_VP9_FRAME_FLAG_REFRESH_FRAME_CTX);
+
+	/*
+	 * The reference driver zeroes this buffer once and then never writes
+	 * it except during adaptation - the microcode loads and maintains the
+	 * four frame contexts itself.  We repack them from our own copies on
+	 * every frame, which would clobber whatever it keeps there.
+	 */
+	for (i = 0; i < 4; i++)
+		aml_vp9_pack_probs(prob + i * 0x1000, &vp9->fctx[i]);
+
+	vp9->cur_probs = vp9->fctx[vp9->fctx_idx];
+	if (hdr)
+		v4l2_vp9_fw_update_probs(&vp9->cur_probs, hdr, dec);
+}
+
+static void vp9_finish_frame_ctx(struct aml_vp9_ctx *vp9)
+{
+	if (vp9->refresh_ctx)
+		vp9->fctx[vp9->fctx_idx] = vp9->cur_probs;
+}
+
+/* Where the double-write unit should put this picture: straight into the
+ * CAPTURE buffer when its geometry allows it.
+ */
+static void vp9_set_dw_target(struct aml_vdec_ctx *ctx)
+{
+	struct v4l2_pix_format_mplane *pf = &ctx->pix_fmt[AML_FMT_DST];
+	struct vb2_v4l2_buffer *dst = v4l2_m2m_next_dst_buf(ctx->m2m_ctx);
+	struct vb2_buffer *vb;
+
+	vp9_dw_target_y = 0;
+	vp9_dw_target_c = 0;
+	vp9_dw_target_size = 0;
+	if (!dst)
+		return;
+	vb = &dst->vb2_buf;
+	if (pf->plane_fmt[0].bytesperline != pf->width)
+		return;
+
+	vp9_dw_target_y = vb2_dma_contig_plane_dma_addr(vb, 0);
+	if (!vp9_dw_target_y)
+		return;
+	if (vb->num_planes >= 2) {
+		vp9_dw_target_c = vb2_dma_contig_plane_dma_addr(vb, 1);
+		vp9_dw_target_size = vb2_plane_size(vb, 0) +
+				     vb2_plane_size(vb, 1);
+		if (!vp9_dw_target_c) {
+			vp9_dw_target_y = 0;
+			return;
+		}
+	} else {
+		vp9_dw_target_c = vp9_dw_target_y +
+				  (dma_addr_t)pf->width * pf->height;
+		vp9_dw_target_size = vb2_plane_size(vb, 0);
+	}
+}
+
+static void vp9_copy_out(struct aml_vp9_ctx *vp9, struct vb2_v4l2_buffer *dst)
+{
+	struct v4l2_pix_format_mplane *pf =
+		&vp9->v4l2_ctx->pix_fmt[AML_FMT_DST];
+	struct vb2_buffer *vb = &dst->vb2_buf;
+	u32 dst_stride = pf->plane_fmt[0].bytesperline ? : vp9_last_width;
+	u32 dst_h = pf->height ? : vp9_last_height;
+	int slot = vp9_out_slot;
+	const u8 *src;
+	u8 *y, *c;
+	u32 row, w, h;
+
+	if (slot < 0)
+		return;
+	vp9_out_slot = -1;
+
+	if (vp9_dw_target_y) {
+		if (vb->num_planes >= 2) {
+			vb2_set_plane_payload(vb, 0, dst_stride * dst_h);
+			vb2_set_plane_payload(vb, 1, dst_stride * dst_h / 2);
+		} else {
+			vb2_set_plane_payload(vb, 0,
+					      dst_stride * dst_h * 3 / 2);
+		}
+		vp9->have_picture = true;
+		return;
+	}
+
+	src = vp9_slots[slot].dw_v;
+	if (!src)
+		return;
+	dma_sync_single_for_cpu(vp9->hw->dev, vp9_slots[slot].dw_p,
+				vp9_slot_dw_size, DMA_FROM_DEVICE);
+
+	w = vp9_slots[slot].width;
+	h = vp9_slots[slot].height;
+	y = vb2_plane_vaddr(vb, 0);
+	if (!y)
+		return;
+	c = vb->num_planes >= 2 ? vb2_plane_vaddr(vb, 1) :
+				  y + (size_t)dst_stride * dst_h;
+	if (!c)
+		return;
+
+	for (row = 0; row < h; row++)
+		memcpy(y + (size_t)row * dst_stride, src + (size_t)row * w, w);
+	for (row = 0; row < h / 2; row++)
+		memcpy(c + (size_t)row * dst_stride,
+		       src + (size_t)(h + row) * w, w);
+	aml_vdec_dst_cpu_written(vp9->hw->dev, vb);
+
+	if (vb->num_planes >= 2) {
+		vb2_set_plane_payload(vb, 0, dst_stride * dst_h);
+		vb2_set_plane_payload(vb, 1, dst_stride * dst_h / 2);
+	} else {
+		vb2_set_plane_payload(vb, 0, dst_stride * dst_h * 3 / 2);
+	}
+	vp9->have_picture = true;
+}
+
+static void vp9_finish(struct aml_vp9_ctx *vp9, enum vb2_buffer_state state)
+{
+	struct aml_vdec_ctx *ctx = vp9->v4l2_ctx;
+	struct vb2_v4l2_buffer *buf;
+	unsigned int i;
+
+	if (!vp9->have_picture) {
+		buf = v4l2_m2m_next_dst_buf(ctx->m2m_ctx);
+		if (buf)
+			for (i = 0; i < buf->vb2_buf.num_planes; i++)
+				vb2_set_plane_payload(&buf->vb2_buf, i, 0);
+	}
+
+	buf = v4l2_m2m_next_src_buf(ctx->m2m_ctx);
+	if (buf && buf->vb2_buf.req_obj.req)
+		v4l2_ctrl_request_complete(buf->vb2_buf.req_obj.req,
+					   &ctx->ctrl_handler);
+
+	vp9_finish_frame_ctx(vp9);
+
+	ctx->int_cond = 1;
+	ctx->job_finished = true;
+	v4l2_m2m_buf_done_and_job_finish(ctx->dev->m2m_dev_dec, ctx->m2m_ctx,
+					 state);
+	wake_up(&ctx->queue);
+}
+
+static irqreturn_t vp9_irq_thread(int irq, void *priv)
+{
+	struct aml_vp9_ctx *vp9 = priv;
+	struct aml_vdec_ctx *ctx = vp9->v4l2_ctx;
+	struct aml_vdec_hw *hw = vp9->hw;
+	u32 st = hw->hevc_irq_status & 0xff;
+	u32 next = VP9_10B_DECODE_SLICE;
+
+	if (++vp9->rounds > 256) {
+		dev_err(hw->dev, "vp9: runaway decode, giving up\n");
+		vp9_finish(vp9, VB2_BUF_STATE_ERROR);
+		return IRQ_HANDLED;
+	}
+
+	aml_vp9_check_adapt(hw);
+
+	switch (st) {
+	case VP9_HEAD_PARSER_DONE: {
+		int cfg = aml_vp9_config_picture(hw);
+
+		if (cfg == -EAGAIN) {
+			/* show_existing_frame, or nothing to decode yet. */
+			struct vb2_v4l2_buffer *dst =
+				v4l2_m2m_next_dst_buf(ctx->m2m_ctx);
+
+			if (dst)
+				vp9_copy_out(vp9, dst);
+			next = VP9_10B_DISCARD_NAL;
+		} else if (cfg) {
+			dev_err(hw->dev, "vp9: picture config failed (%d)\n",
+				cfg);
+			/* As in av1.c: never leave the core pointed at freed slots */
+			regmap_write(hw->map[DOS_BUS], HEVC_MPSR, 0);
+			regmap_write(hw->map[DOS_BUS], HEVC_CPSR, 0);
+			aml_vp9_teardown(hw);
+			vp9->started = false;
+			vp9_finish(vp9, VB2_BUF_STATE_ERROR);
+			return IRQ_HANDLED;
+		}
+		break;
+	}
+	case VP9_DECPIC_DATA_DONE: {
+		struct vb2_v4l2_buffer *dst =
+			v4l2_m2m_next_dst_buf(ctx->m2m_ctx);
+
+		aml_vp9_frame_done(hw);
+		if (dst)
+			vp9_copy_out(vp9, dst);
+		vp9_finish(vp9, VB2_BUF_STATE_DONE);
+		return IRQ_HANDLED;
+	}
+	case VP9_NAL_DECODE_DONE:
+	case HEVC_ACTION_DONE:
+	case VP9_DECODE_BUFEMPTY:
+	case VP9_SEARCH_BUFEMPTY:
+	case VP9_EOS:
+		vp9_finish(vp9, VB2_BUF_STATE_DONE);
+		return IRQ_HANDLED;
+	default:
+		dev_dbg(hw->dev, "vp9: unhandled status %02x\n", st);
+		break;
+	}
+
+	regmap_write(hw->map[DOS_BUS], HEVC_ASSIST_MBOX0_CLR_REG, 1);
+	regmap_write(hw->map[DOS_BUS], HEVC_DEC_STATUS_REG, next);
+	return IRQ_HANDLED;
+}
+
+/*
+ * Backward adaptation (refresh_frame_context set, frame_parallel_decoding_mode
+ * and error_resilient_mode clear) needs a secure-world call this TEE does not
+ * have.  Such a stream does not just decode wrong: the core stops answering
+ * after the first adapted frame and stays that way until a reboot.  Refuse it
+ * up front so the client falls back to software instead.
+ */
+
+int aml_vp9_init(void *priv)
+{
+	struct aml_vdec_ctx *ctx = priv;
+	struct aml_vdec_hw *hw = vdec_get_hw(ctx->dev);
+	struct aml_vp9_ctx *vp9;
+	int i;
+
+	if (!hw)
+		return -ENODEV;
+
+	vp9 = kzalloc_obj(*vp9);
+	if (!vp9)
+		return -ENOMEM;
+
+	vp9->v4l2_ctx = ctx;
+	vp9->hw = hw;
+	for (i = 0; i < 4; i++)
+		vp9->fctx[i] = v4l2_vp9_default_probs;
+
+	/*
+	 * The HEVC core takes one client at a time.  Both this codec and AV1
+	 * keep their whole decode state in file-scope variables (workspace,
+	 * slot table, current-slot index) and hang their interrupt handler off
+	 * the single hw->hevc_priv / hevc_irq_thread_fn pair, so a second
+	 * context does not just fail - it silently overwrites the first one's
+	 * handler and scribbles on its slots.  What that looked like from
+	 * userspace was a page with two VP9 videos wedging the decoder, every
+	 * context (H.264 included) going flat green, tasks stuck in
+	 * v4l2_release and finally a module unload that never returned.
+	 *
+	 * Refuse the second one instead; the client falls back to software.
+	 */
+	if (cmpxchg(&hw->hevc_priv, NULL, vp9)) {
+		dev_info(&ctx->dev->plat_dev->dev,
+			 "vp9: HEVC core already in use, refusing second instance\n");
+		kfree(vp9);
+		return -EBUSY;
+	}
+
+	ctx->codec_priv = vp9;
+	hw->hevc_irq_thread_fn = vp9_irq_thread;
+	return 0;
+}
+
+void aml_vp9_exit(void *priv)
+{
+	struct aml_vdec_ctx *ctx = priv;
+	struct aml_vp9_ctx *vp9 = ctx->codec_priv;
+
+	if (!vp9)
+		return;
+
+	vp9->hw->hevc_irq_thread_fn = NULL;
+	vp9->hw->hevc_priv = NULL;
+	if (vp9->started) {
+		regmap_write(vp9->hw->map[DOS_BUS], HEVC_MPSR, 0);
+		aml_vp9_teardown(vp9->hw);
+		/*
+		 * Deliberately not hvdec_disable(): powering the HEVC core
+		 * down here leaves it in a state the next context's bring-up
+		 * does not recover from, and every frame of the *second*
+		 * decode session on this core then times out - whichever
+		 * codec it is.  The domain is left up until the module is
+		 * unloaded.
+		 */
+	}
+	kfree(vp9);
+	ctx->codec_priv = NULL;
+}
+
+int aml_vp9_dec_run(void *priv)
+{
+	struct aml_vdec_ctx *ctx = priv;
+	struct aml_vp9_ctx *vp9 = ctx->codec_priv;
+	struct aml_vdec_hw *hw = vp9->hw;
+	const struct v4l2_ctrl_vp9_frame *dec;
+	const struct v4l2_ctrl_vp9_compressed_hdr *hdr;
+	struct vb2_v4l2_buffer *src;
+	struct vb2_buffer *vb;
+	int ret;
+
+	src = v4l2_m2m_next_src_buf(ctx->m2m_ctx);
+	if (!src)
+		return -EINVAL;
+	vb = &src->vb2_buf;
+
+	dec = vp9_get_ctrl(ctx, V4L2_CID_STATELESS_VP9_FRAME);
+	if (!dec) {
+		dev_err_once(hw->dev, "vp9: no frame control\n");
+		return -EINVAL;
+	}
+	hdr = vp9_get_ctrl(ctx, V4L2_CID_STATELESS_VP9_COMPRESSED_HDR);
+
+	if ((dec->flags & V4L2_VP9_FRAME_FLAG_REFRESH_FRAME_CTX) &&
+	    !(dec->flags & (V4L2_VP9_FRAME_FLAG_PARALLEL_DEC_MODE |
+			    V4L2_VP9_FRAME_FLAG_ERROR_RESILIENT))) {
+		dev_warn_once(hw->dev,
+			      "vp9: stream needs backward adaptation, not supported\n");
+		return -EOPNOTSUPP;
+	}
+
+	ret = hvdec_enable(hw);
+	if (ret)
+		return ret;
+	dos_enable_hevc(hw);
+
+	if (!vp9->ucode_loaded) {
+		/*
+		 * A 10-bit stream leaves the core decoding the next 8-bit one
+		 * to garbage, every frame, until something cycles its memories
+		 * - the same thing a VP9/AV1 switch needs.  vp9_ten_bit still
+		 * holds the last stream's depth here.
+		 */
+		if (vp9_ten_bit)
+			hw->hevc_dirty = true;
+		vp9_ten_bit = false;
+		hvdec_switch_codec(hw, CODEC_TYPE_VP9);
+
+		/*
+		 * Halts the AMRISC first: the other codec on this core may
+		 * still be running - VP9 and AV1 share it - and loading over a
+		 * running microcode leaves the core timing out every frame.
+		 */
+		ret = hevc_load_fw(hw, CODEC_TYPE_VP9, VP9_FW_FORMAT_MMU, VP9_FW_CORE_HEVC, "vp9");
+		if (ret) {
+			hvdec_disable(hw);
+			return ret;
+		}
+		vp9->ucode_loaded = true;
+	}
+
+	if (!vp9->started) {
+		ret = aml_vp9_bringup(hw, ctx->pix_fmt[AML_FMT_SRC].width,
+				      ctx->pix_fmt[AML_FMT_SRC].height);
+		if (ret) {
+			hvdec_disable(hw);
+			return ret;
+		}
+	}
+
+	vp9_prepare_frame_ctx(vp9, dec, hdr);
+	vp9_set_dw_target(ctx);
+
+	ret = aml_vp9_feed(hw, vb2_plane_vaddr(vb, 0),
+			   vb2_get_plane_payload(vb, 0));
+	if (ret)
+		return ret;
+
+	vp9->rounds = 0;
+	vp9->have_picture = false;
+	ctx->int_cond = 0;
+	atomic_set(&hw->hevc_irq_count, 0);
+
+	/*
+	 * The microcode halts itself at the end of a frame, so every buffer
+	 * starts it again.  Resetting the two AMRISC CPUs first is what the
+	 * vendor's amhevc_start() does; nothing wider is needed once the
+	 * frames carry their AMLV prefix.
+	 */
+	if (vp9->started) {
+		regmap_write(hw->map[DOS_BUS], HEVC_MPSR, 0);
+		regmap_write(hw->map[DOS_BUS], HEVC_CPSR, 0);
+	}
+	regmap_write(hw->map[DOS_BUS], DOS_SW_RESET3, BIT(12) | BIT(11));
+	regmap_write(hw->map[DOS_BUS], DOS_SW_RESET3, 0);
+	regmap_write(hw->map[DOS_BUS], HEVC_ASSIST_MBOX0_CLR_REG, 1);
+	regmap_write(hw->map[DOS_BUS], HEVC_DEC_STATUS_REG, HEVC_ACTION_DONE);
+	regmap_write(hw->map[DOS_BUS], HEVC_CPSR, 0);
+	regmap_write(hw->map[DOS_BUS], HEVC_MPSR, 1);
+	vp9->started = true;
+
+	/*
+	 * Side buffer only: snapshotting the whole 832 KiB workspace out of
+	 * uncached coherent memory takes long enough to blow the decode
+	 * timeout, and prob/count/rpm/lmem all live here anyway.
+	 */
+
+	/*
+	 * The current slot's header and motion-vector buffers are the last
+	 * memory we own that has never been checked for the symbol counts.
+	 */
+
+	/*
+	 * Wait in short steps and poll for the entropy-adaptation request
+	 * between them.  The wait is deliberately *uninterruptible*: a signal
+	 * to the client must not abandon a decode that the hardware is still
+	 * running, because cancelling the m2m job here races the interrupt
+	 * thread finishing the same job - two completions, a
+	 * v4l2_m2m_buf_done_and_job_finish() warning, and surfaces left
+	 * undecoded (they come out flat green).  The 500 ms timeout is what
+	 * bounds this, not the signal.
+	 *
+	 * When a frame does not set frame_parallel_decoding_mode the microcode
+	 * hands the backward update back to the host: it writes 0xfd to
+	 * VP9_ADAPT_PROB_REG and spins until the host clears it.  It raises no
+	 * interrupt for that, so servicing the request only from the interrupt
+	 * thread means never seeing it - the core sits there until the decode
+	 * times out, and every such frame is lost.  Streams encoded
+	 * -frame-parallel 1 never ask, which is why they decoded fine.
+	 */
+	{
+		unsigned long deadline = jiffies + msecs_to_jiffies(VP9_TIMEOUT_MS);
+
+		do {
+			ret = wait_event_timeout(ctx->queue,
+						 ctx->int_cond,
+						 msecs_to_jiffies(1));
+			if (ret)
+				break;
+			aml_vp9_check_adapt(hw);
+		} while (time_before(jiffies, deadline));
+	}
+
+	if (!ret) {
+		int k;
+
+		dev_err(hw->dev, "vp9: decode timeout, status %02x pc %08x\n",
+			hw->hevc_irq_status, read_dos_reg(hw, HEVC_MPC_E));
+		dev_err(hw->dev,
+			"vp9: dec_status %08x adapt %08x parser_ctl %08x shift %08x\n",
+			read_dos_reg(hw, HEVC_DEC_STATUS_REG),
+			read_dos_reg(hw, VP9_ADAPT_PROB_REG),
+			read_dos_reg(hw, REG_ALIGN(0x3050)),
+			read_dos_reg(hw, HEVC_SHIFT_BYTE_COUNT));
+		for (k = 0; k < 16; k++)
+			dev_err(hw->dev, "vp9: scratch[%x] %08x\n", k,
+				read_dos_reg(hw, REG_ALIGN(0x30c0 + k)));
+		/*
+		 * After a timeout the core decodes every later frame to garbage
+		 * - in this session and the next ones - until its memories are
+		 * cycled.  Have the next frame cycle them, reload the microcode
+		 * and bring the decoder up again; the stream picks up at its
+		 * next key frame.
+		 */
+		hw->hevc_dirty = true;
+		vp9->ucode_loaded = false;
+		vp9->started = false;
+		ret = -ETIMEDOUT;
+	} else {
+		ret = 0;
+	}
+	ctx->int_cond = 0;
+	return ret;
+}
diff --git a/drivers/media/platform/amlogic/vdec/vp9.h b/drivers/media/platform/amlogic/vdec/vp9.h
new file mode 100644
index 000000000000..5e4118315e0a
--- /dev/null
+++ b/drivers/media/platform/amlogic/vdec/vp9.h
@@ -0,0 +1,149 @@
+/* SPDX-License-Identifier: (GPL-2.0-only OR MIT) */
+/*
+ * VP9 decoding on the S4 HEVC core.
+ *
+ * The VP9 microcode runs on the same core as AV1 and talks the same mailbox
+ * protocol, so most of the register set comes from av1.h and reg_defines.h.
+ * What differs is collected here: the scratch-register aliases the VP9
+ * microcode uses, its status codes, and its workspace sizes.
+ */
+
+#ifndef _AML_VP9_H_
+#define _AML_VP9_H_
+
+#include "av1.h"
+
+/*
+ * Scratch aliases.  Same registers as AV1, different meanings - the alias
+ * belongs to whichever microcode is loaded, so these deliberately shadow
+ * nothing and are only used from vp9.c.
+ */
+#define HEVC_ASSIST_SCRATCH_3		REG_ALIGN(0x30c3)
+#define HEVC_ASSIST_SCRATCH_F		REG_ALIGN(0x30cf)
+#define HEVC_ASSIST_SCRATCH_4		REG_ALIGN(0x30c4)
+#define HEVC_ASSIST_SCRATCH_A		REG_ALIGN(0x30ca)
+#define HEVC_ASSIST_SCRATCH_B		REG_ALIGN(0x30cb)
+
+#define HEVC_SHORT_TERM_RPS		HEVC_ASSIST_SCRATCH_2
+#define VP9_ADAPT_PROB_REG		HEVC_ASSIST_SCRATCH_3
+#define VP9_MMU_MAP_BUFFER		HEVC_ASSIST_SCRATCH_4
+#define HEVC_PPS_BUFFER			HEVC_ASSIST_SCRATCH_5
+#define HEVC_STREAM_SWAP_BUFFER		HEVC_ASSIST_SCRATCH_7
+#define HEVC_STREAM_SWAP_BUFFER2	HEVC_ASSIST_SCRATCH_8
+#define VP9_PROB_SWAP_BUFFER		HEVC_ASSIST_SCRATCH_9
+#define VP9_COUNT_SWAP_BUFFER		HEVC_ASSIST_SCRATCH_A
+#define VP9_SEG_MAP_BUFFER		HEVC_ASSIST_SCRATCH_B
+#define HEVC_STREAM_SWAP_TEST		HEVC_ASSIST_SCRATCH_L
+
+/*
+ * The VP9 microcode dumps its local memory to SCRATCH_F, not to the register
+ * av1.h aliases as LMEM_DUMP_ADR - av1.h points that at SCRATCH_I, which for
+ * this microcode is NAL_SEARCH_CTL.  Writing the AV1 alias here therefore both
+ * left the real dump address unset and overwrote the parser's search control
+ * with a physical address.
+ */
+#define VP9_LMEM_DUMP_ADR		HEVC_ASSIST_SCRATCH_F
+
+#define HEVC_DBLK_CFGE			REG_ALIGN(0x350e)
+
+/* Not needed by the AV1 path, so they are not in av1.h. */
+#define HEVC_PARSER_PICTURE_SIZE	REG_ALIGN(0x3123)
+#define HEVC_MPRED_MV_RD_END_ADDR	REG_ALIGN(0x3262)
+#define VP9D_MPP_REF_SCALE_ENBL		REG_ALIGN(0x3441)
+#define VP9D_MPP_REFINFO_TBL_ACCCONFIG	REG_ALIGN(0x3442)
+#define VP9D_MPP_REFINFO_DATA		REG_ALIGN(0x3443)
+#define HEVC_SAO_MMU_RESET_CTRL		REG_ALIGN(0x3641)
+#define HEVC_WRRSP_LMEM			REG_ALIGN(0x3353)
+
+/*
+ * RPM layout, union param_u in the reference.  The microcode DMAs this out
+ * after it has parsed a frame header; the entries arrive reversed within
+ * groups of four u16s, which aml_vp9_read_rpm() undoes.
+ */
+#define VP9_RPM_PROFILE			0
+#define VP9_RPM_SHOW_EXISTING_FRAME	1
+#define VP9_RPM_FRAME_TO_SHOW_IDX	2
+#define VP9_RPM_FRAME_TYPE		3
+#define VP9_RPM_SHOW_FRAME		4
+#define VP9_RPM_ERROR_RESILIENT		5
+#define VP9_RPM_INTRA_ONLY		6
+#define VP9_RPM_DISPLAY_SIZE_PRESENT	7
+#define VP9_RPM_RESET_FRAME_CONTEXT	8
+#define VP9_RPM_REFRESH_FRAME_FLAGS	9
+#define VP9_RPM_WIDTH			10
+#define VP9_RPM_HEIGHT			11
+#define VP9_RPM_DISPLAY_WIDTH		12
+#define VP9_RPM_DISPLAY_HEIGHT		13
+#define VP9_RPM_REF_INFO		14
+#define VP9_RPM_SAME_FRAME_SIZE		15
+#define VP9_RPM_MODE_REF_DELTA_ENABLED	16
+#define VP9_RPM_REF_DELTAS		17	/* 4 entries */
+#define VP9_RPM_MODE_DELTAS		21	/* 2 entries */
+#define VP9_RPM_FILTER_LEVEL		23
+#define VP9_RPM_SHARPNESS_LEVEL		24
+#define VP9_RPM_BIT_DEPTH		25
+#define VP9_RPM_SEG_QUANT_INFO		26	/* 8 entries */
+#define VP9_RPM_SEG_ENABLED		34
+#define VP9_RPM_SEG_ABS_DELTA		35
+#define VP9_RPM_SEG_LF_INFO		36	/* 8 entries */
+#define VP9_RPM_COUNT			0x80
+
+#define VP9_KEY_FRAME			0
+#define VP9_MIN_DIM			16
+#define VP9_MAX_DIM			4096
+
+/*
+ * A bare VP9 frame carries no start code, so the microcode must not be asked
+ * to search for one: "no head" frame-based mode decodes HEVC_DECODE_SIZE bytes
+ * from the read pointer as they stand.
+ */
+#define DECODE_MODE_MULTI_FRAMEBASE_NOHEAD	((0x80 << 24) | 4)
+
+/* Microcode status codes, HEVC_DEC_STATUS_REG. */
+#define VP9_10B_DEC_IDLE		0x00
+#define VP9_10B_DEC_FRAME_HEADER	0x01
+#define VP9_10B_DEC_SLICE_SEGMENT	0x02
+#define VP9_10B_DECODE_SLICE		0x05
+#define VP9_10B_DISCARD_NAL		0x06
+#define VP9_DUMP_LMEM			0x07
+#define VP9_DECPIC_DATA_DONE		0x0a
+#define VP9_DECPIC_DATA_ERROR		0x0b
+#define VP9_NAL_DECODE_DONE		0x0e
+#define VP9_DECODE_BUFEMPTY		0x20
+#define VP9_DECODE_TIMEOUT		0x21
+#define VP9_SEARCH_BUFEMPTY		0x22
+#define VP9_DECODE_OVER_SIZE		0x23
+#define VP9_HEAD_PARSER_DONE		0xf0
+#define VP9_HEAD_SEARCH_DONE		0xf1
+#define VP9_EOS				0xf2
+
+/* Firmware id, VIDEO_DEC_VP9_MMU, on the HEVC core. */
+#define VP9_FW_FORMAT_MMU		19
+#define VP9_FW_CORE_HEVC		2
+
+int aml_vp9_bringup(struct aml_vdec_hw *hw, u32 width, u32 height);
+int aml_vp9_feed(struct aml_vdec_hw *hw, const void *data, size_t size);
+void aml_vp9_teardown(struct aml_vdec_hw *hw);
+int aml_vp9_config_picture(struct aml_vdec_hw *hw);
+int aml_vp9_feed_file(struct aml_vdec_hw *hw, const char *name);
+void aml_vp9_dump_state(struct aml_vdec_hw *hw);
+void aml_vp9_dump_params(struct aml_vdec_hw *hw);
+void aml_vp9_frame_done(struct aml_vdec_hw *hw);
+int aml_vp9_save_nv12(struct aml_vdec_hw *hw, const char *path, bool append);
+void aml_vp9_dump_output(struct aml_vdec_hw *hw);
+void aml_vp9_restart(struct aml_vdec_hw *hw);
+void aml_vp9_prot_reinit(struct aml_vdec_hw *hw);
+void aml_vp9_reset_core(struct aml_vdec_hw *hw);
+int aml_vp9_ivf_load(struct aml_vdec_hw *hw, const char *name);
+int aml_vp9_ivf_feed_next(struct aml_vdec_hw *hw);
+void aml_vp9_ivf_release(void);
+bool aml_vp9_check_adapt(struct aml_vdec_hw *hw);
+
+/* Milliseconds to wait for a frame before giving up on the microcode. */
+#define VP9_TIMEOUT_MS			500
+
+int aml_vp9_init(void *priv);
+void aml_vp9_exit(void *priv);
+int aml_vp9_dec_run(void *priv);
+
+#endif
diff --git a/drivers/media/platform/amlogic/vdec/vp9_prob.c b/drivers/media/platform/amlogic/vdec/vp9_prob.c
new file mode 100644
index 000000000000..762340b647c0
--- /dev/null
+++ b/drivers/media/platform/amlogic/vdec/vp9_prob.c
@@ -0,0 +1,213 @@
+// SPDX-License-Identifier: (GPL-2.0-only OR MIT)
+/*
+ * VP9 frame contexts in the packed layout the HEVC core's microcode reads.
+ */
+
+#include <linux/kernel.h>
+
+#include <media/v4l2-vp9.h>
+#include "vp9_prob.h"
+
+/*====================================================
+ *========================================================================
+ *vp9_prob define
+ *========================================================================
+ */
+#define VP9_PARTITION_START      0
+#define VP9_PARTITION_SIZE_STEP  (3 * 4)
+#define VP9_PARTITION_ONE_SIZE   (4 * VP9_PARTITION_SIZE_STEP)
+#define VP9_PARTITION_KEY_START  0
+#define VP9_PARTITION_P_START    VP9_PARTITION_ONE_SIZE
+#define VP9_PARTITION_SIZE       (2 * VP9_PARTITION_ONE_SIZE)
+#define VP9_SKIP_START           (VP9_PARTITION_START + VP9_PARTITION_SIZE)
+#define VP9_SKIP_SIZE            4 /* only use 3*/
+#define VP9_TX_MODE_START        (VP9_SKIP_START + VP9_SKIP_SIZE)
+#define VP9_TX_MODE_8_0_OFFSET   0
+#define VP9_TX_MODE_8_1_OFFSET   1
+#define VP9_TX_MODE_16_0_OFFSET  2
+#define VP9_TX_MODE_16_1_OFFSET  4
+#define VP9_TX_MODE_32_0_OFFSET  6
+#define VP9_TX_MODE_32_1_OFFSET  9
+#define VP9_TX_MODE_SIZE         12
+#define VP9_COEF_START           (VP9_TX_MODE_START + VP9_TX_MODE_SIZE)
+#define VP9_COEF_BAND_0_OFFSET   0
+#define VP9_COEF_BAND_1_OFFSET   (VP9_COEF_BAND_0_OFFSET + 3 * 3 + 1)
+#define VP9_COEF_BAND_2_OFFSET   (VP9_COEF_BAND_1_OFFSET + 6 * 3)
+#define VP9_COEF_BAND_3_OFFSET   (VP9_COEF_BAND_2_OFFSET + 6 * 3)
+#define VP9_COEF_BAND_4_OFFSET   (VP9_COEF_BAND_3_OFFSET + 6 * 3)
+#define VP9_COEF_BAND_5_OFFSET   (VP9_COEF_BAND_4_OFFSET + 6 * 3)
+#define VP9_COEF_SIZE_ONE_SET    100 /* ((3 +5*6)*3 + 1 padding)*/
+#define VP9_COEF_4X4_START       (VP9_COEF_START + 0 * VP9_COEF_SIZE_ONE_SET)
+#define VP9_COEF_8X8_START       (VP9_COEF_START + 4 * VP9_COEF_SIZE_ONE_SET)
+#define VP9_COEF_16X16_START     (VP9_COEF_START + 8 * VP9_COEF_SIZE_ONE_SET)
+#define VP9_COEF_32X32_START     (VP9_COEF_START + 12 * VP9_COEF_SIZE_ONE_SET)
+#define VP9_COEF_SIZE_PLANE      (2 * VP9_COEF_SIZE_ONE_SET)
+#define VP9_COEF_SIZE            (4 * 2 * 2 * VP9_COEF_SIZE_ONE_SET)
+#define VP9_INTER_MODE_START     (VP9_COEF_START + VP9_COEF_SIZE)
+#define VP9_INTER_MODE_SIZE      24 /* only use 21 ( #*7)*/
+#define VP9_INTERP_START         (VP9_INTER_MODE_START + VP9_INTER_MODE_SIZE)
+#define VP9_INTERP_SIZE          8
+#define VP9_INTRA_INTER_START    (VP9_INTERP_START + VP9_INTERP_SIZE)
+#define VP9_INTRA_INTER_SIZE     4
+#define VP9_INTERP_INTRA_INTER_START  VP9_INTERP_START
+#define VP9_INTERP_INTRA_INTER_SIZE   (VP9_INTERP_SIZE + VP9_INTRA_INTER_SIZE)
+#define VP9_COMP_INTER_START     \
+		(VP9_INTERP_INTRA_INTER_START + VP9_INTERP_INTRA_INTER_SIZE)
+#define VP9_COMP_INTER_SIZE      5
+#define VP9_COMP_REF_START       (VP9_COMP_INTER_START + VP9_COMP_INTER_SIZE)
+#define VP9_COMP_REF_SIZE        5
+#define VP9_SINGLE_REF_START     (VP9_COMP_REF_START + VP9_COMP_REF_SIZE)
+#define VP9_SINGLE_REF_SIZE      10
+#define VP9_REF_MODE_START       VP9_COMP_INTER_START
+#define VP9_REF_MODE_SIZE        \
+		(VP9_COMP_INTER_SIZE + VP9_COMP_REF_SIZE + VP9_SINGLE_REF_SIZE)
+#define VP9_IF_Y_MODE_START      (VP9_REF_MODE_START + VP9_REF_MODE_SIZE)
+#define VP9_IF_Y_MODE_SIZE       36
+#define VP9_IF_UV_MODE_START     (VP9_IF_Y_MODE_START + VP9_IF_Y_MODE_SIZE)
+#define VP9_IF_UV_MODE_SIZE      92 /* only use 90*/
+#define VP9_MV_JOINTS_START      (VP9_IF_UV_MODE_START + VP9_IF_UV_MODE_SIZE)
+#define VP9_MV_JOINTS_SIZE       3
+#define VP9_MV_SIGN_0_START      (VP9_MV_JOINTS_START + VP9_MV_JOINTS_SIZE)
+#define VP9_MV_SIGN_0_SIZE       1
+#define VP9_MV_CLASSES_0_START   (VP9_MV_SIGN_0_START + VP9_MV_SIGN_0_SIZE)
+#define VP9_MV_CLASSES_0_SIZE    10
+#define VP9_MV_CLASS0_0_START    (VP9_MV_CLASSES_0_START + VP9_MV_CLASSES_0_SIZE)
+#define VP9_MV_CLASS0_0_SIZE     1
+#define VP9_MV_BITS_0_START      (VP9_MV_CLASS0_0_START + VP9_MV_CLASS0_0_SIZE)
+#define VP9_MV_BITS_0_SIZE       10
+#define VP9_MV_SIGN_1_START      (VP9_MV_BITS_0_START + VP9_MV_BITS_0_SIZE)
+#define VP9_MV_SIGN_1_SIZE       1
+#define VP9_MV_CLASSES_1_START   \
+			(VP9_MV_SIGN_1_START + VP9_MV_SIGN_1_SIZE)
+#define VP9_MV_CLASSES_1_SIZE    10
+#define VP9_MV_CLASS0_1_START    \
+			(VP9_MV_CLASSES_1_START + VP9_MV_CLASSES_1_SIZE)
+#define VP9_MV_CLASS0_1_SIZE     1
+#define VP9_MV_BITS_1_START      \
+			(VP9_MV_CLASS0_1_START + VP9_MV_CLASS0_1_SIZE)
+#define VP9_MV_BITS_1_SIZE       10
+#define VP9_MV_CLASS0_FP_0_START \
+			(VP9_MV_BITS_1_START + VP9_MV_BITS_1_SIZE)
+#define VP9_MV_CLASS0_FP_0_SIZE  9
+#define VP9_MV_CLASS0_FP_1_START \
+			(VP9_MV_CLASS0_FP_0_START + VP9_MV_CLASS0_FP_0_SIZE)
+#define VP9_MV_CLASS0_FP_1_SIZE  9
+#define VP9_MV_CLASS0_HP_0_START \
+			(VP9_MV_CLASS0_FP_1_START + VP9_MV_CLASS0_FP_1_SIZE)
+#define VP9_MV_CLASS0_HP_0_SIZE  2
+#define VP9_MV_CLASS0_HP_1_START \
+			(VP9_MV_CLASS0_HP_0_START + VP9_MV_CLASS0_HP_0_SIZE)
+#define VP9_MV_CLASS0_HP_1_SIZE  2
+#define VP9_MV_START             VP9_MV_JOINTS_START
+#define VP9_MV_SIZE              72 /*only use 69*/
+
+#define VP9_TOTAL_SIZE           (VP9_MV_START + VP9_MV_SIZE)
+
+/*
+ * Pack a frame context into the layout the microcode reads.
+ *
+ * Two things make this necessary.  The microcode **only ever reads** the
+ * probability buffer - measured: not one byte of it, of the two stream swap
+ * buffers or of the segment map is ever written back, while changing its
+ * contents demonstrably changes the picture.  And it reloads its contexts from
+ * there every time it is started, which is once per frame, because it halts
+ * itself when a frame is done.  So the host owns that buffer, and a stream
+ * whose contexts are the defaults - anything encoded frame-parallel or
+ * error-resilient, which is most of what players meet - needs the defaults
+ * written into it or every frame after the first decodes against zeroes.
+ *
+ * The physical layout is not dense: adapt_coef_probs() addresses it as
+ * `prob[offset / 4 * 2]` over u32s, so each group of four bytes sits at an
+ * eight-byte stride.  Everything below indexes logically and pack() applies
+ * that.
+ */
+
+static void pack(u8 *dst, int off, u8 v)
+{
+	dst[(off / 4) * 8 + (off & 3)] = v;
+}
+
+static void pack_n(u8 *dst, int off, const u8 *src, int n)
+{
+	int i;
+
+	for (i = 0; i < n; i++)
+		pack(dst, off + i, src[i]);
+}
+
+void aml_vp9_pack_probs(u8 *dst, const struct v4l2_vp9_frame_context *p)
+{
+	int off, tx, plane, type, band, cxt, cxt_num, i;
+
+	memset(dst, 0, VP9_PROB_CONTEXT_SIZE);
+
+	/* Inter partition probabilities; the key-frame set is the microcode's. */
+	pack_n(dst, VP9_PARTITION_P_START, &p->partition[0][0], 16 * 3);
+
+	pack_n(dst, VP9_SKIP_START, p->skip, 3);
+
+	pack(dst, VP9_TX_MODE_START + VP9_TX_MODE_8_0_OFFSET, p->tx8[0][0]);
+	pack(dst, VP9_TX_MODE_START + VP9_TX_MODE_8_1_OFFSET, p->tx8[1][0]);
+	pack_n(dst, VP9_TX_MODE_START + VP9_TX_MODE_16_0_OFFSET, p->tx16[0], 2);
+	pack_n(dst, VP9_TX_MODE_START + VP9_TX_MODE_16_1_OFFSET, p->tx16[1], 2);
+	pack_n(dst, VP9_TX_MODE_START + VP9_TX_MODE_32_0_OFFSET, p->tx32[0], 3);
+	pack_n(dst, VP9_TX_MODE_START + VP9_TX_MODE_32_1_OFFSET, p->tx32[1], 3);
+
+	/*
+	 * Coefficients.  Band 0 has three contexts and a byte of padding after
+	 * them, the other five have six; three nodes each.
+	 */
+	for (tx = 0; tx < 4; tx++) {
+		int tx_start = VP9_COEF_START + tx * 4 * VP9_COEF_SIZE_ONE_SET;
+
+		for (plane = 0; plane < 2; plane++) {
+			int plane_start = tx_start +
+					  plane * 2 * VP9_COEF_SIZE_ONE_SET;
+
+			for (type = 0; type < 2; type++) {
+				int band_start = plane_start +
+						 type * VP9_COEF_SIZE_ONE_SET;
+
+				for (band = 0; band < 6; band++) {
+					cxt_num = band ? 6 : 3;
+					for (cxt = 0; cxt < cxt_num; cxt++)
+						pack_n(dst,
+						       band_start + cxt * 3,
+						       p->coef[tx][plane][type][band][cxt],
+						       3);
+					band_start += band ? 18 : 10;
+				}
+			}
+		}
+	}
+
+	pack_n(dst, VP9_INTER_MODE_START, &p->inter_mode[0][0], 7 * 3);
+	pack_n(dst, VP9_INTERP_START, &p->interp_filter[0][0], 4 * 2);
+	pack_n(dst, VP9_INTRA_INTER_START, p->is_inter, 4);
+	pack_n(dst, VP9_COMP_INTER_START, p->comp_mode, 5);
+	pack_n(dst, VP9_COMP_REF_START, p->comp_ref, 5);
+	pack_n(dst, VP9_SINGLE_REF_START, &p->single_ref[0][0], 5 * 2);
+	pack_n(dst, VP9_IF_Y_MODE_START, &p->y_mode[0][0], 4 * 9);
+	pack_n(dst, VP9_IF_UV_MODE_START, &p->uv_mode[0][0], 10 * 9);
+
+	pack_n(dst, VP9_MV_JOINTS_START, p->mv.joint, 3);
+	pack(dst, VP9_MV_SIGN_0_START, p->mv.sign[0]);
+	pack_n(dst, VP9_MV_CLASSES_0_START, p->mv.classes[0], 10);
+	pack(dst, VP9_MV_CLASS0_0_START, p->mv.class0_bit[0]);
+	pack_n(dst, VP9_MV_BITS_0_START, p->mv.bits[0], 10);
+	pack(dst, VP9_MV_SIGN_1_START, p->mv.sign[1]);
+	pack_n(dst, VP9_MV_CLASSES_1_START, p->mv.classes[1], 10);
+	pack(dst, VP9_MV_CLASS0_1_START, p->mv.class0_bit[1]);
+	pack_n(dst, VP9_MV_BITS_1_START, p->mv.bits[1], 10);
+
+	for (i = 0; i < 2; i++) {
+		off = i ? VP9_MV_CLASS0_FP_1_START : VP9_MV_CLASS0_FP_0_START;
+		pack_n(dst, off, p->mv.class0_fr[i][0], 3);
+		pack_n(dst, off + 3, p->mv.class0_fr[i][1], 3);
+		pack_n(dst, off + 6, p->mv.fr[i], 3);
+
+		off = i ? VP9_MV_CLASS0_HP_1_START : VP9_MV_CLASS0_HP_0_START;
+		pack(dst, off, p->mv.class0_hp[i]);
+		pack(dst, off + 1, p->mv.hp[i]);
+	}
+}
diff --git a/drivers/media/platform/amlogic/vdec/vp9_prob.h b/drivers/media/platform/amlogic/vdec/vp9_prob.h
new file mode 100644
index 000000000000..22176ec1a4eb
--- /dev/null
+++ b/drivers/media/platform/amlogic/vdec/vp9_prob.h
@@ -0,0 +1,32 @@
+/* SPDX-License-Identifier: (GPL-2.0-only OR MIT) */
+/*
+ * VP9 backward probability adaptation - see vp9_prob.c.
+ */
+
+#ifndef _AML_VP9_PROB_H_
+#define _AML_VP9_PROB_H_
+
+#include <media/v4l2-vp9.h>
+
+/* Bytes of probability state the microcode keeps per frame context. */
+#define VP9_PROB_CONTEXT_SIZE	(496 * 2 * 4)
+
+/*
+ * Fold the symbol counts the microcode collected for the frame just decoded
+ * into the entropy context it will use for the next one.
+ *
+ * @pic_count: pictures decoded so far, for tracing only
+ * @prev_kf:   the previous frame was a key frame
+ * @cur_kf:    the frame just decoded was a key frame
+ * @pre_fc:    the frame-context slot the microcode used, 0..3
+ * @prev_prob: that slot's context, the input probabilities
+ * @cur_prob:  where the adapted context is written
+ * @count:     the count buffer the microcode DMA'd out
+ */
+/*
+ * Write the VP9 default probabilities, or any frame context, into the packed
+ * layout the microcode reads.  See vp9_prob.c for why the host has to.
+ */
+void aml_vp9_pack_probs(u8 *dst, const struct v4l2_vp9_frame_context *p);
+
+#endif
-- 
2.56.0


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

* [PATCH 09/11] media: amlogic: vdec: add HEVC decoding on the HEVC core
  2026-10-25 19:24 [PATCH 00/11] media: amlogic: vdec: S4 decoding beyond H.264 Baseline christian.koever-draxl
                   ` (7 preceding siblings ...)
  2026-10-25 19:25 ` [PATCH 08/11] media: amlogic: vdec: add VP9 " christian.koever-draxl
@ 2026-10-25 19:25 ` christian.koever-draxl
  2026-10-25 20:26   ` sashiko-bot
  2026-10-25 19:25 ` [PATCH 10/11] media: amlogic: vdec: add MPEG-4 Part 2 decoding christian.koever-draxl
                   ` (2 subsequent siblings)
  11 siblings, 1 reply; 20+ messages in thread
From: christian.koever-draxl @ 2026-10-25 19:25 UTC (permalink / raw)
  To: zhentao.guo, mchehab, robh, krzk+dt, conor+dt
  Cc: linux-media, devicetree, linux-kernel,
	Christian Stefan Kövér-Draxl

From: Christian Stefan Kövér-Draxl <ckoever@outlook.de>

Decode HEVC Main and Main 10 (10-bit output as rounded 8-bit NV12) up
to 4K, including scaling lists. The microcode parses slice headers
itself, so the access unit is rebuilt in Annex-B form with VPS/SPS/PPS
from the stateless controls; picture slots are sized from the DPB and
the reference lists are built from the RPS.

RFC: the microcode does not count explicitly coded long-term pictures
in NumPicTotalCurr and hangs on an SPS with a long-term reference
table. A client that rewrites the slice header to avoid this can pass
the final reference lists as POCs in the driver-private control
V4L2_CID_AML_HEVC_REFS. With it LTRPSPS_A_Qualcomm_1 decodes 476 of 500
frames bit-exact. A private control is not acceptable as uAPI; this
needs a different solution before it can be merged.

Signed-off-by: Christian Stefan Kövér-Draxl <ckoever@outlook.de>
---
 drivers/media/platform/amlogic/vdec/Makefile  |    2 +-
 .../media/platform/amlogic/vdec/aml_vdec.c    |   18 +-
 .../platform/amlogic/vdec/aml_vdec_platform.c |   17 +
 drivers/media/platform/amlogic/vdec/hevc.c    | 1728 +++++++++++++++++
 drivers/media/platform/amlogic/vdec/hevc.h    |  125 ++
 5 files changed, 1888 insertions(+), 2 deletions(-)
 create mode 100644 drivers/media/platform/amlogic/vdec/hevc.c
 create mode 100644 drivers/media/platform/amlogic/vdec/hevc.h

diff --git a/drivers/media/platform/amlogic/vdec/Makefile b/drivers/media/platform/amlogic/vdec/Makefile
index e32cac07a4a7..ce2db477a103 100644
--- a/drivers/media/platform/amlogic/vdec/Makefile
+++ b/drivers/media/platform/amlogic/vdec/Makefile
@@ -1,4 +1,4 @@
 # SPDX-License-Identifier: GPL-2.0-only
-aml-vdec-drv-objs := aml_vdec.o aml_vdec_drv.o aml_vdec_hw.o aml_vdec_platform.o h264.o h264_headers.o av1.o vp9.o vp9_prob.o mpeg12.o mjpeg.o aml_vdec_tee_fw.o
+aml-vdec-drv-objs := aml_vdec.o aml_vdec_drv.o aml_vdec_hw.o aml_vdec_platform.o h264.o h264_headers.o av1.o vp9.o vp9_prob.o hevc.o mpeg12.o mjpeg.o aml_vdec_tee_fw.o
 
 obj-$(CONFIG_VIDEO_AMLOGIC_VDEC) += aml-vdec-drv.o
diff --git a/drivers/media/platform/amlogic/vdec/aml_vdec.c b/drivers/media/platform/amlogic/vdec/aml_vdec.c
index f1696aa2fac5..ee020a62836f 100644
--- a/drivers/media/platform/amlogic/vdec/aml_vdec.c
+++ b/drivers/media/platform/amlogic/vdec/aml_vdec.c
@@ -13,6 +13,7 @@
 #include "aml_vdec_hw.h"
 #include "aml_vdec_platform.h"
 #include "aml_vdec_tee_fw.h"
+#include "hevc.h"
 
 #define VCODEC_DRV_NAME "aml-vdec-drv"
 
@@ -112,7 +113,19 @@ static const struct aml_vdec_v4l2_ctrl controls[] = {
 		.cfg = {
 			.id = V4L2_CID_STATELESS_MPEG2_QUANTISATION,
 		},
-	},};
+	}, {
+		.codec_type = CODEC_TYPE_HEVC,
+		.cfg = {
+			.id = V4L2_CID_AML_HEVC_REFS,
+			.name = "HEVC Reference Lists",
+			.type = V4L2_CTRL_TYPE_INTEGER,
+			.min = S32_MIN,
+			.max = S32_MAX,
+			.step = 1,
+			.dims = { AML_HEVC_REFS_LEN },
+		},
+	}
+};
 
 static const struct aml_dec_type dec_type_name[] = {
 	{
@@ -124,6 +137,9 @@ static const struct aml_dec_type dec_type_name[] = {
 	}, {
 		.codec_type = CODEC_TYPE_VP9,
 		.name = "VP9",
+	}, {
+		.codec_type = CODEC_TYPE_HEVC,
+		.name = "HEVC",
 	}, {
 		.codec_type = CODEC_TYPE_MPEG2,
 		.name = "MPEG2",
diff --git a/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c b/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c
index 1620e2764f47..36ae12e6cdd8 100644
--- a/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c
+++ b/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c
@@ -8,6 +8,7 @@
 #include "h264.h"
 #include "av1.h"
 #include "vp9.h"
+#include "hevc.h"
 #include "mpeg12.h"
 #include "mjpeg.h"
 
@@ -45,6 +46,17 @@ static struct aml_video_fmt aml_s4_video_formats[] = {
 		.stepwise = {AML_VDEC_MIN_W, AML_VDEC_4K_MAX_W, 2,
 			AML_VDEC_MIN_H, AML_VDEC_4K_MAX_H, 2},
 	},
+	{
+		.name = "HEVC",
+		.fourcc = V4L2_PIX_FMT_HEVC_SLICE,
+		.type = AML_FMT_DEC,
+		.align = 64,
+		.is_10_bit_support = 0,
+		.codec_type = CODEC_TYPE_HEVC,
+		.num_planes = 1,
+		.stepwise = {AML_VDEC_MIN_W, AML_VDEC_4K_MAX_W, 2,
+			AML_VDEC_MIN_H, AML_VDEC_4K_MAX_H, 2},
+	},
 	{
 		.name = "MPEG-2",
 		.fourcc = V4L2_PIX_FMT_MPEG2_SLICE,
@@ -137,6 +149,11 @@ const struct aml_codec_ops aml_S4_dec_ops[] = {
 		.exit = aml_vp9_exit,
 		.run = aml_vp9_dec_run,
 	},
+	[CODEC_TYPE_HEVC] = {
+		.init = aml_hevc_init,
+		.exit = aml_hevc_exit,
+		.run = aml_hevc_dec_run,
+	},
 	[CODEC_TYPE_MPEG2] = {
 		.init = aml_mpeg12_init,
 		.exit = aml_mpeg12_exit,
diff --git a/drivers/media/platform/amlogic/vdec/hevc.c b/drivers/media/platform/amlogic/vdec/hevc.c
new file mode 100644
index 000000000000..6d9f457436cf
--- /dev/null
+++ b/drivers/media/platform/amlogic/vdec/hevc.c
@@ -0,0 +1,1728 @@
+// SPDX-License-Identifier: (GPL-2.0-only OR MIT)
+/*
+ * HEVC (H.265) decoding on the Amlogic S4 HEVC core.
+ *
+ * Same core as VP9 and AV1, running the HEVC microcode.  The division of work
+ * follows the reference driver (vh265.c):
+ *
+ *   - The microcode finds NAL units and asks what to do with each one.  It
+ *     parses VPS, SPS, PPS and slice headers itself and keeps the parameter
+ *     sets in workspace buffers, so a stream fed one access unit at a time
+ *     only has to carry them once.
+ *   - After a slice header it hands the host a parameter block (RPM): the
+ *     picture geometry, POC lsb, the current reference picture set, the
+ *     reference list modification and so on.
+ *   - The host owns the picture buffers: it computes the POC, marks
+ *     references from the RPS, builds the two reference lists, names the
+ *     reference pictures and the collocated picture to the hardware and
+ *     points the output at a buffer.  Then it says "decode the slice data".
+ *
+ * Pictures are written compressed through the MMU (the reference picture the
+ * hardware reads back) and, by the double-write unit, as plain NV12 - straight
+ * into the CAPTURE buffer when its geometry allows.
+ *
+ * This first version is driven with raw Annex-B access units, one per OUTPUT
+ * buffer, and hands pictures back in decode order.
+ */
+
+#include <linux/delay.h>
+#include <linux/dma-mapping.h>
+#include <linux/module.h>
+#include <linux/slab.h>
+#include <media/v4l2-mem2mem.h>
+#include <media/videobuf2-dma-contig.h>
+
+#include "aml_vdec_drv.h"
+#include "aml_vdec_hw.h"
+#include "hevc.h"
+
+/* Workspace, the reference's amvh265_workbuff_spec[1] (4096x2048). */
+#define H265_IPP_SIZE		0x4000
+#define H265_SAO_ABV_BUF_SIZE	0x30000
+#define H265_SAO_VB_BUF_SIZE	0x30000
+#define H265_SHORT_TERM_RPS_SIZE 0x0800
+#define H265_VPS_SIZE		0x0800
+#define H265_SPS_SIZE		0x0800
+#define H265_PPS_SIZE		0x2000
+#define H265_SAO_UP_SIZE	0x2800
+#define H265_SWAP_BUF_SIZE	0x0800
+#define H265_SWAP_BUF2_SIZE	0x0800
+#define H265_SCALELUT_SIZE	0x8000
+#define H265_DBLK_PARA_SIZE	0x19100
+#define H265_DBLK_DATA_SIZE	0x88800
+#define H265_DBLK_DATA2_SIZE	0x48800
+#define H265_MMU_VBH_SIZE	0x5000
+#define H265_MPRED_ABOVE_SIZE	0x4000
+
+#define H265_RPM_SIZE		(0x80 * 2)
+#define H265_LMEM_SIZE		(0x500 * 2)
+#define H265_MMU_MAP_SIZE	(0x1200 * 4)
+#define H265_AUX_PREFIX_SIZE	(12 * 1024)
+#define H265_AUX_SUFFIX_SIZE	(12 * 1024)
+
+#define H265_PADDING_SIZE	(16 * 1024)
+#define H265_TIMEOUT_MS		500
+
+/* 16 references plus the picture being decoded plus slack. */
+#define H265_SLOTS		18
+#define H265_MAX_REF		16
+#define H265_MAX_TILE_COLS	10
+#define H265_MAX_TILE_ROWS	20
+#define H265_MIN_DIM		16
+#define H265_MAX_DIM		4096
+
+#define H265_INVALID_POC	0x80000000
+#define H265_MAX_INT		0x7fffffff
+
+/* CUR_RPS entries, SUPPORT_LONG_TERM_RPS layout. */
+#define RPS_END			0x8000
+#define RPS_LT_BIT		14
+#define RPS_USED_BIT		13
+
+/* misc_flag0 bits */
+#define PCM_LOOP_FILTER_DISABLED_FLAG_BIT		0
+#define PCM_ENABLE_FLAG_BIT				1
+#define LOOP_FILTER_ACROSS_TILES_ENABLED_FLAG_BIT	2
+#define PPS_LOOP_FILTER_ACROSS_SLICES_ENABLED_FLAG_BIT	3
+#define DEBLOCKING_FILTER_OVERRIDE_ENABLED_FLAG_BIT	4
+#define PPS_DEBLOCKING_FILTER_DISABLED_FLAG_BIT		5
+#define DEBLOCKING_FILTER_OVERRIDE_FLAG_BIT		6
+#define SLICE_DEBLOCKING_FILTER_DISABLED_FLAG_BIT	7
+#define SLICE_SAO_LUMA_FLAG_BIT				8
+#define SLICE_SAO_CHROMA_FLAG_BIT			9
+#define SLICE_LOOP_FILTER_ACROSS_SLICES_ENABLED_FLAG_BIT 10
+
+/* The RPM parameter block, the reference's union param_u, as u16 indices. */
+enum {
+	P_CUR_RPS = 0,			/* 16 entries */
+	P_NUM_REF_IDX_L0_ACTIVE = 16,
+	P_NUM_REF_IDX_L1_ACTIVE,
+	P_SLICE_TYPE,
+	P_SLICE_TEMPORAL_MVP_ENABLE_FLAG,
+	P_DEPENDENT_SLICE_SEGMENT_FLAG,
+	P_SLICE_SEGMENT_ADDRESS,
+	P_NUM_TITLE_ROWS_MINUS1,
+	P_PIC_WIDTH,
+	P_PIC_HEIGHT,
+	P_LOG2_MIN_CB_SIZE_MINUS3,
+	P_LOG2_DIFF_MAX_MIN_CB_SIZE,
+	P_LOG2_MAX_POC_LSB_MINUS4,
+	P_POC_LSB,
+	P_COLLOCATED_FROM_L0_FLAG,
+	P_COLLOCATED_REF_IDX,
+	P_LOG2_PARALLEL_MERGE_LEVEL,
+	P_FIVE_MINUS_MAX_NUM_MERGE_CAND,
+	P_SPS_NUM_REORDER_PICS_0,
+	P_MODIFICATION_FLAG,
+	P_TILES_ENABLED_FLAG,
+	P_NUM_TILE_COLUMNS_MINUS1,
+	P_NUM_TILE_ROWS_MINUS1,
+	P_TILE_WIDTH,			/* 12 entries */
+	P_TILE_HEIGHT = P_TILE_WIDTH + 12,	/* 8 entries */
+	P_MISC_FLAG0 = P_TILE_HEIGHT + 8,
+	P_PPS_BETA_OFFSET_DIV2,
+	P_PPS_TC_OFFSET_DIV2,
+	P_SLICE_BETA_OFFSET_DIV2,
+	P_SLICE_TC_OFFSET_DIV2,
+	P_PPS_CB_QP_OFFSET,
+	P_PPS_CR_QP_OFFSET,
+	P_FIRST_SLICE_SEGMENT_IN_PIC_FLAG,
+	P_TEMPORAL_ID,
+	P_NAL_UNIT_TYPE,
+	P_VUI_NUM_UNITS_IN_TICK_HI,
+	P_VUI_NUM_UNITS_IN_TICK_LO,
+	P_VUI_TIME_SCALE_HI,
+	P_VUI_TIME_SCALE_LO,
+	P_BIT_DEPTH,
+	P_PROFILE_ETC,
+	P_SEI_FRAME_FIELD_INFO,
+	P_VIDEO_SIGNAL_TYPE,
+	P_MODIFICATION_LIST,		/* 0x20 entries */
+	P_CONFORMANCE_WINDOW_FLAG = P_MODIFICATION_LIST + 0x20,
+	P_CONF_WIN_LEFT_OFFSET,
+	P_CONF_WIN_RIGHT_OFFSET,
+	P_CONF_WIN_TOP_OFFSET,
+	P_CONF_WIN_BOTTOM_OFFSET,
+	P_CHROMA_FORMAT_IDC,
+	P_COLOR_DESCRIPTION,
+	P_ASPECT_RATIO_IDC,
+	P_SAR_WIDTH,
+	P_SAR_HEIGHT,
+	P_SPS_MAX_DEC_PIC_BUFFERING_MINUS1_0,
+	P_RPM_COUNT = 0x80
+};
+
+#define PARSER_CMD_SKIP_CFG_0	0x0000090b
+#define PARSER_CMD_SKIP_CFG_1	0x1b14140f
+#define PARSER_CMD_SKIP_CFG_2	0x001b1910
+static const u16 h265_parser_cmd[] = {
+	0x0401, 0x8401, 0x0800, 0x0402, 0x9002, 0x1423,
+	0x8CC3, 0x1423, 0x8804, 0x9825, 0x0800, 0x04FE,
+	0x8406, 0x8411, 0x1800, 0x8408, 0x8409, 0x8C2A,
+	0x9C2B, 0x1C00, 0x840F, 0x8407, 0x8000, 0x8408,
+	0x2000, 0xA800, 0x8410, 0x04DE, 0x840C, 0x840D,
+	0xAC00, 0xA000, 0x08C0, 0x08E0, 0xA40E, 0xFC00,
+	0x7C00
+};
+
+struct h265_pic {
+	bool valid;		/* holds a decoded or decoding picture */
+	bool referenced;
+	bool long_term;
+	int poc;
+	u32 decode_idx;
+	u32 width, height;
+	int slice_type;
+
+	void *body_v, *hdr_v, *mv_v, *dw_v;
+	dma_addr_t body_p, hdr_p, mv_p, dw_p;
+};
+
+struct h265_tile {
+	int start_cu_x, start_cu_y, width, height;
+	dma_addr_t sao_vb, sao_abv;
+};
+
+struct aml_hevc_ctx {
+	struct aml_vdec_ctx *v4l2_ctx;
+	struct aml_vdec_hw *hw;
+	bool ucode_loaded;
+	bool started;
+	int rounds;
+	bool have_picture;
+	bool finished;
+
+	/* workspace */
+	void *ws_v;
+	dma_addr_t ws_p;
+	size_t ws_size;
+	dma_addr_t ipp, sao_abv, sao_vb, short_term_rps, vps, sps, pps, sao_up;
+	dma_addr_t swap_buf, swap_buf2, scalelut, dblk_para, dblk_data;
+	dma_addr_t dblk_data2, mmu_vbh, mpred_above;
+	void *side_v;
+	dma_addr_t side_p;
+	size_t side_size;
+	dma_addr_t rpm, lmem, mmu_map, aux;
+	void *stream_v;
+	dma_addr_t stream_p;
+	size_t stream_size;
+	size_t payload;
+
+	/* picture buffers */
+	struct h265_pic pic[H265_SLOTS];
+	size_t body_size, hdr_size, mv_size, dw_size;
+	u32 alloc_w, alloc_h;
+	/* picture slots in use: the stream's DPB size plus two, at most H265_SLOTS */
+	int nslots, alloc_nslots;
+	bool ten_bit, alloc_ten_bit;
+	dma_addr_t dw_target_y, dw_target_c;
+	size_t dw_target_size;
+
+	/* stream state kept across access units */
+	u32 start_decoding_flag;
+	u32 rps_set_id;
+	bool have_vps, have_sps, have_pps, have_start;
+	int prev_tid0_poc;
+	int poc_random_access;
+	u32 decode_idx;
+
+	/* picture state */
+	u16 p[P_RPM_COUNT];
+	int cur;		/* slot being decoded, -1 when none */
+	int col;
+	int curr_poc, iprev_poc, col_poc;
+	bool new_pic;
+	int nal_type, temporal_id;
+	u32 pic_w, pic_h;
+	int lcu_size, lcu_size_log2, lcu_x_num, lcu_y_num, lcu_total;
+	int slice_addr, slice_segment_addr;
+	int slice_type;
+	bool tmvp, is_next_slice_segment, ldc, col_from_l0;
+	int plevel, max_merge_cand;
+	int ref_num_l0, ref_num_l1;
+	int ref_poc_l0[H265_MAX_REF], ref_poc_l1[H265_MAX_REF];
+	u32 misc_flag0;
+
+	bool tile_enabled;
+	int num_tile_col, num_tile_row;
+	struct h265_tile tile[H265_MAX_TILE_ROWS][H265_MAX_TILE_COLS];
+	int tile_x, tile_y;
+	bool new_tile;
+	int tile_start_lcu_x, tile_start_lcu_y, tile_width_lcu, tile_height_lcu;
+};
+
+static void wr(struct aml_vdec_hw *hw, u32 reg, u32 val)
+{
+	regmap_write(hw->map[DOS_BUS], reg, val);
+}
+
+/* ---------------------------------------------------------------- */
+/* buffers                                                          */
+/* ---------------------------------------------------------------- */
+
+static void h265_free_pics(struct aml_hevc_ctx *h)
+{
+	struct device *dev = h->hw->dev;
+	int i;
+
+	for (i = 0; i < H265_SLOTS; i++) {
+		struct h265_pic *pic = &h->pic[i];
+
+		if (pic->body_v)
+			dma_free_coherent(dev, h->body_size, pic->body_v, pic->body_p);
+		if (pic->hdr_v)
+			dma_free_coherent(dev, h->hdr_size, pic->hdr_v, pic->hdr_p);
+		if (pic->mv_v)
+			dma_free_coherent(dev, h->mv_size, pic->mv_v, pic->mv_p);
+		if (pic->dw_v)
+			dma_free_noncoherent(dev, h->dw_size, pic->dw_v, pic->dw_p,
+					     DMA_FROM_DEVICE);
+		memset(pic, 0, sizeof(*pic));
+	}
+	h->body_size = 0;
+	h->hdr_size = 0;
+	h->mv_size = 0;
+	h->dw_size = 0;
+	h->alloc_w = 0;
+	h->alloc_h = 0;
+	h->alloc_nslots = 0;
+}
+
+static void h265_free(struct aml_hevc_ctx *h)
+{
+	struct device *dev = h->hw->dev;
+
+	h265_free_pics(h);
+	if (h->stream_v)
+		dma_free_coherent(dev, h->stream_size, h->stream_v, h->stream_p);
+	if (h->side_v)
+		dma_free_coherent(dev, h->side_size, h->side_v, h->side_p);
+	if (h->ws_v)
+		dma_free_coherent(dev, h->ws_size, h->ws_v, h->ws_p);
+	h->stream_v = NULL;
+	h->side_v = NULL;
+	h->ws_v = NULL;
+}
+
+/* Hand out @size bytes at *@p and move it on to the next 4K boundary */
+static dma_addr_t h265_carve(dma_addr_t *p, size_t size)
+{
+	dma_addr_t addr = *p;
+
+	*p = ALIGN(addr + size, SZ_4K);
+
+	return addr;
+}
+
+static int h265_alloc_workspace(struct aml_hevc_ctx *h)
+{
+	struct device *dev = h->hw->dev;
+	dma_addr_t p;
+
+	if (h->ws_v)
+		return 0;
+
+	h->ws_size = ALIGN(H265_IPP_SIZE + H265_SAO_ABV_BUF_SIZE + H265_SAO_VB_BUF_SIZE +
+			   H265_SHORT_TERM_RPS_SIZE + H265_VPS_SIZE +
+			   H265_SPS_SIZE + H265_PPS_SIZE + H265_SAO_UP_SIZE +
+			   H265_SWAP_BUF_SIZE + H265_SWAP_BUF2_SIZE +
+			   H265_SCALELUT_SIZE + H265_DBLK_PARA_SIZE +
+			   H265_DBLK_DATA_SIZE + H265_DBLK_DATA2_SIZE +
+			   H265_MMU_VBH_SIZE + H265_MPRED_ABOVE_SIZE +
+			   16 * SZ_4K, SZ_64K);
+	h->ws_v = dma_alloc_coherent(dev, h->ws_size, &h->ws_p, GFP_KERNEL);
+	if (!h->ws_v)
+		return -ENOMEM;
+	memset(h->ws_v, 0, h->ws_size);
+
+	/* The reference's init_buff_spec() order, each piece page aligned. */
+	p = h->ws_p;
+	h->ipp = h265_carve(&p, H265_IPP_SIZE);
+	h->sao_abv = h265_carve(&p, H265_SAO_ABV_BUF_SIZE);
+	h->sao_vb = h265_carve(&p, H265_SAO_VB_BUF_SIZE);
+	h->short_term_rps = h265_carve(&p, H265_SHORT_TERM_RPS_SIZE);
+	h->vps = h265_carve(&p, H265_VPS_SIZE);
+	h->sps = h265_carve(&p, H265_SPS_SIZE);
+	h->pps = h265_carve(&p, H265_PPS_SIZE);
+	h->sao_up = h265_carve(&p, H265_SAO_UP_SIZE);
+	h->swap_buf = h265_carve(&p, H265_SWAP_BUF_SIZE);
+	h->swap_buf2 = h265_carve(&p, H265_SWAP_BUF2_SIZE);
+	h->scalelut = h265_carve(&p, H265_SCALELUT_SIZE);
+	h->dblk_para = h265_carve(&p, H265_DBLK_PARA_SIZE);
+	h->dblk_data = h265_carve(&p, H265_DBLK_DATA_SIZE);
+	h->dblk_data2 = h265_carve(&p, H265_DBLK_DATA2_SIZE);
+	h->mmu_vbh = h265_carve(&p, H265_MMU_VBH_SIZE);
+	h->mpred_above = h265_carve(&p, H265_MPRED_ABOVE_SIZE);
+
+	h->side_size = ALIGN(H265_RPM_SIZE + H265_LMEM_SIZE + H265_MMU_MAP_SIZE +
+			     H265_AUX_PREFIX_SIZE + H265_AUX_SUFFIX_SIZE +
+			     4 * SZ_4K, SZ_64K);
+	h->side_v = dma_alloc_coherent(dev, h->side_size, &h->side_p, GFP_KERNEL);
+	if (!h->side_v) {
+		h265_free(h);
+		return -ENOMEM;
+	}
+	memset(h->side_v, 0, h->side_size);
+	p = h->side_p;
+	h->rpm = h265_carve(&p, H265_RPM_SIZE);
+	h->lmem = h265_carve(&p, H265_LMEM_SIZE);
+	h->mmu_map = h265_carve(&p, H265_MMU_MAP_SIZE);
+	h->aux = h265_carve(&p, H265_AUX_PREFIX_SIZE + H265_AUX_SUFFIX_SIZE);
+
+	dev_info(dev, "hevc: workspace %zu KiB at %pad, side %zu KiB\n",
+		 h->ws_size / 1024, &h->ws_p, h->side_size / 1024);
+	return 0;
+}
+
+/*
+ * Compressed body per 64x32 block: 3200 bytes for 8 bit with memory saving
+ * (as VP9), 4096 for 10 bit, which cannot use it.
+ */
+static u32 h265_comp_body_size(u32 w, u32 h, bool ten_bit)
+{
+	return (ten_bit ? 4096 : 3200) * ((w + 63) >> 6) * ((h + 31) >> 5);
+}
+
+static u32 h265_comp_header_size(u32 w, u32 h)
+{
+	return 32 * ((w + 127) >> 7) * ((h + 63) >> 6);
+}
+
+static size_t h265_hdr_alloc_size(u32 w, u32 h)
+{
+	if (w * h > 4096 * 2304)
+		return 0x120000;
+	if (w * h > 1920 * 1088)
+		return 0x48000;
+	return 0x10000;
+}
+
+static size_t h265_mv_buf_size(u32 w, u32 h, int lcu_size)
+{
+	int unit = lcu_size == 64 ? 0x200 : lcu_size == 32 ? 0x80 : 0x20;
+	u32 xn = DIV_ROUND_UP(w, lcu_size), yn = DIV_ROUND_UP(h, lcu_size);
+
+	return ALIGN(xn * yn * unit, SZ_64K);
+}
+
+static int h265_alloc_pics(struct aml_hevc_ctx *h)
+{
+	struct device *dev = h->hw->dev;
+	u32 w = h->pic_w, ht = h->pic_h;
+	size_t body = ALIGN(h265_comp_body_size(w, ht, h->ten_bit), PAGE_SIZE);
+	/* Size the MV buffers for the smallest LCU, so they fit any. */
+	size_t mv = h265_mv_buf_size(w, ht, 16);
+	size_t hdr = h265_hdr_alloc_size(w, ht);
+	size_t dw = h->dw_target_y ? 0 : ALIGN(w * ht * 3 / 2, SZ_64K);
+	int i;
+
+	if (h->alloc_w == w && h->alloc_h == ht && h->dw_size == dw &&
+	    h->alloc_ten_bit == h->ten_bit && h->alloc_nslots == h->nslots)
+		return 0;
+
+	h265_free_pics(h);
+	for (i = 0; i < h->nslots; i++) {
+		struct h265_pic *pic = &h->pic[i];
+
+		pic->body_v = dma_alloc_coherent(dev, body, &pic->body_p, GFP_KERNEL);
+		pic->hdr_v = dma_alloc_coherent(dev, hdr, &pic->hdr_p, GFP_KERNEL);
+		pic->mv_v = dma_alloc_coherent(dev, mv, &pic->mv_p, GFP_KERNEL);
+		if (dw)
+			pic->dw_v = dma_alloc_noncoherent(dev, dw, &pic->dw_p,
+							  DMA_FROM_DEVICE, GFP_KERNEL);
+		if (!pic->body_v || !pic->hdr_v || !pic->mv_v || (dw && !pic->dw_v)) {
+			h->body_size = body;
+			h->hdr_size = hdr;
+			h->mv_size = mv;
+			h->dw_size = dw;
+			h265_free_pics(h);
+			return -ENOMEM;
+		}
+		memset(pic->mv_v, 0, mv);
+		pic->poc = H265_INVALID_POC;
+	}
+	h->body_size = body;
+	h->hdr_size = hdr;
+	h->mv_size = mv;
+	h->dw_size = dw;
+	h->alloc_w = w;
+	h->alloc_h = ht;
+	h->alloc_ten_bit = h->ten_bit;
+	h->alloc_nslots = h->nslots;
+
+	/* init_pic_list_hw(): the compressed-header address of every picture. */
+	wr(h->hw, HEVCD_MPP_ANC2AXI_TBL_CONF_ADDR, BIT(1) | BIT(2));
+	for (i = 0; i < H265_SLOTS; i++)
+		wr(h->hw, HEVCD_MPP_ANC2AXI_TBL_DATA,
+		   i < h->nslots ? h->pic[i].hdr_p >> 5 : 0);
+	wr(h->hw, HEVCD_MPP_ANC2AXI_TBL_CONF_ADDR, BIT(0));
+	wr(h->hw, HEVCD_MPP_ANC_CANVAS_ACCCONFIG_ADDR, BIT(0));
+	for (i = 0; i < 32; i++)
+		wr(h->hw, HEVCD_MPP_ANC_CANVAS_DATA_ADDR, 0);
+
+	dev_info(dev, "hevc: %d pictures %ux%u, body %zu KiB, mv %zu KiB, dw %zu KiB each\n",
+		 h->nslots, w, ht, body / 1024, mv / 1024, dw / 1024);
+	return 0;
+}
+
+/* ---------------------------------------------------------------- */
+/* hardware setup                                                   */
+/* ---------------------------------------------------------------- */
+
+/* hevc_config_work_space_hw() */
+static void h265_config_workspace(struct aml_hevc_ctx *h)
+{
+	struct aml_vdec_hw *hw = h->hw;
+
+	wr(hw, HEVCD_IPP_LINEBUFF_BASE, h->ipp);
+	wr(hw, HEVC_RPM_BUFFER, h->rpm);
+	wr(hw, HEVC_SHORT_TERM_RPS, h->short_term_rps);
+	wr(hw, H265_VPS_BUFFER, h->vps);
+	wr(hw, H265_SPS_BUFFER, h->sps);
+	wr(hw, HEVC_PPS_BUFFER, h->pps);
+	wr(hw, H265_SAO_UP, h->sao_up);
+	wr(hw, HEVC_ASSIST_MMU_MAP_ADDR, h->mmu_map);
+	wr(hw, H265_SCALELUT, h->scalelut);
+	wr(hw, HEVC_DBLK_CFG3, 0x4010);	/* <= 4096x2304 */
+	wr(hw, HEVC_DBLK_CFG4, h->dblk_para);
+	wr(hw, HEVC_DBLK_CFG5, h->dblk_data);
+	wr(hw, HEVC_DBLK_CFGE, h->dblk_data2);
+	wr(hw, H265_LMEM_DUMP_ADR, h->lmem);
+}
+
+/* hevc_init_decoder_hw(), less the self tests. */
+static void h265_init_decoder(struct aml_hevc_ctx *h)
+{
+	struct aml_vdec_hw *hw = h->hw;
+	u32 v;
+	int i;
+
+	/* reset iqit to start its memory init again */
+	wr(hw, DOS_SW_RESET3, BIT(14));
+	wr(hw, DOS_SW_RESET3, 0);
+	regmap_update_bits(hw->map[DOS_BUS], HEVC_CABAC_CONTROL, 1, 0);
+	regmap_update_bits(hw->map[DOS_BUS], HEVC_PARSER_CORE_CONTROL, 1, 0);
+
+	wr(hw, HEVC_SHIFT_STARTCODE, 0x00000100);
+	wr(hw, HEVC_SHIFT_EMULATECODE, 0x00000300);
+
+	v = read_dos_reg(hw, HEVC_PARSER_INT_CONTROL);
+	v &= 0x03ffffff;
+	v |= (3 << 29) | (2 << 26) |
+	     (1 << 24) |	/* stream_buffer_empty_int_amrisc_enable */
+	     (1 << 22) |	/* stream_fifo_empty_int_amrisc_enable */
+	     (1 << 7) |		/* dec_done_int_cpu_enable */
+	     (1 << 4) |		/* startcode_found_int_cpu_enable */
+	     (1 << 0);		/* parser_int_enable */
+	wr(hw, HEVC_PARSER_INT_CONTROL, v);
+
+	v = read_dos_reg(hw, HEVC_SHIFT_STATUS);
+	v |= BIT(1) | BIT(0);	/* emulation_check_on, startcode_check_on */
+	wr(hw, HEVC_SHIFT_STATUS, v);
+
+	wr(hw, HEVC_SHIFT_CONTROL,
+	   (3 << 6) |		/* sft_valid_wr_position */
+	   (2 << 4) |		/* emulate_code_length_sub_1 */
+	   (2 << 1) |		/* start_code_length_sub_1, 3 bytes */
+	   (1 << 0));		/* stream_shift_enable */
+	wr(hw, HEVC_CABAC_CONTROL, BIT(0));
+	wr(hw, HEVC_PARSER_CORE_CONTROL, BIT(0));
+	wr(hw, HEVC_DEC_STATUS_REG, 0);
+
+	wr(hw, HEVC_IQIT_SCALELUT_WR_ADDR, 0);
+	for (i = 0; i < 1024; i++)
+		wr(hw, HEVC_IQIT_SCALELUT_DATA, 0);
+
+	wr(hw, H265_DECODE_SIZE, 0);
+
+	wr(hw, HEVC_PARSER_CMD_WRITE, BIT(16));
+	for (i = 0; i < ARRAY_SIZE(h265_parser_cmd); i++)
+		wr(hw, HEVC_PARSER_CMD_WRITE, h265_parser_cmd[i]);
+	wr(hw, HEVC_PARSER_CMD_SKIP_0, PARSER_CMD_SKIP_CFG_0);
+	wr(hw, HEVC_PARSER_CMD_SKIP_1, PARSER_CMD_SKIP_CFG_1);
+	wr(hw, HEVC_PARSER_CMD_SKIP_2, PARSER_CMD_SKIP_CFG_2);
+
+	wr(hw, HEVC_PARSER_IF_CONTROL,
+	   (1 << 5) |		/* parser_sao_if_en */
+	   (1 << 2) |		/* parser_mpred_if_en */
+	   (1 << 0));		/* parser_scaler_if_en */
+
+	wr(hw, HEVCD_IPP_TOP_CNTL, BIT(0));	/* reset ipp and mpp */
+	wr(hw, HEVCD_IPP_TOP_CNTL, BIT(1));	/* enable ipp */
+}
+
+static void h265_config_decode_mode(struct aml_hevc_ctx *h)
+{
+	struct aml_vdec_hw *hw = h->hw;
+	u32 mode = H265_DECODE_MODE_MULTI_FRAMEBASE;
+
+	/* performance_profile: multi slice in ucode (HEVC_8K_LFTOFFSET_FIX) */
+	wr(hw, H265_NAL_SEARCH_CTL, read_dos_reg(hw, H265_NAL_SEARCH_CTL) | BIT(21));
+	mode |= (h->start_decoding_flag & 0xff) << 16;
+	mode |= 0x80 << 24;	/* MBX0 interrupt */
+	wr(hw, H265_DECODE_MODE, mode);
+	wr(hw, H265_DECODE_MODE2, h->rps_set_id);
+}
+
+/* vh265_prot_init(), run before every access unit as the reference does. */
+static void h265_prot_init(struct aml_hevc_ctx *h)
+{
+	struct aml_vdec_hw *hw = h->hw;
+
+	h265_config_workspace(h);
+	h265_init_decoder(h);
+
+	wr(hw, HEVC_WAIT_FLAG, 1);
+	wr(hw, HEVC_ASSIST_MBOX0_CLR_REG, 1);
+	wr(hw, HEVC_ASSIST_MBOX0_MASK, 1);
+	wr(hw, HEVC_PSCALE_CTRL, 0);
+	wr(hw, H265_DEBUG_REG1, 0);
+	/* check vps/sps/pps/i-slice in ucode */
+	wr(hw, H265_NAL_SEARCH_CTL, 0x8);
+	wr(hw, H265_DECODE_STOP_POS, 0);
+	h265_config_decode_mode(h);
+
+	wr(hw, H265_AUX_ADR, h->aux);
+	wr(hw, H265_AUX_DATA_SIZE, ((H265_AUX_PREFIX_SIZE >> 4) << 16) |
+				   (H265_AUX_SUFFIX_SIZE >> 4));
+}
+
+static int h265_feed(struct aml_hevc_ctx *h, const void *data, size_t size)
+{
+	struct aml_vdec_hw *hw = h->hw;
+	size_t want = ALIGN(size + H265_PADDING_SIZE, SZ_64K);
+
+	if (!data || !size)
+		return -EINVAL;
+	if (h->stream_v && h->stream_size < want) {
+		dma_free_coherent(hw->dev, h->stream_size, h->stream_v, h->stream_p);
+		h->stream_v = NULL;
+	}
+	if (!h->stream_v) {
+		h->stream_v = dma_alloc_coherent(hw->dev, want, &h->stream_p,
+						 GFP_KERNEL);
+		if (!h->stream_v)
+			return -ENOMEM;
+		h->stream_size = want;
+	}
+
+	memcpy(h->stream_v, data, size);
+	memset(h->stream_v + size, 0, h->stream_size - size);
+	h->payload = size;
+
+	wr(hw, HEVC_STREAM_START_ADDR, h->stream_p);
+	wr(hw, HEVC_STREAM_END_ADDR, h->stream_p + h->stream_size - 1);
+	wr(hw, HEVC_STREAM_RD_PTR, h->stream_p);
+	wr(hw, HEVC_STREAM_WR_PTR, h->stream_p + size + H265_PADDING_SIZE);
+	wr(hw, HEVC_SHIFT_BYTE_COUNT, 0);
+	wr(hw, H265_DECODE_SIZE, size);
+	wr(hw, HEVC_STREAM_CONTROL,
+	   (read_dos_reg(hw, HEVC_STREAM_CONTROL) & ~0xfff0) | (7 << 4) | BIT(0));
+	wr(hw, HEVC_STREAM_FIFO_CTL,
+	   read_dos_reg(hw, HEVC_STREAM_FIFO_CTL) | BIT(29));
+	return 0;
+}
+
+/* ---------------------------------------------------------------- */
+/* picture management                                               */
+/* ---------------------------------------------------------------- */
+
+static int h265_pic_by_poc(struct aml_hevc_ctx *h, int poc, bool ref_only)
+{
+	int i, best = -1;
+
+	if (poc == (int)H265_INVALID_POC)
+		return -1;
+	for (i = 0; i < H265_SLOTS; i++) {
+		struct h265_pic *pic = &h->pic[i];
+
+		if (!pic->valid || pic->poc != poc || i == h->cur)
+			continue;
+		if (ref_only && (!pic->referenced || pic->width != h->pic_w ||
+				 pic->height != h->pic_h))
+			continue;
+		if (best < 0 || pic->decode_idx > h->pic[best].decode_idx)
+			best = i;
+	}
+	return best;
+}
+
+/* Decode a CUR_RPS entry to a POC; returns false at the end marker. */
+static bool h265_rps_poc(struct aml_hevc_ctx *h, int i, int *poc, bool *used, bool *lt)
+{
+	u16 e = h->p[P_CUR_RPS + i];
+	int delt;
+
+	if (e == RPS_END)
+		return false;
+	delt = e & ((1 << (RPS_USED_BIT - 1)) - 1);
+	if (e & (1 << (RPS_USED_BIT - 1)))
+		*poc = h->curr_poc - ((1 << (RPS_USED_BIT - 1)) - delt);
+	else
+		*poc = h->curr_poc + delt;
+	*used = (e >> RPS_USED_BIT) & 1;
+	*lt = (e >> RPS_LT_BIT) & 1;
+	return true;
+}
+
+/* Debug: log each picture's POC, RPS entries and the slots' POCs. */
+
+/* apply_ref_pic_set(): everything the current RPS does not name stops being
+ * a reference.
+ */
+static void h265_apply_rps(struct aml_hevc_ctx *h)
+{
+	int i, j;
+
+	for (i = 0; i < H265_SLOTS; i++) {
+		struct h265_pic *pic = &h->pic[i];
+		bool found = false;
+
+		pic->long_term = false;
+		if (!pic->valid || !pic->referenced || pic->poc == h->curr_poc)
+			continue;
+		for (j = 0; j < 16; j++) {
+			int poc;
+			bool used, lt;
+
+			if (!h265_rps_poc(h, j, &poc, &used, &lt))
+				break;
+			if (poc == pic->poc) {
+				pic->long_term = lt;
+				found = true;
+				break;
+			}
+		}
+		if (!found)
+			pic->referenced = false;
+	}
+}
+
+/*
+ * Entry @c of a reference list built from the short-term pictures in @first
+ * and @second order, followed by the long-term ones.
+ */
+static int h265_ref_entry(int c, const int *first, int n_first,
+			  const int *second, int n_second, const int *lt)
+{
+	if (c >= n_first + n_second)
+		return lt[c - n_first - n_second];
+	if (c >= n_first)
+		return second[c - n_first];
+	return first[c];
+}
+
+/* set_ref_pic_list() */
+static void h265_set_ref_lists(struct aml_hevc_ctx *h)
+{
+	int st0[16], st1[16], lt[16];
+	int n_neg = 0, n_pos = 0, n_lt = 0, total, i, r;
+	int l0 = min_t(int, h->p[P_NUM_REF_IDX_L0_ACTIVE], H265_MAX_REF);
+	int l1 = min_t(int, h->p[P_NUM_REF_IDX_L1_ACTIVE], H265_MAX_REF);
+	u16 mod = h->p[P_MODIFICATION_FLAG];
+
+	memset(h->ref_poc_l0, 0, sizeof(h->ref_poc_l0));
+	memset(h->ref_poc_l1, 0, sizeof(h->ref_poc_l1));
+
+	for (i = 0; i < 16; i++) {
+		u16 e = h->p[P_CUR_RPS + i];
+		int poc;
+		bool used, is_lt;
+
+		if (!h265_rps_poc(h, i, &poc, &used, &is_lt))
+			break;
+		if (!used)
+			continue;
+		if (is_lt)
+			lt[n_lt++] = poc;
+		else if (e & (1 << (RPS_USED_BIT - 1)))
+			st0[n_neg++] = poc;
+		else
+			st1[n_pos++] = poc;
+	}
+	total = n_neg + n_pos + n_lt;
+
+	if (total > 0) {
+		for (r = 0; r < l0; r++) {
+			int c = (mod & 1) ? h->p[P_MODIFICATION_LIST + r] : r % total;
+
+			if (c < total)
+				h->ref_poc_l0[r] = h265_ref_entry(c, st0, n_neg,
+								  st1, n_pos, lt);
+		}
+		if (h->p[P_SLICE_TYPE] == H265_B_SLICE) {
+			for (r = 0; r < l1; r++) {
+				int c;
+
+				if (mod & 2)
+					c = h->p[P_MODIFICATION_LIST + ((mod & 1) ? l0 : 0) + r];
+				else
+					c = r % total;
+				if (c < total)
+					h->ref_poc_l1[r] = h265_ref_entry(c, st1, n_pos,
+									  st0, n_neg, lt);
+			}
+		}
+	}
+	{
+		struct v4l2_ctrl *ctrl = v4l2_ctrl_find(&h->v4l2_ctx->ctrl_handler,
+							V4L2_CID_AML_HEVC_REFS);
+
+		if (ctrl && ctrl->p_cur.p_s32[0] == 1) {
+			for (r = 0; r < l0; r++)
+				h->ref_poc_l0[r] = ctrl->p_cur.p_s32[1 + r];
+			if (h->p[P_SLICE_TYPE] == H265_B_SLICE)
+				for (r = 0; r < l1; r++)
+					h->ref_poc_l1[r] = ctrl->p_cur.p_s32[17 + r];
+		}
+	}
+	h->ref_num_l0 = l0;
+	h->ref_num_l1 = l1;
+}
+
+static void h265_update_tiles(struct aml_hevc_ctx *h, int w_cu, int h_cu,
+			      int sao_mem_unit)
+{
+	int sao_vb_size = (sao_mem_unit + (2 << 4)) * h_cu;
+	int sao_abv_size = sao_mem_unit * w_cu;
+	int i, j, x, y;
+	bool uniform;
+
+	h->tile_enabled = h->p[P_TILES_ENABLED_FLAG] & 1;
+	if (!h->tile_enabled) {
+		h->num_tile_col = 1;
+		h->num_tile_row = 1;
+		h->tile[0][0] = (struct h265_tile){ 0, 0, w_cu, h_cu,
+						    h->sao_vb, h->sao_abv };
+		return;
+	}
+	h->num_tile_col = h->p[P_NUM_TILE_COLUMNS_MINUS1] + 1;
+	h->num_tile_row = h->p[P_NUM_TILE_ROWS_MINUS1] + 1;
+	if (h->num_tile_col > H265_MAX_TILE_COLS || h->num_tile_col <= 0)
+		h->num_tile_col = 1;
+	if (h->num_tile_row > H265_MAX_TILE_ROWS || h->num_tile_row <= 0)
+		h->num_tile_row = 1;
+	uniform = h->p[P_TILES_ENABLED_FLAG] & 2;
+
+	y = 0;
+	for (i = 0; i < h->num_tile_row; i++) {
+		x = 0;
+		for (j = 0; j < h->num_tile_col; j++) {
+			struct h265_tile *t = &h->tile[i][j];
+
+			if (j == h->num_tile_col - 1)
+				t->width = w_cu - x;
+			else
+				t->width = uniform ? w_cu / h->num_tile_col :
+						     h->p[P_TILE_WIDTH + j];
+			if (i == h->num_tile_row - 1)
+				t->height = h_cu - y;
+			else
+				t->height = uniform ? h_cu / h->num_tile_row :
+						      h->p[P_TILE_HEIGHT + i];
+			t->start_cu_x = x;
+			t->start_cu_y = y;
+			t->sao_vb = h->sao_vb + j * sao_vb_size;
+			t->sao_abv = h->sao_abv + i * sao_abv_size;
+			x += t->width;
+		}
+		y += h->tile[i][0].height;
+	}
+}
+
+static int h265_tile_index(struct aml_hevc_ctx *h, int cu_adr, int w_lcu)
+{
+	int cu_x, cu_y, tx = 0, ty = 0, i;
+
+	if (!w_lcu)
+		return -1;
+	cu_x = cu_adr % w_lcu;
+	cu_y = cu_adr / w_lcu;
+	for (i = 0; i < h->num_tile_col; i++) {
+		if (cu_x >= h->tile[0][i].start_cu_x)
+			tx = i;
+		else
+			break;
+	}
+	for (i = 0; i < h->num_tile_row; i++) {
+		if (cu_y >= h->tile[i][0].start_cu_y)
+			ty = i;
+		else
+			break;
+	}
+	return tx | (ty << 8);
+}
+
+/* ---------------------------------------------------------------- */
+/* per-slice hardware configuration                                 */
+/* ---------------------------------------------------------------- */
+
+static void h265_fill_mmu_map(struct aml_hevc_ctx *h)
+{
+	struct h265_pic *pic = &h->pic[h->cur];
+	u32 *map = h->side_v + (h->mmu_map - h->side_p);
+	u32 pages = h->body_size / PAGE_SIZE, first = pic->body_p >> PAGE_SHIFT, i;
+
+	for (i = 0; i < pages && i < H265_MMU_MAP_SIZE / 4; i++)
+		map[i] = first + i;
+}
+
+/* init_decode_head_hw() */
+static void h265_config_comp(struct aml_hevc_ctx *h)
+{
+	struct aml_vdec_hw *hw = h->hw;
+	u32 body = h265_comp_body_size(h->pic_w, h->pic_h, h->ten_bit);
+	u32 v;
+
+	wr(hw, HEVCD_MPP_DECOMP_CTL1, BIT(4));	/* paged memory */
+	wr(hw, HEVCD_MPP_DECOMP_CTL2, 0);
+	wr(hw, HEVC_CM_BODY_LENGTH, body);
+	wr(hw, HEVC_CM_HEADER_OFFSET, body);
+	wr(hw, HEVC_CM_HEADER_LENGTH, h265_comp_header_size(h->pic_w, h->pic_h));
+	wr(hw, HEVC_SAO_MMU_VH0_ADDR, h->mmu_vbh);
+	wr(hw, HEVC_SAO_MMU_VH1_ADDR, h->mmu_vbh + H265_MMU_VBH_SIZE / 2);
+	v = read_dos_reg(hw, HEVC_SAO_CTRL9) | BIT(0);
+	wr(hw, HEVC_SAO_CTRL9, v);
+	v = read_dos_reg(hw, HEVC_SAO_CTRL5) | BIT(10);	/* use CM_HEADER_START_ADDR */
+	wr(hw, HEVC_SAO_CTRL5, v);
+}
+
+static void h265_config_mc_buffer(struct aml_hevc_ctx *h)
+{
+	struct aml_vdec_hw *hw = h->hw;
+	int i;
+
+	if (h->slice_type == H265_I_SLICE)
+		return;
+	wr(hw, HEVCD_MPP_ANC_CANVAS_ACCCONFIG_ADDR, BIT(0));
+	for (i = 0; i < h->ref_num_l0; i++) {
+		int s = h265_pic_by_poc(h, h->ref_poc_l0[i], true);
+
+		if (s < 0)
+			s = h->cur;
+		wr(hw, HEVCD_MPP_ANC_CANVAS_DATA_ADDR, (s << 16) | (s << 8) | s);
+	}
+	if (h->slice_type != H265_B_SLICE)
+		return;
+	wr(hw, HEVCD_MPP_ANC_CANVAS_ACCCONFIG_ADDR, (16 << 8) | BIT(0));
+	for (i = 0; i < h->ref_num_l1; i++) {
+		int s = h265_pic_by_poc(h, h->ref_poc_l1[i], true);
+
+		if (s < 0)
+			s = h->cur;
+		wr(hw, HEVCD_MPP_ANC_CANVAS_DATA_ADDR, (s << 16) | (s << 8) | s);
+	}
+}
+
+/* config_mcrcc_axi_hw() */
+static void h265_config_mcrcc(struct aml_hevc_ctx *h)
+{
+	struct aml_vdec_hw *hw = h->hw;
+	u32 a, b;
+
+	wr(hw, HEVCD_MCRCC_CTL1, 0x2);		/* reset mcrcc */
+	if (h->slice_type == H265_I_SLICE) {
+		wr(hw, HEVCD_MCRCC_CTL1, 0x0);
+		return;
+	}
+	if (h->slice_type == H265_B_SLICE) {
+		wr(hw, HEVCD_MPP_ANC_CANVAS_ACCCONFIG_ADDR, 0);
+		a = read_dos_reg(hw, HEVCD_MPP_ANC_CANVAS_DATA_ADDR) & 0xffff;
+		a |= a << 16;
+		wr(hw, HEVCD_MCRCC_CTL2, a);
+		wr(hw, HEVCD_MPP_ANC_CANVAS_ACCCONFIG_ADDR, (16 << 8) | BIT(1));
+		b = read_dos_reg(hw, HEVCD_MPP_ANC_CANVAS_DATA_ADDR) & 0xffff;
+		b |= b << 16;
+		if (a == b) {
+			b = read_dos_reg(hw, HEVCD_MPP_ANC_CANVAS_DATA_ADDR) & 0xffff;
+			b |= b << 16;
+		}
+		wr(hw, HEVCD_MCRCC_CTL3, b);
+	} else {
+		wr(hw, HEVCD_MPP_ANC_CANVAS_ACCCONFIG_ADDR, BIT(1));
+		a = read_dos_reg(hw, HEVCD_MPP_ANC_CANVAS_DATA_ADDR) & 0xffff;
+		a |= a << 16;
+		wr(hw, HEVCD_MCRCC_CTL2, a);
+		a = read_dos_reg(hw, HEVCD_MPP_ANC_CANVAS_DATA_ADDR) & 0xffff;
+		a |= a << 16;
+		wr(hw, HEVCD_MCRCC_CTL3, a);
+	}
+	wr(hw, HEVCD_MCRCC_CTL1, 0xff0);	/* progressive mode */
+}
+
+/* config_mpred_hw() */
+static void h265_config_mpred(struct aml_hevc_ctx *h)
+{
+	struct aml_vdec_hw *hw = h->hw;
+	struct h265_pic *cur = &h->pic[h->cur], *col = &h->pic[h->col];
+	int unit = h->lcu_size_log2 == 6 ? 0x200 : h->lcu_size_log2 == 5 ? 0x80 : 0x20;
+	dma_addr_t rd_start = col->mv_p;
+	dma_addr_t rd_ptr = rd_start + h->slice_addr * unit;
+	dma_addr_t wr_ptr = cur->mv_p + h->slice_addr * unit;
+	bool above_en = true, mv_wr_en = true;
+	bool mv_rd_en = h->slice_type != H265_I_SLICE;
+	u32 v, lt = 0, en0 = 0, en1 = 0;
+	int i;
+
+	wr(hw, HEVC_MPRED_MV_WR_START_ADDR, cur->mv_p);
+	wr(hw, HEVC_MPRED_MV_RD_START_ADDR, rd_start);
+	v = (h->lcu_x_num - h->tile_width_lcu) * unit;
+	wr(hw, HEVC_MPRED_MV_WR_ROW_JUMP, v);
+	wr(hw, HEVC_MPRED_MV_RD_ROW_JUMP, v);
+
+	v = (h->slice_type & 3) |
+	    (h->new_pic << 2) |
+	    (h->new_tile << 3) |
+	    (h->is_next_slice_segment << 4) |
+	    (h->tmvp << 5) |
+	    (h->ldc << 6) |
+	    (h->col_from_l0 << 7) |
+	    (0 << 8) |			/* above_ptr_ctrl */
+	    (above_en << 9) |
+	    (mv_wr_en << 10) |
+	    (mv_rd_en << 11) |
+	    (0 << 12) |			/* col_isIntra */
+	    (1 << 13) |			/* buffer_linear */
+	    (0 << 14) | (0 << 15) |	/* LongTerm_Curr, LongTerm_Col: no effect seen */
+	    ((h->lcu_size_log2 & 0xf) << 16) |
+	    (3 << 20) |			/* cu_size_log2 */
+	    ((h->plevel & 7) << 24);
+	wr(hw, HEVC_MPRED_CTRL0, v);
+
+	wr(hw, HEVC_MPRED_CTRL1, h->max_merge_cand | (2 << 4) | (3 << 8) |
+				 (5 << 12) | (36 << 16));
+	wr(hw, HEVC_MPRED_PIC_SIZE, h->pic_w | (h->pic_h << 16));
+	wr(hw, HEVC_MPRED_PIC_SIZE_LCU, (h->lcu_x_num - 1) | ((h->lcu_y_num - 1) << 16));
+	wr(hw, HEVC_MPRED_TILE_START, h->tile_start_lcu_x | (h->tile_start_lcu_y << 16));
+	wr(hw, HEVC_MPRED_TILE_SIZE_LCU, h->tile_width_lcu | (h->tile_height_lcu << 16));
+	wr(hw, HEVC_MPRED_REF_NUM, h->ref_num_l0 | (h->ref_num_l1 << 8));
+
+	for (i = 0; i < h->ref_num_l0; i++) {
+		int s = h265_pic_by_poc(h, h->ref_poc_l0[i], true);
+
+		if (s >= 0 && h->pic[s].long_term)
+			lt |= BIT(i);
+		en0 |= BIT(i);
+	}
+	for (i = 0; i < h->ref_num_l1; i++) {
+		int s = h265_pic_by_poc(h, h->ref_poc_l1[i], true);
+
+		if (s >= 0 && h->pic[s].long_term)
+			lt |= BIT(i + 16);
+		en1 |= BIT(i);
+	}
+	wr(hw, HEVC_MPRED_LT_REF, lt);
+	wr(hw, HEVC_MPRED_REF_EN_L0, en0);
+	wr(hw, HEVC_MPRED_REF_EN_L1, en1);
+	wr(hw, HEVC_MPRED_CUR_POC, h->curr_poc);
+	wr(hw, HEVC_MPRED_COL_POC, h->col_poc);
+
+	/* L0/L1 pairs, in this order. */
+	for (i = 0; i < 16; i++) {
+		wr(hw, HEVC_MPRED_L0_REF00_POC + REG_ALIGN(i), h->ref_poc_l0[i]);
+		wr(hw, HEVC_MPRED_L1_REF00_POC + REG_ALIGN(i), h->ref_poc_l1[i]);
+	}
+
+	if (h->new_pic) {
+		wr(hw, HEVC_MPRED_ABV_START_ADDR, h->mpred_above);
+		wr(hw, HEVC_MPRED_MV_WPTR, wr_ptr);
+		wr(hw, HEVC_MPRED_MV_RPTR, rd_start);
+	} else if (!h->is_next_slice_segment) {
+		wr(hw, HEVC_MPRED_MV_RPTR, rd_ptr);
+	}
+	wr(hw, HEVC_MPRED_MV_RD_END_ADDR, rd_start + h->mv_size);
+}
+
+/* config_sao_hw() for compressed reference plus 1:1 NV12 double write */
+static void h265_config_sao(struct aml_hevc_ctx *h)
+{
+	struct aml_vdec_hw *hw = h->hw;
+	struct h265_pic *cur = &h->pic[h->cur];
+	u32 misc = h->misc_flag0;
+	u32 uv_size = h->lcu_total * h->lcu_size * h->lcu_size / 2;
+	u32 uv_h = (uv_size + 0xffff) >> 16;
+	dma_addr_t dw_y, dw_c;
+	u32 v, v2;
+	bool slice_dbk_disabled;
+
+	if (h->dw_target_y) {
+		dw_y = h->dw_target_y;
+		dw_c = h->dw_target_c;
+	} else {
+		dw_y = cur->dw_p;
+		dw_c = dw_y + h->pic_w * h->pic_h;
+	}
+
+	v = read_dos_reg(hw, HEVC_SAO_CTRL0);
+	v &= ~0xf;
+	v |= h->lcu_size_log2;
+	wr(hw, HEVC_SAO_CTRL0, v);
+	wr(hw, HEVC_SAO_PIC_SIZE, h->pic_w | (h->pic_h << 16));
+	wr(hw, HEVC_SAO_PIC_SIZE_LCU, (h->lcu_x_num - 1) | ((h->lcu_y_num - 1) << 16));
+
+	v = read_dos_reg(hw, HEVC_SAO_CTRL5);
+	v &= ~(0xff << 16);		/* double write 1:1 */
+	if (h->ten_bit)
+		v &= ~BIT(9);
+	else
+		v |= BIT(9);		/* 8 bit, memory saving */
+	wr(hw, HEVC_SAO_CTRL5, v);
+
+	wr(hw, HEVC_SAO_Y_START_ADDR, dw_y);
+	wr(hw, HEVC_CM_BODY_START_ADDR, cur->body_p);
+	wr(hw, HEVC_CM_HEADER_START_ADDR, cur->hdr_p);
+	wr(hw, HEVC_SAO_Y_LENGTH, (uv_h << 16) << 1);
+	wr(hw, HEVC_SAO_C_START_ADDR, dw_c);
+	wr(hw, HEVC_SAO_C_LENGTH, uv_h << 16);
+	wr(hw, HEVC_SAO_Y_WPTR, dw_y);
+	wr(hw, HEVC_SAO_C_WPTR, dw_c);
+
+	if (h->new_pic) {
+		wr(hw, HEVC_DBLK_CFG2, h->pic_w | (h->pic_h << 16));
+		v = 0;
+		if ((misc >> PCM_ENABLE_FLAG_BIT) & 1)
+			v = ((misc >> PCM_LOOP_FILTER_DISABLED_FLAG_BIT) & 1) << 3;
+		v |= ((h->p[P_PPS_CB_QP_OFFSET] & 0x1f) << 4) |
+		     ((h->p[P_PPS_CR_QP_OFFSET] & 0x1f) << 9);
+		v |= h->lcu_size == 64 ? 0 : h->lcu_size == 32 ? 1 : 2;
+		v |= h->pic_w <= 64 ? BIT(20) : 0;
+		wr(hw, HEVC_DBLK_CFG1, v);
+		wr(hw, REG_ALIGN(0x3510), BIT(28));	/* sts1 chooses dblk_main */
+	}
+
+	v = read_dos_reg(hw, HEVC_SAO_CTRL1);
+	v &= ~0x3000;			/* linear */
+	v &= ~0xff0;
+	v |= 0xff0;			/* little endian */
+	v &= ~0x3;			/* both outputs on */
+	v |= BIT(8);			/* NV12 */
+	v &= ~(3 << 14);
+	v |= 2 << 14;			/* line align 64 */
+	wr(hw, HEVC_SAO_CTRL1, v);
+
+	/* 1st and 2nd write enabled, H.265 format */
+	v = 0x57 << 8;
+	if (h->pic_w >= 1280)
+		v |= BIT(4);		/* dblk pipeline mode */
+	v |= BIT(8) | BIT(9);
+	wr(hw, HEVC_DBLK_CFGB, v);
+
+	v = read_dos_reg(hw, HEVCD_IPP_AXIIF_CONFIG);
+	v &= ~0x30;			/* linear */
+	v &= ~0xf;
+	v |= 0xf;			/* little endian */
+	v &= ~BIT(12);			/* NV12 */
+	v &= ~(3 << 8);
+	v |= 2 << 8;			/* line align 64 */
+	wr(hw, HEVCD_IPP_AXIIF_CONFIG, v);
+
+	v = 0;
+	v2 = read_dos_reg(hw, HEVC_SAO_CTRL0) & ~0x300;
+	if (h->tile_enabled) {
+		v |= ((misc >> LOOP_FILTER_ACROSS_TILES_ENABLED_FLAG_BIT) & 1) << 0;
+		v2 |= ((misc >> LOOP_FILTER_ACROSS_TILES_ENABLED_FLAG_BIT) & 1) << 8;
+	}
+	slice_dbk_disabled = (misc >> SLICE_DEBLOCKING_FILTER_DISABLED_FLAG_BIT) & 1;
+	if ((misc & BIT(DEBLOCKING_FILTER_OVERRIDE_ENABLED_FLAG_BIT)) &&
+	    (misc & BIT(DEBLOCKING_FILTER_OVERRIDE_FLAG_BIT))) {
+		v |= slice_dbk_disabled << 2;
+		if (!slice_dbk_disabled) {
+			v |= (h->p[P_SLICE_BETA_OFFSET_DIV2] & 0xf) << 3;
+			v |= (h->p[P_SLICE_TC_OFFSET_DIV2] & 0xf) << 7;
+		}
+	} else {
+		v |= ((misc >> PPS_DEBLOCKING_FILTER_DISABLED_FLAG_BIT) & 1) << 2;
+		if (!((misc >> PPS_DEBLOCKING_FILTER_DISABLED_FLAG_BIT) & 1)) {
+			v |= (h->p[P_PPS_BETA_OFFSET_DIV2] & 0xf) << 3;
+			v |= (h->p[P_PPS_TC_OFFSET_DIV2] & 0xf) << 7;
+		}
+	}
+	if ((misc & BIT(PPS_LOOP_FILTER_ACROSS_SLICES_ENABLED_FLAG_BIT)) &&
+	    ((misc & BIT(SLICE_SAO_LUMA_FLAG_BIT)) ||
+	     (misc & BIT(SLICE_SAO_CHROMA_FLAG_BIT)) || !slice_dbk_disabled)) {
+		v |= ((misc >> SLICE_LOOP_FILTER_ACROSS_SLICES_ENABLED_FLAG_BIT) & 1) << 1;
+		v2 |= ((misc >> SLICE_LOOP_FILTER_ACROSS_SLICES_ENABLED_FLAG_BIT) & 1) << 9;
+	} else {
+		v |= ((misc >> PPS_LOOP_FILTER_ACROSS_SLICES_ENABLED_FLAG_BIT) & 1) << 1;
+		v2 |= ((misc >> PPS_LOOP_FILTER_ACROSS_SLICES_ENABLED_FLAG_BIT) & 1) << 9;
+	}
+	wr(hw, HEVC_DBLK_CFG9, v);
+	wr(hw, HEVC_SAO_CTRL0, v2);
+}
+
+static void h265_read_rpm(struct aml_hevc_ctx *h)
+{
+	const u16 *raw = h->side_v + (h->rpm - h->side_p);
+	int i, j;
+
+	for (i = 0; i < P_RPM_COUNT; i += 4)
+		for (j = 0; j < 4; j++)
+			h->p[i + j] = raw[i + 3 - j];
+}
+
+/*
+ * hevc_slice_segment_header_process().  Returns 0 to decode the slice, 1 to
+ * skip it, negative on error.
+ */
+static int h265_slice_header(struct aml_hevc_ctx *h)
+{
+	struct aml_vdec_hw *hw = h->hw;
+	u16 *p = h->p;
+	int i, ret, col_ref;
+
+	h->temporal_id = p[P_TEMPORAL_ID];
+	h->nal_type = p[P_NAL_UNIT_TYPE];
+	h->misc_flag0 = p[P_MISC_FLAG0];
+
+	if (!p[P_FIRST_SLICE_SEGMENT_IN_PIC_FLAG]) {
+		h->slice_segment_addr = p[P_SLICE_SEGMENT_ADDRESS];
+		if (!p[P_DEPENDENT_SLICE_SEGMENT_FLAG])
+			h->slice_addr = h->slice_segment_addr;
+	} else {
+		h->slice_segment_addr = 0;
+		h->slice_addr = 0;
+	}
+	h->iprev_poc = h->curr_poc;
+	h->slice_type = p[P_SLICE_TYPE];
+	h->tmvp = p[P_SLICE_TEMPORAL_MVP_ENABLE_FLAG] & 1;
+	h->is_next_slice_segment = !!p[P_DEPENDENT_SLICE_SEGMENT_FLAG];
+
+	if (p[P_PIC_WIDTH] < H265_MIN_DIM || p[P_PIC_HEIGHT] < H265_MIN_DIM ||
+	    p[P_PIC_WIDTH] > H265_MAX_DIM || p[P_PIC_HEIGHT] > H265_MAX_DIM) {
+		dev_err(hw->dev, "hevc: implausible picture size %ux%u\n",
+			p[P_PIC_WIDTH], p[P_PIC_HEIGHT]);
+		return -EINVAL;
+	}
+	/*
+	 * bit_depth: luma - 8 in bits 3:0, chroma - 8 in bits 7:4.  10 bit is
+	 * decoded in full - the compressed reference keeps all ten bits - and
+	 * the double-write copy the client gets is 8 bit, which is all this
+	 * output path can carry.
+	 */
+	if ((p[P_BIT_DEPTH] & 0xff) != 0 && (p[P_BIT_DEPTH] & 0xff) != 0x22) {
+		dev_err_once(hw->dev, "hevc: bit depth %04x not supported (8 or 10 only)\n",
+			     p[P_BIT_DEPTH]);
+		return -EINVAL;
+	}
+	h->ten_bit = (p[P_BIT_DEPTH] & 0xff) == 0x22;
+	h->pic_w = p[P_PIC_WIDTH];
+	h->pic_h = p[P_PIC_HEIGHT];
+
+	h->lcu_size = 1 << (p[P_LOG2_MIN_CB_SIZE_MINUS3] + 3 +
+			    p[P_LOG2_DIFF_MAX_MIN_CB_SIZE]);
+	if (h->lcu_size < 16 || h->lcu_size > 64)
+		return -EINVAL;
+	h->lcu_size_log2 = ilog2(h->lcu_size);
+	h->lcu_x_num = DIV_ROUND_UP(h->pic_w, h->lcu_size);
+	h->lcu_y_num = DIV_ROUND_UP(h->pic_h, h->lcu_size);
+	h->lcu_total = h->lcu_x_num * h->lcu_y_num;
+
+	/* POC, 8.3.1 */
+	if (h->nal_type == H265_NAL_IDR_W_RADL || h->nal_type == H265_NAL_IDR_N_LP) {
+		h->curr_poc = 0;
+		if (h->temporal_id - 1 == 0)
+			h->prev_tid0_poc = h->curr_poc;
+	} else {
+		int max_lsb = 1 << (p[P_LOG2_MAX_POC_LSB_MINUS4] + 4);
+		int prev_lsb = h->prev_tid0_poc % max_lsb;
+		int prev_msb = h->prev_tid0_poc - prev_lsb;
+		int lsb = p[P_POC_LSB];
+		int msb;
+
+		if (lsb < prev_lsb && prev_lsb - lsb >= max_lsb / 2)
+			msb = prev_msb + max_lsb;
+		else if (lsb > prev_lsb && lsb - prev_lsb > max_lsb / 2)
+			msb = prev_msb - max_lsb;
+		else
+			msb = prev_msb;
+		if (h->nal_type == H265_NAL_BLA_W_LP ||
+		    h->nal_type == H265_NAL_BLA_W_RADL ||
+		    h->nal_type == H265_NAL_BLA_N_LP)
+			msb = 0;
+		h->curr_poc = msb + lsb;
+		if (h->temporal_id - 1 == 0)
+			h->prev_tid0_poc = h->curr_poc;
+	}
+
+	/* skip RASL pictures after the CRA/BLA the stream starts on */
+	if (h->poc_random_access == H265_MAX_INT) {
+		if (h->nal_type == H265_NAL_CRA || h->nal_type == H265_NAL_BLA_W_LP ||
+		    h->nal_type == H265_NAL_BLA_W_RADL || h->nal_type == H265_NAL_BLA_N_LP)
+			h->poc_random_access = h->curr_poc;
+		else
+			h->poc_random_access = -H265_MAX_INT;
+	} else if (h->nal_type == H265_NAL_BLA_W_LP ||
+		   h->nal_type == H265_NAL_BLA_W_RADL ||
+		   h->nal_type == H265_NAL_BLA_N_LP) {
+		h->poc_random_access = h->curr_poc;
+	} else if (h->curr_poc < h->poc_random_access &&
+		   (h->nal_type == H265_NAL_RASL_N || h->nal_type == H265_NAL_RASL_R)) {
+		return 1;
+	}
+
+	wr(hw, HEVC_WAIT_FLAG, read_dos_reg(hw, HEVC_WAIT_FLAG) | 0x2);
+
+	if (p[P_SLICE_SEGMENT_ADDRESS] == 0) {
+		int sao_mem_unit = (h->lcu_size == 16 ? 9 : h->lcu_size == 32 ? 14 : 24) << 4;
+		int w_cu = h->lcu_x_num, h_cu = h->lcu_y_num;
+		int slot = -1;
+
+		h->new_pic = true;
+		h265_apply_rps(h);
+
+		/*
+		 * A slot per picture the DPB can hold, plus the current one and
+		 * one spare.  Allocating the worst case (16 + 2) regardless costs
+		 * 18 x 13 MB at 4K, which with the capture pool does not fit in
+		 * CMA; a 4K stream's DPB is 6 or so.
+		 */
+		h->nslots = clamp_t(int, p[P_SPS_MAX_DEC_PIC_BUFFERING_MINUS1_0] + 3,
+				    4, H265_SLOTS);
+		ret = h265_alloc_pics(h);
+		if (ret)
+			return ret;
+		for (i = 0; i < h->nslots; i++) {
+			if (!h->pic[i].valid || !h->pic[i].referenced) {
+				slot = i;
+				break;
+			}
+		}
+		if (slot < 0) {
+			dev_err(hw->dev, "hevc: no free picture buffer\n");
+			return -ENOBUFS;
+		}
+		h->cur = slot;
+		h->pic[slot].valid = true;
+		h->pic[slot].referenced = true;
+		h->pic[slot].long_term = false;
+		h->pic[slot].poc = h->curr_poc;
+		h->pic[slot].width = h->pic_w;
+		h->pic[slot].height = h->pic_h;
+		h->pic[slot].decode_idx = h->decode_idx++;
+		h->pic[slot].slice_type = h->slice_type;
+
+		h265_config_comp(h);
+		h265_fill_mmu_map(h);
+		wr(hw, HEVC_ASSIST_MMU_MAP_ADDR, h->mmu_map);
+		wr(hw, HEVC_SAO_MMU_DMA_CTRL, h->mmu_map);
+
+		h265_update_tiles(h, w_cu, h_cu, sao_mem_unit);
+		wr(hw, H265_SAO_MEM_UNIT, sao_mem_unit);
+		wr(hw, H265_SAO_ABV, h->sao_abv);
+		wr(hw, H265_SAO_VB_SIZE, (sao_mem_unit + (2 << 4)) * h_cu);
+		wr(hw, H265_SAO_VB, h->sao_vb);
+	} else {
+		if (h->cur < 0)
+			return 1;	/* a picture's first slice went missing */
+		h->new_pic = false;
+	}
+
+	if (h->iprev_poc != h->curr_poc) {
+		h->new_tile = true;
+		h->tile_x = 0;
+		h->tile_y = 0;
+	} else if (h->tile_enabled) {
+		int t = h265_tile_index(h, p[P_SLICE_SEGMENT_ADDRESS], h->lcu_x_num);
+
+		if (t != -1 && t != (h->tile_x | (h->tile_y << 8))) {
+			h->new_tile = true;
+			h->tile_x = t & 0xff;
+			h->tile_y = (t >> 8) & 0xff;
+		} else {
+			h->new_tile = false;
+		}
+	} else {
+		h->new_tile = false;
+	}
+	if (h->tile_x >= H265_MAX_TILE_COLS || h->tile_y >= H265_MAX_TILE_ROWS)
+		h->new_tile = false;
+	if (h->new_tile) {
+		struct h265_tile *t = &h->tile[h->tile_y][h->tile_x];
+
+		h->tile_start_lcu_x = t->start_cu_x;
+		h->tile_start_lcu_y = t->start_cu_y;
+		h->tile_width_lcu = t->width;
+		h->tile_height_lcu = t->height;
+	}
+
+	h265_set_ref_lists(h);
+	col_ref = p[P_COLLOCATED_REF_IDX];
+	h->ldc = false;
+	if (h->slice_type != H265_I_SLICE) {
+		h->ldc = true;
+		for (i = 0; i < h->ref_num_l0 && h->ldc; i++)
+			if (h->ref_poc_l0[i] > h->curr_poc)
+				h->ldc = false;
+		if (h->slice_type == H265_B_SLICE)
+			for (i = 0; i < h->ref_num_l1 && h->ldc; i++)
+				if (h->ref_poc_l1[i] > h->curr_poc)
+					h->ldc = false;
+	}
+	h->col_from_l0 = p[P_COLLOCATED_FROM_L0_FLAG] & 1;
+	h->plevel = p[P_LOG2_PARALLEL_MERGE_LEVEL];
+	h->max_merge_cand = 5 - p[P_FIVE_MINUS_MAX_NUM_MERGE_CAND];
+
+	if (h->slice_type == H265_B_SLICE && !h->col_from_l0)
+		h->col_poc = col_ref < h->ref_num_l1 ? h->ref_poc_l1[col_ref] : H265_INVALID_POC;
+	else
+		h->col_poc = col_ref < h->ref_num_l0 ? h->ref_poc_l0[col_ref] : H265_INVALID_POC;
+
+	h->col = h->cur;
+	if (h->slice_type != H265_I_SLICE && h->col_poc != (int)H265_INVALID_POC) {
+		int c = h265_pic_by_poc(h, h->col_poc, true);
+
+		if (c >= 0)
+			h->col = c;
+		else
+			dev_dbg(hw->dev, "hevc: collocated poc %d missing\n", h->col_poc);
+	}
+
+	if (!h->tile_width_lcu || !h->tile_height_lcu)
+		return -EINVAL;
+
+	h265_config_mc_buffer(h);
+	h265_config_mcrcc(h);
+	h265_config_mpred(h);
+	h265_config_sao(h);
+
+	dev_dbg(hw->dev,
+		"hevc: slice poc %d type %d nal %d addr %u new %d slot %d refs %d/%d col %d\n",
+		h->curr_poc, h->slice_type, h->nal_type, p[P_SLICE_SEGMENT_ADDRESS],
+		h->new_pic, h->cur, h->ref_num_l0, h->ref_num_l1, h->col_poc);
+	return 0;
+}
+
+/* ---------------------------------------------------------------- */
+/* V4L2 glue                                                        */
+/* ---------------------------------------------------------------- */
+
+static void h265_set_dw_target(struct aml_hevc_ctx *h)
+{
+	struct aml_vdec_ctx *ctx = h->v4l2_ctx;
+	struct v4l2_pix_format_mplane *pf = &ctx->pix_fmt[AML_FMT_DST];
+	struct vb2_v4l2_buffer *dst = v4l2_m2m_next_dst_buf(ctx->m2m_ctx);
+	struct vb2_buffer *vb;
+
+	h->dw_target_y = 0;
+	h->dw_target_c = 0;
+	h->dw_target_size = 0;
+	if (!dst || pf->plane_fmt[0].bytesperline != pf->width)
+		return;
+	vb = &dst->vb2_buf;
+	h->dw_target_y = vb2_dma_contig_plane_dma_addr(vb, 0);
+	if (!h->dw_target_y)
+		return;
+	if (vb->num_planes >= 2) {
+		h->dw_target_c = vb2_dma_contig_plane_dma_addr(vb, 1);
+		h->dw_target_size = vb2_plane_size(vb, 0) + vb2_plane_size(vb, 1);
+		if (!h->dw_target_c)
+			h->dw_target_y = 0;
+	} else {
+		h->dw_target_c = h->dw_target_y + (dma_addr_t)pf->width * pf->height;
+		h->dw_target_size = vb2_plane_size(vb, 0);
+	}
+}
+
+static void h265_copy_out(struct aml_hevc_ctx *h)
+{
+	struct aml_vdec_ctx *ctx = h->v4l2_ctx;
+	struct v4l2_pix_format_mplane *pf = &ctx->pix_fmt[AML_FMT_DST];
+	struct vb2_v4l2_buffer *dst = v4l2_m2m_next_dst_buf(ctx->m2m_ctx);
+	struct h265_pic *pic;
+	struct vb2_buffer *vb;
+	u32 stride, dst_h, row, w, ht;
+	u8 *y, *c;
+	const u8 *src;
+
+	if (!dst || h->cur < 0)
+		return;
+	vb = &dst->vb2_buf;
+	pic = &h->pic[h->cur];
+	stride = pf->plane_fmt[0].bytesperline ? : pic->width;
+	dst_h = pf->height ? : pic->height;
+
+	if (!h->dw_target_y) {
+		src = pic->dw_v;
+		if (!src)
+			return;
+		dma_sync_single_for_cpu(h->hw->dev, pic->dw_p, h->dw_size, DMA_FROM_DEVICE);
+		w = pic->width;
+		ht = pic->height;
+		y = vb2_plane_vaddr(vb, 0);
+		if (!y)
+			return;
+		c = vb->num_planes >= 2 ? vb2_plane_vaddr(vb, 1) : y + (size_t)stride * dst_h;
+		if (!c)
+			return;
+		for (row = 0; row < ht; row++)
+			memcpy(y + (size_t)row * stride, src + (size_t)row * w, w);
+		for (row = 0; row < ht / 2; row++)
+			memcpy(c + (size_t)row * stride, src + (size_t)(ht + row) * w, w);
+		aml_vdec_dst_cpu_written(h->hw->dev, vb);
+	}
+	if (vb->num_planes >= 2) {
+		vb2_set_plane_payload(vb, 0, stride * dst_h);
+		vb2_set_plane_payload(vb, 1, stride * dst_h / 2);
+	} else {
+		vb2_set_plane_payload(vb, 0, stride * dst_h * 3 / 2);
+	}
+	h->have_picture = true;
+}
+
+static void h265_finish(struct aml_hevc_ctx *h, enum vb2_buffer_state state)
+{
+	struct aml_vdec_ctx *ctx = h->v4l2_ctx;
+	struct vb2_v4l2_buffer *buf;
+	unsigned int i;
+
+	if (h->finished)
+		return;
+	h->finished = true;
+
+	if (!h->have_picture) {
+		buf = v4l2_m2m_next_dst_buf(ctx->m2m_ctx);
+		if (buf)
+			for (i = 0; i < buf->vb2_buf.num_planes; i++)
+				vb2_set_plane_payload(&buf->vb2_buf, i, 0);
+	}
+	buf = v4l2_m2m_next_src_buf(ctx->m2m_ctx);
+	if (buf && buf->vb2_buf.req_obj.req)
+		v4l2_ctrl_request_complete(buf->vb2_buf.req_obj.req, &ctx->ctrl_handler);
+
+	ctx->int_cond = 1;
+	ctx->job_finished = true;
+	v4l2_m2m_buf_done_and_job_finish(ctx->dev->m2m_dev_dec, ctx->m2m_ctx, state);
+	wake_up(&ctx->queue);
+}
+
+static void h265_command(struct aml_vdec_hw *hw, u32 cmd)
+{
+	wr(hw, HEVC_ASSIST_MBOX0_CLR_REG, 1);
+	wr(hw, HEVC_DEC_STATUS_REG, cmd);
+	wr(hw, HEVC_MCPU_INTR_REQ, AMRISC_MAIN_REQ);
+}
+
+static void h265_picture_done(struct aml_hevc_ctx *h)
+{
+	u32 info = read_dos_reg(h->hw, H265_DECODE_INFO);
+
+	h->start_decoding_flag |= info & 0xff;
+	h->rps_set_id = (info >> 8) & 0xff;
+	if (h->cur >= 0)
+		h265_copy_out(h);
+	h->cur = -1;
+}
+
+static irqreturn_t h265_irq_thread(int irq, void *priv)
+{
+	struct aml_hevc_ctx *h = priv;
+	struct aml_vdec_hw *hw = h->hw;
+	u32 st = hw->hevc_irq_status & 0xff;
+
+	if (h->finished)
+		return IRQ_HANDLED;
+	if (++h->rounds > 4096) {
+		dev_err(hw->dev, "hevc: runaway decode, giving up\n");
+		h265_finish(h, VB2_BUF_STATE_ERROR);
+		return IRQ_HANDLED;
+	}
+
+	switch (st) {
+	case H265_NAL_SEARCH_DONE: {
+		int nal = read_dos_reg(hw, H265_CUR_NAL_UNIT_TYPE);
+		u32 parse = H265_DISCARD_NAL;
+
+		if (nal == H265_NAL_EOS) {
+			h->curr_poc = H265_INVALID_POC;
+			h->poc_random_access = H265_MAX_INT;
+		} else if (nal == H265_NAL_VPS) {
+			parse = H265_NAL_UNIT_VPS;
+			h->have_vps = true;
+		} else if (h->have_vps && nal == H265_NAL_SPS) {
+			parse = H265_NAL_UNIT_SPS;
+			h->have_sps = true;
+		} else if (h->have_vps && nal == H265_NAL_PPS) {
+			parse = H265_NAL_UNIT_PPS;
+			h->have_pps = true;
+		} else if (h->have_vps && h->have_sps && h->have_pps && nal <= H265_NAL_CRA) {
+			if (nal >= H265_NAL_BLA_W_LP) {
+				parse = H265_NAL_UNIT_CODED_SLICE;
+				h->have_start = true;
+			} else if (h->have_start) {
+				parse = H265_NAL_UNIT_CODED_SLICE;
+			}
+		}
+		if (h->have_vps && h->have_sps && h->have_pps && h->have_start) {
+			/* from here on let the microcode find NALs itself */
+			wr(hw, H265_NAL_SEARCH_CTL, 0x2);
+			h265_config_decode_mode(h);
+		}
+		dev_dbg(hw->dev, "hevc: nal %d -> %02x\n", nal, parse);
+		h265_command(hw, parse);
+		return IRQ_HANDLED;
+	}
+	case H265_SLICE_SEGMENT_DONE: {
+		int ret;
+		u32 info = read_dos_reg(hw, H265_DECODE_INFO);
+
+		h->start_decoding_flag |= info & 0xff;
+		h->rps_set_id = (info >> 8) & 0xff;
+		h265_read_rpm(h);
+		ret = h265_slice_header(h);
+		if (ret < 0) {
+			dev_err(hw->dev, "hevc: slice setup failed (%d)\n", ret);
+			h265_finish(h, VB2_BUF_STATE_ERROR);
+			return IRQ_HANDLED;
+		}
+		if (ret > 0) {
+			wr(hw, HEVC_WAIT_FLAG, read_dos_reg(hw, HEVC_WAIT_FLAG) & ~0x2);
+			h265_command(hw, H265_ACTION_DONE);
+			return IRQ_HANDLED;
+		}
+		h265_command(hw, H265_CODED_SLICE_SEGMENT_DAT);
+		return IRQ_HANDLED;
+	}
+	case H265_DECPIC_DATA_DONE:
+	case H265_DECODE_BUFEMPTY:
+	case H265_DECODE_BUFEMPTY2:
+	case H265_SEARCH_BUFEMPTY:
+	case H265_NAL_DECODE_DONE:
+	case H265_FIND_NEXT_PIC_NAL:
+		h265_picture_done(h);
+		h265_finish(h, VB2_BUF_STATE_DONE);
+		return IRQ_HANDLED;
+	case H265_DECPIC_DATA_ERROR:
+	case H265_DECODE_OVER_SIZE:
+	case H265_DECODE_TIMEOUT:
+		dev_err(hw->dev, "hevc: decoder reported %02x\n", st);
+		h->cur = -1;
+		h265_finish(h, VB2_BUF_STATE_ERROR);
+		return IRQ_HANDLED;
+	default:
+		dev_dbg(hw->dev, "hevc: unhandled status %02x\n", st);
+		h265_command(hw, H265_ACTION_DONE);
+		return IRQ_HANDLED;
+	}
+}
+
+int aml_hevc_init(void *priv)
+{
+	struct aml_vdec_ctx *ctx = priv;
+	struct aml_vdec_hw *hw = vdec_get_hw(ctx->dev);
+	struct aml_hevc_ctx *h;
+
+	if (!hw)
+		return -ENODEV;
+	h = kzalloc_obj(*h);
+	if (!h)
+		return -ENOMEM;
+	h->v4l2_ctx = ctx;
+	h->hw = hw;
+	h->cur = -1;
+	h->curr_poc = H265_INVALID_POC;
+	h->poc_random_access = H265_MAX_INT;
+
+	/* One client of the HEVC core at a time - see aml_vp9_init(). */
+	if (cmpxchg(&hw->hevc_priv, NULL, h)) {
+		dev_info(&ctx->dev->plat_dev->dev,
+			 "hevc: HEVC core already in use, refusing second instance\n");
+		kfree(h);
+		return -EBUSY;
+	}
+	ctx->codec_priv = h;
+	hw->hevc_irq_thread_fn = h265_irq_thread;
+	return 0;
+}
+
+void aml_hevc_exit(void *priv)
+{
+	struct aml_vdec_ctx *ctx = priv;
+	struct aml_hevc_ctx *h = ctx->codec_priv;
+
+	if (!h)
+		return;
+	h->hw->hevc_irq_thread_fn = NULL;
+	h->hw->hevc_priv = NULL;
+	if (h->started)
+		regmap_write(h->hw->map[DOS_BUS], HEVC_MPSR, 0);
+	h265_free(h);
+	kfree(h);
+	ctx->codec_priv = NULL;
+}
+
+int aml_hevc_dec_run(void *priv)
+{
+	struct aml_vdec_ctx *ctx = priv;
+	struct aml_hevc_ctx *h = ctx->codec_priv;
+	struct aml_vdec_hw *hw = h->hw;
+	struct vb2_v4l2_buffer *src = v4l2_m2m_next_src_buf(ctx->m2m_ctx);
+	struct vb2_buffer *vb;
+	int ret;
+
+	if (!src)
+		return -EINVAL;
+	vb = &src->vb2_buf;
+
+	ret = hvdec_enable(hw);
+	if (ret)
+		return ret;
+	dos_enable_hevc(hw);
+
+	if (!h->ucode_loaded) {
+		hvdec_switch_codec(hw, CODEC_TYPE_HEVC);
+		ret = hevc_load_fw(hw, CODEC_TYPE_HEVC, H265_FW_FORMAT_MMU,
+				   H265_FW_CORE_HEVC, "hevc");
+		if (ret)
+			return ret;
+		h->ucode_loaded = true;
+	}
+
+	ret = h265_alloc_workspace(h);
+	if (ret)
+		return ret;
+
+	if (h->started) {
+		wr(hw, HEVC_MPSR, 0);
+		wr(hw, HEVC_CPSR, 0);
+	}
+	aml_vp9_reset_core(hw);
+	h265_prot_init(h);
+	h265_set_dw_target(h);
+	if (h->dw_target_y && (size_t)h->alloc_w * h->alloc_h * 3 / 2 > h->dw_target_size &&
+	    h->alloc_w) {
+		h->dw_target_y = 0;	/* stream outgrew the capture buffer */
+	}
+
+	ret = h265_feed(h, vb2_plane_vaddr(vb, 0), vb2_get_plane_payload(vb, 0));
+	if (ret)
+		return ret;
+
+	h->rounds = 0;
+	h->have_picture = false;
+	h->finished = false;
+	h->cur = -1;
+	ctx->int_cond = 0;
+
+	wr(hw, DOS_SW_RESET3, BIT(12) | BIT(11));
+	wr(hw, DOS_SW_RESET3, 0);
+	wr(hw, HEVC_ASSIST_MBOX0_CLR_REG, 1);
+	wr(hw, HEVC_DEC_STATUS_REG, 0);
+	wr(hw, HEVC_CPSR, 0);
+	wr(hw, HEVC_MPSR, 1);
+	h->started = true;
+
+	/* Uninterruptible on purpose - see aml_vp9_dec_run(). */
+	ret = wait_event_timeout(ctx->queue, ctx->int_cond,
+				 msecs_to_jiffies(H265_TIMEOUT_MS));
+	ctx->int_cond = 0;
+	if (!ret) {
+		dev_err(hw->dev, "hevc: decode timeout, status %02x pc %08x shift %08x\n",
+			hw->hevc_irq_status, read_dos_reg(hw, HEVC_MPC_E),
+			read_dos_reg(hw, HEVC_SHIFT_BYTE_COUNT));
+		wr(hw, HEVC_MPSR, 0);
+		h->cur = -1;
+		hw->hevc_dirty = true;
+		return -ETIMEDOUT;
+	}
+	return 0;
+}
diff --git a/drivers/media/platform/amlogic/vdec/hevc.h b/drivers/media/platform/amlogic/vdec/hevc.h
new file mode 100644
index 000000000000..e85e5c838845
--- /dev/null
+++ b/drivers/media/platform/amlogic/vdec/hevc.h
@@ -0,0 +1,125 @@
+/* SPDX-License-Identifier: (GPL-2.0-only OR MIT) */
+/*
+ * HEVC (H.265) decoding on the Amlogic S4 HEVC core.
+ */
+
+#ifndef _AML_HEVC_H_
+#define _AML_HEVC_H_
+
+#include "vp9.h"
+
+/*
+ * Scratch register meanings for the HEVC microcode.  Several of these names
+ * are already taken by av1.h for a different scratch register - each codec's
+ * microcode gives the same register its own meaning - so the HEVC ones carry
+ * an H265_ prefix.  From the reference's vh265.c.
+ */
+#define HEVC_ASSIST_SCRATCH_6		REG_ALIGN(0x30c6)
+#define HEVC_ASSIST_SCRATCH_C		REG_ALIGN(0x30cc)
+#define HEVC_ASSIST_SCRATCH_D		REG_ALIGN(0x30cd)
+
+#define H265_VPS_BUFFER			HEVC_ASSIST_SCRATCH_3
+#define H265_SPS_BUFFER			HEVC_ASSIST_SCRATCH_4
+#define H265_SAO_UP			HEVC_ASSIST_SCRATCH_6
+#define H265_SAO_MEM_UNIT		HEVC_ASSIST_SCRATCH_9
+#define H265_SAO_ABV			HEVC_ASSIST_SCRATCH_A
+#define H265_SAO_VB_SIZE		HEVC_ASSIST_SCRATCH_B
+#define H265_SAO_VB			HEVC_ASSIST_SCRATCH_C
+#define H265_SCALELUT			HEVC_ASSIST_SCRATCH_D
+#define H265_LMEM_DUMP_ADR		HEVC_ASSIST_SCRATCH_F
+#define H265_DEBUG_REG1			HEVC_ASSIST_SCRATCH_G
+#define H265_DECODE_MODE2		HEVC_ASSIST_SCRATCH_H
+#define H265_NAL_SEARCH_CTL		HEVC_ASSIST_SCRATCH_I
+#define H265_CUR_NAL_UNIT_TYPE		HEVC_ASSIST_SCRATCH_J	/* read */
+#define H265_DECODE_MODE		HEVC_ASSIST_SCRATCH_J	/* write */
+#define H265_DECODE_STOP_POS		HEVC_ASSIST_SCRATCH_K
+#define H265_AUX_ADR			HEVC_ASSIST_SCRATCH_L
+#define H265_AUX_DATA_SIZE		HEVC_ASSIST_SCRATCH_M
+#define H265_DECODE_SIZE		HEVC_ASSIST_SCRATCH_N
+/* Read back after a chunk: [15:8] rps_set_id, [7:0] start_decoding_flag. */
+#define H265_DECODE_INFO		HEVC_ASSIST_SCRATCH_1
+
+/* Not in av1.h/vp9.h yet; addresses from the staging meson hevc_regs.h. */
+#define HEVC_PARSER_VERSION		REG_ALIGN(0x3100)
+#define HEVC_PARSER_LCU_START		REG_ALIGN(0x3124)
+#define HEVC_MCPU_INTR_REQ		REG_ALIGN(0x3305)
+#define HEVC_MPRED_CTRL1		REG_ALIGN(0x3202)
+#define HEVC_MPRED_PIC_SIZE		REG_ALIGN(0x3205)
+#define HEVC_MPRED_PIC_SIZE_LCU		REG_ALIGN(0x3206)
+#define HEVC_MPRED_TILE_START		REG_ALIGN(0x3207)
+#define HEVC_MPRED_TILE_SIZE_LCU	REG_ALIGN(0x3208)
+#define HEVC_MPRED_REF_NUM		REG_ALIGN(0x3209)
+#define HEVC_MPRED_LT_REF		REG_ALIGN(0x320a)
+#define HEVC_MPRED_REF_EN_L0		REG_ALIGN(0x320c)
+#define HEVC_MPRED_REF_EN_L1		REG_ALIGN(0x320d)
+#define HEVC_MPRED_L1_REF00_POC		REG_ALIGN(0x3230)
+#define HEVC_MPRED_CUR_POC		REG_ALIGN(0x3260)
+#define HEVC_MPRED_COL_POC		REG_ALIGN(0x3261)
+#define HEVC_SAO_PIC_SIZE		REG_ALIGN(0x3605)
+#define HEVC_SAO_PIC_SIZE_LCU		REG_ALIGN(0x3606)
+#define HEVC_DBLK_CFG1			REG_ALIGN(0x3501)
+#define HEVC_DBLK_CFG2			REG_ALIGN(0x3502)
+#define HEVC_DBLK_STS1			REG_ALIGN(0x350c)
+
+#define AMRISC_MAIN_REQ			0x04
+
+/* Status the microcode posts, and the commands the host answers with. */
+#define H265_NAL_UNIT_VPS		0x01
+#define H265_NAL_UNIT_SPS		0x02
+#define H265_NAL_UNIT_PPS		0x03
+#define H265_NAL_UNIT_CODED_SLICE	0x04
+#define H265_CODED_SLICE_SEGMENT_DAT	0x05
+#define H265_SLICE_SEGMENT_DONE		0x08
+#define H265_NAL_SEARCH_DONE		0x09
+#define H265_DECPIC_DATA_DONE		0x0a
+#define H265_DECPIC_DATA_ERROR		0x0b
+#define H265_NAL_DECODE_DONE		0x0e
+#define H265_OVER_DECODE		0x0f
+#define H265_DECODE_BUFEMPTY		0x20
+#define H265_DECODE_TIMEOUT		0x21
+#define H265_SEARCH_BUFEMPTY		0x22
+#define H265_DECODE_OVER_SIZE		0x23
+#define H265_DECODE_BUFEMPTY2		0x24
+#define H265_FIND_NEXT_PIC_NAL		0x50
+#define H265_DISCARD_NAL		0xf0
+#define H265_ACTION_DONE		0xff
+
+#define H265_DECODE_MODE_MULTI_FRAMEBASE	0x1
+
+/* NAL unit types, H.265 table 7-1. */
+#define H265_NAL_IDR_W_RADL		19
+#define H265_NAL_IDR_N_LP		20
+#define H265_NAL_CRA			21
+#define H265_NAL_BLA_W_LP		16
+#define H265_NAL_BLA_W_RADL		17
+#define H265_NAL_BLA_N_LP		18
+#define H265_NAL_RASL_N			8
+#define H265_NAL_RASL_R			9
+#define H265_NAL_VPS			32
+#define H265_NAL_SPS			33
+#define H265_NAL_PPS			34
+#define H265_NAL_EOS			36
+
+/* The microcode's slice_type: B, P, I. */
+#define H265_B_SLICE			0
+#define H265_P_SLICE			1
+#define H265_I_SLICE			2
+
+#define H265_FW_FORMAT_MMU		17	/* VIDEO_DEC_HEVC_MMU */
+#define H265_FW_CORE_HEVC		2
+
+int aml_hevc_init(void *priv);
+void aml_hevc_exit(void *priv);
+int aml_hevc_dec_run(void *priv);
+
+/*
+ * Reference lists for the picture, as POCs: [0] = 1 when valid, [1..16] L0,
+ * [17..32] L1.  The microcode does not count explicitly coded long-term
+ * pictures in NumPicTotalCurr and so misreads list_entry; a client that
+ * strips ref_pic_lists_modification() from the slice passes the final lists
+ * here instead.  Not set (0) = build them from the RPS as usual.
+ */
+#define V4L2_CID_AML_HEVC_REFS	(V4L2_CID_USER_BASE + 0x1f01)
+#define AML_HEVC_REFS_LEN	33
+
+#endif
-- 
2.56.0


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

* [PATCH 10/11] media: amlogic: vdec: add MPEG-4 Part 2 decoding
  2026-10-25 19:24 [PATCH 00/11] media: amlogic: vdec: S4 decoding beyond H.264 Baseline christian.koever-draxl
                   ` (8 preceding siblings ...)
  2026-10-25 19:25 ` [PATCH 09/11] media: amlogic: vdec: add HEVC " christian.koever-draxl
@ 2026-10-25 19:25 ` christian.koever-draxl
  2026-10-25 20:39   ` sashiko-bot
  2026-10-25 19:25 ` [PATCH 11/11] media: amlogic: vdec: give the regmaps their own names christian.koever-draxl
  2026-10-26  5:53 ` [PATCH 00/11] media: amlogic: vdec: S4 decoding beyond H.264 Baseline Zhentao Guo
  11 siblings, 1 reply; 20+ messages in thread
From: christian.koever-draxl @ 2026-10-25 19:25 UTC (permalink / raw)
  To: zhentao.guo, mchehab, robh, krzk+dt, conor+dt
  Cc: linux-media, devicetree, linux-kernel,
	Christian Stefan Kövér-Draxl

From: Christian Stefan Kövér-Draxl <ckoever@outlook.de>

Decode MPEG-4 Part 2 Simple and Advanced Simple profile, including
B-VOPs, on the VDEC core with the M45M microcode. Each buffer carries a
VOL and a VOP; B-VOPs need the microcode's saved context restored.
Quarter-pel and GMC are decoded wrongly by the microcode and have to be
refused by the client.

RFC: V4L2 has no stateless MPEG-4 Part 2 interface. This uses the
V4L2_PIX_FMT_MPEG4 fourcc on a request-based queue and passes the
reference timestamps in the driver-private control
V4L2_CID_AML_MPEG4_REFS. It needs a proper stateless uAPI before it
can be merged.

Signed-off-by: Christian Stefan Kövér-Draxl <ckoever@outlook.de>
---
 drivers/media/platform/amlogic/vdec/Makefile  |   2 +-
 .../media/platform/amlogic/vdec/aml_vdec.c    |  15 +
 .../platform/amlogic/vdec/aml_vdec_platform.c |  17 +
 .../platform/amlogic/vdec/aml_vdec_tee_fw.c   |   5 +
 drivers/media/platform/amlogic/vdec/mpeg4.c   | 480 ++++++++++++++++++
 drivers/media/platform/amlogic/vdec/mpeg4.h   |  52 ++
 6 files changed, 570 insertions(+), 1 deletion(-)
 create mode 100644 drivers/media/platform/amlogic/vdec/mpeg4.c
 create mode 100644 drivers/media/platform/amlogic/vdec/mpeg4.h

diff --git a/drivers/media/platform/amlogic/vdec/Makefile b/drivers/media/platform/amlogic/vdec/Makefile
index ce2db477a103..80c75ae75d56 100644
--- a/drivers/media/platform/amlogic/vdec/Makefile
+++ b/drivers/media/platform/amlogic/vdec/Makefile
@@ -1,4 +1,4 @@
 # SPDX-License-Identifier: GPL-2.0-only
-aml-vdec-drv-objs := aml_vdec.o aml_vdec_drv.o aml_vdec_hw.o aml_vdec_platform.o h264.o h264_headers.o av1.o vp9.o vp9_prob.o hevc.o mpeg12.o mjpeg.o aml_vdec_tee_fw.o
+aml-vdec-drv-objs := aml_vdec.o aml_vdec_drv.o aml_vdec_hw.o aml_vdec_platform.o h264.o h264_headers.o av1.o vp9.o vp9_prob.o hevc.o mpeg12.o mjpeg.o mpeg4.o aml_vdec_tee_fw.o
 
 obj-$(CONFIG_VIDEO_AMLOGIC_VDEC) += aml-vdec-drv.o
diff --git a/drivers/media/platform/amlogic/vdec/aml_vdec.c b/drivers/media/platform/amlogic/vdec/aml_vdec.c
index ee020a62836f..117f0d4345ed 100644
--- a/drivers/media/platform/amlogic/vdec/aml_vdec.c
+++ b/drivers/media/platform/amlogic/vdec/aml_vdec.c
@@ -13,6 +13,7 @@
 #include "aml_vdec_hw.h"
 #include "aml_vdec_platform.h"
 #include "aml_vdec_tee_fw.h"
+#include "mpeg4.h"
 #include "hevc.h"
 
 #define VCODEC_DRV_NAME "aml-vdec-drv"
@@ -113,6 +114,17 @@ static const struct aml_vdec_v4l2_ctrl controls[] = {
 		.cfg = {
 			.id = V4L2_CID_STATELESS_MPEG2_QUANTISATION,
 		},
+	}, {
+		.codec_type = CODEC_TYPE_MPEG4,
+		.cfg = {
+			.id = V4L2_CID_AML_MPEG4_REFS,
+			.name = "MPEG-4 Reference Timestamps",
+			.type = V4L2_CTRL_TYPE_INTEGER64,
+			.min = 0,
+			.max = S64_MAX,
+			.step = 1,
+			.dims = { 2 },
+		},
 	}, {
 		.codec_type = CODEC_TYPE_HEVC,
 		.cfg = {
@@ -146,6 +158,9 @@ static const struct aml_dec_type dec_type_name[] = {
 	}, {
 		.codec_type = CODEC_TYPE_MJPEG,
 		.name = "MJPEG",
+	}, {
+		.codec_type = CODEC_TYPE_MPEG4,
+		.name = "MPEG4",
 	},
 };
 
diff --git a/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c b/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c
index 36ae12e6cdd8..f8e2d0bef3fe 100644
--- a/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c
+++ b/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c
@@ -11,6 +11,7 @@
 #include "hevc.h"
 #include "mpeg12.h"
 #include "mjpeg.h"
+#include "mpeg4.h"
 
 static struct aml_video_fmt aml_s4_video_formats[] = {
 	{
@@ -68,6 +69,17 @@ static struct aml_video_fmt aml_s4_video_formats[] = {
 		.stepwise = {AML_VDEC_MIN_W, AML_VDEC_1080P_MAX_W, 2,
 			AML_VDEC_MIN_H, AML_VDEC_1080P_MAX_H, 2},
 	},
+	{
+		.name = "MPEG-4",
+		.fourcc = V4L2_PIX_FMT_MPEG4,
+		.type = AML_FMT_DEC,
+		.align = 64,
+		.is_10_bit_support = 0,
+		.codec_type = CODEC_TYPE_MPEG4,
+		.num_planes = 1,
+		.stepwise = {AML_VDEC_MIN_W, AML_VDEC_1080P_MAX_W, 2,
+			AML_VDEC_MIN_H, AML_VDEC_1080P_MAX_H, 2},
+	},
 	{
 		.name = "JPEG",
 		.fourcc = V4L2_PIX_FMT_JPEG,
@@ -164,6 +176,11 @@ const struct aml_codec_ops aml_S4_dec_ops[] = {
 		.exit = aml_mjpeg_exit,
 		.run = aml_mjpeg_dec_run,
 	},
+	[CODEC_TYPE_MPEG4] = {
+		.init = aml_mpeg4_init,
+		.exit = aml_mpeg4_exit,
+		.run = aml_mpeg4_dec_run,
+	},
 };
 
 const struct aml_dev_platform_data aml_vdec_s4_pdata = {
diff --git a/drivers/media/platform/amlogic/vdec/aml_vdec_tee_fw.c b/drivers/media/platform/amlogic/vdec/aml_vdec_tee_fw.c
index a92405047c6e..25928612326b 100644
--- a/drivers/media/platform/amlogic/vdec/aml_vdec_tee_fw.c
+++ b/drivers/media/platform/amlogic/vdec/aml_vdec_tee_fw.c
@@ -92,6 +92,11 @@ static struct aml_tee_fw firmware[] = {
 		.core = CORE_VDEC_LEGENCY,
 		.is_swap = 0,
 	},
+	[CODEC_TYPE_MPEG4] = {
+		.fw_format = VIDEO_DEC_MPEG4_5_MULTI,
+		.core = CORE_VDEC_LEGENCY,
+		.is_swap = 0,
+	},
 };
 
 static int optee_ctx_match(struct tee_ioctl_version_data *ver, const void *data)
diff --git a/drivers/media/platform/amlogic/vdec/mpeg4.c b/drivers/media/platform/amlogic/vdec/mpeg4.c
new file mode 100644
index 000000000000..9cc97fa46209
--- /dev/null
+++ b/drivers/media/platform/amlogic/vdec/mpeg4.c
@@ -0,0 +1,480 @@
+// SPDX-License-Identifier: (GPL-2.0-only OR MIT)
+/*
+ * MPEG-4 Part 2 (Simple / Advanced Simple) decoding on the Amlogic legacy
+ * VDEC core.
+ *
+ * Ported from the reference's frame-based vmpeg4_multi.c.  Like the MPEG-2
+ * one, this microcode parses the elementary stream itself - VOL and VOP
+ * headers included - so there is no parsed-header uAPI: every OUTPUT buffer
+ * holds a VOL header followed by one VOP, exactly as it appears in the
+ * stream.
+ *
+ * Unlike MPEG-2 the picture cannot be decoded from its own headers alone: a
+ * B-VOP needs the time distances to its anchors (TRD/TRB), which the
+ * microcode remembers from the anchors' VOP headers in its local memory.
+ * So, like the reference, the decoder context is saved after every picture
+ * - a few VLD/VCOP registers here, the microcode's local memory in the
+ * workspace - and restored before the next one (ctx_valid).
+ *
+ * The references are the one thing the microcode cannot find by itself; the
+ * client names them in a driver control holding the capture timestamps of
+ * the forward and backward reference, as every stateless codec does.
+ */
+
+#include <linux/delay.h>
+#include <linux/dma-mapping.h>
+#include <linux/module.h>
+#include <linux/sizes.h>
+#include <linux/slab.h>
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-mem2mem.h>
+#include <media/videobuf2-dma-contig.h>
+
+#include "aml_vdec_drv.h"
+#include "aml_vdec_hw.h"
+#include "mpeg4.h"
+
+#define MPEG4_TIMEOUT_MS	500
+#define MPEG4_TAIL		4
+
+struct mpeg4_canvas {
+	int y;
+	int uv;
+};
+
+struct aml_mpeg4_ctx {
+	struct aml_vdec_ctx *v4l2_ctx;
+
+	void *wks_vaddr;
+	dma_addr_t wks_paddr;
+
+	void *stage_vaddr;
+	dma_addr_t stage_paddr;
+	u32 stage_size;
+
+	/* rec, ref0, ref1 */
+	struct mpeg4_canvas canvas[3];
+
+	u32 status;
+	u32 decoded;
+
+	/* decoder context carried between pictures, see the top of the file */
+	bool ctx_valid;
+	u32 reg_mpeg1_2;
+	u32 reg_vcop_ctrl;
+	u32 reg_pic_head_info;
+	u32 reg_slice_qp;
+	u32 reg_pic_wh;
+	u32 reg_mb_info;
+	u32 reg_dc_ac_ctrl;
+	u32 reg_iqidct_control;
+	u32 reg_resync_marker_length;
+	u32 reg_rv_ai_mb_count;
+};
+
+static void mpeg4_save_hw_context(struct aml_vdec_hw *hw, struct aml_mpeg4_ctx *m)
+{
+	m->reg_mpeg1_2 = read_dos_reg(hw, MPEG1_2_REG);
+	m->reg_vcop_ctrl = read_dos_reg(hw, VCOP_CTRL_REG);
+	m->reg_pic_head_info = read_dos_reg(hw, PIC_HEAD_INFO);
+	m->reg_slice_qp = read_dos_reg(hw, SLICE_QP);
+	m->reg_pic_wh = read_dos_reg(hw, MP4_PIC_WH);
+	m->reg_mb_info = read_dos_reg(hw, MB_INFO);
+	m->reg_dc_ac_ctrl = read_dos_reg(hw, DC_AC_CTRL);
+	m->reg_iqidct_control = read_dos_reg(hw, IQIDCT_CONTROL);
+	m->reg_resync_marker_length = read_dos_reg(hw, RESYNC_MARKER_LENGTH);
+	m->reg_rv_ai_mb_count = read_dos_reg(hw, RV_AI_MB_COUNT);
+	m->ctx_valid = true;
+}
+
+static void mpeg4_canvas_free(struct aml_vdec_hw *hw, struct aml_mpeg4_ctx *m)
+{
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(m->canvas); i++) {
+		if (m->canvas[i].y >= 0)
+			meson_canvas_free(hw->canvas, m->canvas[i].y);
+		if (m->canvas[i].uv >= 0)
+			meson_canvas_free(hw->canvas, m->canvas[i].uv);
+		m->canvas[i].y = -1;
+		m->canvas[i].uv = -1;
+	}
+}
+
+/* Returns the canvas spec (y | uv << 8 | uv << 16) for the buffer, or ~0. */
+static u32 mpeg4_canvas_setup(struct aml_vdec_ctx *ctx,
+			      struct mpeg4_canvas *c, struct vb2_buffer *vb)
+{
+	struct aml_vdec_hw *hw = vdec_get_hw(ctx->dev);
+	u32 w = ctx->pic_info.coded_width;
+	u32 h = ctx->pic_info.coded_height;
+	dma_addr_t y, uv;
+	u8 yi, ci;
+
+	y = vb2_dma_contig_plane_dma_addr(vb, 0);
+	if (ctx->pic_info.plane_num > 1)
+		uv = vb2_dma_contig_plane_dma_addr(vb, 1);
+	else
+		uv = y + ctx->pic_info.fb_size[0];
+
+	/* a slot reused within one job must not leak its old canvases */
+	if (c->y >= 0)
+		meson_canvas_free(hw->canvas, c->y);
+	if (c->uv >= 0)
+		meson_canvas_free(hw->canvas, c->uv);
+	c->y = -1;
+	c->uv = -1;
+
+	if (meson_canvas_alloc(hw->canvas, &yi))
+		return ~0u;
+	if (meson_canvas_alloc(hw->canvas, &ci)) {
+		meson_canvas_free(hw->canvas, yi);
+		return ~0u;
+	}
+	c->y = yi;
+	c->uv = ci;
+
+	meson_canvas_config(hw->canvas, yi, y, w, h, MESON_CANVAS_WRAP_NONE,
+			    MESON_CANVAS_BLKMODE_LINEAR, MESON_CANVAS_ENDIAN_SWAP64);
+	regmap_write(hw->map[DOS_BUS], VDEC_ASSIST_CANVAS_BLK32,
+		     (1 << 11) | (1 << 8) | yi);
+	meson_canvas_config(hw->canvas, ci, uv, w, h / 2, MESON_CANVAS_WRAP_NONE,
+			    MESON_CANVAS_BLKMODE_LINEAR, MESON_CANVAS_ENDIAN_SWAP64);
+	regmap_write(hw->map[DOS_BUS], VDEC_ASSIST_CANVAS_BLK32,
+		     (1 << 11) | (1 << 8) | ci);
+
+	return yi | (ci << 8) | (ci << 16);
+}
+
+static u32 mpeg4_ref_spec(struct aml_vdec_ctx *ctx, struct mpeg4_canvas *c,
+			  u64 ts)
+{
+	struct vb2_queue *vq = v4l2_m2m_get_dst_vq(ctx->fh.m2m_ctx);
+	struct vb2_buffer *vb = vb2_find_buffer(vq, ts);
+
+	if (!vb)
+		return 0xffffffff;
+	return mpeg4_canvas_setup(ctx, c, vb);
+}
+
+static int mpeg4_prepare_input(struct aml_vdec_ctx *ctx,
+			       struct vb2_v4l2_buffer *src)
+{
+	struct aml_vdec_hw *hw = vdec_get_hw(ctx->dev);
+	struct aml_mpeg4_ctx *m = ctx->codec_priv;
+	struct vb2_buffer *vb = &src->vb2_buf;
+	u32 len = vb2_get_plane_payload(vb, 0);
+	u32 need = len + MPEG4_TAIL + VLD_PADDING_SIZE + 64;
+	u8 *p;
+
+	if (need > m->stage_size) {
+		if (m->stage_vaddr)
+			dma_free_coherent(hw->dev, m->stage_size,
+					  m->stage_vaddr, m->stage_paddr);
+		m->stage_size = PAGE_ALIGN(need + SZ_64K);
+		m->stage_vaddr = dma_alloc_coherent(hw->dev, m->stage_size,
+						    &m->stage_paddr, GFP_KERNEL);
+		if (!m->stage_vaddr) {
+			m->stage_size = 0;
+			return -ENOMEM;
+		}
+	}
+
+	p = m->stage_vaddr;
+	memcpy(p, vb2_plane_vaddr(vb, 0), len);
+	/* The microcode only ends the VOP on the next start code. */
+	p[len] = 0;
+	p[len + 1] = 0;
+	p[len + 2] = 1;
+	p[len + 3] = 0xb6;
+	memset(p + len + MPEG4_TAIL, 0, VLD_PADDING_SIZE);
+
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_CONTROL, 0);
+	regmap_write(hw->map[DOS_BUS], DOS_SW_RESET0,
+		     (1 << 5) | (1 << 4) | (1 << 3));
+	regmap_write(hw->map[DOS_BUS], DOS_SW_RESET0, 0);
+	regmap_write(hw->map[DOS_BUS], POWER_CTL_VLD, 1 << 4);
+
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_START_PTR, m->stage_paddr);
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_END_PTR,
+		     m->stage_paddr + m->stage_size - VDEC_FIFO_ALIGN);
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_CURR_PTR, m->stage_paddr);
+
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_CONTROL, 1);
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_CONTROL, 0);
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_BUF_CNTL, 2);
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_RP, m->stage_paddr);
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_WP,
+		     round_down(m->stage_paddr + len + MPEG4_TAIL +
+				VLD_PADDING_SIZE, VDEC_FIFO_ALIGN));
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_BUF_CNTL, 3);
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_BUF_CNTL, 2);
+	regmap_write(hw->map[DOS_BUS], VLD_MEM_VIFIFO_CONTROL,
+		     (0x11 << 16) | (1 << 10) | (7 << 3));
+
+	regmap_write(hw->map[DOS_BUS], VIFF_BIT_CNT, len * 8);
+
+	return 0;
+}
+
+static void mpeg4_job_done(struct aml_vdec_ctx *ctx, enum vb2_buffer_state st)
+{
+	struct aml_vdec_dev *dev = ctx->dev;
+
+	mpeg4_canvas_free(dev->dec_hw, ctx->codec_priv);
+	ctx->int_cond = 1;
+	ctx->job_finished = true;
+	v4l2_m2m_buf_done_and_job_finish(dev->m2m_dev_dec, ctx->m2m_ctx, st);
+	wake_up(&ctx->queue);
+}
+
+static irqreturn_t mpeg4_isr(int irq, void *priv)
+{
+	struct aml_vdec_dev *dev = priv;
+
+	regmap_write(dev->dec_hw->map[DOS_BUS], VDEC_ASSIST_MBOX1_CLR_REG, 1);
+
+	return IRQ_WAKE_THREAD;
+}
+
+static irqreturn_t mpeg4_threaded_isr(int irq, void *priv)
+{
+	struct aml_vdec_dev *dev = priv;
+	struct aml_vdec_hw *hw = dev->dec_hw;
+	struct aml_mpeg4_ctx *m = hw->curr_ctx;
+	struct aml_vdec_ctx *ctx;
+	u32 reg;
+
+	if (!m)
+		return IRQ_HANDLED;
+	ctx = m->v4l2_ctx;
+	if (ctx->job_finished)
+		return IRQ_HANDLED;
+
+	/* Picture size report: the buffers are already set up, go on. */
+	if (read_dos_reg(hw, MP4_PIC_INFO) == 1) {
+		dev_dbg(hw->dev, "mpeg4 size %ux%u\n",
+			read_dos_reg(hw, MP4_PIC_WH) >> 16,
+			 read_dos_reg(hw, MP4_PIC_WH) & 0xffff);
+		regmap_write(hw->map[DOS_BUS], MP4_PIC_INFO, 0);
+		return IRQ_HANDLED;
+	}
+
+	if (read_dos_reg(hw, MP4_DEBUG)) {
+		regmap_write(hw->map[DOS_BUS], MP4_DEBUG, 0);
+		return IRQ_HANDLED;
+	}
+
+	reg = read_dos_reg(hw, MP4_BUFFEROUT);
+	m->status = reg;
+	dev_dbg(hw->dev,
+		"mpeg4 irq out %x type %u rec %06x wh %08x err %u notcoded %u\n",
+		 reg, (reg >> 3) & 7, read_dos_reg(hw, REC_CANVAS_ADDR),
+		 read_dos_reg(hw, MP4_PIC_WH),
+		 read_dos_reg(hw, MP4_ERR_COUNT),
+		 read_dos_reg(hw, MP4_NOT_CODED_CNT));
+
+	if (reg == MP4_BUFFEROUT_IDLE)
+		return IRQ_HANDLED;	/* not ours */
+	if (reg == MP4_BUFFEROUT_NEED_DATA) {
+		mpeg4_job_done(ctx, VB2_BUF_STATE_ERROR);
+		return IRQ_HANDLED;
+	}
+
+	m->decoded++;
+	mpeg4_save_hw_context(hw, m);
+	mpeg4_job_done(ctx, VB2_BUF_STATE_DONE);
+	regmap_write(hw->map[DOS_BUS], MP4_BUFFEROUT, 0);
+
+	return IRQ_HANDLED;
+}
+
+static void mpeg4_restore_hw(struct aml_vdec_ctx *ctx)
+{
+	struct aml_vdec_hw *hw = vdec_get_hw(ctx->dev);
+	struct aml_mpeg4_ctx *m = ctx->codec_priv;
+	struct regmap *map = hw->map[DOS_BUS];
+
+	regmap_write(map, MP4_MEM_OFFSET, m->wks_paddr);
+
+	/* disable PSCALE for hardware sharing */
+	regmap_write(map, PSCALE_CTRL, 0);
+	regmap_write(map, MP4_BUFFEROUT, MP4_BUFFEROUT_IDLE);
+	regmap_write(map, VDEC_ASSIST_MBOX1_CLR_REG, 1);
+	regmap_write(map, VDEC_ASSIST_MBOX1_MASK, 1);
+	regmap_write(map, MP4_NOT_CODED_CNT, 0);
+	regmap_write(map, MP4_PIC_INFO, 0);
+	regmap_write(map, MP4_DEBUG, 0);
+	regmap_write(map, MP4_STOP_POS, 0);
+
+	regmap_update_bits(map, MDEC_PIC_DC_CTRL, 1 << 17, 1 << 17);
+	if (ctx->dec_fmt[AML_FMT_DST].fourcc == V4L2_PIX_FMT_NV21 ||
+	    ctx->dec_fmt[AML_FMT_DST].fourcc == V4L2_PIX_FMT_NV21M)
+		regmap_update_bits(map, MDEC_PIC_DC_CTRL, 1 << 16, 1 << 16);
+	else
+		regmap_update_bits(map, MDEC_PIC_DC_CTRL, 1 << 16, 0);
+	regmap_write(map, MDEC_PIC_DC_THRESH, 0x404038aa);
+
+	/* size from the VOL until a picture has reported it */
+	regmap_write(map, MP4_PIC_WH, m->ctx_valid ? m->reg_pic_wh : 0);
+	regmap_write(map, MP4_SYS_RATE, 0);
+
+	if (m->ctx_valid) {
+		regmap_write(map, DC_AC_CTRL, m->reg_dc_ac_ctrl);
+		regmap_write(map, IQIDCT_CONTROL, m->reg_iqidct_control);
+		regmap_write(map, RESYNC_MARKER_LENGTH,
+			     m->reg_resync_marker_length);
+		regmap_write(map, RV_AI_MB_COUNT, m->reg_rv_ai_mb_count);
+	}
+	regmap_write(map, MPEG1_2_REG, m->ctx_valid ? m->reg_mpeg1_2 : 1);
+	regmap_write(map, VCOP_CTRL_REG, m->reg_vcop_ctrl);
+	regmap_write(map, PIC_HEAD_INFO, m->reg_pic_head_info);
+	regmap_write(map, SLICE_QP, m->reg_slice_qp);
+	regmap_write(map, MB_INFO, m->reg_mb_info);
+
+	/* frame based input, offset 0 */
+	regmap_write(map, MP4_INPUT, (1 << 7) | (m->ctx_valid << 6));
+}
+
+int aml_mpeg4_init(void *priv)
+{
+	struct aml_vdec_ctx *ctx = priv;
+	struct aml_vdec_hw *hw = vdec_get_hw(ctx->dev);
+	struct aml_mpeg4_ctx *m;
+	int i;
+
+	if (!hw)
+		return -ENODEV;
+
+	m = kzalloc_obj(*m);
+	if (!m)
+		return -ENOMEM;
+	m->v4l2_ctx = ctx;
+	for (i = 0; i < ARRAY_SIZE(m->canvas); i++) {
+		m->canvas[i].y = -1;
+		m->canvas[i].uv = -1;
+	}
+
+	m->wks_vaddr = dma_alloc_coherent(hw->dev, MPEG4_WORKSPACE_SIZE,
+					  &m->wks_paddr, GFP_KERNEL);
+	if (!m->wks_vaddr) {
+		kfree(m);
+		return -ENOMEM;
+	}
+
+	ctx->codec_priv = m;
+	return 0;
+}
+
+void aml_mpeg4_exit(void *priv)
+{
+	struct aml_vdec_ctx *ctx = priv;
+	struct aml_mpeg4_ctx *m = ctx->codec_priv;
+	struct aml_vdec_hw *hw;
+
+	if (!m)
+		return;
+	hw = vdec_get_hw(ctx->dev);
+
+	if (ctx->dos_clk_en)
+		aml_stop_vdec_hw(hw);
+	mpeg4_canvas_free(hw, m);
+
+	if (m->stage_vaddr)
+		dma_free_coherent(hw->dev, m->stage_size,
+				  m->stage_vaddr, m->stage_paddr);
+	dma_free_coherent(hw->dev, MPEG4_WORKSPACE_SIZE,
+			  m->wks_vaddr, m->wks_paddr);
+
+	if (hw->curr_ctx == m)
+		hw->curr_ctx = NULL;
+	kfree(m);
+	ctx->codec_priv = NULL;
+}
+
+int aml_mpeg4_dec_run(void *priv)
+{
+	struct aml_vdec_ctx *ctx = priv;
+	struct aml_mpeg4_ctx *m = ctx->codec_priv;
+	struct aml_vdec_hw *hw = vdec_get_hw(ctx->dev);
+	struct v4l2_ctrl *refs_ctrl;
+	struct vb2_v4l2_buffer *src, *dst;
+	u32 rec, ref0 = 0xffffffff, ref1 = 0xffffffff;
+	u64 fwd = 0, bwd = 0;
+	long ret;
+
+	src = v4l2_m2m_next_src_buf(ctx->fh.m2m_ctx);
+	dst = v4l2_m2m_next_dst_buf(ctx->fh.m2m_ctx);
+	if (!src || !dst)
+		return -EINVAL;
+
+	refs_ctrl = v4l2_ctrl_find(&ctx->ctrl_handler, V4L2_CID_AML_MPEG4_REFS);
+	if (refs_ctrl) {
+		fwd = refs_ctrl->p_cur.p_s64[0];
+		bwd = refs_ctrl->p_cur.p_s64[1];
+	}
+
+	hw->curr_ctx = m;
+	hw->hw_ops.irq_handler = mpeg4_isr;
+	hw->hw_ops.irq_threaded_func = mpeg4_threaded_isr;
+
+	if (mpeg4_prepare_input(ctx, src) < 0)
+		return -ENOMEM;
+
+	mpeg4_restore_hw(ctx);
+
+	rec = mpeg4_canvas_setup(ctx, &m->canvas[0], &dst->vb2_buf);
+	if (rec == ~0u)
+		goto fail;
+
+	/*
+	 * REF0 is the older anchor, REF1 the newer one; a P-VOP predicts from
+	 * REF1, a B-VOP from both.  0 means "none" in the control.
+	 */
+	if (fwd && bwd) {
+		ref0 = mpeg4_ref_spec(ctx, &m->canvas[1], fwd);
+		ref1 = mpeg4_ref_spec(ctx, &m->canvas[2], bwd);
+	} else if (fwd) {
+		ref1 = mpeg4_ref_spec(ctx, &m->canvas[2], fwd);
+		ref0 = ref1;
+	}
+	regmap_write(hw->map[DOS_BUS], MP4_REF0, ref0);
+	regmap_write(hw->map[DOS_BUS], MP4_REF1, ref1);
+	regmap_write(hw->map[DOS_BUS], REC_CANVAS_ADDR, rec);
+	regmap_write(hw->map[DOS_BUS], ANC2_CANVAS_ADDR, rec);
+
+	dev_dbg(hw->dev,
+		"mpeg4 run %lu bytes ts %llu rec %06x ref0 %08x (%llu) ref1 %08x (%llu)\n",
+		 vb2_get_plane_payload(&src->vb2_buf, 0),
+		 src->vb2_buf.timestamp, rec, ref0, fwd, ref1, bwd);
+
+	ctx->int_cond = 0;
+	regmap_update_bits(hw->map[DOS_BUS], VLD_MEM_VIFIFO_CONTROL, 0x6, 0x6);
+	aml_start_vdec_hw(hw);
+
+	ret = wait_event_timeout(ctx->queue, ctx->int_cond,
+				 msecs_to_jiffies(MPEG4_TIMEOUT_MS));
+	ctx->int_cond = 0;
+	if (!ret) {
+		dev_err(hw->dev,
+			"mpeg4 timeout: out %x info %x pc %x lvl %x rp %x wp %x bitcnt %x err %x\n",
+			read_dos_reg(hw, MP4_BUFFEROUT),
+			read_dos_reg(hw, MP4_PIC_INFO),
+			read_dos_reg(hw, MPC_E),
+			read_dos_reg(hw, VLD_MEM_VIFIFO_LEVEL),
+			read_dos_reg(hw, VLD_MEM_VIFIFO_RP),
+			read_dos_reg(hw, VLD_MEM_VIFIFO_WP),
+			read_dos_reg(hw, VIFF_BIT_CNT),
+			read_dos_reg(hw, MP4_ERR_COUNT));
+		aml_stop_vdec_hw(hw);
+		mpeg4_canvas_free(hw, m);
+		return -ETIMEDOUT;
+	}
+
+	aml_stop_vdec_hw(hw);
+	return 0;
+
+fail:
+	mpeg4_canvas_free(hw, m);
+	return -ENOMEM;
+}
diff --git a/drivers/media/platform/amlogic/vdec/mpeg4.h b/drivers/media/platform/amlogic/vdec/mpeg4.h
new file mode 100644
index 000000000000..213e8d390ebb
--- /dev/null
+++ b/drivers/media/platform/amlogic/vdec/mpeg4.h
@@ -0,0 +1,52 @@
+/* SPDX-License-Identifier: (GPL-2.0-only OR MIT) */
+/*
+ * MPEG-4 Part 2 decoding on the Amlogic legacy VDEC core.
+ */
+
+#ifndef _AML_MPEG4_H_
+#define _AML_MPEG4_H_
+
+/* Scratch register meanings for the mpeg4 microcode (vmpeg4_multi.c). */
+#define MP4_REF0		AV_SCRATCH_1
+#define MP4_REF1		AV_SCRATCH_2
+#define MP4_RATE		AV_SCRATCH_3
+#define MP4_PIC_RATIO		AV_SCRATCH_5
+#define MP4_ERR_COUNT		AV_SCRATCH_6
+#define MP4_PIC_WH		AV_SCRATCH_7
+#define MP4_INPUT		AV_SCRATCH_8
+#define MP4_BUFFEROUT		AV_SCRATCH_9
+#define MP4_NOT_CODED_CNT	AV_SCRATCH_A
+#define MP4_VOP_TIME_INC	AV_SCRATCH_B
+#define MP4_OFFSET_REG		AV_SCRATCH_C
+#define MP4_VOS_INFO		AV_SCRATCH_D
+#define MP4_SYS_RATE		AV_SCRATCH_E
+#define MP4_MEM_OFFSET		AV_SCRATCH_F
+#define MP4_PIC_INFO		AV_SCRATCH_H	/* 1: size report */
+#define MP4_STOP_POS		AV_SCRATCH_K
+#define MP4_DEBUG		AV_SCRATCH_M
+
+#define MP4_BUFFEROUT_IDLE	0x10000
+#define MP4_BUFFEROUT_NEED_DATA	2
+
+#define MPEG1_2_REG		REG_ALIGN(0x0c01)
+#define PIC_HEAD_INFO		REG_ALIGN(0x0c03)
+/* from the vendor kernel's dos_regs.h */
+#define SLICE_QP		REG_ALIGN(0x0c0a)
+#define RESYNC_MARKER_LENGTH	REG_ALIGN(0x0c0e)
+#define DC_AC_CTRL		REG_ALIGN(0x0e05)
+#define RV_AI_MB_COUNT		REG_ALIGN(0x0e0c)
+#define IQIDCT_CONTROL		REG_ALIGN(0x0e0e)
+
+#define MPEG4_WORKSPACE_SIZE	(12 * SZ_64K)
+
+/*
+ * Driver control: the capture timestamps of the forward and backward
+ * reference (s64[2], 0 = none).  There is no MPEG-4 Part 2 stateless uAPI.
+ */
+#define V4L2_CID_AML_MPEG4_REFS	(V4L2_CID_USER_BASE + 0x1f00)
+
+int aml_mpeg4_init(void *priv);
+void aml_mpeg4_exit(void *priv);
+int aml_mpeg4_dec_run(void *priv);
+
+#endif
-- 
2.56.0


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

* [PATCH 11/11] media: amlogic: vdec: give the regmaps their own names
  2026-10-25 19:24 [PATCH 00/11] media: amlogic: vdec: S4 decoding beyond H.264 Baseline christian.koever-draxl
                   ` (9 preceding siblings ...)
  2026-10-25 19:25 ` [PATCH 10/11] media: amlogic: vdec: add MPEG-4 Part 2 decoding christian.koever-draxl
@ 2026-10-25 19:25 ` christian.koever-draxl
  2026-10-26  5:53 ` [PATCH 00/11] media: amlogic: vdec: S4 decoding beyond H.264 Baseline Zhentao Guo
  11 siblings, 0 replies; 20+ messages in thread
From: christian.koever-draxl @ 2026-10-25 19:25 UTC (permalink / raw)
  To: zhentao.guo, mchehab, robh, krzk+dt, conor+dt
  Cc: linux-media, devicetree, linux-kernel,
	Christian Stefan Kövér-Draxl

From: Christian Stefan Kövér-Draxl <ckoever@outlook.de>

The DOS and DMC regmaps are both registered on the decoder device
without a name, so debugfs refuses the second one:

  debugfs: 'fe320000.video-codec' already exists in 'regmap'

Name them after the register blocks they cover.

Signed-off-by: Christian Stefan Kövér-Draxl <ckoever@outlook.de>
---
 drivers/media/platform/amlogic/vdec/aml_vdec_hw.c | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/drivers/media/platform/amlogic/vdec/aml_vdec_hw.c b/drivers/media/platform/amlogic/vdec/aml_vdec_hw.c
index 935018ea96c6..50b867755771 100644
--- a/drivers/media/platform/amlogic/vdec/aml_vdec_hw.c
+++ b/drivers/media/platform/amlogic/vdec/aml_vdec_hw.c
@@ -555,6 +555,7 @@ struct aml_vdec_hw *vdec_get_hw(void *priv)
 }
 
 static const struct regmap_config dos_regmap_conf = {
+	.name = "dos",
 	.reg_bits = 32,
 	.val_bits = 32,
 	.reg_stride = 4,
@@ -562,6 +563,7 @@ static const struct regmap_config dos_regmap_conf = {
 };
 
 static const struct regmap_config dmc_regmap_conf = {
+	.name = "dmc",
 	.reg_bits = 32,
 	.val_bits = 32,
 	.reg_stride = 4,
-- 
2.56.0


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

* Re: [PATCH 06/11] media: amlogic: vdec: add MJPEG decoding
  2026-10-25 19:25 ` [PATCH 06/11] media: amlogic: vdec: add MJPEG decoding christian.koever-draxl
@ 2026-10-25 20:20   ` sashiko-bot
  0 siblings, 0 replies; 20+ messages in thread
From: sashiko-bot @ 2026-10-25 20:20 UTC (permalink / raw)
  To: christian.koever-draxl; +Cc: devicetree, robh, conor+dt, media-ci

Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
- [Critical] media: amlogic: vdec: premature M2M job completion in mjpeg_job_done()
- [Critical] media: amlogic: vdec: unsynchronized teardown and HW stop in aml_mjpeg_exit()
- [High] media: amlogic: vdec: timeout race with IRQ thread in aml_mjpeg_dec_run()
- [High] media: amlogic: vdec: NULL dereference on vb2_plane_vaddr() in mjpeg.c
- [Medium] media: amlogic: vdec: missing V4L2_PIX_FMT_MJPEG in aml_s4_video_formats
- [Medium] media: amlogic: vdec: unrestricted CAPTURE formats for MJPEG negotiation
--

commit 03dd48f8ca05ff47abd88fc4288b36619393b97e
Author: Christian Stefan Kövér-Draxl <ckoever@outlook.de>

media: amlogic: vdec: add MJPEG decoding

This commit adds Motion-JPEG decoding support on the Amlogic legacy VDEC
core, including format registration, firmware table entry, and the MJPEG
codec implementation.

> diff --git a/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c b/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c
> index 83efadcb8aeff..d54b09b6bbc02 100644
> --- a/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c
> +++ b/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c
> @@ -7,6 +7,7 @@
>  #include "aml_vdec_hw.h"
>  #include "h264.h"
>  #include "mpeg12.h"
> +#include "mjpeg.h"
>  
>  static struct aml_video_fmt aml_s4_video_formats[] = {
>  	{
> @@ -31,6 +32,17 @@ static struct aml_video_fmt aml_s4_video_formats[] = {
>  		.stepwise = {AML_VDEC_MIN_W, AML_VDEC_1080P_MAX_W, 2,
>  			AML_VDEC_MIN_H, AML_VDEC_1080P_MAX_H, 2},
>  	},
> +	{
> +		.name = "JPEG",
> +		.fourcc = V4L2_PIX_FMT_JPEG,
> +		.type = AML_FMT_DEC,
> +		.align = 64,
> +		.is_10_bit_support = 0,
> +		.codec_type = CODEC_TYPE_MJPEG,
> +		.num_planes = 1,
> +		.stepwise = {AML_VDEC_MIN_W, AML_VDEC_1080P_MAX_W, 2,
> +			AML_VDEC_MIN_H, AML_VDEC_1080P_MAX_H, 2},
> +	},

[Severity: Medium]
The commit description states that this patch decodes V4L2_PIX_FMT_MJPEG
frames, and mjpeg.c describes Motion-JPEG decoding, but aml_s4_video_formats
only registers V4L2_PIX_FMT_JPEG.

Because aml_vdec_get_video_fmt() in drivers/media/platform/amlogic/vdec/aml_vdec.c
matches strictly on fmt->fourcc == format:

    for (k = 0; k < dev->pvdec_data->num_fmts; k++) {
        fmt = &dev->pvdec_data->dec_fmt[k];
        if (fmt->fourcc == format)
            return fmt;
    }

clients requesting V4L2_PIX_FMT_MJPEG via VIDIOC_ENUM_FMT,
VIDIOC_ENUM_FRAMESIZES, or VIDIOC_S_FMT will not find the format, and
vdec_s_fmt_output() will fall back to the default H.264 format.

Also, the commit message mentions that the planar 4:2:0 CAPTURE format is
exposed by this patch, whereas V4L2_PIX_FMT_YUV420 was already present in
aml_s4_video_formats from commit a516ebbbab5a.

Should V4L2_PIX_FMT_MJPEG be added to aml_s4_video_formats as well?

[ ... ]

> diff --git a/drivers/media/platform/amlogic/vdec/mjpeg.c b/drivers/media/platform/amlogic/vdec/mjpeg.c
> new file mode 100644
> index 0000000000000..7989b5f42a563
> --- /dev/null
> +++ b/drivers/media/platform/amlogic/vdec/mjpeg.c
> @@ -0,0 +1,471 @@

[ ... ]

> +static void mjpeg_job_done(struct aml_vdec_ctx *ctx, enum vb2_buffer_state st)
> +{
> +	struct aml_vdec_dev *dev = ctx->dev;
> +	struct vb2_v4l2_buffer *dst = v4l2_m2m_next_dst_buf(ctx->fh.m2m_ctx);
> +	struct vb2_v4l2_buffer *src = v4l2_m2m_next_src_buf(ctx->fh.m2m_ctx);
> +
> +	/*
> +	 * A JPEG request carries no controls, so the buffer is its only
> +	 * object: once that is done the request is complete, and completing
> +	 * the control side afterwards (as device_run does for the other
> +	 * legacy codecs) binds to a finished request and WARNs.  Do it first.
> +	 */
> +	if (src && src->vb2_buf.req_obj.req)
> +		v4l2_ctrl_request_complete(src->vb2_buf.req_obj.req,
> +					   &ctx->ctrl_handler);
> +	mjpeg_canvas_free(dev->dec_hw, ctx->codec_priv);
> +	if (dst)
> +		vb2_set_plane_payload(&dst->vb2_buf, 0,
> +				      ctx->pix_fmt[AML_FMT_DST].plane_fmt[0].sizeimage);
> +	ctx->int_cond = 1;
> +	ctx->job_finished = true;
> +	v4l2_m2m_buf_done_and_job_finish(dev->m2m_dev_dec, ctx->m2m_ctx, st);
> +	wake_up(&ctx->queue);
> +}

[Severity: Critical]
Does calling v4l2_m2m_buf_done_and_job_finish() here in
mjpeg_threaded_isr() -> mjpeg_job_done() while aml_mjpeg_dec_run() is still
sleeping in wait_event_timeout() cause use-after-free and hardware races?

When m2mops_vdec_device_run() in drivers/media/platform/amlogic/vdec/aml_vdec.c
runs a job:

    ctx->job_finished = false;
    ret = ctx->codec_ops->run(ctx);

    v4l2_m2m_buf_copy_metadata(src_buf, dst_buf);

completing the M2M job inside mjpeg_job_done() before aml_mjpeg_dec_run()
wakes up and stops the hardware creates several issues:

1. v4l2_m2m_buf_done_and_job_finish() returns src_buf and dst_buf to vb2 and
wakes userspace poll() / VIDIOC_DQBUF waiters before aml_mjpeg_dec_run()
returns to m2mops_vdec_device_run() to call
v4l2_m2m_buf_copy_metadata(src_buf, dst_buf). Userspace can dequeue dst_buf
without its copied timestamp, field, timecode, or flags, or re-queue/free
the buffers before v4l2_m2m_buf_copy_metadata() touches them.

2. v4l2_m2m_buf_done_and_job_finish() clears TRANS_RUNNING and schedules the
next M2M job via v4l2_m2m_schedule_next_job(). A subsequent job can enter
m2mops_vdec_device_run() on another CPU, set ctx->int_cond = 0, and call
aml_start_vdec_hw(hw) before the previous job's aml_mjpeg_dec_run() finishes
waking up and executes ctx->int_cond = 0 and aml_stop_vdec_hw(hw), stopping
and resetting the VDEC hardware mid-decode.

3. Because v4l2_m2m_cancel_job() only waits for TRANS_RUNNING to be cleared,
a concurrent VIDIOC_STREAMOFF or fops_vcodec_release() can proceed into
vb2ops_vdec_stop_streaming() -> aml_mjpeg_exit() (freeing m) and kfree(ctx)
while the job thread is still executing ctx->int_cond = 0,
aml_stop_vdec_hw(hw), and m2mops_vdec_device_run().

4. mjpeg_canvas_free() releases the DMC canvases and v4l2_m2m_buf_done()
invalidates CPU caches for the capture buffer before aml_stop_vdec_hw(hw)
stops the AMRISC processor and waits for LMEM_DMA_CTRL and WRRSP_LMEM writes
to drain.

Should aml_mjpeg_dec_run() stop the hardware after wait_event_timeout()
before the canvases are freed and the M2M job is finished?

[ ... ]

> +void aml_mjpeg_exit(void *priv)
> +{
> +	struct aml_vdec_ctx *ctx = priv;
> +	struct aml_mjpeg_ctx *m = ctx->codec_priv;
> +	struct aml_vdec_hw *hw;
> +
> +	if (!m)
> +		return;
> +	hw = vdec_get_hw(ctx->dev);
> +
> +	if (ctx->dos_clk_en)
> +		aml_stop_vdec_hw(hw);
> +	mjpeg_canvas_free(hw, m);
> +	if (m->stage_vaddr)
> +		dma_free_coherent(hw->dev, m->stage_size,
> +				  m->stage_vaddr, m->stage_paddr);
> +
> +	if (hw->curr_ctx == m)
> +		hw->curr_ctx = NULL;
> +	kfree(m);
> +	ctx->codec_priv = NULL;
> +}

[Severity: Critical]
Can aml_mjpeg_exit() reset the shared VDEC core while another context is
decoding, or free m while mjpeg_threaded_isr() is still running?

First, ctx->dos_clk_en is set to 1 in m2mops_vdec_device_run() on the first
job and is never cleared. In aml_mjpeg_exit(), aml_stop_vdec_hw(hw) is
called whenever ctx->dos_clk_en is set, without checking if
hw->curr_ctx == m. If userspace calls VIDIOC_STREAMOFF or closes an idle
MJPEG context while another VDEC context is actively decoding on the shared
hardware, aml_stop_vdec_hw(hw) will halt the AMRISC processor and reset the
core mid-frame.

Second, aml_mjpeg_exit() clears hw->curr_ctx and immediately calls kfree(m)
without masking VDEC_ASSIST_MBOX1_MASK or calling
synchronize_irq(hw->dec_irq). If mjpeg_threaded_isr() is in flight and has
already read m = hw->curr_ctx (for example, right after a timeout or job
completion), can it dereference m and m->v4l2_ctx after kfree(m)?

> +
> +int aml_mjpeg_dec_run(void *priv)
> +{
> +	struct aml_vdec_ctx *ctx = priv;
> +	struct aml_mjpeg_ctx *m = ctx->codec_priv;
> +	struct aml_vdec_hw *hw = vdec_get_hw(ctx->dev);
> +	struct vb2_v4l2_buffer *src, *dst;
> +	u32 spec;
> +	long ret;
> +
> +	if (ctx->pix_fmt[AML_FMT_DST].pixelformat != V4L2_PIX_FMT_YUV420) {
> +		dev_err(hw->dev, "mjpeg: capture format must be YUV420\n");
> +		return -EINVAL;
> +	}

[Severity: Medium]
What happens during format negotiation when userspace configures MJPEG on the
OUTPUT queue?

In drivers/media/platform/amlogic/vdec/aml_vdec.c:

1. vidioc_vdec_enum_fmt() enumerates all AML_FMT_FRAME formats (NV21M, NV21,
NV12M, YUV420, NV12) on the CAPTURE queue regardless of the selected OUTPUT
codec.

2. vdec_s_fmt_output() copies the OUTPUT pix_mp into ctx->pix_fmt[AML_FMT_DST]
and leaves ctx->dec_fmt[AML_FMT_DST] at its initial default of
V4L2_PIX_FMT_NV12 instead of updating the CAPTURE format to
V4L2_PIX_FMT_YUV420:

    ret = vdec_try_fmt_mp(ctx, f->type, pix_mp);
    set_pic_info(ctx, pix_mp, f->type);

    ctx->pix_fmt[AML_FMT_SRC] = *pix_mp;
    ctx->pix_fmt[AML_FMT_DST] = *pix_mp;

3. vdec_s_fmt_capture() accepts NV12, NV21, NV12M, or NV21M when the OUTPUT
format is JPEG (and accepts YUV420 when the OUTPUT format is H.264 or
MPEG-2), so buffer allocation and VIDIOC_STREAMON succeed and only fail
later here in aml_mjpeg_dec_run() with -EINVAL.

Should VIDIOC_ENUM_FMT, vdec_s_fmt_output(), and vdec_s_fmt_capture()
restrict and default the CAPTURE format to V4L2_PIX_FMT_YUV420 when the
OUTPUT codec is CODEC_TYPE_MJPEG?

> +
> +	src = v4l2_m2m_next_src_buf(ctx->fh.m2m_ctx);
> +	dst = v4l2_m2m_next_dst_buf(ctx->fh.m2m_ctx);
> +	if (!src || !dst)
> +		return -EINVAL;
> +
> +	if (!mjpeg_supported(hw->dev, vb2_plane_vaddr(&src->vb2_buf, 0),
> +			     vb2_get_plane_payload(&src->vb2_buf, 0)))
> +		return -EINVAL;

[Severity: High]
Can vb2_plane_vaddr(&src->vb2_buf, 0) return NULL here and in
mjpeg_prepare_input()?

In aml_vdec_queue_init(), the OUTPUT queue is configured with
io_modes = VB2_MMAP | VB2_DMABUF and mem_ops = &vb2_dma_contig_memops. When
userspace queues a VB2_MEMORY_DMABUF buffer whose exporter does not implement
vmap or when dma_buf_vmap_unlocked() fails, vb2_plane_vaddr() returns NULL
(unlike h264_stage_with_headers() in h264.c, which checks for a NULL
virtual address).

When vb2_plane_vaddr() returns NULL and the payload length is at least 4,
mjpeg_supported() will dereference p[0]:

    if (len < 4 || p[0] != 0xff || p[1] != 0xd8)
        return false;

and mjpeg_prepare_input() also calls vb2_plane_vaddr(vb, 0) without a NULL
check before reading in[3].

> +
> +	hw->curr_ctx = m;
> +	hw->hw_ops.irq_handler = mjpeg_isr;
> +	hw->hw_ops.irq_threaded_func = mjpeg_threaded_isr;
> +
> +	if (mjpeg_prepare_input(ctx, src) < 0)
> +		return -ENOMEM;
> +
> +	spec = mjpeg_canvas_setup(ctx, &dst->vb2_buf);
> +	if (spec == ~0u)
> +		return -ENOMEM;
> +	mjpeg_restore_hw(ctx, spec);
> +
> +	dev_dbg(hw->dev, "mjpeg run %lu bytes spec %06x\n",
> +		vb2_get_plane_payload(&src->vb2_buf, 0), spec);
> +
> +	ctx->int_cond = 0;
> +	regmap_update_bits(hw->map[DOS_BUS], VLD_MEM_VIFIFO_CONTROL, 0x6, 0x6);
> +	aml_start_vdec_hw(hw);
> +
> +	ret = wait_event_timeout(ctx->queue, ctx->int_cond,
> +				 msecs_to_jiffies(MJPEG_TIMEOUT_MS));
> +	ctx->int_cond = 0;
> +	if (!ret) {
> +		dev_err(hw->dev,
> +			"mjpeg timeout: status %x from %x pc %x lvl %x rp %x wp %x bitcnt %x\n",
> +			read_dos_reg(hw, MJPEG_DEC_STATUS),
> +			read_dos_reg(hw, MJPEG_FROM_AMRISC),
> +			read_dos_reg(hw, MPC_E),
> +			read_dos_reg(hw, VLD_MEM_VIFIFO_LEVEL),
> +			read_dos_reg(hw, VLD_MEM_VIFIFO_RP),
> +			read_dos_reg(hw, VLD_MEM_VIFIFO_WP),
> +			read_dos_reg(hw, VIFF_BIT_CNT));
> +		aml_stop_vdec_hw(hw);
> +		mjpeg_canvas_free(hw, m);
> +		return -ETIMEDOUT;
> +	}

[Severity: High]
If wait_event_timeout() expires just as the VDEC interrupt fires and
mjpeg_threaded_isr() runs on another CPU, can the timeout path race with
mjpeg_job_done()?

On timeout, aml_mjpeg_dec_run() does not mask VDEC_ASSIST_MBOX1_MASK, call
synchronize_irq(hw->dec_irq), clear hw->curr_ctx, or set
ctx->job_finished = true before stopping the hardware and freeing the
canvases.

As a result:

1. Both mjpeg_threaded_isr() -> mjpeg_job_done() and the timeout path call
mjpeg_canvas_free(hw, m) without locking:

    for (i = 0; i < ARRAY_SIZE(m->canvas); i++) {
        if (m->canvas[i] >= 0)
            meson_canvas_free(hw->canvas, m->canvas[i]);
        m->canvas[i] = -1;
    }

Because m->canvas[i] is only set to -1 after meson_canvas_free() returns,
both threads can free the same canvas indices concurrently.

2. When aml_mjpeg_dec_run() returns -ETIMEDOUT to m2mops_vdec_device_run() in
drivers/media/platform/amlogic/vdec/aml_vdec.c:

    if (ret < 0 && !ctx->job_finished)
        goto err_cancel_job;
    ...
err_cancel_job:
    if (src_req)
        v4l2_ctrl_request_complete(src_req, &ctx->ctrl_handler);
    v4l2_m2m_buf_done_and_job_finish(dev->m2m_dev_dec, ctx->m2m_ctx,
                                     VB2_BUF_STATE_ERROR);

m2mops_vdec_device_run() checks !ctx->job_finished without synchronization
against mjpeg_threaded_isr(), so both mjpeg_job_done() and err_cancel_job can
call v4l2_ctrl_request_complete() and v4l2_m2m_buf_done_and_job_finish() for
the same job.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261025192507.210058-1-christian.koever-draxl@student.uibk.ac.at?part=6

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

* Re: [PATCH 08/11] media: amlogic: vdec: add VP9 decoding on the HEVC core
  2026-10-25 19:25 ` [PATCH 08/11] media: amlogic: vdec: add VP9 " christian.koever-draxl
@ 2026-10-25 20:23   ` sashiko-bot
  0 siblings, 0 replies; 20+ messages in thread
From: sashiko-bot @ 2026-10-25 20:23 UTC (permalink / raw)
  To: christian.koever-draxl; +Cc: devicetree, robh, conor+dt, media-ci

Thank you for your contribution! Sashiko AI review found 11 potential issue(s) to consider:
- [Critical] media: amlogic: vdec: missing CAPTURE buffer bounds checks in vp9_copy_out()
- [Critical] media: amlogic: vdec: missing core stop and IRQ sync in aml_vp9_exit()
- [High] media: amlogic: vdec: broken alt-ref and show_existing_frame in vp9_copy_out()
- [High] media: amlogic: vdec: intra-only frames reset refs in aml_vp9_config_picture()
- [High] media: amlogic: vdec: vp9_alloc_slots() frees active reference slots on resize
- [High] media: amlogic: vdec: workspace overflow for height > 2304 in vp9.c
- [High] media: amlogic: vdec: aml_vp9_feed() omits AMLV header from FIFO buffer size
- [High] media: amlogic: vdec: DMA buffer leak in aml_vp9_exit() after timeout or error
- [High] media: amlogic: vdec: premature job completion in vp9_finish() races with run()
- [Medium] media: amlogic: vdec: aml_vp9_feed() frees active FIFO before halting AMRISC
- [Low] media: amlogic: vdec: dead IVF harness and file I/O in aml_vp9_save_nv12()
--

commit 068064c3014d8dcbf3ec35bb56cb4ba67e04474c
Author: Christian Stefan Kövér-Draxl <ckoever@outlook.de>

media: amlogic: vdec: add VP9 decoding on the HEVC core

This commit adds stateless V4L2 VP9 decoding support for profile 0 and
profile 2 on the Amlogic S4 HEVC core. It manages the hardware workspace,
reference frame slots, and frame context probability buffers to drive the
VP9 microcode.

> diff --git a/drivers/media/platform/amlogic/vdec/vp9.c b/drivers/media/platform/amlogic/vdec/vp9.c
> new file mode 100644
> index 0000000000000..b67d503b93511
> --- /dev/null
> +++ b/drivers/media/platform/amlogic/vdec/vp9.c

[ ... ]

> +int aml_vp9_feed(struct aml_vdec_hw *hw, const void *data, size_t size)
> +{
> +	size_t want = ALIGN(size + VP9_PADDING_SIZE, SZ_64K);
> +	u32 end;
> +
> +	if (!data || !size)
> +		return -EINVAL;
> +
> +	if (vp9_ws.stream_vaddr && vp9_ws.stream_size < want) {
> +		dma_free_coherent(hw->dev, vp9_ws.stream_size,
> +				  vp9_ws.stream_vaddr, vp9_ws.stream_paddr);
> +		vp9_ws.stream_vaddr = NULL;
> +	}

[Severity: Medium]
In aml_vp9_dec_run(), aml_vp9_feed() is called before HEVC_MPSR and
HEVC_CPSR are cleared and before DOS_SW_RESET3 is pulsed. At the end of a
frame, HEVC_STREAM_CONTROL bit 0 (stream fetch enable) and HEVC_MPSR
remain set.

If a subsequent frame is larger and triggers this reallocation in
aml_vp9_feed(), can dma_free_coherent() free vp9_ws.stream_vaddr while
stream fetching is still enabled in HEVC_STREAM_CONTROL and the hardware
FIFO registers still point to the old physical address?

> +	if (!vp9_ws.stream_vaddr) {
> +		vp9_ws.stream_vaddr = dma_alloc_coherent(hw->dev, want,
> +							 &vp9_ws.stream_paddr,
> +							 GFP_KERNEL);
> +		if (!vp9_ws.stream_vaddr)
> +			return -ENOMEM;
> +		vp9_ws.stream_size = want;
> +	}

[ ... ]

> +	u32 psize = size + VP9_AMLV_HEADER_SIZE;
> +	u8 *h = vp9_ws.stream_vaddr;
> +
> +	memset(h, 0, VP9_AMLV_HEADER_SIZE);
> +	h[0] = psize >> 24;
> +	h[1] = psize >> 16;
> +	h[2] = psize >> 8;
> +	h[3] = psize;
> +	h[4] = h[0] ^ 0xff;
> +	h[5] = h[1] ^ 0xff;
> +	h[6] = h[2] ^ 0xff;
> +	h[7] = h[3] ^ 0xff;
> +	h[11] = 1;
> +	h[12] = 'A';
> +	h[13] = 'M';
> +	h[14] = 'L';
> +	h[15] = 'V';
> +	memcpy(vp9_ws.stream_vaddr + VP9_AMLV_HEADER_SIZE, data, size);
> +	size += VP9_AMLV_HEADER_SIZE;
> +	vp9_ws.payload = size;
> +
> +	/*
> +	 * The end pointer bounds the FIFO, not the payload - ending it at the
> +	 * payload puts the write pointer outside the ring and the microcode
> +	 * waits for data that can never arrive.  This is the bug that cost the
> +	 * AV1 path a long time; keep the whole buffer in the ring.
> +	 */
> +	end = vp9_ws.stream_paddr + vp9_ws.stream_size - 1;
> +
> +	wr(hw, HEVC_STREAM_START_ADDR, vp9_ws.stream_paddr);
> +	wr(hw, HEVC_STREAM_END_ADDR, end);
> +	wr(hw, HEVC_STREAM_RD_PTR, vp9_ws.stream_paddr);
> +	wr(hw, HEVC_STREAM_WR_PTR,
> +	   vp9_ws.stream_paddr + vp9_ws.payload + VP9_PADDING_SIZE);

[Severity: High]
At the top of aml_vp9_feed(), the allocation size is calculated without
VP9_AMLV_HEADER_SIZE:

    size_t want = ALIGN(size + VP9_PADDING_SIZE, SZ_64K);

After prepending the 16-byte AMLV header, size is incremented by
VP9_AMLV_HEADER_SIZE and stored in vp9_ws.payload, and HEVC_STREAM_WR_PTR
is set to vp9_ws.stream_paddr + vp9_ws.payload + VP9_PADDING_SIZE.

When the original size + VP9_PADDING_SIZE falls within 16 bytes of a
64 KiB boundary (for example, size = 49152 so want = 65536), doesn't
vp9_ws.payload + VP9_PADDING_SIZE reach up to 65552, placing
HEVC_STREAM_WR_PTR past HEVC_STREAM_END_ADDR and up to 16 bytes outside
the allocated vp9_ws.stream_vaddr DMA buffer?

[ ... ]

> +static int vp9_alloc_slots(struct aml_vdec_hw *hw, u32 width, u32 height)
> +{
> +	size_t body = ALIGN(vp9_comp_body_size(width, height), PAGE_SIZE);
> +	size_t hdr = vp9_hdr_alloc_size(width, height);
> +	size_t mv = vp9_mv_buf_size(width, height);
> +	size_t dw = vp9_dw_target_y ? 0 : ALIGN(width * height * 3 / 2, SZ_64K);
> +	int i;
> +
> +	if (vp9_slots[0].body_v && body == vp9_slot_body_size &&
> +	    hdr == vp9_slot_hdr_size && mv == vp9_slot_mv_size &&
> +	    dw == vp9_slot_dw_size)
> +		return 0;
> +
> +	vp9_free_slots(hw);

[Severity: High]
VP9 allows inter frames to change resolution and scale from prior reference
frames, and vp9_config_mc_buffer() sets up reference scaling using each
reference slot's ref->width and ref->height:

    for (i = 0; i < VP9_REFS_PER_FRAME; i++) {
        struct vp9_slot *ref = &vp9_slots[vp9_frame_slot[i]];
        u32 rw = ref->width ? ref->width : width;
        u32 rh = ref->height ? ref->height : height;

However, aml_vp9_config_picture() calls vp9_alloc_slots() before calling
vp9_config_pic_list() and vp9_config_mc_buffer(). When an inter frame
changes dimensions such that body, hdr, mv, or dw changes size, won't
vp9_alloc_slots() call vp9_free_slots(hw) and wipe out all 10 vp9_slots[]
buffers along with their stored width and height before the inter frame is
decoded?

[ ... ]

> +int aml_vp9_config_picture(struct aml_vdec_hw *hw)
> +{
> +	const u16 *raw;
> +	u16 pr[VP9_RPM_COUNT];
> +	u32 width, height, lcu_total, uv_size, uv_h;
> +	u32 data32, body, header;
> +	dma_addr_t dw_y, dw_c;
> +	bool inter, intra_only, use_prev_mvs;
> +	int i, j, ret;
> +
> +	if (!vp9_ws.side_vaddr)
> +		return -EINVAL;
> +
> +	raw = vp9_ws.side_vaddr + (vp9_ws.rpm - vp9_ws.side_paddr);
> +	for (i = 0; i < VP9_RPM_COUNT; i += 4)
> +		for (j = 0; j < 4; j++)
> +			pr[i + j] = raw[i + 3 - j];
> +
> +	width = pr[VP9_RPM_WIDTH];
> +	height = pr[VP9_RPM_HEIGHT];
> +
> +	/*
> +	 * The MMU page table, the compressed body and the picture slots are
> +	 * all sized from these.  If the microcode never actually parsed a
> +	 * header they hold whatever was left in the RPM, and building a page
> +	 * table from that points the hardware at memory it does not own -
> +	 * which resets the board outright with no oops to read afterwards.
> +	 */
> +	if (width < VP9_MIN_DIM || height < VP9_MIN_DIM ||
> +	    width > VP9_MAX_DIM || height > VP9_MAX_DIM) {
> +		dev_err(hw->dev, "vp9: implausible picture size %ux%u, refusing\n",
> +			width, height);
> +		return -EINVAL;
> +	}

[Severity: High]
The workspace buffers in vp9_alloc() (such as VP9_SEG_MAP_SIZE = 0xd800 and
VP9_MMU_VBH_SIZE = 0x5000), HEVC_DBLK_CFG3, and the advertised format limit
AML_VDEC_4K_MAX_H (2304) are all sized for a maximum resolution of
4096x2304.

Since VP9_MAX_DIM is 4096, an 8-bit frame with height between 2305 and 4096
(for example, 2160x3840 or 4096x2880, which fits within the 0x1200-page
limit in vp9_fill_mmu_map()) passes this check. Can this cause the hardware
to DMA past the end of vp9_ws.seg_map and vp9_ws.mmu_vbh?

[ ... ]

> +	intra_only = (pr[VP9_RPM_SHOW_FRAME] & 1) ? false :
> +		     (pr[VP9_RPM_INTRA_ONLY] & 1);
> +	inter = pr[VP9_RPM_FRAME_TYPE] != VP9_KEY_FRAME && !intra_only;

[ ... ]

> +	/* A key frame resets the reference state; otherwise take a slot no
> +	 * reference names, so decoding cannot destroy a picture still needed.
> +	 */
> +	if (!inter) {
> +		vp9_cur_slot = 0;
> +		for (i = 0; i < VP9_REF_FRAMES; i++)
> +			vp9_ref_slot[i] = 0;
> +	} else {

[Severity: High]
Because inter is false for both keyframes and intra-only frames, checking
!inter here causes an intra-only frame to select slot 0 (overwriting the
existing reference picture in slot 0) and reset all 8 entries of
vp9_ref_slot[] to 0.

Per the VP9 specification, intra-only frames only update the reference
slots indicated by refresh_frame_flags (which vp9_pending_refresh preserves
just above) and must leave the remaining reference slots intact for later
inter frames. Should this check vp9_cur_kf instead of !inter?

[ ... ]

> +void aml_vp9_frame_done(struct aml_vdec_hw *hw)
> +{
> +	int i;
> +
> +	for (i = 0; i < VP9_REF_FRAMES; i++)
> +		if (vp9_pending_refresh & (1 << i))
> +			vp9_ref_slot[i] = vp9_cur_slot;
> +
> +	if (vp9_cur_show)
> +		vp9_out_slot = vp9_cur_slot;
> +	vp9_prev_slot = vp9_cur_slot;
> +	/* The next frame's adaptation needs this frame's type. */
> +}

[Severity: High]
In V4L2 stateless VP9 decoding (V4L2_PIX_FMT_VP9_FRAME), userspace submits
a CAPTURE buffer for every coded frame, including hidden alt-ref frames
where show_frame == 0, and later displays that decoded CAPTURE buffer itself
when show_existing_frame == 1 occurs.

When a hidden alt-ref frame finishes decoding (vp9_cur_show == false):
1. aml_vp9_frame_done() leaves vp9_out_slot at -1 (unlike
   aml_av1_frame_done() in av1.c, which checks av1_cur_show || av1_stateless).
2. vp9_copy_out() sees slot < 0 and returns early without copying the
   decoded frame (when vp9_dw_target_y == 0) and without setting
   vp9->have_picture = true.
3. vp9_finish() sees !vp9->have_picture and sets the CAPTURE buffer plane
   payloads to 0, discarding the decoded alt-ref frame.

Conversely, if a frame with show_existing_frame == 1 is submitted while
vp9_dw_target_y is non-zero, vp9_alloc_slots() skipped allocating per-slot
dw_v buffers (dw = 0), aml_vp9_config_picture() sets
vp9_out_slot = vp9_ref_slot[idx] and returns -EAGAIN without running the
decoder, and vp9_copy_out() takes the if (vp9_dw_target_y) branch and marks
the new CAPTURE buffer as having a full payload even though nothing was
written to it.

How are alt-ref frames and show_existing_frame intended to work with
V4L2 stateless CAPTURE buffers here?

[ ... ]

> +int aml_vp9_save_nv12(struct aml_vdec_hw *hw, const char *path, bool append)
> +{
> +	const u8 *y;
> +	size_t i, nz = 0, len;
> +	struct file *f;
> +	loff_t pos = 0;
> +	int slot = vp9_out_slot;
> +
> +	if (slot < 0)
> +		return -EAGAIN;
> +	vp9_out_slot = -1;
> +	y = vp9_slots[slot].dw_v;
> +	if (!y)
> +		return -ENODEV;
> +	dma_sync_single_for_cpu(hw->dev, vp9_slots[slot].dw_p,
> +				vp9_slot_dw_size, DMA_FROM_DEVICE);
> +
> +	len = (size_t)vp9_slots[slot].width * vp9_slots[slot].height * 3 / 2;
> +	for (i = 0; i < len; i++)
> +		if (y[i])
> +			nz++;
> +	dev_info(hw->dev, "vp9: nv12 %zu/%zu non-zero, Y[0..15] %*ph\n",
> +		 nz, len, 16, y);
> +
> +	f = filp_open(path, O_WRONLY | O_CREAT |
> +		      (append ? O_APPEND : O_TRUNC), 0644);
> +	if (IS_ERR(f))
> +		return PTR_ERR(f);
> +	if (append)
> +		pos = i_size_read(file_inode(f));
> +	kernel_write(f, y, len, &pos);
> +	filp_close(f, NULL);
> +	dev_info(hw->dev, "vp9: wrote %zu bytes of NV12 to %s\n", len, path);
> +	return 0;
> +}
> +
> +/* ------------------------------------------------------------------------
> + * IVF test harness
> + *
> + * The decoder is frame based and start-code free, so it wants exactly one
> + * frame in front of the shifter at a time.  An IVF file is the easiest way to
> + * carry a sequence of them: a 32-byte file header, then a 12-byte length and
> + * timestamp in front of each frame.
> + * ------------------------------------------------------------------------
> + */
> +
> +static u8 *vp9_ivf;
> +static size_t vp9_ivf_size;
> +static size_t vp9_ivf_pos;
> +
> +void aml_vp9_ivf_release(void)
> +{
> +	kfree(vp9_ivf);
> +	vp9_ivf = NULL;
> +	vp9_ivf_size = 0;
> +	vp9_ivf_pos = 0;
> +}
> +
> +int aml_vp9_ivf_load(struct aml_vdec_hw *hw, const char *name)
> +{

[Severity: Low]
aml_vp9_save_nv12() performs direct file I/O from kernel space using
filp_open() and kernel_write(), and along with the IVF test harness
(aml_vp9_ivf_load(), aml_vp9_ivf_feed_next(), aml_vp9_ivf_release()) and
debug helpers (aml_vp9_feed_file(), aml_vp9_dump_state(),
aml_vp9_dump_params(), aml_vp9_dump_output(), aml_vp9_reset_core(),
aml_vp9_prot_reinit()) has no callers in the tree.

Additionally, the comments at the top of vp9.c and above aml_vp9_feed() say
there is no AMLV reframing even though aml_vp9_feed() prepends a 16-byte
AMLV header, and vp9_prob.h has a leftover kernel-doc comment for a removed
adaptation function. Could this unused test/debug code be removed and the
comments updated?

[ ... ]

> +static void vp9_set_dw_target(struct aml_vdec_ctx *ctx)
> +{
> +	struct v4l2_pix_format_mplane *pf = &ctx->pix_fmt[AML_FMT_DST];
> +	struct vb2_v4l2_buffer *dst = v4l2_m2m_next_dst_buf(ctx->m2m_ctx);
> +	struct vb2_buffer *vb;
> +
> +	vp9_dw_target_y = 0;
> +	vp9_dw_target_c = 0;
> +	vp9_dw_target_size = 0;
> +	if (!dst)
> +		return;
> +	vb = &dst->vb2_buf;
> +	if (pf->plane_fmt[0].bytesperline != pf->width)
> +		return;
> +
> +	vp9_dw_target_y = vb2_dma_contig_plane_dma_addr(vb, 0);
> +	if (!vp9_dw_target_y)
> +		return;
> +	if (vb->num_planes >= 2) {
> +		vp9_dw_target_c = vb2_dma_contig_plane_dma_addr(vb, 1);
> +		vp9_dw_target_size = vb2_plane_size(vb, 0) +
> +				     vb2_plane_size(vb, 1);
> +		if (!vp9_dw_target_c) {
> +			vp9_dw_target_y = 0;
> +			return;
> +		}
> +	} else {
> +		vp9_dw_target_c = vp9_dw_target_y +
> +				  (dma_addr_t)pf->width * pf->height;
> +		vp9_dw_target_size = vb2_plane_size(vb, 0);
> +	}
> +}
> +
> +static void vp9_copy_out(struct aml_vp9_ctx *vp9, struct vb2_v4l2_buffer *dst)
> +{
> +	struct v4l2_pix_format_mplane *pf =
> +		&vp9->v4l2_ctx->pix_fmt[AML_FMT_DST];
> +	struct vb2_buffer *vb = &dst->vb2_buf;
> +	u32 dst_stride = pf->plane_fmt[0].bytesperline ? : vp9_last_width;
> +	u32 dst_h = pf->height ? : vp9_last_height;
> +	int slot = vp9_out_slot;
> +	const u8 *src;
> +	u8 *y, *c;
> +	u32 row, w, h;
> +
> +	if (slot < 0)
> +		return;
> +	vp9_out_slot = -1;
> +
> +	if (vp9_dw_target_y) {
> +		if (vb->num_planes >= 2) {
> +			vb2_set_plane_payload(vb, 0, dst_stride * dst_h);
> +			vb2_set_plane_payload(vb, 1, dst_stride * dst_h / 2);
> +		} else {
> +			vb2_set_plane_payload(vb, 0,
> +					      dst_stride * dst_h * 3 / 2);
> +		}
> +		vp9->have_picture = true;
> +		return;
> +	}
> +
> +	src = vp9_slots[slot].dw_v;
> +	if (!src)
> +		return;
> +	dma_sync_single_for_cpu(vp9->hw->dev, vp9_slots[slot].dw_p,
> +				vp9_slot_dw_size, DMA_FROM_DEVICE);
> +
> +	w = vp9_slots[slot].width;
> +	h = vp9_slots[slot].height;
> +	y = vb2_plane_vaddr(vb, 0);
> +	if (!y)
> +		return;
> +	c = vb->num_planes >= 2 ? vb2_plane_vaddr(vb, 1) :
> +				  y + (size_t)dst_stride * dst_h;
> +	if (!c)
> +		return;
> +
> +	for (row = 0; row < h; row++)
> +		memcpy(y + (size_t)row * dst_stride, src + (size_t)row * w, w);
> +	for (row = 0; row < h / 2; row++)
> +		memcpy(c + (size_t)row * dst_stride,
> +		       src + (size_t)(h + row) * w, w);

[Severity: Critical]
Can this corrupt CAPTURE buffer memory when the coded frame dimensions in
the VP9 bitstream do not match the negotiated CAPTURE format?

1. When vp9_dw_target_y == 0 (for instance, if bytesperline != pf->width),
   aml_vp9_config_picture() does not check that the parsed frame's width
   and height fit within the CAPTURE buffer. In vp9_copy_out(), the loops
   copy h rows of w bytes into y and h / 2 rows of w bytes into c without
   checking that w <= dst_stride, h <= dst_h, or that the destination
   vb2_buffer planes are large enough, which can overflow the CAPTURE
   planes via memcpy().
2. When vp9_dw_target_y != 0, vp9_set_dw_target() computes vp9_dw_target_c
   from pf->width * pf->height for single-plane NV12 and sums both plane
   sizes into a single vp9_dw_target_size for two-plane NV12M before the
   bitstream dimensions are parsed. In aml_vp9_config_picture(), the
   hardware is configured for 64-byte line alignment (ALIGN(width, 64)),
   but only (size_t)width * height * 3 / 2 > vp9_dw_target_size is checked:

       if (vp9_dw_target_y) {
           if ((size_t)width * height * 3 / 2 > vp9_dw_target_size) {
               ...
               return -EINVAL;
           }
           dw_y = vp9_dw_target_y;
           dw_c = vp9_dw_target_c;
       }

   If ALIGN(width, 64) != pf->width or height > pf->height while
   width * height * 3 / 2 <= vp9_dw_target_size (or if plane 0 of an NV12M
   buffer is smaller than ALIGN(width, 64) * height), won't the double-write
   unit DMA past the end of the CAPTURE buffer planes?

[ ... ]

> +static void vp9_finish(struct aml_vp9_ctx *vp9, enum vb2_buffer_state state)
> +{
> +	struct aml_vdec_ctx *ctx = vp9->v4l2_ctx;
> +	struct vb2_v4l2_buffer *buf;
> +	unsigned int i;
> +
> +	if (!vp9->have_picture) {
> +		buf = v4l2_m2m_next_dst_buf(ctx->m2m_ctx);
> +		if (buf)
> +			for (i = 0; i < buf->vb2_buf.num_planes; i++)
> +				vb2_set_plane_payload(&buf->vb2_buf, i, 0);
> +	}
> +
> +	buf = v4l2_m2m_next_src_buf(ctx->m2m_ctx);
> +	if (buf && buf->vb2_buf.req_obj.req)
> +		v4l2_ctrl_request_complete(buf->vb2_buf.req_obj.req,
> +					   &ctx->ctrl_handler);
> +
> +	vp9_finish_frame_ctx(vp9);
> +
> +	ctx->int_cond = 1;
> +	ctx->job_finished = true;
> +	v4l2_m2m_buf_done_and_job_finish(ctx->dev->m2m_dev_dec, ctx->m2m_ctx,
> +					 state);
> +	wake_up(&ctx->queue);
> +}

[Severity: High]
Does calling v4l2_m2m_buf_done_and_job_finish() here in vp9_finish() race
with aml_vp9_dec_run() and m2mops_vdec_device_run()?

m2mops_vdec_device_run() calls ctx->codec_ops->run(ctx) (aml_vp9_dec_run()),
which blocks in wait_event_timeout(ctx->queue, ctx->int_cond, ...). When
vp9_irq_thread() calls vp9_finish(), v4l2_m2m_buf_done_and_job_finish()
completes src_buf and dst_buf, clears TRANS_RUNNING, and can start the next
m2m job or unblock a concurrent v4l2_m2m_cancel_job() in streamoff/close
before wake_up(&ctx->queue) runs and before aml_vp9_dec_run() returns:

drivers/media/platform/amlogic/vdec/aml_vdec.c:m2mops_vdec_device_run() {
    ...
    ctx->job_finished = false;
    ret = ctx->codec_ops->run(ctx);

    v4l2_m2m_buf_copy_metadata(src_buf, dst_buf);
    ...
}

When aml_vp9_dec_run() wakes up, it writes ctx->int_cond = 0 (which can
clobber the next job's state), m2mops_vdec_device_run() calls
v4l2_m2m_buf_copy_metadata(src_buf, dst_buf) on buffers already returned to
vb2/userspace, and if streamoff/close ran after TRANS_RUNNING was cleared,
ctx and vp9 may already be freed.

> +
> +static irqreturn_t vp9_irq_thread(int irq, void *priv)
> +{
> +	struct aml_vp9_ctx *vp9 = priv;
> +	struct aml_vdec_ctx *ctx = vp9->v4l2_ctx;
> +	struct aml_vdec_hw *hw = vp9->hw;
> +	u32 st = hw->hevc_irq_status & 0xff;
> +	u32 next = VP9_10B_DECODE_SLICE;

[ ... ]

> +	case VP9_NAL_DECODE_DONE:
> +	case HEVC_ACTION_DONE:
> +	case VP9_DECODE_BUFEMPTY:
> +	case VP9_SEARCH_BUFEMPTY:
> +	case VP9_EOS:
> +		vp9_finish(vp9, VB2_BUF_STATE_DONE);
> +		return IRQ_HANDLED;
> +	default:
> +		dev_dbg(hw->dev, "vp9: unhandled status %02x\n", st);
> +		break;
> +	}
> +
> +	regmap_write(hw->map[DOS_BUS], HEVC_ASSIST_MBOX0_CLR_REG, 1);
> +	regmap_write(hw->map[DOS_BUS], HEVC_DEC_STATUS_REG, next);
> +	return IRQ_HANDLED;
> +}

[Severity: High]
In vp9_irq_thread(), next is initialized to VP9_10B_DECODE_SLICE. If the
microcode posts any status code that hits the default branch (such as
VP9_HEAD_SEARCH_DONE, VP9_DECPIC_DATA_ERROR, or VP9_DECODE_OVER_SIZE)
before VP9_HEAD_PARSER_DONE has run aml_vp9_config_picture(), won't writing
next to HEVC_DEC_STATUS_REG command the microcode to decode a slice using
unconfigured or stale DMA addresses?

[ ... ]

> +void aml_vp9_exit(void *priv)
> +{
> +	struct aml_vdec_ctx *ctx = priv;
> +	struct aml_vp9_ctx *vp9 = ctx->codec_priv;
> +
> +	if (!vp9)
> +		return;
> +
> +	vp9->hw->hevc_irq_thread_fn = NULL;
> +	vp9->hw->hevc_priv = NULL;
> +	if (vp9->started) {
> +		regmap_write(vp9->hw->map[DOS_BUS], HEVC_MPSR, 0);
> +		aml_vp9_teardown(vp9->hw);
> +		/*
> +		 * Deliberately not hvdec_disable(): powering the HEVC core
> +		 * down here leaves it in a state the next context's bring-up
> +		 * does not recover from, and every frame of the *second*
> +		 * decode session on this core then times out - whichever
> +		 * codec it is.  The domain is left up until the module is
> +		 * unloaded.
> +		 */
> +	}
> +	kfree(vp9);
> +	ctx->codec_priv = NULL;
> +}

[Severity: Critical]
Can aml_vp9_exit() race with an in-flight threaded interrupt or a second
context opening the decoder?

1. hevc_irq_thread() in aml_vdec_hw.c reads hw->hevc_irq_thread_fn and
   hw->hevc_priv without synchronization:

       if (hw->hevc_irq_thread_fn)
           return hw->hevc_irq_thread_fn(irq, hw->hevc_priv);

   Because aml_vp9_exit() clears hevc_irq_thread_fn and hevc_priv before
   halting the hardware and never calls synchronize_irq(vp9->hw->hevc_irq),
   an in-flight threaded IRQ can either call vp9_irq_thread(irq, NULL) or
   continue running vp9_irq_thread() while aml_vp9_teardown() and
   kfree(vp9) free vp9_ws, vp9_slots, and vp9.
2. Clearing vp9->hw->hevc_priv = NULL before aml_vp9_teardown(vp9->hw)
   allows another thread in aml_vp9_init() to succeed at
   cmpxchg(&hw->hevc_priv, NULL, vp9) and access the global vp9_ws and
   vp9_slots while aml_vp9_teardown() is freeing them.

> +
> +int aml_vp9_dec_run(void *priv)
> +{

[ ... ]

> +	if (!vp9->started) {
> +		ret = aml_vp9_bringup(hw, ctx->pix_fmt[AML_FMT_SRC].width,
> +				      ctx->pix_fmt[AML_FMT_SRC].height);
> +		if (ret) {
> +			hvdec_disable(hw);
> +			return ret;
> +		}
> +	}
> +
> +	vp9_prepare_frame_ctx(vp9, dec, hdr);
> +	vp9_set_dw_target(ctx);
> +
> +	ret = aml_vp9_feed(hw, vb2_plane_vaddr(vb, 0),
> +			   vb2_get_plane_payload(vb, 0));
> +	if (ret)
> +		return ret;

[ ... ]

> +	if (!ret) {
> +		int k;
> +
> +		dev_err(hw->dev, "vp9: decode timeout, status %02x pc %08x\n",
> +			hw->hevc_irq_status, read_dos_reg(hw, HEVC_MPC_E));
> +		dev_err(hw->dev,
> +			"vp9: dec_status %08x adapt %08x parser_ctl %08x shift %08x\n",
> +			read_dos_reg(hw, HEVC_DEC_STATUS_REG),
> +			read_dos_reg(hw, VP9_ADAPT_PROB_REG),
> +			read_dos_reg(hw, REG_ALIGN(0x3050)),
> +			read_dos_reg(hw, HEVC_SHIFT_BYTE_COUNT));
> +		for (k = 0; k < 16; k++)
> +			dev_err(hw->dev, "vp9: scratch[%x] %08x\n", k,
> +				read_dos_reg(hw, REG_ALIGN(0x30c0 + k)));
> +		/*
> +		 * After a timeout the core decodes every later frame to garbage
> +		 * - in this session and the next ones - until its memories are
> +		 * cycled.  Have the next frame cycle them, reload the microcode
> +		 * and bring the decoder up again; the stream picks up at its
> +		 * next key frame.
> +		 */
> +		hw->hevc_dirty = true;
> +		vp9->ucode_loaded = false;
> +		vp9->started = false;
> +		ret = -ETIMEDOUT;
> +	} else {
> +		ret = 0;
> +	}

[Severity: High]
There seem to be two issues when aml_vp9_dec_run() fails or times out:

1. Does this leak the DMA buffers in vp9_ws and vp9_slots[]?
   In aml_vp9_exit(), aml_vp9_teardown(vp9->hw) is only called if
   vp9->started is true. However, on timeout here, vp9->started is set to
   false without calling aml_vp9_teardown(). Similarly, if aml_vp9_feed()
   fails on the first frame after aml_vp9_bringup() has allocated vp9_ws,
   aml_vp9_dec_run() returns early before vp9->started is set to true. If
   the user then closes the context, aml_vp9_exit() skips
   aml_vp9_teardown(), leaking vp9_ws.vaddr, vp9_ws.side_vaddr,
   vp9_ws.stream_vaddr, and all 10 vp9_slots[] allocations (as well as
   leaving hvdec_enable(hw) unbalanced on every frame).
2. On timeout, aml_vp9_dec_run() returns -ETIMEDOUT without stopping the
   AMRISC (HEVC_MPSR / HEVC_CPSR), disabling stream fetch
   (HEVC_STREAM_CONTROL), masking HEVC_ASSIST_MBOX0_MASK, or synchronizing
   with hw->hevc_irq. When m2mops_vdec_device_run() sees the error return,
   it immediately completes the CAPTURE buffer with VB2_BUF_STATE_ERROR and
   finishes the job. Can the still-running hardware continue DMA-writing
   into vp9_dw_target_y after the CAPTURE buffer is returned to userspace,
   or raise a late interrupt that calls vp9_finish() a second time?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261025192507.210058-1-christian.koever-draxl@student.uibk.ac.at?part=8

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

* Re: [PATCH 03/11] media: amlogic: vdec: fix the microcode loading and prepare the HEVC core
  2026-10-25 19:24 ` [PATCH 03/11] media: amlogic: vdec: fix the microcode loading and prepare the HEVC core christian.koever-draxl
@ 2026-10-25 20:25   ` sashiko-bot
  0 siblings, 0 replies; 20+ messages in thread
From: sashiko-bot @ 2026-10-25 20:25 UTC (permalink / raw)
  To: christian.koever-draxl; +Cc: devicetree, robh, conor+dt, media-ci

Thank you for your contribution! Sashiko AI review found 7 potential issue(s) to consider:
- [High] media: amlogic: vdec: broken src_req error handling in m2mops_vdec_device_run()
- [High] media: amlogic: vdec: metadata copy and request completion after job finish
- [High] media: amlogic: vdec: unbalanced hvdec_enable() and stale hevc_fw on poweroff
- [High] media: amlogic: vdec: premature IRQ registration and race in hevc_irq_thread()
- [Medium] media: amlogic: vdec: V4L2_PIX_FMT_YUV420 exposed to non-MJPEG codecs
- [Medium] media: amlogic: vdec: DMA direction mismatch in aml_vdec_dst_cpu_written()
- [Low] media: amlogic: vdec: undocumented hevcf_rate module parameter
--

commit a516ebbbab5a6b016789fa31a24a5ab176743824
Author: Christian Stefan Kövér-Draxl <ckoever@outlook.de>

media: amlogic: vdec: fix the microcode loading and prepare the HEVC core

This commit fixes TEE microcode loading by preserving the 256-byte signature
header and issuing SYS_BOOT_COMPLETE before the first load. It also adds
power domain, clock, reset, IRQ, and request handling preparation for the
HEVC core.

> diff --git a/drivers/media/platform/amlogic/vdec/aml_vdec.c b/drivers/media/platform/amlogic/vdec/aml_vdec.c
> index d4dcd0180d2d0..2a5ff842fd39c 100644
> --- a/drivers/media/platform/amlogic/vdec/aml_vdec.c
> +++ b/drivers/media/platform/amlogic/vdec/aml_vdec.c

[ ... ]

> @@ -130,23 +129,46 @@ static void m2mops_vdec_device_run(void *m2m_priv)
>  	/* incase of bus hang in stop_streaming */
>  	ctx->dos_clk_en = 1;
>  
> -	if (ctx->curr_dec_type == CODEC_TYPE_H264)
> +	if (ctx->curr_dec_type == CODEC_TYPE_H264 ||
> +	    ctx->curr_dec_type == CODEC_TYPE_MPEG2 ||
> +	    ctx->curr_dec_type == CODEC_TYPE_MJPEG ||
> +	    ctx->curr_dec_type == CODEC_TYPE_MPEG4)
>  		aml_vdec_reset_core(dev->dec_hw);
>  
>  	if (load_firmware(dev->dec_hw, ctx->curr_dec_type) < 0)
>  		goto err_cancel_job;
>  
> +	ctx->job_finished = false;
>  	ret = ctx->codec_ops->run(ctx);
>  
>  	v4l2_m2m_buf_copy_metadata(src_buf, dst_buf);
> -	if (src_req)
> +	/*
> +	 * AV1 completes the request itself, in its interrupt handler, before
> +	 * the buffer is done - by the time run() returns here the job has
> +	 * already finished and the request with it.
> +	 */
> +	if (src_req && ctx->curr_dec_type != CODEC_TYPE_AV1 &&
> +	    ctx->curr_dec_type != CODEC_TYPE_VP9 &&
> +	    ctx->curr_dec_type != CODEC_TYPE_HEVC &&
> +	    ctx->curr_dec_type != CODEC_TYPE_MJPEG)
>  		v4l2_ctrl_request_complete(src_req, &ctx->ctrl_handler);

[Severity: High]
Isn't calling v4l2_m2m_buf_copy_metadata(src_buf, dst_buf) and
v4l2_ctrl_request_complete(src_req, &ctx->ctrl_handler) after
ctx->codec_ops->run(ctx) too late in the buffer lifecycle?

Every codec's ->run() callback waits synchronously for the decode to finish
and calls v4l2_m2m_buf_done_and_job_finish() (either in its threaded ISR,
such as h264_threaded_isr_func(), or directly in aml_mpeg12_dec_run() when
stashing the first field of an interlaced pair) before returning to
m2mops_vdec_device_run().

Because v4l2_m2m_buf_copy_metadata() runs after
v4l2_m2m_buf_done_and_job_finish():

1. For non-held capture buffers, v4l2_m2m_buf_done_and_job_finish() marks
   dst_buf done via vb2_buffer_done() and wakes VIDIOC_DQBUF before
   dst_buf->vb2_buf.timestamp, dst_buf->field, dst_buf->flags, and
   dst_buf->vb2_buf.copied_timestamp are copied from src_buf, mutating
   dst_buf after it has already been returned to videobuf2/userspace.

2. With VB2_V4L2_FL_SUPPORTS_M2M_HOLD_CAPTURE_BUF enabled in
   aml_vdec_queue_init(), the call chain:

   v4l2_m2m_buf_done_and_job_finish()
     -> v4l2_m2m_schedule_next_job()
       -> __v4l2_m2m_try_queue()

   checks dst->vb2_buf.copied_timestamp and compares dst->vb2_buf.timestamp
   against src->vb2_buf.timestamp before v4l2_m2m_buf_copy_metadata() has
   updated dst_buf. When dst_buf is reused from a previous frame (so
   copied_timestamp is already 1 with the old timestamp), __v4l2_m2m_try_queue()
   sees a timestamp mismatch against the queued second field and prematurely
   finishes the held capture buffer. When dst_buf is used for the first time
   (copied_timestamp is 0), __v4l2_m2m_try_queue() fails to flush the held
   buffer on a timestamp mismatch. In addition, aml_mpeg12_dec_run() (added
   in the later commit "media: amlogic: vdec: add MPEG-1/2 decoding") reads
   dst->vb2_buf.timestamp during ->run() while it still holds the stale
   timestamp.

3. For CODEC_TYPE_H264, CODEC_TYPE_MPEG2, and CODEC_TYPE_MPEG4,
   v4l2_ctrl_request_complete() runs after v4l2_m2m_buf_done_and_job_finish()
   has already called vb2_buffer_done(src_buf) and unbound
   src_buf->vb2_buf.req_obj. If the request had no control objects bound,
   unbinding src_buf transitions req->state to MEDIA_REQUEST_STATE_COMPLETE,
   so v4l2_ctrl_request_complete() hits WARN_ON() in
   media_request_object_bind() or accesses a freed media_request if userspace
   has already dequeued the buffer and closed the request fd.

4. Also, in this commit, h264_threaded_isr_func() in
   drivers/media/platform/amlogic/vdec/h264.c calls
   v4l2_m2m_buf_done_and_job_finish() without setting ctx->job_finished = true
   (which is only added in the later commit "media: amlogic: vdec: h264:
   decode Main and High profile bit-exact").

Should v4l2_m2m_buf_copy_metadata() be called before ctx->codec_ops->run(ctx),
and v4l2_ctrl_request_complete() be called before
v4l2_m2m_buf_done_and_job_finish()?

> -	if (ret < 0)
> +	/*
> +	 * Only cancel a job that is still running: a codec that finished it in
> +	 * its own interrupt handler must not finish it twice.
> +	 */
> +	if (ret < 0 && !ctx->job_finished)
>  		goto err_cancel_job;
>  
>  	return;
>  
>  err_cancel_job:
> +	/*
> +	 * Whatever went wrong, the request must not be left incomplete or the
> +	 * client waits for it forever.  AV1 completes it in its own done path
> +	 * on success, so this only covers the paths that never got there.
> +	 */
> +	if (src_req)
> +		v4l2_ctrl_request_complete(src_req, &ctx->ctrl_handler);
>  	v4l2_m2m_buf_done_and_job_finish(dev->m2m_dev_dec, ctx->m2m_ctx,
>  					 VB2_BUF_STATE_ERROR);
>  }

[Severity: High]
Can src_req be used uninitialized or completed twice when jumping to
err_cancel_job in m2mops_vdec_device_run()?

First, earlier in m2mops_vdec_device_run():

	struct media_request *src_req;
	...
	if (WARN_ON_ONCE(!ctx->codec_ops->run))
		goto err_cancel_job;

	src_req = src_buf->vb2_buf.req_obj.req;

If !ctx->codec_ops->run triggers, goto err_cancel_job jumps past the
assignment of src_req and evaluates if (src_req) on an uninitialized pointer.

Second, when ctx->codec_ops->run(ctx) returns a negative error with
!ctx->job_finished for CODEC_TYPE_H264, CODEC_TYPE_MPEG2, or CODEC_TYPE_MPEG4
(for example, when control lookup, input preparation, or wait_event_timeout()
fails in aml_h264_dec_run()), m2mops_vdec_device_run() calls
v4l2_ctrl_request_complete(src_req, &ctx->ctrl_handler) right after ->run(),
and then takes:

	if (ret < 0 && !ctx->job_finished)
		goto err_cancel_job;

Under err_cancel_job, it calls v4l2_ctrl_request_complete(src_req,
&ctx->ctrl_handler) a second time on the same request, which triggers
WARN_ON(!hdl->request_is_queued) and a duplicate
media_request_object_complete(obj) in v4l2_ctrl_request_complete().

> diff --git a/drivers/media/platform/amlogic/vdec/aml_vdec_hw.c b/drivers/media/platform/amlogic/vdec/aml_vdec_hw.c
> index 29a195eb68721..935018ea96c62 100644
> --- a/drivers/media/platform/amlogic/vdec/aml_vdec_hw.c
> +++ b/drivers/media/platform/amlogic/vdec/aml_vdec_hw.c

[ ... ]

> @@ -249,6 +263,14 @@ static void dos_local_config(struct aml_vdec_hw *hw, bool is_on, int id)
>  	dev_dbg(hw->dev, "%s end, id %d, is_on %d\n", __func__, id, is_on);
>  }
>  
> +/*
> + * The core clock rates come from assigned-clock-rates in the device tree.
> + * hevcf_rate overrides the HEVC core's for testing.
> + */
> +static ulong hevcf_rate;
> +module_param(hevcf_rate, ulong, 0444);
> +MODULE_PARM_DESC(hevcf_rate, "Override the HEVC core clock in Hz (0 = device tree)");

[Severity: Low]
Since hevcf_rate is exposed as a module and kernel command-line parameter via
module_param(), should it be documented in
Documentation/admin-guide/kernel-parameters.txt?

[ ... ]

> @@ -364,6 +386,91 @@ int vdec_enable(struct aml_vdec_hw *hw)
>  	return 0;
>  }
>  
> +/*
> + * The HEVC core is a second, independent power domain with its own clock
> + * ("hevcf") and its own reset in DOS_SW_RESET3.  Nothing in this driver decodes
> + * on it yet; it is powered only so secure world has somewhere to put the
> + * microcode when the AV1/HEVC load is probed.
> + */
> +int hvdec_enable(struct aml_vdec_hw *hw)
> +{
> +	vdec_poweron(hw, HVDEC);
> +
> +	return 0;
> +}
> +
> +int hvdec_disable(struct aml_vdec_hw *hw)
> +{
> +	vdec_poweroff(hw, HVDEC);
> +
> +	return 0;
> +}
> +
> +/*
> + * VP9 and AV1 share the HEVC core, and once one of them has run the other
> + * cannot: it loads, parses a header and never completes a picture.  Putting
> + * back every register the other codec changed does not help, reloading the
> + * module does - and what a reload does that nothing else here does is power
> + * the core's internal memories down and up.  hvdec_disable() cannot be used
> + * for that: hvdec_enable() runs for every frame and only counts, so the core
> + * never actually goes off.  Cycle the memories and reset the core directly,
> + * on a switch only, leaving the power domain, the clock and secure world
> + * alone.
> + */
> +int hvdec_switch_codec(struct aml_vdec_hw *hw, int codec)
> +{
> +	if (hw->hevc_dirty || (hw->hevc_codec && hw->hevc_codec != codec)) {
> +		dev_info(hw->dev, "HEVC core: %s, cycling core memories\n",
> +			 hw->hevc_dirty ? "left dirty" : "codec switch");
> +		mutex_lock(&hw->pm_mutex);
> +		dos_local_config(hw, 0, HVDEC);
> +		dos_local_config(hw, 1, HVDEC);
> +		mutex_unlock(&hw->pm_mutex);
> +		hw->hevc_fw = 0;
> +	}
> +	hw->hevc_codec = codec;
> +	hw->hevc_dirty = false;
> +	return 0;
> +}
> +
> +/*
> + * Load a microcode into the HEVC core through secure world, unless it is
> + * already there. Every decoder context used to load it again, and a browser
> + * opens a new context for every seek, quality switch or advert: after a few
> + * hundred loads the secure loader answers with TEEC_ERROR_BAD_STATE and
> + * stays there until the next power cycle. The image survives in the core
> + * until hvdec_switch_codec() cycles the memories.
> + */
> +int hevc_load_fw(struct aml_vdec_hw *hw, int codec, u32 format, u32 core,
> +		 const char *name)
> +{
> +	int ret, sm_ret = 0;
> +
> +	/*
> +	 * Halt the AMRISC in any case: the previous context may have left it
> +	 * running, and the decoder start below expects it stopped.
> +	 */
> +	regmap_write(hw->map[DOS_BUS], HEVC_MPSR, 0);
> +	regmap_write(hw->map[DOS_BUS], HEVC_CPSR, 0);
> +
> +	if (hw->hevc_fw == codec)
> +		return 0;

[Severity: High]
What happens to hw->hevc_fw and the HVDEC power domain state across error
paths and driver removal?

1. If HVDEC is powered off via hvdec_disable() -> vdec_poweroff(hw, HVDEC) ->
   pm_vdec_power_domain_power_off(hw, HVDEC) (for example, when bringup fails
   after hevc_load_fw() on the first frame in aml_av1_dec_run() or
   aml_vp9_dec_run()), dos_local_config(hw, 0, HVDEC) powers down
   DOS_MEM_PD_HEVC and pm_runtime_put_sync() powers down the domain, but
   hw->hevc_fw is not cleared. On the next decode attempt for the same codec,
   hvdec_switch_codec() sees hw->hevc_codec == codec and hevc_load_fw() sees
   hw->hevc_fw == codec, so the driver skips reloading the microcode into the
   wiped core.

2. During normal decoding, hvdec_enable() increments
   vdec_domain_data[HVDEC].ref_count on every frame without matching
   hvdec_disable() calls. On driver teardown, aml_vdec_drv_remove() only calls
   vdec_disable() for VDEC, and pm_vdec_power_domain_release() detaches the
   power domains without powering off HVDEC or resetting ref_count. Doesn't
   this leak the clk_prepare_enable() reference on the hevcf clock and the
   runtime PM reference on the hvdec power domain device, as well as leave
   the static vdec_domain_data[HVDEC].ref_count non-zero across driver
   unbind/rebind?

[ ... ]

> @@ -382,6 +489,40 @@ static const struct power_manager_s pm[] = {
>  	},
>  };
>  
> +static irqreturn_t hevc_irq_handler(int irq, void *priv)
> +{
> +	struct aml_vdec_hw *hw = priv;
> +	int n;
> +
> +	hw->hevc_irq_status = read_dos_reg(hw, HEVC_ASSIST_SCRATCH_0);
> +	regmap_write(hw->map[DOS_BUS], HEVC_ASSIST_MBOX0_CLR_REG, 1);
> +
> +	n = atomic_inc_return(&hw->hevc_irq_count);
> +	if (hw->hevc_irq_thread_fn)
> +		return IRQ_WAKE_THREAD;
> +
> +	/*
> +	 * Runaway guard for the probe path, which has no state machine to end
> +	 * a decode.  It must not apply once a real handler is driving the
> +	 * core: a stream produces a few interrupts per picture, so masking the
> +	 * mailbox at a fixed count silently stops decoding part way through.
> +	 */
> +	if (n > 64)
> +		regmap_write(hw->map[DOS_BUS], HEVC_ASSIST_MBOX0_MASK, 0);
> +
> +	return IRQ_HANDLED;
> +}
> +
> +static irqreturn_t hevc_irq_thread(int irq, void *priv)
> +{
> +	struct aml_vdec_hw *hw = priv;
> +
> +	if (hw->hevc_irq_thread_fn)
> +		return hw->hevc_irq_thread_fn(irq, hw->hevc_priv);
> +
> +	return IRQ_HANDLED;
> +}
> +
>  static irqreturn_t vdec_irq_handler(int irq, void *priv)
>  {
>  	struct aml_vdec_dev *dev = (struct aml_vdec_dev *)priv;
> @@ -445,6 +586,25 @@ int dev_request_hw_resources(void *priv)
>  	hw = dev->dec_hw;
>  	hw->dev = &pdev->dev;
>  
> +	/*
> +	 * The HEVC/AV1 core signals on its own interrupt (IRQ_HVDEC, the first
> +	 * entry in the DT, GIC SPI 91) through mailbox 0.  Nothing decodes on
> +	 * that core yet, so this handler only acknowledges and counts - it is
> +	 * how the microcode's replies are observed at all.  If it ever storms,
> +	 * the mask is dropped rather than letting it wedge the board.
> +	 */
> +	hw->hevc_irq = platform_get_irq(pdev, IRQ_HVDEC);
> +	if (hw->hevc_irq >= 0) {
> +		atomic_set(&hw->hevc_irq_count, 0);
> +		ret = devm_request_threaded_irq(&pdev->dev, hw->hevc_irq,
> +						hevc_irq_handler,
> +						hevc_irq_thread,
> +						IRQF_ONESHOT, "aml-hevc", hw);
> +		if (ret)
> +			dev_warn(&pdev->dev, "hevc irq %d request failed (%d)\n",
> +				 hw->hevc_irq, ret);
> +	}

[Severity: High]
Could registering hw->hevc_irq here in dev_request_hw_resources() before
hw->map[DOS_BUS], hw->pm->init, and vdec_clock_gate_init() are set up cause
an invalid or unpowered register access if an interrupt is pending at probe or
rebind time?

Unlike vdec_irq_handler(), which checks hw->hw_ops.irq_handler before touching
hardware, hevc_irq_handler() unconditionally calls read_dos_reg(hw,
HEVC_ASSIST_SCRATCH_0) and regmap_write(hw->map[DOS_BUS],
HEVC_ASSIST_MBOX0_CLR_REG, 1). If IRQ_HVDEC fires before
devm_regmap_init_mmio() initializes hw->map[DOS_BUS] (or after it stores an
ERR_PTR on failure), hevc_irq_handler() will dereference a NULL or ERR_PTR
regmap in hard IRQ context. Even after regmap initialization, if an interrupt
arrives while the HVDEC power domain or DOS_GCLK_EN3 clock gate is off,
hevc_irq_handler() accesses unpowered DOS registers.

Also, in hevc_irq_thread():

	if (hw->hevc_irq_thread_fn)
		return hw->hevc_irq_thread_fn(irq, hw->hevc_priv);

hw->hevc_irq_thread_fn and hw->hevc_priv are read and called without locking,
READ_ONCE(), or synchronize_irq() when a codec instance tears down (such as
in aml_av1_exit(), aml_vp9_exit(), and aml_hevc_exit() in later commits of the
series). If a session exits after hevc_irq_handler() returns IRQ_WAKE_THREAD,
can hevc_irq_thread() race with teardown and call a NULL function pointer or
dereference a freed hw->hevc_priv context?

> diff --git a/drivers/media/platform/amlogic/vdec/aml_vdec_hw.h b/drivers/media/platform/amlogic/vdec/aml_vdec_hw.h
> index 9dc84d0d52bf4..35645aad23758 100644
> --- a/drivers/media/platform/amlogic/vdec/aml_vdec_hw.h
> +++ b/drivers/media/platform/amlogic/vdec/aml_vdec_hw.h

[ ... ]

> @@ -149,9 +180,42 @@ struct aml_vdec_hw *vdec_get_hw(void *priv);

[ ... ]

> +/*
> + * The CPU wrote a decoded picture into a capture buffer.  A buffer the client
> + * asked to be cacheable (V4L2_MEMORY_FLAG_NON_COHERENT) still has it in dirty
> + * cache lines, which vb2 would invalidate on dequeue: write them back first.
> + */
> +static inline void aml_vdec_dst_cpu_written(struct device *dev,
> +					    struct vb2_buffer *vb)
> +{
> +	unsigned int i;
> +
> +	if (!vb->vb2_queue->non_coherent_mem)
> +		return;
> +	for (i = 0; i < vb->num_planes; i++)
> +		dma_sync_single_for_device(dev,
> +					   vb2_dma_contig_plane_dma_addr(vb, i),
> +					   vb2_plane_size(vb, i), DMA_TO_DEVICE);
> +}

[Severity: Medium]
Does passing DMA_TO_DEVICE to dma_sync_single_for_device() in
aml_vdec_dst_cpu_written() match the DMA direction of the capture queue?

In aml_vdec_queue_init(), dst_vq is a V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE
queue initialized with dst_vq->allow_cache_hints = 1 and bidirectional = 0, so
vb2_core_queue_init() sets dst_vq->dma_dir = DMA_FROM_DEVICE. When non-coherent
capture buffers are allocated via vb2_dc_alloc_non_coherent(), they are mapped
with DMA_FROM_DEVICE. Calling dma_sync_single_for_device(..., DMA_TO_DEVICE)
on those buffers violates the DMA API direction contract and triggers a
warning in check_sync() when CONFIG_DMA_API_DEBUG is enabled.

> diff --git a/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c b/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c
> index 60d20efb1d746..fa4f979b07ee2 100644
> --- a/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c
> +++ b/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c

[ ... ]

> @@ -46,10 +46,21 @@ static struct aml_video_fmt aml_s4_video_formats[] = {
>  		.align = 64,
>  		.codec_type = CODEC_TYPE_FRAME,
>  		.num_planes = 2,
> -		.stepwise = {AML_VDEC_MIN_W, AML_VDEC_1080P_MAX_W, 2,
> -			AML_VDEC_MIN_H, AML_VDEC_1080P_MAX_H, 2},
> +		.stepwise = {AML_VDEC_MIN_W, AML_VDEC_4K_MAX_W, 2,
> +			AML_VDEC_MIN_H, AML_VDEC_4K_MAX_H, 2},
>  
>  	},
> +	{
> +		/* planar 4:2:0, only what the MJPEG microcode writes */
> +		.name = "YUV420",
> +		.fourcc = V4L2_PIX_FMT_YUV420,
> +		.type = AML_FMT_FRAME,
> +		.align = 64,
> +		.codec_type = CODEC_TYPE_FRAME,
> +		.num_planes = 1,
> +		.stepwise = {AML_VDEC_MIN_W, AML_VDEC_4K_MAX_W, 2,
> +			AML_VDEC_MIN_H, AML_VDEC_4K_MAX_H, 2},
> +	},

[Severity: Medium]
As the comment notes, only the MJPEG microcode writes 3-plane planar
V4L2_PIX_FMT_YUV420, whereas H.264 (in h264_restore_hw_ctx() and
config_decode_canvas()) and the other non-MJPEG codecs only output semi-planar
NV12 or NV21.

Because vidioc_vdec_enum_fmt(), vdec_try_fmt_mp(), and vdec_s_fmt_capture() in
drivers/media/platform/amlogic/vdec/aml_vdec.c expose all AML_FMT_FRAME
entries without filtering by the active output format in
ctx->dec_fmt[AML_FMT_SRC], won't userspace clients negotiating H.264 (or
AV1/VP9/HEVC/MPEG-2/MPEG-4) be able to enumerate and select
V4L2_PIX_FMT_YUV420 on the capture queue and receive NV12 data misinterpreted
as planar YUV420 (and conversely, select NV12/NV21 for MJPEG and fail in
aml_mjpeg_dec_run())?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261025192507.210058-1-christian.koever-draxl@student.uibk.ac.at?part=3

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

* Re: [PATCH 09/11] media: amlogic: vdec: add HEVC decoding on the HEVC core
  2026-10-25 19:25 ` [PATCH 09/11] media: amlogic: vdec: add HEVC " christian.koever-draxl
@ 2026-10-25 20:26   ` sashiko-bot
  0 siblings, 0 replies; 20+ messages in thread
From: sashiko-bot @ 2026-10-25 20:26 UTC (permalink / raw)
  To: christian.koever-draxl; +Cc: devicetree, robh, conor+dt, media-ci

Thank you for your contribution! Sashiko AI review found 14 potential issue(s) to consider:
- [Critical] media: amlogic: vdec: CAPTURE overflow in h265_config_sao() and h265_copy_out()
- [Critical] media: amlogic: vdec: MMU map truncation in h265_fill_mmu_map() above 2304p
- [Critical] media: amlogic: vdec: unvalidated non-first slice state in h265_slice_header()
- [Critical] media: amlogic: vdec: unsynchronized IRQ teardown in aml_hevc_exit()
- [High] media: amlogic: vdec: V4L2_PIX_FMT_HEVC_SLICE lacks stateless HEVC controls
- [High] media: amlogic: vdec: OOB read and invalid tile sizes in h265_update_tiles()
- [High] media: amlogic: vdec: shift overflow and broken POC math in h265_slice_header()
- [High] media: amlogic: vdec: DPB slot leak on IDR pictures in h265_apply_rps()
- [High] media: amlogic: vdec: swap buffers not programmed in h265_config_workspace()
- [High] media: amlogic: vdec: freed DMA addresses left active on slice header error
- [High] media: amlogic: vdec: timeout recovery and IRQ race in aml_hevc_dec_run()
- [High] media: amlogic: vdec: premature job completion in h265_finish() from IRQ thread
- [High] media: amlogic: vdec: NULL deref in aml_hevc_dec_run() after CAPTURE restart
- [Medium] media: amlogic: vdec: skipped tile init on same POC in h265_slice_header()
--

commit 6edd9c45f2e4b6fa27b412395f4321725272a652
Author: Christian Stefan Kövér-Draxl <ckoever@outlook.de>

media: amlogic: vdec: add HEVC decoding on the HEVC core

This commit adds HEVC Main and Main 10 decoding support up to 4K on the
Amlogic S4 HEVC hardware core. It registers the V4L2_PIX_FMT_HEVC_SLICE
format, manages workspace and DPB picture buffers, and configures the
hardware decoder per slice from the microcode RPM parameter block.

> diff --git a/drivers/media/platform/amlogic/vdec/aml_vdec.c b/drivers/media/platform/amlogic/vdec/aml_vdec.c
> index f1696aa2fac53..ee020a62836fc 100644
> --- a/drivers/media/platform/amlogic/vdec/aml_vdec.c
> +++ b/drivers/media/platform/amlogic/vdec/aml_vdec.c

[ ... ]

> @@ -112,7 +113,19 @@ static const struct aml_vdec_v4l2_ctrl controls[] = {
>  		.cfg = {
>  			.id = V4L2_CID_STATELESS_MPEG2_QUANTISATION,
>  		},
> -	},};
> +	}, {
> +		.codec_type = CODEC_TYPE_HEVC,
> +		.cfg = {
> +			.id = V4L2_CID_AML_HEVC_REFS,
> +			.name = "HEVC Reference Lists",
> +			.type = V4L2_CTRL_TYPE_INTEGER,
> +			.min = S32_MIN,
> +			.max = S32_MAX,
> +			.step = 1,
> +			.dims = { AML_HEVC_REFS_LEN },
> +		},
> +	}
> +};

[Severity: High]
In drivers/media/platform/amlogic/vdec/aml_vdec_platform.c,
aml_s4_video_formats[] advertises V4L2_PIX_FMT_HEVC_SLICE:

	{
		.name = "HEVC",
		.fourcc = V4L2_PIX_FMT_HEVC_SLICE,
		.type = AML_FMT_DEC,
		...
		.codec_type = CODEC_TYPE_HEVC,

However, controls[] in drivers/media/platform/amlogic/vdec/aml_vdec.c only
registers the private V4L2_CID_AML_HEVC_REFS control (defined in
drivers/media/platform/amlogic/vdec/hevc.h as V4L2_CID_USER_BASE + 0x1f01)
and omits the standard V4L2 stateless HEVC controls
(V4L2_CID_STATELESS_HEVC_SPS, V4L2_CID_STATELESS_HEVC_PPS,
V4L2_CID_STATELESS_HEVC_DECODE_PARAMS, V4L2_CID_STATELESS_HEVC_SLICE_PARAMS,
V4L2_CID_STATELESS_HEVC_SCALING_MATRIX, V4L2_CID_STATELESS_HEVC_DECODE_MODE,
and V4L2_CID_STATELESS_HEVC_START_CODE).

In addition, although the commit description states that the access unit is
rebuilt in Annex-B form with VPS, SPS, and PPS from the stateless controls,
aml_hevc_dec_run() passes the raw OUTPUT buffer directly to h265_feed(), and
h265_irq_thread() under H265_NAL_SEARCH_DONE discards coded slices with
H265_DISCARD_NAL until in-band H265_NAL_VPS, H265_NAL_SPS, and H265_NAL_PPS
NAL units have been seen in the bitstream:

	case H265_NAL_SEARCH_DONE: {
		int nal = read_dos_reg(hw, H265_CUR_NAL_UNIT_TYPE);
		u32 parse = H265_DISCARD_NAL;
		...
		} else if (h->have_vps && h->have_sps && h->have_pps && nal <= H265_NAL_CRA) {

How can standard V4L2 stateless HEVC clients drive V4L2_PIX_FMT_HEVC_SLICE
without the stateless HEVC controls and Annex-B header reconstruction, or
was this intended to use the stateless controls rather than in-band parameter
sets and a driver-private control?

> diff --git a/drivers/media/platform/amlogic/vdec/hevc.c b/drivers/media/platform/amlogic/vdec/hevc.c
> new file mode 100644
> index 0000000000000..6d9f457436cfd
> --- /dev/null
> +++ b/drivers/media/platform/amlogic/vdec/hevc.c
> @@ -0,0 +1,1728 @@

[ ... ]

> +static int h265_alloc_pics(struct aml_hevc_ctx *h)
> +{
> +	struct device *dev = h->hw->dev;
> +	u32 w = h->pic_w, ht = h->pic_h;
> +	size_t body = ALIGN(h265_comp_body_size(w, ht, h->ten_bit), PAGE_SIZE);
> +	/* Size the MV buffers for the smallest LCU, so they fit any. */
> +	size_t mv = h265_mv_buf_size(w, ht, 16);
> +	size_t hdr = h265_hdr_alloc_size(w, ht);
> +	size_t dw = h->dw_target_y ? 0 : ALIGN(w * ht * 3 / 2, SZ_64K);
> +	int i;
> +
> +	if (h->alloc_w == w && h->alloc_h == ht && h->dw_size == dw &&
> +	    h->alloc_ten_bit == h->ten_bit && h->alloc_nslots == h->nslots)
> +		return 0;
> +
> +	h265_free_pics(h);
> +	for (i = 0; i < h->nslots; i++) {
> +		struct h265_pic *pic = &h->pic[i];
> +
> +		pic->body_v = dma_alloc_coherent(dev, body, &pic->body_p, GFP_KERNEL);
> +		pic->hdr_v = dma_alloc_coherent(dev, hdr, &pic->hdr_p, GFP_KERNEL);
> +		pic->mv_v = dma_alloc_coherent(dev, mv, &pic->mv_p, GFP_KERNEL);
> +		if (dw)
> +			pic->dw_v = dma_alloc_noncoherent(dev, dw, &pic->dw_p,
> +							  DMA_FROM_DEVICE, GFP_KERNEL);
> +		if (!pic->body_v || !pic->hdr_v || !pic->mv_v || (dw && !pic->dw_v)) {
> +			h->body_size = body;
> +			h->hdr_size = hdr;
> +			h->mv_size = mv;
> +			h->dw_size = dw;
> +			h265_free_pics(h);
> +			return -ENOMEM;
> +		}

[Severity: High]
When h265_alloc_pics() reallocates picture buffers on a resolution, bit-depth,
or DPB size change, it first frees the existing buffers with h265_free_pics(h).
If one of the subsequent allocations fails and returns -ENOMEM,
HEVCD_MPP_ANC2AXI_TBL_DATA is not cleared, and if a smaller body_size is
allocated later, h265_fill_mmu_map() only overwrites the first pages entries
of h->mmu_map, leaving stale PFNs from the freed buffers in the remaining
entries.

Furthermore, when h265_slice_header() returns a negative error in
h265_irq_thread():

		ret = h265_slice_header(h);
		if (ret < 0) {
			dev_err(hw->dev, "hevc: slice setup failed (%d)\n", ret);
			h265_finish(h, VB2_BUF_STATE_ERROR);
			return IRQ_HANDLED;
		}

neither HEVC_MPSR nor HEVC_CPSR is cleared to halt the AMRISC, and h->cur is
not reset to -1 even if a slot was already claimed and marked valid and
referenced in h265_slice_header().

Could this leave the hardware running with stale physical addresses in
HEVCD_MPP_ANC2AXI_TBL_DATA and h->mmu_map, or leak the claimed slot on slice
setup errors?

[ ... ]

> +/* hevc_config_work_space_hw() */
> +static void h265_config_workspace(struct aml_hevc_ctx *h)
> +{
> +	struct aml_vdec_hw *hw = h->hw;
> +
> +	wr(hw, HEVCD_IPP_LINEBUFF_BASE, h->ipp);
> +	wr(hw, HEVC_RPM_BUFFER, h->rpm);
> +	wr(hw, HEVC_SHORT_TERM_RPS, h->short_term_rps);
> +	wr(hw, H265_VPS_BUFFER, h->vps);
> +	wr(hw, H265_SPS_BUFFER, h->sps);
> +	wr(hw, HEVC_PPS_BUFFER, h->pps);
> +	wr(hw, H265_SAO_UP, h->sao_up);
> +	wr(hw, HEVC_ASSIST_MMU_MAP_ADDR, h->mmu_map);
> +	wr(hw, H265_SCALELUT, h->scalelut);
> +	wr(hw, HEVC_DBLK_CFG3, 0x4010);	/* <= 4096x2304 */
> +	wr(hw, HEVC_DBLK_CFG4, h->dblk_para);
> +	wr(hw, HEVC_DBLK_CFG5, h->dblk_data);
> +	wr(hw, HEVC_DBLK_CFGE, h->dblk_data2);
> +	wr(hw, H265_LMEM_DUMP_ADR, h->lmem);
> +}

[Severity: High]
In h265_alloc_workspace(), h->swap_buf and h->swap_buf2 are carved out of the
workspace allocation:

	h->swap_buf = h265_carve(&p, H265_SWAP_BUF_SIZE);
	h->swap_buf2 = h265_carve(&p, H265_SWAP_BUF2_SIZE);

However, h265_config_workspace() never writes h->swap_buf or h->swap_buf2 to
HEVC_STREAM_SWAP_BUFFER (HEVC_ASSIST_SCRATCH_7) and HEVC_STREAM_SWAP_BUFFER2
(HEVC_ASSIST_SCRATCH_8), and those scratch registers are not cleared before
starting the microcode.

If the microcode accesses the stream swap buffers, will it DMA to address 0
or to stale physical addresses left in the scratch registers by a previous
VP9 or AV1 session?

[ ... ]

> +/* apply_ref_pic_set(): everything the current RPS does not name stops being
> + * a reference.
> + */
> +static void h265_apply_rps(struct aml_hevc_ctx *h)
> +{
> +	int i, j;
> +
> +	for (i = 0; i < H265_SLOTS; i++) {
> +		struct h265_pic *pic = &h->pic[i];
> +		bool found = false;
> +
> +		pic->long_term = false;
> +		if (!pic->valid || !pic->referenced || pic->poc == h->curr_poc)
> +			continue;
> +		for (j = 0; j < 16; j++) {
> +			int poc;
> +			bool used, lt;
> +
> +			if (!h265_rps_poc(h, j, &poc, &used, &lt))
> +				break;
> +			if (poc == pic->poc) {
> +				pic->long_term = lt;
> +				found = true;
> +				break;
> +			}
> +		}
> +		if (!found)
> +			pic->referenced = false;
> +	}
> +}

[Severity: High]
In h265_slice_header(), h265_apply_rps(h) is called on a new picture before
a slot is chosen and before h->cur is updated. When a new IDR picture arrives,
h->curr_poc is set to 0 first:

	if (h->nal_type == H265_NAL_IDR_W_RADL || h->nal_type == H265_NAL_IDR_N_LP) {
		h->curr_poc = 0;

Because h265_apply_rps() skips any existing slot where pic->poc == h->curr_poc,
any picture from an earlier GOP that still has pic->poc == 0 and
pic->referenced == true is skipped instead of having pic->referenced cleared.
When subsequent frames in the new GOP reference POC 0 in their RPS, both the
old POC 0 slot and the new IDR slot match poc == pic->poc and remain marked
referenced.

Across multiple IDR frames, doesn't this leak one DPB slot per IDR until all
h->nslots slots remain referenced and h265_slice_header() fails with -ENOBUFS?

[ ... ]

> +static void h265_update_tiles(struct aml_hevc_ctx *h, int w_cu, int h_cu,
> +			      int sao_mem_unit)
> +{
> +	int sao_vb_size = (sao_mem_unit + (2 << 4)) * h_cu;
> +	int sao_abv_size = sao_mem_unit * w_cu;
> +	int i, j, x, y;
> +	bool uniform;
> +
> +	h->tile_enabled = h->p[P_TILES_ENABLED_FLAG] & 1;
> +	if (!h->tile_enabled) {
> +		h->num_tile_col = 1;
> +		h->num_tile_row = 1;
> +		h->tile[0][0] = (struct h265_tile){ 0, 0, w_cu, h_cu,
> +						    h->sao_vb, h->sao_abv };
> +		return;
> +	}
> +	h->num_tile_col = h->p[P_NUM_TILE_COLUMNS_MINUS1] + 1;
> +	h->num_tile_row = h->p[P_NUM_TILE_ROWS_MINUS1] + 1;
> +	if (h->num_tile_col > H265_MAX_TILE_COLS || h->num_tile_col <= 0)
> +		h->num_tile_col = 1;
> +	if (h->num_tile_row > H265_MAX_TILE_ROWS || h->num_tile_row <= 0)
> +		h->num_tile_row = 1;
> +	uniform = h->p[P_TILES_ENABLED_FLAG] & 2;
> +
> +	y = 0;
> +	for (i = 0; i < h->num_tile_row; i++) {
> +		x = 0;
> +		for (j = 0; j < h->num_tile_col; j++) {
> +			struct h265_tile *t = &h->tile[i][j];
> +
> +			if (j == h->num_tile_col - 1)
> +				t->width = w_cu - x;
> +			else
> +				t->width = uniform ? w_cu / h->num_tile_col :
> +						     h->p[P_TILE_WIDTH + j];
> +			if (i == h->num_tile_row - 1)
> +				t->height = h_cu - y;
> +			else
> +				t->height = uniform ? h_cu / h->num_tile_row :
> +						      h->p[P_TILE_HEIGHT + i];
> +			t->start_cu_x = x;
> +			t->start_cu_y = y;
> +			t->sao_vb = h->sao_vb + j * sao_vb_size;
> +			t->sao_abv = h->sao_abv + i * sao_abv_size;
> +			x += t->width;
> +		}
> +		y += h->tile[i][0].height;
> +	}
> +}

[Severity: High]
In the RPM parameter layout enum, P_TILE_HEIGHT has 8 entries before
P_MISC_FLAG0:

	P_TILE_WIDTH,			/* 12 entries */
	P_TILE_HEIGHT = P_TILE_WIDTH + 12,	/* 8 entries */
	P_MISC_FLAG0 = P_TILE_HEIGHT + 8,

However, H265_MAX_TILE_ROWS is 20, so when uniform is false and
h->num_tile_row > 9, indexing h->p[P_TILE_HEIGHT + i] for i >= 8 reads past
P_TILE_HEIGHT into P_MISC_FLAG0 and subsequent RPM fields.

Also, if non-uniform tile widths or heights in h->p[] sum to more than w_cu or
h_cu, w_cu - x or h_cu - y becomes negative, which passes the zero-only check
in h265_slice_header():

	if (!h->tile_width_lcu || !h->tile_height_lcu)
		return -EINVAL;

and writes negative dimensions into the MPRED registers. Should
h265_update_tiles() bound P_TILE_HEIGHT accesses to the RPM array size and
reject non-positive tile widths and heights?

[ ... ]

> +static void h265_fill_mmu_map(struct aml_hevc_ctx *h)
> +{
> +	struct h265_pic *pic = &h->pic[h->cur];
> +	u32 *map = h->side_v + (h->mmu_map - h->side_p);
> +	u32 pages = h->body_size / PAGE_SIZE, first = pic->body_p >> PAGE_SHIFT, i;
> +
> +	for (i = 0; i < pages && i < H265_MMU_MAP_SIZE / 4; i++)
> +		map[i] = first + i;
> +}

[Severity: Critical]
H265_MMU_MAP_SIZE is 0x1200 * 4 bytes (4,608 page entries), which matches the
page count of a 4096x2304 10-bit compressed body. However, h265_slice_header()
accepts picture heights up to H265_MAX_DIM (4096):

	if (p[P_PIC_WIDTH] < H265_MIN_DIM || p[P_PIC_HEIGHT] < H265_MIN_DIM ||
	    p[P_PIC_WIDTH] > H265_MAX_DIM || p[P_PIC_HEIGHT] > H265_MAX_DIM) {

For a 4096x4096 picture, h265_comp_body_size() is up to 8,192 pages, and
h265_config_comp() programs that full length into HEVC_CM_BODY_LENGTH and
HEVC_CM_HEADER_OFFSET while h265_fill_mmu_map() silently stops filling map[]
at 4,608 entries.

When the hardware writes beyond page 4,607, won't the MMU DMA engine read past
h->mmu_map into h->aux and DMA compressed frame data over unrelated physical
memory (including page 0)?

[ ... ]

> +static int h265_slice_header(struct aml_hevc_ctx *h)
> +{
> +	struct aml_vdec_hw *hw = h->hw;
> +	u16 *p = h->p;
> +	int i, ret, col_ref;
> +
> +	h->temporal_id = p[P_TEMPORAL_ID];
> +	h->nal_type = p[P_NAL_UNIT_TYPE];
> +	h->misc_flag0 = p[P_MISC_FLAG0];
> +
> +	if (!p[P_FIRST_SLICE_SEGMENT_IN_PIC_FLAG]) {
> +		h->slice_segment_addr = p[P_SLICE_SEGMENT_ADDRESS];
> +		if (!p[P_DEPENDENT_SLICE_SEGMENT_FLAG])
> +			h->slice_addr = h->slice_segment_addr;
> +	} else {
> +		h->slice_segment_addr = 0;
> +		h->slice_addr = 0;
> +	}
> +	h->iprev_poc = h->curr_poc;
> +	h->slice_type = p[P_SLICE_TYPE];
> +	h->tmvp = p[P_SLICE_TEMPORAL_MVP_ENABLE_FLAG] & 1;
> +	h->is_next_slice_segment = !!p[P_DEPENDENT_SLICE_SEGMENT_FLAG];

[Severity: Critical]
When p[P_FIRST_SLICE_SEGMENT_IN_PIC_FLAG] is set, h->slice_segment_addr is
set to 0 above, but the rest of h265_slice_header() never reads
h->slice_segment_addr and instead checks raw p[P_SLICE_SEGMENT_ADDRESS] and
p[P_DEPENDENT_SLICE_SEGMENT_FLAG].

On a non-first slice (p[P_SLICE_SEGMENT_ADDRESS] != 0 with h->cur >= 0),
h265_slice_header() also updates h->pic_w, h->pic_h, h->ten_bit, and
h->lcu_size without reallocating picture buffers or verifying that they match
h->alloc_w, h->alloc_h, and h->alloc_ten_bit, and it does not check that
h->slice_addr < h->lcu_total before h265_config_mpred() computes:

	dma_addr_t rd_ptr = rd_start + h->slice_addr * unit;
	dma_addr_t wr_ptr = cur->mv_p + h->slice_addr * unit;

Can a continuation slice with a larger SPS/PPS or an out-of-range
slice_segment_address cause the hardware to DMA past the allocated MV, SAO,
and picture buffers?

[ ... ]

> +	h->lcu_size = 1 << (p[P_LOG2_MIN_CB_SIZE_MINUS3] + 3 +
> +			    p[P_LOG2_DIFF_MAX_MIN_CB_SIZE]);
> +	if (h->lcu_size < 16 || h->lcu_size > 64)
> +		return -EINVAL;
> +	h->lcu_size_log2 = ilog2(h->lcu_size);
> +	h->lcu_x_num = DIV_ROUND_UP(h->pic_w, h->lcu_size);
> +	h->lcu_y_num = DIV_ROUND_UP(h->pic_h, h->lcu_size);
> +	h->lcu_total = h->lcu_x_num * h->lcu_y_num;
> +
> +	/* POC, 8.3.1 */
> +	if (h->nal_type == H265_NAL_IDR_W_RADL || h->nal_type == H265_NAL_IDR_N_LP) {
> +		h->curr_poc = 0;
> +		if (h->temporal_id - 1 == 0)
> +			h->prev_tid0_poc = h->curr_poc;
> +	} else {
> +		int max_lsb = 1 << (p[P_LOG2_MAX_POC_LSB_MINUS4] + 4);
> +		int prev_lsb = h->prev_tid0_poc % max_lsb;
> +		int prev_msb = h->prev_tid0_poc - prev_lsb;
> +		int lsb = p[P_POC_LSB];
> +		int msb;
> +
> +		if (lsb < prev_lsb && prev_lsb - lsb >= max_lsb / 2)
> +			msb = prev_msb + max_lsb;
> +		else if (lsb > prev_lsb && lsb - prev_lsb > max_lsb / 2)
> +			msb = prev_msb - max_lsb;
> +		else
> +			msb = prev_msb;
> +		if (h->nal_type == H265_NAL_BLA_W_LP ||
> +		    h->nal_type == H265_NAL_BLA_W_RADL ||
> +		    h->nal_type == H265_NAL_BLA_N_LP)
> +			msb = 0;
> +		h->curr_poc = msb + lsb;
> +		if (h->temporal_id - 1 == 0)
> +			h->prev_tid0_poc = h->curr_poc;
> +	}

[Severity: High]
There appear to be three issues in the LCU and POC calculations here in
h265_slice_header():

1. Both 1 << (p[P_LOG2_MIN_CB_SIZE_MINUS3] + 3 + p[P_LOG2_DIFF_MAX_MIN_CB_SIZE])
   and max_lsb = 1 << (p[P_LOG2_MAX_POC_LSB_MINUS4] + 4) shift a signed 32-bit
   int by unvalidated u16 values from the RPM buffer. If
   p[P_LOG2_MAX_POC_LSB_MINUS4] is corrupted (for example >= 27), max_lsb can
   overflow to INT_MIN or 0 and trigger a divide error in
   h->prev_tid0_poc % max_lsb.

2. When h->prev_tid0_poc is negative, h->prev_tid0_poc % max_lsb produces a
   negative prev_lsb in C rather than h->prev_tid0_poc & (max_lsb - 1),
   miscomputing prev_msb and h->curr_poc.

3. Per ITU-T H.265 section 8.3.1, prevTid0Pic should only be updated when the
   picture is not a RASL, RADL, or sub-layer non-reference picture, whereas
   h->prev_tid0_poc is updated here for all pictures with temporal_id == 1
   (even RASL pictures skipped immediately below).

Should the shift exponents be bounds-checked first, prev_lsb masked with
max_lsb - 1, and h->prev_tid0_poc restricted to non-RASL/RADL reference
pictures?

[ ... ]

> +	if (h->iprev_poc != h->curr_poc) {
> +		h->new_tile = true;
> +		h->tile_x = 0;
> +		h->tile_y = 0;
> +	} else if (h->tile_enabled) {
> +		int t = h265_tile_index(h, p[P_SLICE_SEGMENT_ADDRESS], h->lcu_x_num);
> +
> +		if (t != -1 && t != (h->tile_x | (h->tile_y << 8))) {
> +			h->new_tile = true;
> +			h->tile_x = t & 0xff;
> +			h->tile_y = (t >> 8) & 0xff;
> +		} else {
> +			h->new_tile = false;
> +		}
> +	} else {
> +		h->new_tile = false;
> +	}
> +	if (h->tile_x >= H265_MAX_TILE_COLS || h->tile_y >= H265_MAX_TILE_ROWS)
> +		h->new_tile = false;
> +	if (h->new_tile) {
> +		struct h265_tile *t = &h->tile[h->tile_y][h->tile_x];
> +
> +		h->tile_start_lcu_x = t->start_cu_x;
> +		h->tile_start_lcu_y = t->start_cu_y;
> +		h->tile_width_lcu = t->width;
> +		h->tile_height_lcu = t->height;
> +	}

[Severity: Medium]
If two consecutive pictures have the same POC (such as back-to-back IDR
pictures with curr_poc == 0, or an IDR picture following a skipped RASL
picture whose curr_poc was 0), h->iprev_poc != h->curr_poc is false on the
first slice of the new picture.

When that happens, h->new_tile is set to false and h->tile_width_lcu /
h->tile_height_lcu are either left at 0 (failing the check below with -EINVAL)
or retain stale dimensions from the previous picture. Should this test
h->new_pic instead of h->iprev_poc != h->curr_poc?

[ ... ]

> +static void h265_copy_out(struct aml_hevc_ctx *h)
> +{
> +	struct aml_vdec_ctx *ctx = h->v4l2_ctx;
> +	struct v4l2_pix_format_mplane *pf = &ctx->pix_fmt[AML_FMT_DST];
> +	struct vb2_v4l2_buffer *dst = v4l2_m2m_next_dst_buf(ctx->m2m_ctx);
> +	struct h265_pic *pic;
> +	struct vb2_buffer *vb;
> +	u32 stride, dst_h, row, w, ht;
> +	u8 *y, *c;
> +	const u8 *src;
> +
> +	if (!dst || h->cur < 0)
> +		return;
> +	vb = &dst->vb2_buf;
> +	pic = &h->pic[h->cur];
> +	stride = pf->plane_fmt[0].bytesperline ? : pic->width;
> +	dst_h = pf->height ? : pic->height;
> +
> +	if (!h->dw_target_y) {
> +		src = pic->dw_v;
> +		if (!src)
> +			return;
> +		dma_sync_single_for_cpu(h->hw->dev, pic->dw_p, h->dw_size, DMA_FROM_DEVICE);
> +		w = pic->width;
> +		ht = pic->height;
> +		y = vb2_plane_vaddr(vb, 0);
> +		if (!y)
> +			return;
> +		c = vb->num_planes >= 2 ? vb2_plane_vaddr(vb, 1) : y + (size_t)stride * dst_h;
> +		if (!c)
> +			return;
> +		for (row = 0; row < ht; row++)
> +			memcpy(y + (size_t)row * stride, src + (size_t)row * w, w);
> +		for (row = 0; row < ht / 2; row++)
> +			memcpy(c + (size_t)row * stride, src + (size_t)(ht + row) * w, w);
> +		aml_vdec_dst_cpu_written(h->hw->dev, vb);
> +	}

[Severity: Critical]
Can a bitstream with dimensions larger than the configured CAPTURE format
overflow the CAPTURE buffer in both h265_config_sao() and h265_copy_out()?

In aml_hevc_dec_run(), the CAPTURE buffer size check runs before the hardware
parses the current access unit's SPS and slice header:

	h265_set_dw_target(h);
	if (h->dw_target_y && (size_t)h->alloc_w * h->alloc_h * 3 / 2 > h->dw_target_size &&
	    h->alloc_w) {
		h->dw_target_y = 0;	/* stream outgrew the capture buffer */
	}

On the first frame (where h->alloc_w is 0) or on a mid-stream resolution
increase, h->dw_target_y stays non-zero even if the new h->pic_w and h->pic_h
exceed pf->width, pf->height, and h->dw_target_size. When h265_config_sao()
then programs HEVC_SAO_Y_START_ADDR and HEVC_SAO_C_START_ADDR with
h->dw_target_y and h->dw_target_c without checking h->pic_w and h->pic_h
against pf->width and h->dw_target_size, the hardware double-write unit DMAs
past the end of the CAPTURE buffer.

Then, on the second frame, h->alloc_w is non-zero, so aml_hevc_dec_run() clears
h->dw_target_y to 0. That changes dw in h265_alloc_pics() from 0 to non-zero,
which calls h265_free_pics(h) and wipes all decoded reference pictures in the
DPB. Finally, in the fallback path in h265_copy_out() above, memcpy() copies
ht rows of w bytes into y and c without verifying that w <= stride,
ht <= dst_h, or that the destination planes are large enough.

> +	if (vb->num_planes >= 2) {
> +		vb2_set_plane_payload(vb, 0, stride * dst_h);
> +		vb2_set_plane_payload(vb, 1, stride * dst_h / 2);
> +	} else {
> +		vb2_set_plane_payload(vb, 0, stride * dst_h * 3 / 2);
> +	}
> +	h->have_picture = true;
> +}
> +
> +static void h265_finish(struct aml_hevc_ctx *h, enum vb2_buffer_state state)
> +{
> +	struct aml_vdec_ctx *ctx = h->v4l2_ctx;
> +	struct vb2_v4l2_buffer *buf;
> +	unsigned int i;
> +
> +	if (h->finished)
> +		return;
> +	h->finished = true;
> +
> +	if (!h->have_picture) {
> +		buf = v4l2_m2m_next_dst_buf(ctx->m2m_ctx);
> +		if (buf)
> +			for (i = 0; i < buf->vb2_buf.num_planes; i++)
> +				vb2_set_plane_payload(&buf->vb2_buf, i, 0);
> +	}
> +	buf = v4l2_m2m_next_src_buf(ctx->m2m_ctx);
> +	if (buf && buf->vb2_buf.req_obj.req)
> +		v4l2_ctrl_request_complete(buf->vb2_buf.req_obj.req, &ctx->ctrl_handler);
> +
> +	ctx->int_cond = 1;
> +	ctx->job_finished = true;
> +	v4l2_m2m_buf_done_and_job_finish(ctx->dev->m2m_dev_dec, ctx->m2m_ctx, state);
> +	wake_up(&ctx->queue);
> +}

[Severity: High]
h265_finish() runs in h265_irq_thread() and calls
v4l2_m2m_buf_done_and_job_finish() before waking ctx->queue and before
aml_hevc_dec_run() returns to m2mops_vdec_device_run() in
drivers/media/platform/amlogic/vdec/aml_vdec.c:

	ctx->job_finished = false;
	ret = ctx->codec_ops->run(ctx);

	v4l2_m2m_buf_copy_metadata(src_buf, dst_buf);

Because v4l2_m2m_buf_done_and_job_finish() removes src_buf and dst_buf from
their queues, returns them to userspace, and schedules the next M2M job
immediately, doesn't this return the buffers to userspace before
v4l2_m2m_buf_copy_metadata() has copied the timestamp and flags, and allow the
next M2M job (or a concurrent streamoff/close after v4l2_m2m_cancel_job()) to
race with the tail of h265_finish() and aml_hevc_dec_run()?

[ ... ]

> +void aml_hevc_exit(void *priv)
> +{
> +	struct aml_vdec_ctx *ctx = priv;
> +	struct aml_hevc_ctx *h = ctx->codec_priv;
> +
> +	if (!h)
> +		return;
> +	h->hw->hevc_irq_thread_fn = NULL;
> +	h->hw->hevc_priv = NULL;
> +	if (h->started)
> +		regmap_write(h->hw->map[DOS_BUS], HEVC_MPSR, 0);
> +	h265_free(h);
> +	kfree(h);
> +	ctx->codec_priv = NULL;
> +}

[Severity: Critical]
In aml_hevc_exit(), h->hw->hevc_irq_thread_fn and h->hw->hevc_priv are cleared
before stopping HEVC_MPSR, HEVC_ASSIST_MBOX0_MASK is not cleared, and
synchronize_irq(h->hw->hevc_irq) is never called before h265_free(h) and
kfree(h).

In drivers/media/platform/amlogic/vdec/aml_vdec_hw.c, hevc_irq_thread()
invokes the callback without locking:

static irqreturn_t hevc_irq_thread(int irq, void *priv)
{
	struct aml_vdec_hw *hw = priv;

	if (hw->hevc_irq_thread_fn)
		return hw->hevc_irq_thread_fn(irq, hw->hevc_priv);

	return IRQ_HANDLED;
}

If hevc_irq_thread() is running or about to run when aml_hevc_exit() executes,
can h265_irq_thread() be called with priv == NULL or continue accessing h and
its DMA buffers after h265_free(h) and kfree(h) have freed them?

> +
> +int aml_hevc_dec_run(void *priv)
> +{
> +	struct aml_vdec_ctx *ctx = priv;
> +	struct aml_hevc_ctx *h = ctx->codec_priv;
> +	struct aml_vdec_hw *hw = h->hw;

[Severity: High]
In drivers/media/platform/amlogic/vdec/aml_vdec.c, vb2ops_vdec_stop_streaming()
calls aml_vdec_release_instance(ctx) -> aml_hevc_exit(ctx) (setting
ctx->codec_priv = NULL) when either the OUTPUT or CAPTURE queue stops, whereas
vb2ops_vdec_start_streaming() only calls aml_vdec_init_dec_inst(ctx) when the
OUTPUT queue starts.

If userspace stops and restarts the CAPTURE queue while OUTPUT remains
streaming, ctx->codec_priv is NULL when aml_hevc_dec_run() is called. Won't
dereferencing h->hw here cause a NULL pointer dereference?

[ ... ]

> +	/* Uninterruptible on purpose - see aml_vp9_dec_run(). */
> +	ret = wait_event_timeout(ctx->queue, ctx->int_cond,
> +				 msecs_to_jiffies(H265_TIMEOUT_MS));
> +	ctx->int_cond = 0;
> +	if (!ret) {
> +		dev_err(hw->dev, "hevc: decode timeout, status %02x pc %08x shift %08x\n",
> +			hw->hevc_irq_status, read_dos_reg(hw, HEVC_MPC_E),
> +			read_dos_reg(hw, HEVC_SHIFT_BYTE_COUNT));
> +		wr(hw, HEVC_MPSR, 0);
> +		h->cur = -1;
> +		hw->hevc_dirty = true;
> +		return -ETIMEDOUT;
> +	}
> +	return 0;
> +}

[Severity: High]
When wait_event_timeout() times out here in aml_hevc_dec_run(), there appear
to be two recovery issues:

1. The timeout path sets h->cur = -1 and returns -ETIMEDOUT without masking
   HEVC_ASSIST_MBOX0_MASK, setting h->finished = true, or calling
   synchronize_irq(hw->hevc_irq). If h265_irq_thread() is running concurrently
   (for example sleeping in h265_alloc_pics() inside h265_slice_header()),
   setting h->cur = -1 races with h265_fill_mmu_map(), h265_config_mpred(),
   and h265_config_sao() indexing h->pic[h->cur] (reading h->pic[-1] out of
   bounds), and h265_finish() can race with err_cancel_job in
   m2mops_vdec_device_run() to complete the request and M2M job twice.

2. Although hw->hevc_dirty is set to true on timeout, aml_hevc_dec_run() only
   calls hvdec_switch_codec() and hevc_load_fw() when !h->ucode_loaded:

	if (!h->ucode_loaded) {
		hvdec_switch_codec(hw, CODEC_TYPE_HEVC);
		ret = hevc_load_fw(hw, CODEC_TYPE_HEVC, H265_FW_FORMAT_MMU,
				   H265_FW_CORE_HEVC, "hevc");

   Because h->ucode_loaded is not reset to false on timeout (and hw->hevc_dirty
   is not set when decoding 10-bit frames), won't subsequent frames in the
   same context skip cycling the core memories and reloading the firmware?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261025192507.210058-1-christian.koever-draxl@student.uibk.ac.at?part=9

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

* Re: [PATCH 07/11] media: amlogic: vdec: add AV1 decoding on the HEVC core
  2026-10-25 19:25 ` [PATCH 07/11] media: amlogic: vdec: add AV1 decoding on the HEVC core christian.koever-draxl
@ 2026-10-25 20:27   ` sashiko-bot
  0 siblings, 0 replies; 20+ messages in thread
From: sashiko-bot @ 2026-10-25 20:27 UTC (permalink / raw)
  To: christian.koever-draxl; +Cc: devicetree, robh, conor+dt, media-ci

Thank you for your contribution! Sashiko AI review found 17 potential issue(s) to consider:
- [Critical] media: amlogic: vdec: active DMA and IRQ race on timeout in aml_av1_dec_run()
- [Critical] media: amlogic: vdec: unsynchronized IRQ teardown in aml_av1_exit()
- [Critical] media: amlogic: vdec: broken stride/bounds check and show_existing_frame in DW path
- [Critical] media: amlogic: vdec: out-of-bounds memcpy in av1_copy_out() when av1_dw_target_y is 0
- [Critical] media: amlogic: vdec: height bound in aml_av1_config_picture() allows workspace overrun
- [High] media: amlogic: vdec: av1_finish() completes buffers before metadata copy
- [High] media: amlogic: vdec: race on av1->started in aml_av1_dec_run() causes DMA UAF
- [High] media: amlogic: vdec: stream FIFO updated before halting core in aml_av1_dec_run()
- [High] media: amlogic: vdec: DMA buffers leaked on early error in aml_av1_dec_run()
- [High] media: amlogic: vdec: unbalanced hvdec_enable() in aml_av1_dec_run() leaks PM refs
- [High] media: amlogic: vdec: LEB128 size truncation and missing bounds in OBU parsers
- [Medium] media: amlogic: vdec: static AV1 decoder state is not reset across sessions
- [Medium] media: amlogic: vdec: av1_apply_reference_ts() skips ts 0 and matches stale slots
- [Medium] media: amlogic: vdec: wrong show_existing_frame ref reset in aml_av1_config_picture()
- [Medium] media: amlogic: vdec: aml_av1_config_picture() treats SWITCH_FRAME as intra
- [Medium] media: amlogic: vdec: aml_av1_config_picture() misprograms loop filter registers
- [Medium] media: amlogic: vdec: dead bring-up helpers with filp_open() and unused DMA in av1.c
--

commit a46d92fb9c757233f35e346c8e20a00518a6480a
Author: Christian Stefan Kövér-Draxl <ckoever@outlook.de>

media: amlogic: vdec: add AV1 decoding on the HEVC core

This commit adds V4L2 stateless AV1 decoding support on the Amlogic S4 HEVC
core, handling 8-bit and 10-bit main profile streams up to 4096x2304. It
rebuilds sequence and frame OBUs with AMLV headers, manages reference slots
and the frame MMU, and outputs NV12 frames into CAPTURE buffers.

> diff --git a/drivers/media/platform/amlogic/vdec/av1.c b/drivers/media/platform/amlogic/vdec/av1.c
> new file mode 100644
> index 0000000000000..ddadc83c8d866
> --- /dev/null
> +++ b/drivers/media/platform/amlogic/vdec/av1.c
> @@ -0,0 +1,3140 @@

[ ... ]

> +static struct av1_slot av1_slots[AV1_SLOTS];
> +static size_t av1_slot_body_size, av1_slot_mv_size, av1_slot_hdr_size;
> +static size_t av1_slot_dw_size;
> +static int av1_cur_slot;
> +static u32 av1_last_width, av1_last_height;
> +static int av1_prev_slot;
> +/* False for a key frame and for any frame with PRIMARY_REF_NONE. */
> +static bool av1_prev_valid;
> +/* Slot a show_existing_frame header asked to be displayed again. */
> +static int av1_shown_slot = -1;
> +/* Slot that should be handed to the display next, -1 when there is none. */
> +static int av1_out_slot = -1;
> +static bool av1_cur_show;

[ ... ]

> +static bool av1_stateless;

[ ... ]

> +static dma_addr_t av1_dw_target_y, av1_dw_target_c;
> +
> +/* Bytes the hardware may write at av1_dw_target_y, the CAPTURE buffer size. */
> +static size_t av1_dw_target_size;
> +/* AV1 keeps eight reference slots; each names one of our picture slots. */
> +static int av1_ref_slot[8];
> +static u32 av1_pending_refresh;
> +/* OUTPUT timestamp of the frame being decoded, stateless path only. */
> +static u64 av1_cur_ts;
> +/* The picture slot each reference frame LAST..ALTREF resolves to, plus [0] = current. */
> +static int av1_frame_slot[8];
> +
> +static struct av1_workspace av1_ws;

[Severity: Medium]
What happens when a second AV1 decoding session is opened after a previous
session closes?

All of the stream and reference tracking state (av1_seq_obu, av1_seq_len,
av1_ref_slot[], av1_cur_slot, av1_prev_slot, av1_prev_valid, av1_shown_slot,
av1_out_slot, av1_last_width, av1_last_height, and av1_ws.dw_width /
av1_ws.dw_height) is kept in file-scope static variables.

None of these variables are reset in aml_av1_init(), aml_av1_bringup(),
aml_av1_teardown(), or aml_av1_exit(). On a subsequent session,
aml_av1_have_sequence() will return true using the previous session's cached
av1_seq_obu, and the slot and reference indices will start from stale values.

Should this state live in struct aml_av1_ctx or be explicitly cleared during
session initialization and teardown?

[ ... ]

> +static int av1_reframe(struct aml_vdec_hw *hw, const u8 *src, size_t len,
> +		       u8 *dst, size_t dst_cap, size_t *out_len)
> +{
> +	size_t pos = 0, outp = 0, obus = 0;
> +
> +	while (pos < len) {
> +		size_t hdr_len = 1, size_len = 0, consumed, total;
> +		u64 payload;
> +		u32 obu_size;
> +		u8 h[AV1_AMLV_HEADER_SIZE] = { 0 };
> +
> +		bool synth = false;
> +		u8 obu_hdr[2];
> +		u8 leb[8];
> +		size_t leb_len = 0;
> +
> +		if (src[pos] & 0x04)		/* obu_extension_flag */
> +			hdr_len = 2;
> +		if (pos + hdr_len > len)
> +			return -EINVAL;
> +
> +		if (src[pos] & 0x02) {
> +			if (pos + hdr_len >= len)
> +				return -EINVAL;
> +			if (av1_read_leb128(src + pos + hdr_len,
> +					    len - pos - hdr_len,
> +					    &payload, &size_len))
> +				return -EINVAL;
> +			consumed = hdr_len + size_len;
> +			total = consumed + payload;
> +			if (pos + total > len)
> +				return -EINVAL;

[ ... ]

> +static u8 av1_seq_obu[192];
> +static size_t av1_seq_len;
> +
> +static void av1_cache_seq_header(const u8 *src, size_t len)
> +{
> +	size_t pos = 0;
> +
> +	while (pos < len) {
> +		size_t hdr_len = 1, j;
> +		u64 payload;
> +		int type = (src[pos] >> 3) & 0xf;
> +
> +		if (src[pos] & 0x04)
> +			hdr_len = 2;
> +		if (!(src[pos] & 0x02)) {
> +			/* no size field: this OBU runs to the end */
> +			if (type == 1 && len - pos <= sizeof(av1_seq_obu)) {
> +				av1_seq_len = len - pos;
> +				memcpy(av1_seq_obu, src + pos, av1_seq_len);
> +			}
> +			return;
> +		}
> +		if (pos + hdr_len >= len)
> +			return -EINVAL;
> +		j = pos + hdr_len;
> +		if (av1_read_leb128(src + j, len - j, &payload, &hdr_len))
> +			return;
> +		j += hdr_len;
> +		if (j + payload > len)
> +			return;
> +		if (type == 1 && j + payload - pos <= sizeof(av1_seq_obu)) {
> +			av1_seq_len = j + payload - pos;
> +			memcpy(av1_seq_obu, src + pos, av1_seq_len);
> +			return;
> +		}
> +		pos = j + payload;
> +	}
> +}
> +
> +static bool av1_has_seq_header(const u8 *src, size_t len)
> +{
> +	size_t pos = 0;
> +
> +	while (pos < len) {
> +		size_t hdr_len = 1, j;
> +		u64 payload;
> +
> +		if (((src[pos] >> 3) & 0xf) == 1)
> +			return true;
> +		if (src[pos] & 0x04)
> +			hdr_len = 2;
> +		if (!(src[pos] & 0x02) || pos + hdr_len >= len)
> +			return false;
> +		j = pos + hdr_len;
> +		if (av1_read_leb128(src + j, len - j, &payload, &hdr_len))
> +			return false;
> +		pos = j + hdr_len + payload;
> +	}
> +	return false;
> +}

[Severity: High]
Can a crafted LEB128 payload value cause size truncation or an infinite loop
in av1_has_seq_header() and av1_reframe()?

av1_read_leb128() decodes up to 56 bits into u64 payload. In
av1_has_seq_header(), pos = j + hdr_len + payload is updated without checking
whether payload exceeds len - j - hdr_len. On 32-bit builds (allowed via
COMPILE_TEST or 32-bit Meson), a payload such as 0x100000000ULL - (j + hdr_len)
wraps size_t pos back to its previous offset and loops forever.

Similarly, in av1_reframe(), total = consumed + payload truncates the 64-bit
sum to size_t before checking pos + total > len, which can wrap total to 0 on
32-bit systems and repeatedly emit headers until dst_cap is exhausted. Even on
64-bit systems, u32 obu_size = total + 4 and the 28-bit LEB128 encoding in
h[16..19] will truncate if payload exceeds 28 bits.

In addition, when src[pos] & 0x04 is set (hdr_len = 2) and !(src[pos] & 0x02)
on the final byte of the buffer (len - pos == 1), av1_has_seq_header() returns
true and av1_cache_seq_header() copies a truncated 1-byte OBU into av1_seq_obu
without verifying pos + hdr_len <= len first.

Should payload be validated against len - j - hdr_len (and capped to the
28-bit AMLV limit) before adding it to size_t offsets, and should pos + hdr_len
<= len be checked before inspecting the OBU type?

[ ... ]

> +static void av1_apply_reference_ts(const struct v4l2_ctrl_av1_frame *f)
> +{
> +	int s, k;
> +
> +	for (s = 0; s < 8; s++) {
> +		u64 ts = f->reference_frame_ts[s];
> +
> +		if (!ts)
> +			continue;
> +		for (k = 0; k < AV1_SLOTS; k++) {
> +			if (av1_slots[k].ts == ts) {
> +				av1_ref_slot[s] = k;
> +				break;
> +			}
> +		}
> +	}
> +}

[Severity: Medium]
Could av1_apply_reference_ts() miss a valid reference frame with timestamp 0
or match a stale slot after a keyframe reset?

In V4L2, 0 is a valid buffer timestamp (commonly used for the first frame in a
stream), so skipping !ts ignores references to a frame whose OUTPUT buffer had
timestamp 0. Also, av1_slots[].ts is initialized to 0 and is not invalidated
when a shown keyframe resets av1_cur_slot and av1_ref_slot[] in
aml_av1_config_picture(), so unused or pre-reset slots still hold old
timestamps.

Would tracking a per-slot valid flag instead of treating ts == 0 as empty
avoid this?

[ ... ]

> +int aml_av1_config_picture(struct aml_vdec_hw *hw)
> +{

[ ... ]

> +	width = pr[AV1_RPM_MAX_FRAME_WIDTH];
> +	height = pr[AV1_RPM_MAX_FRAME_HEIGHT];
> +	/*
> +	 * Everything downstream sizes DMA from these: the MMU page table, the
> +	 * compressed body, the picture slots.  If the microcode never saw a
> +	 * sequence header they are whatever was left in the RPM, and building
> +	 * a page table from that points the hardware at memory it does not
> +	 * own - which resets the board outright, with no oops to read
> +	 * afterwards.  Refuse instead.
> +	 */
> +	if (width < AV1_MIN_DIM || height < AV1_MIN_DIM ||
> +	    width > AV1_MAX_DIM || height > AV1_MAX_DIM) {
> +		dev_err(hw->dev, "av1: implausible picture size %ux%u, refusing\n",
> +			width, height);
> +		return -EINVAL;
> +	}

[Severity: Critical]
Can a stream with height between 2305 and 4096 overrun the workspace buffers
in av1_ws?

The workspace sub-buffers allocated in av1_alloc() (such as AV1_SEG_MAP_SIZE,
AV1_DBLK_PARA_SIZE, AV1_DBLK_DATA_SIZE, AV1_CDEF_DATA_SIZE, AV1_UPS_DATA_SIZE,
and AV1_MMU_VBH_SIZE) are sized for 4096x2304, matching AML_VDEC_4K_MAX_W and
AML_VDEC_4K_MAX_H.

However, aml_av1_config_picture() checks height against AV1_MAX_DIM (4096). A
bitstream with height > 2304 and <= 4096 (for example 2048x4096 or 4096x2880
at 8-bit, where comp_body_size() also fits within AV1_MMU_MAP_SIZE) passes
this check and lets the hardware write past the end of the height-dependent
workspace buffers.

Should height be bounded by AML_VDEC_4K_MAX_H (2304) instead of AV1_MAX_DIM?

[ ... ]

> +	if (pr[AV1_RPM_SHOW_EXISTING_FRAME] & 1) {
> +		u32 idx = pr[AV1_RPM_EXISTING_FRAME_IDX] & 7;
> +
> +		av1_shown_slot = av1_ref_slot[idx];
> +		av1_out_slot = av1_shown_slot;
> +		if (av1_slots[av1_shown_slot].intra)
> +			for (i = 0; i < 8; i++)
> +				av1_ref_slot[i] = av1_shown_slot;
> +		av1_pending_refresh = 0;
> +		dev_dbg(hw->dev, "av1: show existing frame, slot %d\n",
> +			av1_shown_slot);
> +		return -EAGAIN;
> +	}

[Severity: Medium]
When show_existing_frame is set in aml_av1_config_picture(), does checking
av1_slots[av1_shown_slot].intra reset the reference map on intra-only frames
as well as keyframes?

In aml_av1_config_picture(), slot.intra is set to !inter, which is true for
both KEY_FRAME (0) and INTRA_ONLY_FRAME (2). Per the AV1 specification,
show_existing_frame only refreshes all eight reference slots when the target
frame's frame_type is KEY_FRAME (0). In addition, returning -EAGAIN here skips
aml_av1_frame_done(), so av1_config_next_ref_info() is not called to update
the parser's reference table at 0x11a0 until the core is restarted.

Should this check av1_slots[av1_shown_slot].frame_type == 0 and call
av1_config_next_ref_info() when the reference slots are updated?

> +
> +	frame_type = pr[AV1_RPM_FRAME_TYPE];
> +	inter = (frame_type == 1);

[Severity: Medium]
Does setting inter = (frame_type == 1) in aml_av1_config_picture() misclassify
AV1 S-frames (SWITCH_FRAME, frame_type == 3) as intra frames?

In AV1, a SWITCH_FRAME is an inter-predicted frame that uses active reference
frames. With inter false for frame_type == 3, aml_av1_config_picture() points
av1_frame_slot[1..7] at av1_cur_slot instead of the reference slots, clears
av1_prev_valid, resets GMC to identity, and zeroes the motion prediction
reference registers.

Should inter be true for both frame_type == 1 and frame_type == 3?

[ ... ]

> +	base[0] = pr[AV1_RPM_LF_LEVEL_0];
> +	base[1] = pr[AV1_RPM_LF_LEVEL_U];
> +	base[2] = pr[AV1_RPM_LF_LEVEL_V];
> +
> +	for (i = 0; i < 8; i++)
> +		ref_deltas[i] = (s8)(i == 0 ? 1 : (i == 4 || i >= 6 ? -1 : 0));
> +	mode_deltas[0] = 0;
> +	mode_deltas[1] = 0;
> +
> +	/*
> +	 * lfi->lvl[plane][seg][dir][ref][mode], flattened the way the hardware
> +	 * wants it: 192 words, each packing the two directions and two modes for
> +	 * one (plane, segment, ref).  Every block in a key frame is intra, so
> +	 * only the INTRA_FRAME reference delta contributes.
> +	 */
> +	for (i = 0; i < 192; i++) {
> +		u32 lvl;
> +		int seg;
> +		int ref = i & 7;
> +		int scale, v;
> +
> +		plane = (i >> 6) & 3;
> +		seg = (i >> 3) & 7;
> +		(void)seg;
> +
> +		scale = 1 << (base[plane] >> 5);
> +		v = av1_clamp_lvl(base[plane] + ref_deltas[ref] * scale);
> +		lvl = ((v & 0x3f) << 24) | ((v & 0x3f) << 16) |
> +		      ((v & 0x3f) << 8) | (v & 0x3f);
> +		if (!pr[AV1_RPM_LF_LEVEL_0] && !pr[AV1_RPM_LF_LEVEL_1])
> +			lvl = 0;
> +		wr(hw, HEVC_DBLK_CFGA, lvl);
> +	}

[Severity: Medium]
Are the loop filter levels in HEVC_DBLK_CFGA and HEVC_DBLK_DBLK1 programmed
accurately when horizontal and vertical luma levels differ or when custom
loop filter deltas are present?

In aml_av1_config_picture(), all four bytes of each HEVC_DBLK_CFGA word (covering
both directions and both modes) are computed from base[0] (AV1_RPM_LF_LEVEL_0),
ignoring AV1_RPM_LF_LEVEL_1 for the second luma direction. Also, ref_deltas[]
and mode_deltas[] are hardcoded to the default table rather than reading
AV1_RPM_LF_REF_DELTAS_0 and AV1_RPM_LF_MODE_DELTAS_0 from the RPM buffer, and
ref_deltas[ref] * scale is applied even when AV1_RPM_LF_MODE_REF_DELTA is
disabled or when base[plane] is 0.

Should the loop filter table incorporate AV1_RPM_LF_LEVEL_1 and the parsed
ref/mode deltas from the RPM buffer?

[ ... ]

> +int aml_av1_save_nv12(struct aml_vdec_hw *hw, const char *path, bool append)
> +{
> +	const u8 *y;
> +	size_t i, nz = 0;
> +	struct file *f;
> +	loff_t pos = 0;
> +	size_t len;
> +
> +	/* Only write a frame the stream actually asked to be displayed. */
> +	if (av1_out_slot < 0)
> +		return -EAGAIN;
> +	y = av1_slots[av1_out_slot].dw_v;
> +	dma_sync_single_for_cpu(hw->dev, av1_slots[av1_out_slot].dw_p,
> +				av1_slot_dw_size, DMA_FROM_DEVICE);
> +	av1_out_slot = -1;
> +	if (!y)
> +		return -ENODEV;
> +
> +	len = av1_ws.dw_width * av1_ws.dw_height * 3 / 2;
> +	for (i = 0; i < len; i++)
> +		if (y[i])
> +			nz++;
> +	dev_info(hw->dev, "av1: nv12 %zu/%zu non-zero, Y[0..15] %*ph\n",
> +		 nz, len, 16, y);
> +
> +	f = filp_open(path, O_WRONLY | O_CREAT |
> +		      (append ? O_APPEND : O_TRUNC), 0644);
> +	if (IS_ERR(f))
> +		return PTR_ERR(f);
> +	if (append)
> +		pos = i_size_read(file_inode(f));
> +	kernel_write(f, y, len, &pos);
> +	filp_close(f, NULL);
> +	dev_info(hw->dev, "av1: wrote %zu bytes of NV12 to %s\n", len, path);
> +	return 0;
> +}

[Severity: Medium]
Can the unused bring-up helpers and unused workspace carve-outs be removed?

aml_av1_save_nv12() writes directly to a filesystem path using filp_open() and
kernel_write(), and along with aml_av1_feed(), aml_av1_dump_stream_state(),
aml_av1_dump_ucode_output(), aml_av1_frame_type(), aml_av1_dump_params(), and
aml_av1_dump_output(), has no callers in the tree.

Similarly, in av1_alloc(), side_vaddr allocates and carves out prob, count, mv,
and cm_header[16] (~249 KiB of coherent DMA memory) which are never referenced
once the per-slot allocations are used.

[ ... ]

> +static void av1_copy_out(struct aml_av1_ctx *av1, struct vb2_v4l2_buffer *dst)
> +{
> +	struct v4l2_pix_format_mplane *pf =
> +		&av1->v4l2_ctx->pix_fmt[AML_FMT_DST];
> +	struct vb2_buffer *vb = &dst->vb2_buf;
> +	u32 w = av1_ws.dw_width, h = av1_ws.dw_height;
> +	u32 dst_stride = pf->plane_fmt[0].bytesperline ? : w;
> +	u32 dst_h = pf->height ? : h;
> +	const u8 *src;
> +	u8 *y, *c;
> +	u32 row;
> +	int slot = av1_out_slot;
> +
> +	if (slot < 0)
> +		return;
> +	src = av1_slots[slot].dw_v;
> +	av1_out_slot = -1;
> +
> +	/*
> +	 * Zero copy: the hardware already wrote this picture into the CAPTURE
> +	 * buffer.  Nothing to move, only the payload to declare.
> +	 */
> +	if (av1_dw_target_y) {
> +		if (vb->num_planes >= 2) {
> +			vb2_set_plane_payload(vb, 0, dst_stride * dst_h);
> +			vb2_set_plane_payload(vb, 1, dst_stride * dst_h / 2);
> +		} else {
> +			vb2_set_plane_payload(vb, 0, dst_stride * dst_h * 3 / 2);
> +		}
> +		av1->have_picture = true;
> +		return;
> +	}
> +
> +	dma_sync_single_for_cpu(av1->hw->dev, av1_slots[slot].dw_p,
> +				av1_slot_dw_size, DMA_FROM_DEVICE);
> +	if (!src)
> +		return;
> +
> +	/*
> +	 * The decoder writes a tightly packed w x h picture; the capture
> +	 * buffer may be wider and taller, because the format is aligned up.
> +	 * Copy row by row rather than assuming the two agree - they do not:
> +	 * a 320x240 stream gets a 320x256 capture buffer, so chroma starts a
> +	 * stride times 16 rows further in than the source has it.
> +	 */
> +	y = vb2_plane_vaddr(vb, 0);
> +	c = vb->num_planes >= 2 ? vb2_plane_vaddr(vb, 1)
> +				: y + dst_stride * dst_h;
> +	if (!y || !c)
> +		return;
> +
> +	for (row = 0; row < h; row++)
> +		memcpy(y + row * dst_stride, src + row * w, w);
> +	for (row = 0; row < h / 2; row++)
> +		memcpy(c + row * dst_stride, src + w * h + row * w, w);

[Severity: Critical]
Does av1_copy_out() overflow the destination CAPTURE buffer when
av1_dw_target_y is 0 and the stream dimensions exceed the negotiated CAPTURE
format?

When av1_dw_target_y is 0 (such as on the raw-OBU path or when direct
double-write is not used), aml_av1_config_picture() does not check width and
height against the CAPTURE buffer size. In av1_copy_out(), the loops copy h
rows of w bytes into y and h / 2 rows of w bytes into c without verifying that
w <= dst_stride, h <= dst_h, or that vb2_plane_size() for each plane can hold
the data.

If userspace leaves the CAPTURE format at a smaller resolution (e.g. 64x64) and
feeds a larger frame, will memcpy() write past the end of the CAPTURE buffer?

> +	aml_vdec_dst_cpu_written(av1->hw->dev, vb);
> +
> +	if (vb->num_planes >= 2) {
> +		vb2_set_plane_payload(vb, 0, dst_stride * dst_h);
> +		vb2_set_plane_payload(vb, 1, dst_stride * dst_h / 2);
> +	} else {
> +		vb2_set_plane_payload(vb, 0, dst_stride * dst_h * 3 / 2);
> +	}
> +	av1->have_picture = true;
> +}
> +
> +static void av1_finish(struct aml_av1_ctx *av1, enum vb2_buffer_state state)
> +{
> +	struct aml_vdec_ctx *ctx = av1->v4l2_ctx;
> +	struct vb2_v4l2_buffer *dst;
> +	unsigned int i;
> +
> +	/*
> +	 * A buffer holding only a hidden frame decodes fine and produces
> +	 * nothing to show.  Say so with a zero payload - otherwise the
> +	 * capture buffer goes back to userspace still describing whatever it
> +	 * held last time, which reads as a repeat of an older picture.
> +	 */
> +	if (!av1->have_picture) {
> +		dst = v4l2_m2m_next_dst_buf(ctx->m2m_ctx);
> +		if (dst)
> +			for (i = 0; i < dst->vb2_buf.num_planes; i++)
> +				vb2_set_plane_payload(&dst->vb2_buf, i, 0);
> +	}
> +
> +	/*
> +	 * Complete the request before the buffer is handed back.  Doing it
> +	 * afterwards - which is what device_run() would do, since run() only
> +	 * returns once this has already finished the job - binds the control
> +	 * objects to a request that has already completed, and the media
> +	 * controller warns about it.
> +	 */
> +	dst = v4l2_m2m_next_src_buf(ctx->m2m_ctx);
> +	if (dst && dst->vb2_buf.req_obj.req)
> +		v4l2_ctrl_request_complete(dst->vb2_buf.req_obj.req,
> +					   &ctx->ctrl_handler);
> +
> +	ctx->int_cond = 1;
> +	ctx->job_finished = true;
> +	v4l2_m2m_buf_done_and_job_finish(ctx->dev->m2m_dev_dec, ctx->m2m_ctx,
> +					 state);
> +	wake_up(&ctx->queue);
> +}

[Severity: High]
Does calling v4l2_m2m_buf_done_and_job_finish() inside av1_finish() return
src_buf and dst_buf to userspace before m2mops_vdec_device_run() copies buffer
metadata?

In drivers/media/platform/amlogic/vdec/aml_vdec.c:m2mops_vdec_device_run():

	ctx->job_finished = false;
	ret = ctx->codec_ops->run(ctx);

	v4l2_m2m_buf_copy_metadata(src_buf, dst_buf);

While aml_av1_dec_run() waits on ctx->queue, av1_irq_thread() calls
av1_finish(), which invokes v4l2_m2m_buf_done_and_job_finish(). That marks
src_buf and dst_buf done (allowing userspace to dequeue dst_buf without its
timestamp or flags copied) and finishes the M2M job before aml_av1_dec_run()
returns to m2mops_vdec_device_run(), which then touches src_buf and dst_buf in
v4l2_m2m_buf_copy_metadata().

[ ... ]

> +void aml_av1_exit(void *priv)
> +{
> +	struct aml_vdec_ctx *ctx = priv;
> +	struct aml_av1_ctx *av1 = ctx->codec_priv;
> +
> +	if (!av1)
> +		return;
> +
> +	av1->hw->hevc_irq_thread_fn = NULL;
> +	av1->hw->hevc_priv = NULL;
> +	if (av1->started) {
> +		regmap_write(av1->hw->map[DOS_BUS], HEVC_MPSR, 0);
> +		aml_av1_teardown(av1->hw);
> +		/*
> +		 * Deliberately not hvdec_disable(): powering the HEVC core
> +		 * down here leaves it in a state the next context's bring-up
> +		 * does not recover from, and every frame of the *second*
> +		 * decode session on this core then times out - whichever
> +		 * codec it is.  The domain is left up until the module is
> +		 * unloaded.
> +		 */
> +	}
> +	kfree(av1);
> +	ctx->codec_priv = NULL;
> +}

[Severity: Critical]
What prevents aml_av1_exit() from racing with hevc_irq_thread() and freeing
av1, av1_ws, and av1_slots while av1_irq_thread() is running?

In drivers/media/platform/amlogic/vdec/aml_vdec_hw.c:hevc_irq_thread():

	if (hw->hevc_irq_thread_fn)
		return hw->hevc_irq_thread_fn(irq, hw->hevc_priv);

In aml_av1_exit(), hevc_irq_thread_fn and hevc_priv are cleared before halting
HEVC_MPSR or masking HEVC_ASSIST_MBOX0_MASK, and synchronize_irq(av1->hw->hevc_irq)
is never called.

If hevc_irq_thread() loads hw->hevc_irq_thread_fn just before it is cleared and
then passes a NULL hw->hevc_priv to av1_irq_thread(), av1_irq_thread() will
dereference NULL. Furthermore, if av1_irq_thread() is still executing (for
example right after calling wake_up(&ctx->queue) in av1_finish(), or after a
decode timeout), aml_av1_exit() can tear down all DMA buffers and kfree(av1)
underneath the running IRQ thread.

Should aml_av1_exit() halt the processor and mask the mailbox interrupt first,
call synchronize_irq(av1->hw->hevc_irq), and only then clear the IRQ callback
pointers and free the buffers?

> +
> +/*
> + * Point the double-write unit at the CAPTURE buffer this frame will be handed
> + * back in.  Only safe when the buffer is laid out exactly the way the hardware
> + * writes: stride equal to the picture width, chroma immediately after luma.
> + * Anything else falls back to the slot buffer and the row-by-row copy.
> + */
> +static void av1_set_dw_target(struct aml_vdec_ctx *ctx)
> +{
> +	struct v4l2_pix_format_mplane *pf = &ctx->pix_fmt[AML_FMT_DST];
> +	struct vb2_v4l2_buffer *dst = v4l2_m2m_next_dst_buf(ctx->m2m_ctx);
> +	struct vb2_buffer *vb;
> +
> +	av1_dw_target_y = 0;
> +	av1_dw_target_c = 0;
> +	av1_dw_target_size = 0;
> +	if (!dst)
> +		return;
> +	vb = &dst->vb2_buf;
> +
> +	if (pf->plane_fmt[0].bytesperline != pf->width)
> +		return;
> +
> +	av1_dw_target_y = vb2_dma_contig_plane_dma_addr(vb, 0);
> +	if (!av1_dw_target_y)
> +		return;
> +	if (vb->num_planes >= 2) {
> +		av1_dw_target_c = vb2_dma_contig_plane_dma_addr(vb, 1);
> +		av1_dw_target_size = vb2_plane_size(vb, 0) +
> +				     vb2_plane_size(vb, 1);
> +		if (!av1_dw_target_c) {
> +			av1_dw_target_y = 0;
> +			return;
> +		}
> +	} else {
> +		av1_dw_target_c = av1_dw_target_y +
> +				  (dma_addr_t)pf->width * pf->height;
> +		av1_dw_target_size = vb2_plane_size(vb, 0);
> +	}
> +}

[Severity: Critical]
Are there three issues with the direct double-write path configured by
av1_set_dw_target()?

1. In aml_vdec.c:vdec_try_fmt_mp(), pf->width and pf->height are aligned up to
64 before v4l2_fill_pixfmt_mp() sets pf->plane_fmt[0].bytesperline = pf->width.
Because pf->plane_fmt[0].bytesperline == pf->width is always true,
av1_set_dw_target() enables direct double-write even when the stream's actual
width (e.g. 854x480 or 720x480) is not a multiple of 64. The hardware then
writes tightly packed rows of width bytes into a CAPTURE buffer expecting a
64-aligned stride, and av1_copy_out() skips the row-by-row stride conversion.

2. For NV12M (vb->num_planes >= 2), av1_set_dw_target() sums the two plane
sizes into a single av1_dw_target_size, and aml_av1_config_picture() only
checks width * height * 3 / 2 <= av1_dw_target_size instead of checking each
plane's size individually. For single-plane NV12, av1_dw_target_c is placed at
av1_dw_target_y + pf->width * pf->height without verifying in
aml_av1_config_picture() that the parsed width and height match pf->width and
pf->height, so a height or width mismatch can cause luma DMA to overwrite
chroma or chroma DMA to write past the end of the buffer.

3. When aml_av1_config_picture() encounters show_existing_frame, it returns
-EAGAIN without running a decode. Because av1_dw_target_y is non-zero,
av1_alloc_slots() did not allocate per-slot dw_v buffers, and av1_copy_out()
returns the untouched CAPTURE buffer to userspace as a valid frame.

> +
> +int aml_av1_dec_run(void *priv)
> +{
> +	struct aml_vdec_ctx *ctx = priv;
> +	struct aml_av1_ctx *av1 = ctx->codec_priv;
> +	struct aml_vdec_hw *hw = av1->hw;
> +	struct vb2_v4l2_buffer *src;
> +	struct vb2_buffer *vb;
> +	struct v4l2_ctrl *seq_ctrl;
> +	int ret;
> +
> +	src = v4l2_m2m_next_src_buf(ctx->m2m_ctx);
> +	if (!src)
> +		return -EINVAL;
> +	vb = &src->vb2_buf;
> +
> +	ret = hvdec_enable(hw);
> +	if (ret)
> +		return ret;
> +	dos_enable_hevc(hw);

[Severity: High]
Does calling hvdec_enable(hw) on every frame in aml_av1_dec_run() leak the
HVDEC power-domain reference count and clock?

hvdec_enable() calls vdec_poweron(hw, HVDEC), which increments
hw->pm->pd_data[HVDEC].ref_count on every invocation and enables the hevcf
clock and runtime PM on the first call. However, hvdec_disable() is only
called on the two early error paths in aml_av1_dec_run() when loading
firmware or bringing up the workspace fails, and is never called when a frame
finishes or in aml_av1_exit().

> +
> +	if (!av1->ucode_loaded) {
> +		/*
> +		 * As for VP9: after a 10-bit stream the core decodes the next
> +		 * 8-bit one to garbage until its memories are cycled.
> +		 * av1_ten_bit still holds the last stream's depth here.
> +		 */
> +		if (av1_ten_bit)
> +			hw->hevc_dirty = true;
> +		av1_ten_bit = false;
> +		hvdec_switch_codec(hw, CODEC_TYPE_AV1);
> +
> +		/*
> +		 * Halts the AMRISC first: the other codec on this core may
> +		 * still be running - VP9 and AV1 share it - and loading over a
> +		 * running microcode leaves the core timing out every frame.
> +		 */
> +		ret = hevc_load_fw(hw, CODEC_TYPE_AV1, AV1_FW_FORMAT_MMU, AV1_FW_CORE_HEVC, "av1");
> +		if (ret) {
> +			hvdec_disable(hw);
> +			return ret;
> +		}
> +		av1->ucode_loaded = true;
> +	}
> +
> +	if (!av1->started) {
> +		ret = aml_av1_bringup(hw, ctx->pix_fmt[AML_FMT_SRC].width,
> +				      ctx->pix_fmt[AML_FMT_SRC].height);
> +		if (ret) {
> +			hvdec_disable(hw);
> +			return ret;
> +		}
> +	}
> +
> +	/*
> +	 * Two shapes of OUTPUT buffer have to work.
> +	 *
> +	 * The stateless API keeps the sequence in a control and hands over the
> +	 * frame OBU's *payload*, header stripped - that is what GStreamer's
> +	 * v4l2slav1dec sends.  Rebuild the sequence header from the control
> +	 * and put the frame's OBU header back.
> +	 *
> +	 * Without that control the buffer is taken to be a plain OBU stream,
> +	 * carrying its own sequence header, which is what a simple client
> +	 * feeding temporal units does.  The microcode parses the bitstream
> +	 * either way, so it has to end up with real OBUs.
> +	 */
> +	seq_ctrl = v4l2_ctrl_find(&ctx->ctrl_handler,
> +				  V4L2_CID_STATELESS_AV1_SEQUENCE);
> +	if (seq_ctrl && aml_av1_set_sequence(seq_ctrl->p_cur.p)) {
> +		struct v4l2_ctrl *frame_ctrl =
> +			v4l2_ctrl_find(&ctx->ctrl_handler,
> +				       V4L2_CID_STATELESS_AV1_FRAME);
> +
> +		av1_cur_ts = vb->timestamp;
> +		if (frame_ctrl)
> +			av1_apply_reference_ts(frame_ctrl->p_cur.p);
> +		av1_set_dw_target(ctx);
> +		ret = aml_av1_feed_frame(hw, vb2_plane_vaddr(vb, 0),
> +					 vb2_get_plane_payload(vb, 0));
> +	} else {
> +		av1_dw_target_y = 0;
> +		av1_dw_target_c = 0;
> +		av1_dw_target_size = 0;
> +		if (!aml_av1_have_sequence() &&
> +		    !av1_buf_has_sequence(vb2_plane_vaddr(vb, 0),
> +					  vb2_get_plane_payload(vb, 0))) {
> +			dev_err_once(hw->dev,
> +				     "av1: no sequence header, and no usable sequence control\n");
> +			return -EINVAL;
> +		}
> +		ret = aml_av1_feed_buf(hw, vb2_plane_vaddr(vb, 0),
> +				       vb2_get_plane_payload(vb, 0),
> +				       AOM_AV1_SEARCH_HEAD);
> +	}
> +	if (ret)
> +		return ret;

[Severity: High]
If aml_av1_dec_run() returns an error here before av1->started is set to true,
does it leak the coherent DMA buffers allocated in aml_av1_bringup() and
av1_alloc_stream()?

When !av1->started, aml_av1_bringup() allocates av1_ws.vaddr,
av1_ws.side_vaddr, and av1_ws.guard_vaddr, and aml_av1_feed_frame() /
aml_av1_feed_buf() allocates av1_ws.stream_vaddr. If feeding the buffer fails
(or returns -EINVAL when no sequence header is present), aml_av1_dec_run()
returns while av1->started is still false. When the context is later closed,
aml_av1_exit() only calls aml_av1_teardown() if (av1->started), leaving ~6 MiB
of coherent DMA allocations leaked.

> +
> +	av1->rounds = 0;
> +	av1->have_picture = false;
> +	ctx->int_cond = 0;
> +	atomic_set(&hw->hevc_irq_count, 0);
> +
> +	/*
> +	 * The microcode runs for the life of the stream: it is started once
> +	 * and afterwards simply told to look for the next frame header.
> +	 * Restarting it per buffer - or powering the core down between
> +	 * buffers - loses its state, and every buffer after the first then
> +	 * times out with the program counter parked at 1 or 2.
> +	 */
> +	if (av1->started) {
> +		/*
> +		 * The microcode reports NAL_DECODE_DONE when it runs out of
> +		 * data and parks in its search loop; handing it another
> +		 * SEARCH_HEAD there does not make it look at a refilled FIFO.
> +		 * Restart it instead.  Everything that has to persist between
> +		 * buffers - the picture slots, their compressed data, the CDF
> +		 * contexts - lives in DRAM and is re-pointed for every
> +		 * picture, so a restart costs only the parser's reference map.
> +		 */
> +		regmap_write(hw->map[DOS_BUS], HEVC_MPSR, 0);
> +		regmap_write(hw->map[DOS_BUS], HEVC_CPSR, 0);
> +	}

[Severity: High]
On frames after the first (when av1->started is true), is it safe to call
aml_av1_feed_frame() or aml_av1_feed_buf() before halting HEVC_MPSR and
resetting the core?

While HEVC_MPSR and HEVC_STREAM_CONTROL bit 0 are still active from the
previous frame, aml_av1_feed_frame() / aml_av1_feed_buf() can free and
reallocate av1_ws.stream_vaddr in av1_alloc_stream(), zero the live stream
buffer, rewrite the FIFO registers, and write AOM_AV1_SEARCH_HEAD to
HEVC_DEC_STATUS_REG in av1_program_fifo() before aml_av1_dec_run() resets
av1->rounds, av1->have_picture, ctx->int_cond, HEVC_MPSR, and DOS_SW_RESET3.

Should the AMRISC halt (HEVC_MPSR = 0, HEVC_CPSR = 0) and state reset happen
before modifying the stream buffer and programming the FIFO?

> +	regmap_write(hw->map[DOS_BUS], DOS_SW_RESET3, BIT(12) | BIT(11));
> +	regmap_write(hw->map[DOS_BUS], DOS_SW_RESET3, 0);
> +	if (av1->started)
> +		aml_av1_restore_refs(hw);
> +	regmap_write(hw->map[DOS_BUS], HEVC_ASSIST_MBOX0_CLR_REG, 1);
> +	regmap_write(hw->map[DOS_BUS], HEVC_DEC_STATUS_REG, AOM_AV1_SEARCH_HEAD);
> +	regmap_write(hw->map[DOS_BUS], HEVC_MPSR, 1);
> +	av1->started = true;

[Severity: High]
Can setting av1->started = true after writing HEVC_MPSR = 1 race with the
error path in av1_irq_thread()?

Once HEVC_MPSR = 1 is written, the microcode starts immediately and can trigger
an interrupt on another CPU before aml_av1_dec_run() executes
av1->started = true. If aml_av1_config_picture() fails in av1_irq_thread():

			regmap_write(hw->map[DOS_BUS], HEVC_MPSR, 0);
			regmap_write(hw->map[DOS_BUS], HEVC_CPSR, 0);
			aml_av1_teardown(hw);
			av1->started = false;

aml_av1_dec_run() can then overwrite av1->started back to true after
aml_av1_teardown() has already freed all workspace buffers, causing the next
frame to skip aml_av1_bringup() and DMA into freed memory.

Should av1->started = true be set before writing HEVC_MPSR = 1?

> +
> +	/*
> +	 * Uninterruptible on purpose: a signal must not abandon a decode the
> +	 * hardware is still running - see the note in vp9.c.
> +	 */
> +	ret = wait_event_timeout(ctx->queue, ctx->int_cond,
> +				 msecs_to_jiffies(AV1_TIMEOUT_MS));
> +	if (!ret) {
> +		/*
> +		 * Report the failure and let device_run() cancel the job.
> +		 * Finishing it here as well was a double completion: the
> +		 * caller runs its own error path on a negative return, and
> +		 * the media core warns about the second one.
> +		 */
> +		dev_err(hw->dev, "av1: decode timeout, status %02x pc %08x\n",
> +			hw->hevc_irq_status,
> +			read_dos_reg(hw, HEVC_MPC_E));
> +		hw->hevc_dirty = true;
> +		ret = -ETIMEDOUT;
> +	} else {
> +		ret = 0;
> +	}
> +	ctx->int_cond = 0;
> +	return ret;
> +}

[Severity: Critical]
When wait_event_timeout() times out here, isn't the hardware still running and
able to DMA into the CAPTURE buffer or fire av1_irq_thread() after the job is
cancelled?

On timeout, aml_av1_dec_run() returns -ETIMEDOUT without halting HEVC_MPSR /
HEVC_CPSR, resetting DOS_SW_RESET3, masking HEVC_ASSIST_MBOX0_MASK, or calling
synchronize_irq(hw->hevc_irq). m2mops_vdec_device_run() then jumps to
err_cancel_job and returns the CAPTURE buffer to userspace while the hardware
may still be DMA-writing into av1_dw_target_y / av1_dw_target_c.

In addition:
1. If av1_irq_thread() is currently running (for instance, blocked in
dma_alloc_coherent() inside av1_alloc_slots()) or fires right after the
timeout, it does not check whether the job was already cancelled and will call
av1_finish(), completing the media request and M2M job a second time.
2. In av1_irq_thread(), the runaway decode path (++av1->rounds > 256) also
calls av1_finish() without halting HEVC_MPSR or masking the mailbox interrupt.
3. Setting hw->hevc_dirty = true on timeout does not cycle the core memories on
the next frame of the same context, because aml_av1_dec_run() only calls
hvdec_switch_codec() and hevc_load_fw() when !av1->ucode_loaded.

Should the timeout and runaway paths halt the AMRISC, reset the core, mask the
mailbox interrupt, synchronize the IRQ, and force microcode reload on the next
run?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261025192507.210058-1-christian.koever-draxl@student.uibk.ac.at?part=7

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

* Re: [PATCH 05/11] media: amlogic: vdec: add MPEG-1/2 decoding
  2026-10-25 19:25 ` [PATCH 05/11] media: amlogic: vdec: add MPEG-1/2 decoding christian.koever-draxl
@ 2026-10-25 20:28   ` sashiko-bot
  0 siblings, 0 replies; 20+ messages in thread
From: sashiko-bot @ 2026-10-25 20:28 UTC (permalink / raw)
  To: christian.koever-draxl; +Cc: devicetree, robh, conor+dt, media-ci

Thank you for your contribution! Sashiko AI review found 10 potential issue(s) to consider:
- [Critical] media: amlogic: vdec: out-of-bounds UV DMA in mpeg12_canvas_setup() for NV12M
- [Critical] media: amlogic: vdec: unchecked MPEG-2 dimensions in aml_mpeg12_dec_run()
- [High] media: amlogic: vdec: unchecked NULL vb2_plane_vaddr() in mpeg12.c memcpy()
- [High] media: amlogic: vdec: aml_mpeg12_dec_run() uses dst timestamp before copy
- [High] media: amlogic: vdec: premature job completion in mpeg12_job_done() races
- [High] media: amlogic: vdec: missing IRQ sync in aml_mpeg12_dec_run() and exit
- [High] media: amlogic: vdec: NULL codec_priv in aml_mpeg12_dec_run() on cap restart
- [High] media: amlogic: vdec: multi-instance hw stop and IRQ race in aml_mpeg12_exit()
- [Medium] media: amlogic: vdec: double v4l2_ctrl_request_complete() on run() error
- [Medium] media: amlogic: vdec: unchecked ref0/ref1 failure in aml_mpeg12_dec_run()
--

commit 539f5aa58a2516451024d2007f3188610dee819b
Author: Christian Stefan Kövér-Draxl <ckoever@outlook.de>

media: amlogic: vdec: add MPEG-1/2 decoding

This commit adds stateless MPEG-1 and MPEG-2 slice decoding support for the
Amlogic legacy VDEC core using the mpeg12 multi-instance microcode. It
rebuilds sequence, picture, and quantisation headers from V4L2 stateless
controls in front of slice data and supports field-pair decoding into held
capture buffers.

> diff --git a/drivers/media/platform/amlogic/vdec/mpeg12.c b/drivers/media/platform/amlogic/vdec/mpeg12.c
> new file mode 100644
> index 0000000000000..e09ff4d79902f
> --- /dev/null
> +++ b/drivers/media/platform/amlogic/vdec/mpeg12.c

[ ... ]

> +static u32 mpeg12_write_headers(u8 *buf,
> +				const struct v4l2_ctrl_mpeg2_sequence *seq,
> +				const struct v4l2_ctrl_mpeg2_picture *pic,
> +				const struct v4l2_ctrl_mpeg2_quantisation *quant,
> +				bool with_seq)
> +{
> +	struct bitw bw = { .buf = buf };
> +	u32 vbv = seq->vbv_buffer_size / 2048;
> +	int i;
> +
> +	if (!vbv)
> +		vbv = 112;
> +
> +	if (!with_seq)
> +		goto picture;
> +
> +	put_start(&bw, 0xb3);
> +	put_bits(&bw, 12, seq->horizontal_size & 0xfff);
> +	put_bits(&bw, 12, seq->vertical_size & 0xfff);

[Severity: Critical]
Could this write past the end of the capture buffer if the sequence or output
dimensions exceed the negotiated capture format?

When userspace configures a smaller capture buffer via vdec_s_fmt_capture()
while setting a larger output format via vdec_s_fmt_output() ->
set_pic_info(), or passes larger horizontal_size and vertical_size values in
V4L2_CID_STATELESS_MPEG2_SEQUENCE, mpeg12_write_headers() writes
seq->horizontal_size and seq->vertical_size into the staged stream without
checking that they are non-zero or fit within ctx->pic_info and
ctx->pix_fmt[AML_FMT_DST].

Later, mpeg12_threaded_isr() clears MREG_SEQ_REPORT without verifying
MREG_PIC_WIDTH and MREG_PIC_HEIGHT against the capture buffer, and
mpeg12_canvas_setup() configures the destination canvases using
ctx->pic_info.coded_width, ctx->pic_info.coded_height, and
ctx->pic_info.fb_size[0] without checking vb2_plane_size() on the capture
buffer.

[ ... ]

> +/* Stash the first field of a pair: headers and slices, for the next job. */
> +static int mpeg12_stash_field(struct aml_vdec_ctx *ctx,
> +			      const struct v4l2_ctrl_mpeg2_sequence *seq,
> +			      const struct v4l2_ctrl_mpeg2_picture *pic,
> +			      const struct v4l2_ctrl_mpeg2_quantisation *quant,
> +			      struct vb2_v4l2_buffer *src)
> +{
> +	struct aml_mpeg12_ctx *m = ctx->codec_priv;
> +	u32 payload = vb2_get_plane_payload(&src->vb2_buf, 0);
> +	u32 need = MPEG12_HDR_MAX + payload;
> +	u32 hdr;
> +
> +	if (need > m->fld_cap) {
> +		kvfree(m->fld_buf);
> +		m->fld_cap = 0;
> +		m->fld_buf = kvmalloc(need, GFP_KERNEL);
> +		if (!m->fld_buf)
> +			return -ENOMEM;
> +		m->fld_cap = need;
> +	}
> +	hdr = mpeg12_write_headers(m->fld_buf, seq, pic, quant, true);
> +	memcpy(m->fld_buf + hdr, vb2_plane_vaddr(&src->vb2_buf, 0), payload);

[Severity: High]
What happens if vb2_plane_vaddr() returns NULL here or in
mpeg12_prepare_input()?

In aml_vdec_queue_init(), the output queue enables VB2_DMABUF with
vb2_dma_contig_memops. If userspace queues an imported DMABUF where
dma_buf_vmap_unlocked() fails or the exporter does not implement vmap,
vb2_plane_vaddr() returns NULL.

Passing that return value directly to memcpy() in mpeg12_stash_field() and
mpeg12_prepare_input() without a NULL check (unlike h264_stage_with_headers()
in h264.c) will dereference a NULL pointer when payload is non-zero.

[ ... ]

> +/* Returns the canvas spec (y | uv << 8 | uv << 16) for the buffer, or ~0. */
> +static u32 mpeg12_canvas_setup(struct aml_vdec_ctx *ctx,
> +			       struct mpeg12_canvas *c, struct vb2_buffer *vb)
> +{
> +	struct aml_vdec_hw *hw = vdec_get_hw(ctx->dev);
> +	u32 w = ctx->pic_info.coded_width;
> +	u32 h = ctx->pic_info.coded_height;
> +	dma_addr_t y, uv;
> +	u8 yi, ci;
> +
> +	y = vb2_dma_contig_plane_dma_addr(vb, 0);
> +	if (ctx->pic_info.plane_num > 1)
> +		uv = vb2_dma_contig_plane_dma_addr(vb, 1);
> +	else
> +		uv = y + ctx->pic_info.fb_size[0];

[Severity: Critical]
Does this DMA into unallocated memory past plane 0 when a two-plane capture
format such as V4L2_PIX_FMT_NV12M or V4L2_PIX_FMT_NV21M is selected?

In drivers/media/platform/amlogic/vdec/aml_vdec.c, set_pic_info() hardcodes
ctx->pic_info.plane_num = 1 when the output format is configured, and
vdec_s_fmt_capture() never updates ctx->pic_info.plane_num:

drivers/media/platform/amlogic/vdec/aml_vdec.c:set_pic_info() {
	if (V4L2_TYPE_IS_OUTPUT(type)) {
		...
		ctx->pic_info.fb_size[0] =
		    ctx->pic_info.coded_width * ctx->pic_info.coded_height;
		ctx->pic_info.fb_size[1] = ctx->pic_info.fb_size[0] / 2;
		ctx->pic_info.plane_num = 1;
	}
}

When userspace selects V4L2_PIX_FMT_NV12M or V4L2_PIX_FMT_NV21M on the
capture queue, vb2ops_vdec_queue_setup() allocates two separate planes (plane
0 of size fb_size[0] for Y, and plane 1 of size fb_size[1] for UV). Because
ctx->pic_info.plane_num is still 1, mpeg12_canvas_setup() always takes the
else branch and sets uv = y + ctx->pic_info.fb_size[0], programming the UV
canvas immediately past the end of plane 0's DMA allocation instead of using
plane 1. Should this check vb->num_planes > 1 or
ctx->dec_fmt[AML_FMT_DST].num_planes > 1 instead?

[ ... ]

> +static void mpeg12_job_done(struct aml_vdec_ctx *ctx, enum vb2_buffer_state st)
> +{
> +	struct aml_vdec_dev *dev = ctx->dev;
> +
> +	mpeg12_canvas_free(dev->dec_hw, ctx->codec_priv);
> +	ctx->int_cond = 1;
> +	ctx->job_finished = true;
> +	v4l2_m2m_buf_done_and_job_finish(dev->m2m_dev_dec, ctx->m2m_ctx, st);
> +	wake_up(&ctx->queue);
> +}

[Severity: High]
Can calling v4l2_m2m_buf_done_and_job_finish() here in the interrupt thread
race with aml_mpeg12_dec_run() and m2mops_vdec_device_run()?

When mpeg12_threaded_isr() calls mpeg12_job_done(),
v4l2_m2m_buf_done_and_job_finish() clears TRANS_RUNNING, wakes
m2m_ctx->finished, and schedules the next M2M job before wake_up(&ctx->queue)
is called and before aml_mpeg12_dec_run() returns to
m2mops_vdec_device_run():

1. v4l2_m2m_cancel_job() in v4l2_m2m_streamoff() and v4l2_m2m_ctx_release()
   only waits for TRANS_RUNNING to clear. Once mpeg12_job_done() clears
   TRANS_RUNNING, a concurrent VIDIOC_STREAMOFF or close() can run
   aml_mpeg12_exit() (freeing struct aml_mpeg12_ctx) and fops_vcodec_release()
   (freeing struct aml_vdec_ctx) while aml_mpeg12_dec_run() and
   m2mops_vdec_device_run() are still executing and accessing ctx, src_buf,
   dst_buf, and ctx->ctrl_handler.

2. If another decode job is queued, v4l2_m2m_schedule_next_job() can start the
   next job's m2mops_vdec_device_run() on the hardware before the waking
   aml_mpeg12_dec_run() thread reaches ctx->int_cond = 0 and
   aml_stop_vdec_hw(hw), halting the VDEC core in the middle of the new job.

3. In m2mops_vdec_device_run(), v4l2_m2m_buf_copy_metadata(src_buf, dst_buf)
   and v4l2_ctrl_request_complete(src_req, &ctx->ctrl_handler) run only after
   ctx->codec_ops->run(ctx) returns, so userspace can dequeue dst_buf before
   its metadata is copied, and src_req can be completed before
   v4l2_ctrl_request_complete() runs.

[ ... ]

> +void aml_mpeg12_exit(void *priv)
> +{
> +	struct aml_vdec_ctx *ctx = priv;
> +	struct aml_mpeg12_ctx *m = ctx->codec_priv;
> +	struct aml_vdec_hw *hw;
> +
> +	if (!m)
> +		return;
> +	hw = vdec_get_hw(ctx->dev);
> +
> +	if (ctx->dos_clk_en)
> +		aml_stop_vdec_hw(hw);

[Severity: High]
Could aml_mpeg12_exit() reset the shared VDEC core while another decoder
instance is actively decoding, or race with h264 IRQ handler registration?

Because m2mops_vdec_device_run() runs on m2m_dev->job_work without holding
dev->dev_mutex and blocks in wait_event_timeout(), a second instance (where
ctx->dos_clk_en was set during an earlier job) can call VIDIOC_STREAMOFF or
close() and invoke aml_mpeg12_exit(). Checking ctx->dos_clk_en without
verifying hw->curr_ctx == m will call aml_stop_vdec_hw(hw) and reset the
shared hardware while the first instance is mid-decode.

Similarly, aml_mpeg12_dec_run() sets hw->curr_ctx = m and
hw->hw_ops.irq_threaded_func = mpeg12_threaded_isr when a job starts, but
aml_h264_init() overwrites dec_hw->hw_ops.irq_handler and
dec_hw->hw_ops.irq_threaded_func during VIDIOC_STREAMON(OUTPUT). If a second
instance starts an H.264 stream while an MPEG-2 job is running on the
hardware, the MPEG-2 interrupt will invoke h264_threaded_isr_func() with
hw->curr_ctx pointing to struct aml_mpeg12_ctx.

> +	mpeg12_canvas_free(hw, m);
> +
> +	if (m->stage_vaddr)
> +		dma_free_coherent(hw->dev, m->stage_size,
> +				  m->stage_vaddr, m->stage_paddr);
> +	dma_free_coherent(hw->dev, MPEG12_WORKSPACE_SIZE,
> +			  m->wks_vaddr, m->wks_paddr);
> +	dma_free_coherent(hw->dev, MPEG12_CCBUF_SIZE,
> +			  m->cc_vaddr, m->cc_paddr);
> +	kvfree(m->fld_buf);
> +
> +	if (hw->curr_ctx == m)
> +		hw->curr_ctx = NULL;
> +	kfree(m);
> +	ctx->codec_priv = NULL;
> +}
> +
> +int aml_mpeg12_dec_run(void *priv)
> +{
> +	struct aml_vdec_ctx *ctx = priv;
> +	struct aml_mpeg12_ctx *m = ctx->codec_priv;

[Severity: High]
Can m (ctx->codec_priv) be NULL here if userspace restarts the capture queue?

In drivers/media/platform/amlogic/vdec/aml_vdec.c,
vb2ops_vdec_start_streaming() only calls aml_vdec_init_dec_inst(ctx) ->
aml_mpeg12_init(ctx) when starting the output queue, whereas
vb2ops_vdec_stop_streaming() unconditionally calls
aml_vdec_release_instance(ctx) -> aml_mpeg12_exit(ctx) when stopping either
queue.

If userspace calls VIDIOC_STREAMOFF and VIDIOC_STREAMON on the capture queue
while the output queue remains streaming, ctx->codec_priv is freed and set to
NULL, and the next aml_mpeg12_dec_run() call will dereference m when checking
m->fld_pending.

> +	struct aml_vdec_hw *hw = vdec_get_hw(ctx->dev);
> +	const struct v4l2_ctrl_mpeg2_sequence *seq;
> +	const struct v4l2_ctrl_mpeg2_picture *pic;
> +	const struct v4l2_ctrl_mpeg2_quantisation *quant;
> +	const struct v4l2_ctrl_mpeg2_picture *rpic;
> +	struct vb2_v4l2_buffer *src, *dst;
> +	u32 rec, ref0 = 0xffffffff, ref1 = 0xffffffff;
> +	u64 fwd;
> +	bool field, pair;
> +	long ret;
> +
> +	seq = mpeg12_ctrl(ctx, V4L2_CID_STATELESS_MPEG2_SEQUENCE);
> +	pic = mpeg12_ctrl(ctx, V4L2_CID_STATELESS_MPEG2_PICTURE);
> +	quant = mpeg12_ctrl(ctx, V4L2_CID_STATELESS_MPEG2_QUANTISATION);
> +	if (!seq || !pic || !quant)
> +		return -EINVAL;

[Severity: Medium]
When aml_mpeg12_dec_run() returns a negative error code with
ctx->job_finished == false (here, on -ENOMEM, or on -ETIMEDOUT), doesn't
m2mops_vdec_device_run() call v4l2_ctrl_request_complete() twice on src_req?

In drivers/media/platform/amlogic/vdec/aml_vdec.c:

drivers/media/platform/amlogic/vdec/aml_vdec.c:m2mops_vdec_device_run() {
	...
	ctx->job_finished = false;
	ret = ctx->codec_ops->run(ctx);

	v4l2_m2m_buf_copy_metadata(src_buf, dst_buf);
	if (src_req && ctx->curr_dec_type != CODEC_TYPE_AV1 &&
	    ctx->curr_dec_type != CODEC_TYPE_VP9 &&
	    ctx->curr_dec_type != CODEC_TYPE_HEVC &&
	    ctx->curr_dec_type != CODEC_TYPE_MJPEG)
		v4l2_ctrl_request_complete(src_req, &ctx->ctrl_handler);
	if (ret < 0 && !ctx->job_finished)
		goto err_cancel_job;

	return;

err_cancel_job:
	if (src_req)
		v4l2_ctrl_request_complete(src_req, &ctx->ctrl_handler);
	...
}

For CODEC_TYPE_MPEG2, the first v4l2_ctrl_request_complete() runs before the
ret < 0 && !ctx->job_finished check, and jumping to err_cancel_job calls
v4l2_ctrl_request_complete() a second time, triggering
WARN_ON(!hdl->request_is_queued) in v4l2-ctrls-request.c.

[ ... ]

> +	if (field && !pair &&
> +	    (src->flags & V4L2_BUF_FLAG_M2M_HOLD_CAPTURE_BUF)) {
> +		if (mpeg12_stash_field(ctx, seq, pic, quant, src) < 0)
> +			return -ENOMEM;
> +		dev_dbg(hw->dev, "mpeg12 stash field %u type %u\n",
> +			pic->picture_structure,
> +			 pic->picture_coding_type);
> +		/* the capture buffer stays held for the second field */
> +		ctx->int_cond = 1;
> +		ctx->job_finished = true;
> +		v4l2_m2m_buf_done_and_job_finish(ctx->dev->m2m_dev_dec,
> +						 ctx->m2m_ctx,
> +						 VB2_BUF_STATE_DONE);
> +		return 0;
> +	}

[ ... ]

> +	rpic = pic;
> +	if (pair && m->fld_pic.picture_coding_type !=
> +		    V4L2_MPEG2_PIC_CODING_TYPE_I)
> +		rpic = &m->fld_pic;
> +	fwd = rpic->forward_ref_ts;
> +	if (fwd == dst->vb2_buf.timestamp && m->anchor_valid)
> +		fwd = m->anchor_ts;
> +	if (rpic->picture_coding_type == V4L2_MPEG2_PIC_CODING_TYPE_P) {
> +		ref1 = mpeg12_ref_spec(ctx, &m->canvas[2], fwd);
> +		ref0 = ref1;
> +	} else if (rpic->picture_coding_type == V4L2_MPEG2_PIC_CODING_TYPE_B) {
> +		ref0 = mpeg12_ref_spec(ctx, &m->canvas[1], fwd);
> +		ref1 = mpeg12_ref_spec(ctx, &m->canvas[2],
> +				       rpic->backward_ref_ts);
> +	}
> +	if (rpic->picture_coding_type != V4L2_MPEG2_PIC_CODING_TYPE_B) {
> +		m->anchor_ts = dst->vb2_buf.timestamp;
> +		m->anchor_valid = true;
> +	}

[Severity: High]
Isn't dst->vb2_buf.timestamp still 0 here because
v4l2_m2m_buf_copy_metadata(src_buf, dst_buf) is only called in
m2mops_vdec_device_run() after ctx->codec_ops->run(ctx) returns?

On any frame-coded picture or non-paired field, dst->vb2_buf.timestamp has not
yet been copied from src->vb2_buf.timestamp:

1. For frame-coded I/P pictures, m->anchor_ts = dst->vb2_buf.timestamp always
   records 0 instead of the picture's timestamp. When a later I/P field pair
   predicts from its first field, fwd is replaced with 0 rather than the
   previous anchor frame's timestamp.

2. On any non-paired P or B picture whose forward_ref_ts is legitimately 0,
   fwd == dst->vb2_buf.timestamp evaluates to true (0 == 0) and overwrites
   fwd with m->anchor_ts.

3. In the first-field stash path above, v4l2_m2m_buf_done_and_job_finish()
   sets dst_buf->is_held = true and runs __v4l2_m2m_try_queue() before
   v4l2_m2m_buf_copy_metadata() sets dst->vb2_buf.copied_timestamp = 1 and
   dst->vb2_buf.timestamp, bypassing __v4l2_m2m_try_queue()'s held-buffer
   timestamp mismatch check if the next output buffer is already queued.

> +	regmap_write(hw->map[DOS_BUS], MREG_REF0, ref0);
> +	regmap_write(hw->map[DOS_BUS], MREG_REF1, ref1);

[Severity: Medium]
Should ref0 and ref1 be checked before writing MREG_REF0 and MREG_REF1?

mpeg12_ref_spec() returns 0xffffffff (~0u) both when vb2_find_buffer() cannot
find the reference timestamp (for example on open-GOP B-frames after a seek)
and when meson_canvas_alloc() fails inside mpeg12_canvas_setup(). Unlike rec,
which is checked for ~0u, ref0 and ref1 are written directly to MREG_REF0 and
MREG_REF1 as 0xffffffff without failing on canvas allocation errors or falling
back to a valid canvas such as rec when a reference buffer is absent.

[ ... ]

> +	/* Uninterruptible: see the note in vp9.c. */
> +	ret = wait_event_timeout(ctx->queue, ctx->int_cond,
> +				 msecs_to_jiffies(MPEG12_TIMEOUT_MS));
> +	ctx->int_cond = 0;
> +	if (!ret) {
> +		dev_err(hw->dev,
> +			"mpeg12 timeout: out %x info %x pc %x lvl %x rp %x wp %x bitcnt %x err %x\n",
> +			read_dos_reg(hw, MREG_BUFFEROUT),
> +			read_dos_reg(hw, MREG_PIC_INFO),
> +			read_dos_reg(hw, MPC_E),
> +			read_dos_reg(hw, VLD_MEM_VIFIFO_LEVEL),
> +			read_dos_reg(hw, VLD_MEM_VIFIFO_RP),
> +			read_dos_reg(hw, VLD_MEM_VIFIFO_WP),
> +			read_dos_reg(hw, VIFF_BIT_CNT),
> +			read_dos_reg(hw, MREG_ERROR_COUNT));
> +		aml_stop_vdec_hw(hw);
> +		mpeg12_canvas_free(hw, m);
> +		return -ETIMEDOUT;
> +	}

[Severity: High]
Can the timeout path here and aml_mpeg12_exit() race with an in-flight
mpeg12_threaded_isr()?

If the hardware interrupt fires just as wait_event_timeout() expires,
mpeg12_threaded_isr() can run concurrently with the timeout error path because
VDEC_ASSIST_MBOX1_MASK is not cleared and synchronize_irq(hw->dec_irq) is not
called before mpeg12_canvas_free(hw, m) and returning -ETIMEDOUT:

1. Both mpeg12_job_done() and the timeout path call mpeg12_canvas_free(hw, m)
   concurrently without locking, which can free the same meson canvases twice
   or pass (u8)-1 to meson_canvas_free().

2. Both mpeg12_job_done() and m2mops_vdec_device_run()'s err_cancel_job path
   can call v4l2_m2m_buf_done_and_job_finish() for the same job.

3. In aml_mpeg12_exit(), VDEC_ASSIST_MBOX1_MASK is not cleared and
   synchronize_irq(hw->dec_irq) is not called before kfree(m), so an in-flight
   mpeg12_threaded_isr() that read hw->curr_ctx before it was cleared can
   dereference m after it has been freed.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261025192507.210058-1-christian.koever-draxl@student.uibk.ac.at?part=5

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

* Re: [PATCH 10/11] media: amlogic: vdec: add MPEG-4 Part 2 decoding
  2026-10-25 19:25 ` [PATCH 10/11] media: amlogic: vdec: add MPEG-4 Part 2 decoding christian.koever-draxl
@ 2026-10-25 20:39   ` sashiko-bot
  0 siblings, 0 replies; 20+ messages in thread
From: sashiko-bot @ 2026-10-25 20:39 UTC (permalink / raw)
  To: christian.koever-draxl; +Cc: devicetree, robh, conor+dt, media-ci

Thank you for your contribution! Sashiko AI review found 8 potential issue(s) to consider:
- [Critical] media: amlogic: vdec: out-of-bounds DMA setup in mpeg4_canvas_setup()
- [Critical] media: amlogic: vdec: unvalidated VOL dimensions in mpeg4_threaded_isr()
- [High] media: amlogic: vdec: premature job completion in mpeg4_job_done()
- [High] media: amlogic: vdec: mpeg4_threaded_isr() races with timeout and exit paths
- [High] media: amlogic: vdec: NULL deref and u32 overflow in mpeg4_prepare_input()
- [High] media: amlogic: vdec: unchecked mpeg4_ref_spec() in aml_mpeg4_dec_run()
- [High] media: amlogic: vdec: NULL codec_priv dereference in aml_mpeg4_dec_run()
- [Medium] media: amlogic: vdec: stateful V4L2_PIX_FMT_MPEG4 used with private control
--

commit 16518bdbcc7ac5cd882698a45b99c4af42c31b6c
Author: Christian Stefan Kövér-Draxl <ckoever@outlook.de>

media: amlogic: vdec: add MPEG-4 Part 2 decoding

This commit adds MPEG-4 Part 2 Simple and Advanced Simple profile decoding
support on the Amlogic legacy VDEC core using the M45M microcode. It
exposes V4L2_PIX_FMT_MPEG4 on a request-based queue and passes reference
timestamps via the driver-private V4L2_CID_AML_MPEG4_REFS control.

> diff --git a/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c b/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c
> index 36ae12e6cdd82..f8e2d0bef3fe4 100644
> --- a/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c
> +++ b/drivers/media/platform/amlogic/vdec/aml_vdec_platform.c
[ ... ]
> @@ -68,6 +69,17 @@ static struct aml_video_fmt aml_s4_video_formats[] = {
>  		.stepwise = {AML_VDEC_MIN_W, AML_VDEC_1080P_MAX_W, 2,
>  			AML_VDEC_MIN_H, AML_VDEC_1080P_MAX_H, 2},
>  	},
> +	{
> +		.name = "MPEG-4",
> +		.fourcc = V4L2_PIX_FMT_MPEG4,
> +		.type = AML_FMT_DEC,
> +		.align = 64,
> +		.is_10_bit_support = 0,
> +		.codec_type = CODEC_TYPE_MPEG4,
> +		.num_planes = 1,
> +		.stepwise = {AML_VDEC_MIN_W, AML_VDEC_1080P_MAX_W, 2,
> +			AML_VDEC_MIN_H, AML_VDEC_1080P_MAX_H, 2},
> +	},

[Severity: Medium]
When a standard V4L2 application enumerates formats via VIDIOC_ENUM_FMT and
sees V4L2_PIX_FMT_MPEG4 in aml_s4_video_formats[], will it treat the device as
a standard stateful decoder and omit the driver-private V4L2_CID_AML_MPEG4_REFS
control, causing all P-VOPs and B-VOPs to run with MP4_REF0 and MP4_REF1 set
to 0xffffffff?

Also, V4L2_CID_AML_MPEG4_REFS in drivers/media/platform/amlogic/vdec/mpeg4.h
uses an unreserved offset (V4L2_CID_USER_BASE + 0x1f00) in a driver-internal
header rather than a reserved base in include/uapi/linux/v4l2-controls.h.
Should this wait for a dedicated stateless pixel format and reserved control
IDs in the UAPI headers?

> diff --git a/drivers/media/platform/amlogic/vdec/mpeg4.c b/drivers/media/platform/amlogic/vdec/mpeg4.c
> new file mode 100644
> index 0000000000000..9cc97fa462091
> --- /dev/null
> +++ b/drivers/media/platform/amlogic/vdec/mpeg4.c
[ ... ]
> +/* Returns the canvas spec (y | uv << 8 | uv << 16) for the buffer, or ~0. */
> +static u32 mpeg4_canvas_setup(struct aml_vdec_ctx *ctx,
> +			      struct mpeg4_canvas *c, struct vb2_buffer *vb)
> +{
> +	struct aml_vdec_hw *hw = vdec_get_hw(ctx->dev);
> +	u32 w = ctx->pic_info.coded_width;
> +	u32 h = ctx->pic_info.coded_height;
> +	dma_addr_t y, uv;
> +	u8 yi, ci;
> +
> +	y = vb2_dma_contig_plane_dma_addr(vb, 0);
> +	if (ctx->pic_info.plane_num > 1)
> +		uv = vb2_dma_contig_plane_dma_addr(vb, 1);
> +	else
> +		uv = y + ctx->pic_info.fb_size[0];

[Severity: Critical]
Can mpeg4_canvas_setup() program the hardware to DMA out of bounds when a
2-plane capture format (V4L2_PIX_FMT_NV12M or V4L2_PIX_FMT_NV21M) is selected,
or when the CAPTURE queue dimensions are smaller than the OUTPUT queue
dimensions?

In drivers/media/platform/amlogic/vdec/aml_vdec.c, set_pic_info() is only
called for the OUTPUT queue and hardcodes plane_num to 1:

drivers/media/platform/amlogic/vdec/aml_vdec.c:set_pic_info() {
	if (V4L2_TYPE_IS_OUTPUT(type)) {
		ctx->pic_info.output_pix_fmt = pix_mp->pixelformat;
		ctx->pic_info.coded_width = ALIGN(pix_mp->width, 64);
		ctx->pic_info.coded_height = ALIGN(pix_mp->height, 64);
		ctx->pic_info.fb_size[0] =
		    ctx->pic_info.coded_width * ctx->pic_info.coded_height;
		ctx->pic_info.fb_size[1] = ctx->pic_info.fb_size[0] / 2;
		ctx->pic_info.plane_num = 1;
	}
}

Because vdec_s_fmt_capture() never updates ctx->pic_info,
ctx->pic_info.plane_num > 1 is always false. When userspace configures NV12M
or NV21M (which mpeg4_restore_hw() explicitly checks for),
vb2ops_vdec_queue_setup() allocates plane 0 for luma only (w * h bytes), but
mpeg4_canvas_setup() sets uv = y + ctx->pic_info.fb_size[0] instead of reading
plane 1, causing the VDEC hardware to DMA w * h / 2 bytes of chroma past the
end of plane 0.

Similarly, if VIDIOC_S_FMT on the CAPTURE queue configures smaller dimensions
than the OUTPUT queue, mpeg4_canvas_setup() still uses
ctx->pic_info.coded_width and ctx->pic_info.coded_height from the OUTPUT
format. Should mpeg4_canvas_setup() check vb->num_planes > 1 and use
ctx->pix_fmt[AML_FMT_DST] for the capture buffer dimensions and stride?

[ ... ]
> +static int mpeg4_prepare_input(struct aml_vdec_ctx *ctx,
> +			       struct vb2_v4l2_buffer *src)
> +{
> +	struct aml_vdec_hw *hw = vdec_get_hw(ctx->dev);
> +	struct aml_mpeg4_ctx *m = ctx->codec_priv;
> +	struct vb2_buffer *vb = &src->vb2_buf;
> +	u32 len = vb2_get_plane_payload(vb, 0);
> +	u32 need = len + MPEG4_TAIL + VLD_PADDING_SIZE + 64;
> +	u8 *p;
> +
> +	if (need > m->stage_size) {
> +		if (m->stage_vaddr)
> +			dma_free_coherent(hw->dev, m->stage_size,
> +					  m->stage_vaddr, m->stage_paddr);
> +		m->stage_size = PAGE_ALIGN(need + SZ_64K);
> +		m->stage_vaddr = dma_alloc_coherent(hw->dev, m->stage_size,
> +						    &m->stage_paddr, GFP_KERNEL);
> +		if (!m->stage_vaddr) {
> +			m->stage_size = 0;
> +			return -ENOMEM;
> +		}
> +	}
> +
> +	p = m->stage_vaddr;
> +	memcpy(p, vb2_plane_vaddr(vb, 0), len);
> +	/* The microcode only ends the VOP on the next start code. */
> +	p[len] = 0;
> +	p[len + 1] = 0;
> +	p[len + 2] = 1;
> +	p[len + 3] = 0xb6;
> +	memset(p + len + MPEG4_TAIL, 0, VLD_PADDING_SIZE);
[ ... ]
> +	regmap_write(hw->map[DOS_BUS], VIFF_BIT_CNT, len * 8);
> +
> +	return 0;
> +}

[Severity: High]
What happens in mpeg4_prepare_input() if userspace queues a V4L2_MEMORY_DMABUF
buffer on the OUTPUT queue (which enables VB2_DMABUF in aml_vdec_queue_init())
and vb2_plane_vaddr(vb, 0) returns NULL because the exporter does not support
vmap or mapping fails? Passing the unchecked return value of
vb2_plane_vaddr(vb, 0) directly to memcpy() will dereference a NULL pointer,
unlike h264_stage_with_headers(), h265_feed(), and aml_vp9_feed() which check
for NULL first.

Also, can the 32-bit u32 calculations for need, PAGE_ALIGN(need + SZ_64K), and
len * 8 in VIFF_BIT_CNT overflow if a large plane payload is passed?

> +static void mpeg4_job_done(struct aml_vdec_ctx *ctx, enum vb2_buffer_state st)
> +{
> +	struct aml_vdec_dev *dev = ctx->dev;
> +
> +	mpeg4_canvas_free(dev->dec_hw, ctx->codec_priv);
> +	ctx->int_cond = 1;
> +	ctx->job_finished = true;
> +	v4l2_m2m_buf_done_and_job_finish(dev->m2m_dev_dec, ctx->m2m_ctx, st);
> +	wake_up(&ctx->queue);
> +}

[Severity: High]
Does calling v4l2_m2m_buf_done_and_job_finish() inside mpeg4_job_done() from
mpeg4_threaded_isr() finish the M2M job prematurely while aml_mpeg4_dec_run()
and m2mops_vdec_device_run() are still executing?

In drivers/media/platform/amlogic/vdec/aml_vdec.c, m2mops_vdec_device_run()
runs metadata copy and request completion after ctx->codec_ops->run(ctx)
returns:

drivers/media/platform/amlogic/vdec/aml_vdec.c:m2mops_vdec_device_run() {
	ctx->job_finished = false;
	ret = ctx->codec_ops->run(ctx);

	v4l2_m2m_buf_copy_metadata(src_buf, dst_buf);
	if (src_req && ctx->curr_dec_type != CODEC_TYPE_AV1 &&
	    ctx->curr_dec_type != CODEC_TYPE_VP9 &&
	    ctx->curr_dec_type != CODEC_TYPE_HEVC &&
	    ctx->curr_dec_type != CODEC_TYPE_MJPEG)
		v4l2_ctrl_request_complete(src_req, &ctx->ctrl_handler);
	...
err_cancel_job:
	if (src_req && ctx->curr_dec_type != CODEC_TYPE_AV1 &&
	    ctx->curr_dec_type != CODEC_TYPE_VP9 &&
	    ctx->curr_dec_type != CODEC_TYPE_HEVC &&
	    ctx->curr_dec_type != CODEC_TYPE_MJPEG)
		v4l2_ctrl_request_complete(src_req, &ctx->ctrl_handler);

	if (!ctx->job_finished)
		v4l2_m2m_buf_done_and_job_finish(dev->m2m_dev_dec,
						 ctx->m2m_ctx,
						 VB2_BUF_STATE_ERROR);
}

Because mpeg4_job_done() calls v4l2_m2m_buf_done_and_job_finish() in the IRQ
thread before waking ctx->queue, several problems arise:

1. Unlike AV1, VP9, HEVC, and MJPEG, CODEC_TYPE_MPEG4 does not call
   v4l2_ctrl_request_complete() before v4l2_m2m_buf_done_and_job_finish() and
   is not excluded in m2mops_vdec_device_run(). When an MPEG-4 request carries
   no controls (such as an I-VOP), src_buf is the only object in src_req, so
   v4l2_m2m_buf_done_and_job_finish() completes src_req and drops its
   reference. When aml_mpeg4_dec_run() returns, v4l2_ctrl_request_complete()
   attempts to bind to an already-completed (or freed) src_req, triggering a
   WARN_ON in media_request_object_bind() or a use-after-free. Conversely, if
   aml_mpeg4_dec_run() fails with -ENOMEM or -ETIMEDOUT before setting
   ctx->job_finished, m2mops_vdec_device_run() calls
   v4l2_ctrl_request_complete() twice on src_req (once after run() and again at
   err_cancel_job).

2. dst_buf is marked done and placed on done_list before
   v4l2_m2m_buf_copy_metadata(src_buf, dst_buf) runs, so userspace VIDIOC_DQBUF
   and subsequent mpeg4_ref_spec() -> vb2_find_buffer() lookups can see dst_buf
   before its timestamp and copied_timestamp flag are set.

3. v4l2_m2m_buf_done_and_job_finish() finishes the M2M job and wakes
   v4l2_m2m_cancel_job() (or schedules the next M2M job) before
   aml_mpeg4_dec_run() executes ctx->int_cond = 0 and aml_stop_vdec_hw(hw), and
   before mpeg4_threaded_isr() clears MP4_BUFFEROUT. A concurrent
   VIDIOC_STREAMOFF or close() can free m in aml_mpeg4_exit() and ctx in
   fops_vcodec_release() while aml_mpeg4_dec_run() and
   m2mops_vdec_device_run() are still accessing them, or the next job can start
   while aml_stop_vdec_hw(hw) is still halting the hardware.

[ ... ]
> +static irqreturn_t mpeg4_threaded_isr(int irq, void *priv)
> +{
> +	struct aml_vdec_dev *dev = priv;
> +	struct aml_vdec_hw *hw = dev->dec_hw;
> +	struct aml_mpeg4_ctx *m = hw->curr_ctx;
> +	struct aml_vdec_ctx *ctx;
> +	u32 reg;
> +
> +	if (!m)
> +		return IRQ_HANDLED;
> +	ctx = m->v4l2_ctx;
> +	if (ctx->job_finished)
> +		return IRQ_HANDLED;
> +
> +	/* Picture size report: the buffers are already set up, go on. */
> +	if (read_dos_reg(hw, MP4_PIC_INFO) == 1) {
> +		dev_dbg(hw->dev, "mpeg4 size %ux%u\n",
> +			read_dos_reg(hw, MP4_PIC_WH) >> 16,
> +			 read_dos_reg(hw, MP4_PIC_WH) & 0xffff);
> +		regmap_write(hw->map[DOS_BUS], MP4_PIC_INFO, 0);
> +		return IRQ_HANDLED;
> +	}

[Severity: Critical]
When the microcode parses the VOL header on the first frame and reports the
bitstream dimensions via MP4_PIC_INFO == 1, what prevents a bitstream with
dimensions in MP4_PIC_WH larger than the negotiated capture buffer (or larger
than the 1080p workspace limit) from overflowing the capture buffer via DMA?

In mpeg4_threaded_isr(), MP4_PIC_INFO is cleared to resume decoding without
checking MP4_PIC_WH against ctx->pix_fmt[AML_FMT_DST] (unlike
mjpeg_threaded_isr(), which checks w > pf->plane_fmt[0].bytesperline ||
h > pf->height and aborts the job with VB2_BUF_STATE_ERROR), and
mpeg4_save_hw_context() saves that unvalidated MP4_PIC_WH into m->reg_pic_wh
for subsequent frames.

[ ... ]
> +void aml_mpeg4_exit(void *priv)
> +{
> +	struct aml_vdec_ctx *ctx = priv;
> +	struct aml_mpeg4_ctx *m = ctx->codec_priv;
> +	struct aml_vdec_hw *hw;
> +
> +	if (!m)
> +		return;
> +	hw = vdec_get_hw(ctx->dev);
> +
> +	if (ctx->dos_clk_en)
> +		aml_stop_vdec_hw(hw);
> +	mpeg4_canvas_free(hw, m);
> +
> +	if (m->stage_vaddr)
> +		dma_free_coherent(hw->dev, m->stage_size,
> +				  m->stage_vaddr, m->stage_paddr);
> +	dma_free_coherent(hw->dev, MPEG4_WORKSPACE_SIZE,
> +			  m->wks_vaddr, m->wks_paddr);
> +
> +	if (hw->curr_ctx == m)
> +		hw->curr_ctx = NULL;
> +	kfree(m);
> +	ctx->codec_priv = NULL;
> +}
> +
> +int aml_mpeg4_dec_run(void *priv)
> +{
> +	struct aml_vdec_ctx *ctx = priv;
> +	struct aml_mpeg4_ctx *m = ctx->codec_priv;
> +	struct aml_vdec_hw *hw = vdec_get_hw(ctx->dev);
> +	struct v4l2_ctrl *refs_ctrl;
> +	struct vb2_v4l2_buffer *src, *dst;
> +	u32 rec, ref0 = 0xffffffff, ref1 = 0xffffffff;
> +	u64 fwd = 0, bwd = 0;
> +	long ret;
> +
> +	src = v4l2_m2m_next_src_buf(ctx->fh.m2m_ctx);
> +	dst = v4l2_m2m_next_dst_buf(ctx->fh.m2m_ctx);
> +	if (!src || !dst)
> +		return -EINVAL;
> +
> +	refs_ctrl = v4l2_ctrl_find(&ctx->ctrl_handler, V4L2_CID_AML_MPEG4_REFS);
> +	if (refs_ctrl) {
> +		fwd = refs_ctrl->p_cur.p_s64[0];
> +		bwd = refs_ctrl->p_cur.p_s64[1];
> +	}
> +
> +	hw->curr_ctx = m;
> +	hw->hw_ops.irq_handler = mpeg4_isr;
> +	hw->hw_ops.irq_threaded_func = mpeg4_threaded_isr;
> +
> +	if (mpeg4_prepare_input(ctx, src) < 0)
> +		return -ENOMEM;

[Severity: High]
Can ctx->codec_priv (m) be NULL when aml_mpeg4_dec_run() is called?

In drivers/media/platform/amlogic/vdec/aml_vdec.c, vb2ops_vdec_start_streaming()
only calls aml_vdec_init_dec_inst() -> aml_mpeg4_init() when starting the
OUTPUT queue, whereas vb2ops_vdec_stop_streaming() calls
aml_vdec_release_instance() -> aml_mpeg4_exit() when stopping either queue. If
userspace stops and restarts the CAPTURE queue while the OUTPUT queue remains
streaming, ctx->codec_priv is NULL and mpeg4_prepare_input() and
mpeg4_restore_hw() will dereference a NULL pointer.

> +
> +	mpeg4_restore_hw(ctx);
> +
> +	rec = mpeg4_canvas_setup(ctx, &m->canvas[0], &dst->vb2_buf);
> +	if (rec == ~0u)
> +		goto fail;
> +
> +	/*
> +	 * REF0 is the older anchor, REF1 the newer one; a P-VOP predicts from
> +	 * REF1, a B-VOP from both.  0 means "none" in the control.
> +	 */
> +	if (fwd && bwd) {
> +		ref0 = mpeg4_ref_spec(ctx, &m->canvas[1], fwd);
> +		ref1 = mpeg4_ref_spec(ctx, &m->canvas[2], bwd);
> +	} else if (fwd) {
> +		ref1 = mpeg4_ref_spec(ctx, &m->canvas[2], fwd);
> +		ref0 = ref1;
> +	}
> +	regmap_write(hw->map[DOS_BUS], MP4_REF0, ref0);
> +	regmap_write(hw->map[DOS_BUS], MP4_REF1, ref1);

[Severity: High]
If vb2_find_buffer() does not find the requested timestamp in mpeg4_ref_spec(),
if meson_canvas_alloc() fails while setting up a reference canvas, or if a
P-VOP or B-VOP is submitted with fwd == 0, ref0 and ref1 remain 0xffffffff
(~0u). Won't writing 0xffffffff to MP4_REF0 and MP4_REF1 cause the hardware to
fetch motion-compensation reference data from unallocated DMC canvas 0xff
instead of failing the job or falling back to rec?

[ ... ]
> +	ret = wait_event_timeout(ctx->queue, ctx->int_cond,
> +				 msecs_to_jiffies(MPEG4_TIMEOUT_MS));
> +	ctx->int_cond = 0;
> +	if (!ret) {
> +		dev_err(hw->dev,
> +			"mpeg4 timeout: out %x info %x pc %x lvl %x rp %x wp %x bitcnt %x err %x\n",
> +			read_dos_reg(hw, MP4_BUFFEROUT),
> +			read_dos_reg(hw, MP4_PIC_INFO),
> +			read_dos_reg(hw, MPC_E),
> +			read_dos_reg(hw, VLD_MEM_VIFIFO_LEVEL),
> +			read_dos_reg(hw, VLD_MEM_VIFIFO_RP),
> +			read_dos_reg(hw, VLD_MEM_VIFIFO_WP),
> +			read_dos_reg(hw, VIFF_BIT_CNT),
> +			read_dos_reg(hw, MP4_ERR_COUNT));
> +		aml_stop_vdec_hw(hw);
> +		mpeg4_canvas_free(hw, m);
> +		return -ETIMEDOUT;
> +	}

[Severity: High]
Can the timeout path in aml_mpeg4_dec_run() and the teardown path in
aml_mpeg4_exit() race with mpeg4_threaded_isr()?

If the interrupt fires right as wait_event_timeout() expires, mpeg4_isr() wakes
mpeg4_threaded_isr(). Because the timeout path does not mask
VDEC_ASSIST_MBOX1_MASK or call synchronize_irq(hw->dec_irq) before running
mpeg4_canvas_free(hw, m) and returning -ETIMEDOUT, mpeg4_threaded_isr() can run
concurrently on another CPU:

1. Both mpeg4_job_done() and the timeout path call mpeg4_canvas_free(hw, m)
   without locking, reading m->canvas[i].y >= 0 and m->canvas[i].uv >= 0 before
   the other sets them to -1 and calling meson_canvas_free() twice on the same
   canvas indices.

2. Concurrently, mpeg4_job_done() sets ctx->job_finished = true and calls
   v4l2_m2m_buf_done_and_job_finish() while m2mops_vdec_device_run() takes the
   err_cancel_job path and also calls v4l2_m2m_buf_done_and_job_finish().

3. In aml_mpeg4_exit(), VDEC_ASSIST_MBOX1_MASK is not cleared and
   synchronize_irq(hw->dec_irq) is not called before freeing m->stage_vaddr,
   m->wks_vaddr, and m itself, so an in-flight mpeg4_threaded_isr() that
   already read m = hw->curr_ctx can dereference m after kfree(m).

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261025192507.210058-1-christian.koever-draxl@student.uibk.ac.at?part=10

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

* Re: [PATCH 00/11] media: amlogic: vdec: S4 decoding beyond H.264 Baseline
  2026-10-25 19:24 [PATCH 00/11] media: amlogic: vdec: S4 decoding beyond H.264 Baseline christian.koever-draxl
                   ` (10 preceding siblings ...)
  2026-10-25 19:25 ` [PATCH 11/11] media: amlogic: vdec: give the regmaps their own names christian.koever-draxl
@ 2026-10-26  5:53 ` Zhentao Guo
  11 siblings, 0 replies; 20+ messages in thread
From: Zhentao Guo @ 2026-10-26  5:53 UTC (permalink / raw)
  To: christian.koever-draxl, mchehab, robh, krzk+dt, conor+dt
  Cc: linux-media, devicetree, linux-kernel,
	Christian Stefan Kövér-Draxl

Hi Christian,

Very glad to see your contributions, I went through your patches this 
morning, the way you added support for other codecs is almost exactly 
what I had imagined. It seems that some of the code you added isn’t 
actually necessary for a stateless decoder (like the H264_CONFIG_REQUEST 
process in the h264 driver). You probably added it because you weren’t 
sure how the firmware behaves and assumed it was required. I have some 
feedback for you, hopefully I can get these ready this week.

To sync with you:

1.The H264 Main & high profiles are supported, please see: 
https://lore.kernel.org/linux-media/20260924-b4-s4-vdec-upstream-v10-0-1289303a58a4@amlogic.com/

2. MAINTAINERS path was fixed in patch v8, please see: 
https://lore.kernel.org/linux-media/20260831-b4-s4-vdec-upstream-v8-4-830195b9bd91@amlogic.com/

Regards,

Zhentao

> From: Christian Stefan Kövér-Draxl <ckoever@outlook.de>
>
> This builds on Zhentao Guo's "[PATCH v6 0/4] Add Amlogic V4L2 stateless
> decoder" and makes the S4 decoder usable for real streams:
>
>   1-2:  MAINTAINERS path fix, binding update for the second (HEVC) core;
>   3:    microcode loading that the secure world accepts, HEVC core setup;
>   4:    H.264 Main and High profile, bit-exact;
>   5-6:  MPEG-1/2 and MJPEG on the VDEC core;
>   7-9:  AV1, VP9 (profile 0 and 2) and HEVC (Main, Main 10) on the HEVC
>         core, up to 4K;
>   10:   MPEG-4 Part 2 SP/ASP;
>   11:   regmap naming.
>
> Bit-exactness was checked against ffmpeg/libavcodec frame by frame for
> H.264, VP9, AV1 and HEVC conformance and real-world streams; MPEG-2
> differs by the hardware's IDCT rounding. Tested with GStreamer
> v4l2sl*dec and a VA-API backend (Firefox) on an S905W2.
>
> Depends on Zhentao's v6 series (not resent).
>
> Tool use (Documentation/process/generated-content.rst):
> This work was done with the help of an AI coding assistant, Claude
> (Anthropic), used through Claude Code over many sessions. It did a large
> part of the hands-on work: it read the vendor kernel and the firmware
> interfaces, probed registers on the board, drafted the code and the
> changelogs, and ran the builds, checkpatch, dt_binding_check and the
> tests on the hardware. I directed the work, reviewed every change and
> the final patches, and take responsibility for them. There is no single
> prompt to quote; the sessions were of the form "find out why X fails on
> this board, fix it, test it on the box".
>
> Tested on an Amediatech X98Q TV box (Amlogic S905W2, S4 family, 2 GiB),
> vendor U-Boot and BL31/OP-TEE, Linux 7.3 plus this work.
>
> Christian Stefan Kövér-Draxl (11):
>    MAINTAINERS: fix the Amlogic VDEC binding path
>    dt-bindings: media: amlogic,s4-vdec: describe both decoder cores
>    media: amlogic: vdec: fix the microcode loading and prepare the HEVC
>      core
>    media: amlogic: vdec: h264: decode Main and High profile bit-exact
>    media: amlogic: vdec: add MPEG-1/2 decoding
>    media: amlogic: vdec: add MJPEG decoding
>    media: amlogic: vdec: add AV1 decoding on the HEVC core
>    media: amlogic: vdec: add VP9 decoding on the HEVC core
>    media: amlogic: vdec: add HEVC decoding on the HEVC core
>    media: amlogic: vdec: add MPEG-4 Part 2 decoding
>    media: amlogic: vdec: give the regmaps their own names
>
>   .../bindings/media/amlogic,s4-vdec.yaml       |   24 +-
>   MAINTAINERS                                   |    2 +-
>   drivers/media/platform/amlogic/vdec/Kconfig   |    1 +
>   drivers/media/platform/amlogic/vdec/Makefile  |    2 +-
>   .../media/platform/amlogic/vdec/aml_vdec.c    |  157 +-
>   .../platform/amlogic/vdec/aml_vdec_drv.h      |   21 +
>   .../media/platform/amlogic/vdec/aml_vdec_hw.c |  178 +-
>   .../media/platform/amlogic/vdec/aml_vdec_hw.h |   64 +
>   .../platform/amlogic/vdec/aml_vdec_platform.c |  129 +-
>   .../platform/amlogic/vdec/aml_vdec_tee_fw.c   |  102 +-
>   drivers/media/platform/amlogic/vdec/av1.c     | 3140 +++++++++++++++++
>   drivers/media/platform/amlogic/vdec/av1.h     |  308 ++
>   drivers/media/platform/amlogic/vdec/h264.c    |  477 ++-
>   drivers/media/platform/amlogic/vdec/h264.h    |    7 +
>   .../platform/amlogic/vdec/h264_headers.c      |  249 ++
>   drivers/media/platform/amlogic/vdec/hevc.c    | 1728 +++++++++
>   drivers/media/platform/amlogic/vdec/hevc.h    |  125 +
>   drivers/media/platform/amlogic/vdec/mjpeg.c   |  471 +++
>   drivers/media/platform/amlogic/vdec/mjpeg.h   |   33 +
>   drivers/media/platform/amlogic/vdec/mpeg12.c  |  708 ++++
>   drivers/media/platform/amlogic/vdec/mpeg12.h  |   51 +
>   drivers/media/platform/amlogic/vdec/mpeg4.c   |  480 +++
>   drivers/media/platform/amlogic/vdec/mpeg4.h   |   52 +
>   .../media/platform/amlogic/vdec/reg_defines.h |   24 +
>   drivers/media/platform/amlogic/vdec/vp9.c     | 2004 +++++++++++
>   drivers/media/platform/amlogic/vdec/vp9.h     |  149 +
>   .../media/platform/amlogic/vdec/vp9_prob.c    |  213 ++
>   .../media/platform/amlogic/vdec/vp9_prob.h    |   32 +
>   28 files changed, 10847 insertions(+), 84 deletions(-)
>   create mode 100644 drivers/media/platform/amlogic/vdec/av1.c
>   create mode 100644 drivers/media/platform/amlogic/vdec/av1.h
>   create mode 100644 drivers/media/platform/amlogic/vdec/h264_headers.c
>   create mode 100644 drivers/media/platform/amlogic/vdec/hevc.c
>   create mode 100644 drivers/media/platform/amlogic/vdec/hevc.h
>   create mode 100644 drivers/media/platform/amlogic/vdec/mjpeg.c
>   create mode 100644 drivers/media/platform/amlogic/vdec/mjpeg.h
>   create mode 100644 drivers/media/platform/amlogic/vdec/mpeg12.c
>   create mode 100644 drivers/media/platform/amlogic/vdec/mpeg12.h
>   create mode 100644 drivers/media/platform/amlogic/vdec/mpeg4.c
>   create mode 100644 drivers/media/platform/amlogic/vdec/mpeg4.h
>   create mode 100644 drivers/media/platform/amlogic/vdec/vp9.c
>   create mode 100644 drivers/media/platform/amlogic/vdec/vp9.h
>   create mode 100644 drivers/media/platform/amlogic/vdec/vp9_prob.c
>   create mode 100644 drivers/media/platform/amlogic/vdec/vp9_prob.h
>
>
> base-commit: 7be4bc52fd9e0e55dc9a2e0232b89427d2c4b614
> prerequisite-patch-id: 106aac9a55fa72f68fe458d405a69a66678c7040
> prerequisite-patch-id: ed3f15728a5c616aca82b5086dc924adba315993
> prerequisite-patch-id: 6845fd22135330ee201405f38d5c2a72bbe9d452
> prerequisite-patch-id: 626eaa3d309664901ba13acc331d327a4ddc64c1
> --
> 2.56.0

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

end of thread, other threads:[~2026-10-26  5:53 UTC | newest]

Thread overview: 20+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-25 19:24 [PATCH 00/11] media: amlogic: vdec: S4 decoding beyond H.264 Baseline christian.koever-draxl
2026-10-25 19:24 ` [PATCH 01/11] MAINTAINERS: fix the Amlogic VDEC binding path christian.koever-draxl
2026-10-25 19:24 ` [PATCH 02/11] dt-bindings: media: amlogic,s4-vdec: describe both decoder cores christian.koever-draxl
2026-10-25 19:24 ` [PATCH 03/11] media: amlogic: vdec: fix the microcode loading and prepare the HEVC core christian.koever-draxl
2026-10-25 20:25   ` sashiko-bot
2026-10-25 19:25 ` [PATCH 04/11] media: amlogic: vdec: h264: decode Main and High profile bit-exact christian.koever-draxl
2026-10-25 19:25 ` [PATCH 05/11] media: amlogic: vdec: add MPEG-1/2 decoding christian.koever-draxl
2026-10-25 20:28   ` sashiko-bot
2026-10-25 19:25 ` [PATCH 06/11] media: amlogic: vdec: add MJPEG decoding christian.koever-draxl
2026-10-25 20:20   ` sashiko-bot
2026-10-25 19:25 ` [PATCH 07/11] media: amlogic: vdec: add AV1 decoding on the HEVC core christian.koever-draxl
2026-10-25 20:27   ` sashiko-bot
2026-10-25 19:25 ` [PATCH 08/11] media: amlogic: vdec: add VP9 " christian.koever-draxl
2026-10-25 20:23   ` sashiko-bot
2026-10-25 19:25 ` [PATCH 09/11] media: amlogic: vdec: add HEVC " christian.koever-draxl
2026-10-25 20:26   ` sashiko-bot
2026-10-25 19:25 ` [PATCH 10/11] media: amlogic: vdec: add MPEG-4 Part 2 decoding christian.koever-draxl
2026-10-25 20:39   ` sashiko-bot
2026-10-25 19:25 ` [PATCH 11/11] media: amlogic: vdec: give the regmaps their own names christian.koever-draxl
2026-10-26  5:53 ` [PATCH 00/11] media: amlogic: vdec: S4 decoding beyond H.264 Baseline Zhentao Guo

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