From 0d809fafc5947e21a48d09c4e46e8bcedc0af4e9 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sun, 1 Nov 2026 18:57:57 +1100 Subject: [PATCH 1/2] media: apple: avd: overlap AV1 pictures with immutable tail inputs Keep two complete AV1 scratch banks on queued firmware advertising tagged VP DONE. Look ahead at the pending request's frame control and follow its actual primary-reference timestamp. Default CDFs and references outside the in-flight set do not wait for an unrelated picture. The open firmware's VP event does not establish CPU visibility of AV1 hardware-written CDF, segmentation or motion-field tails. Retain PP retirement dependencies for those inputs. When frame-end CDF updates or segmentation-map writes are disabled, copy preparation's immutable input snapshots into the consumer bank instead of reading live CAPTURE storage. Only PP retirement releases the saved picture's bank. Preserve the legacy emitters and tail preparation, add production host flag/ownership tests and a routed AV1 Unicorn overlap test, and document the remaining visibility evidence requirement. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/media/platform/apple/avd/avd-av1.c | 328 +++++++++++++++-- .../media/platform/apple/avd/tests/README.rst | 122 +++++-- .../platform/apple/avd/tests/av1-test.py | 330 ++++++++++++++++++ .../apple/avd/tests/unicorn-av1-test.py | 144 ++++++++ tools/asahi/avd-codec-tail-tests.py | 4 + 5 files changed, 863 insertions(+), 65 deletions(-) create mode 100644 drivers/media/platform/apple/avd/tests/av1-test.py create mode 100644 drivers/media/platform/apple/avd/tests/unicorn-av1-test.py diff --git a/drivers/media/platform/apple/avd/avd-av1.c b/drivers/media/platform/apple/avd/avd-av1.c index ba7beea61c842..6a87a4de6fe9b 100644 --- a/drivers/media/platform/apple/avd/avd-av1.c +++ b/drivers/media/platform/apple/avd/avd-av1.c @@ -225,12 +225,15 @@ struct avd_av1_run { }; struct avd_av1_ctx { + struct avd_av1_stream *stream; u8 submit_num; struct { struct avd_buf inst; struct avd_buf pipe_state; struct avd_buf probs; struct avd_buf no_prev_seg; + struct avd_buf saved_seg; + struct avd_buf prev_seg; struct avd_buf above_info; struct avd_buf rf_above_info; struct avd_buf az_above; @@ -246,6 +249,24 @@ struct avd_av1_ctx { } bufs; }; +#define AVD_AV1_BANKS 2 + +struct avd_av1_bank { + struct avd_av1_ctx codec; + bool busy; + bool prepared; + struct avd_decoded_buffer *dst; + u64 timestamp; + u32 flags; + u8 seg_flags; +}; + +struct avd_av1_stream { + unsigned int nbanks; + struct avd_av1_bank bank[AVD_AV1_BANKS]; + struct avd_av1_bank *b; +}; + /* * Timestamp of the reference a ref_frame_idx entry names. The entries are * -1 on frames that use no references (FFmpeg fills key frames that way); @@ -259,6 +280,111 @@ static u64 avd_av1_ref_ts(const struct v4l2_ctrl_av1_frame *frame, int idx) return frame->reference_frame_ts[idx]; } +static bool avd_av1_default_cdf(const struct v4l2_ctrl_av1_frame *frame) +{ + return frame->primary_ref_frame == AV1_PRIMARY_REF_NONE || + frame->frame_type == V4L2_AV1_KEY_FRAME || + frame->frame_type == V4L2_AV1_INTRA_ONLY_FRAME || + (frame->flags & V4L2_AV1_FRAME_FLAG_ERROR_RESILIENT_MODE); +} + +static bool avd_av1_writes_seg(u8 flags) +{ + return (flags & V4L2_AV1_SEGMENTATION_FLAG_ENABLED) && + (flags & V4L2_AV1_SEGMENTATION_FLAG_UPDATE_MAP); +} + +/* + * No available hardware contract proves AV1 tails CPU-visible at VP DONE. + * Only preparation's immutable CDF/map snapshots can be consumed before PP + * retirement. The consumer's disable_frame_end_update_cdf does not remove + * its CDF input dependency. Slot timestamps, not refresh order, select it. + */ +static bool avd_av1_depends(const struct v4l2_ctrl_av1_frame *frame, + const struct avd_av1_bank *bank) +{ + if (frame->flags & V4L2_AV1_FRAME_FLAG_USE_REF_FRAME_MVS) + for (unsigned int i = 0; i < V4L2_AV1_REFS_PER_FRAME; i++) + if (avd_av1_ref_ts(frame, frame->ref_frame_idx[i]) == bank->timestamp) + return true; + + if (frame->primary_ref_frame >= AV1_PRIMARY_REF_NONE || + avd_av1_ref_ts(frame, frame->ref_frame_idx[frame->primary_ref_frame]) != + bank->timestamp) + return false; + if (!avd_av1_default_cdf(frame) && + !(bank->flags & V4L2_AV1_FRAME_FLAG_DISABLE_FRAME_END_UPDATE_CDF)) + return true; + return (frame->segmentation.flags & V4L2_AV1_SEGMENTATION_FLAG_ENABLED) && + avd_av1_writes_seg(bank->seg_flags) && + DIV_ROUND_UP(bank->dst->av1.width + 1, 8) == + DIV_ROUND_UP(frame->frame_width_minus_1 + 1, 8) && + DIV_ROUND_UP(bank->dst->av1.height + 1, 8) == + DIV_ROUND_UP(frame->frame_height_minus_1 + 1, 8); +} + +static bool avd_av1_ready(struct avd_ctx *ctx) +{ + struct avd_av1_ctx *codec = smp_load_acquire(&ctx->priv); + struct avd_av1_stream *av1_ctx; + struct vb2_v4l2_buffer *src; + struct v4l2_ctrl_handler *hdl; + struct v4l2_ctrl_ref *ref; + const struct v4l2_ctrl_av1_frame *frame = NULL; + bool ready = false, free = false, busy = false; + + if (!codec) + return false; + av1_ctx = codec->stream; + for (unsigned int i = 0; i < av1_ctx->nbanks; i++) { + if (!READ_ONCE(av1_ctx->bank[i].busy)) + free = true; + else if (!smp_load_acquire(&av1_ctx->bank[i].prepared)) + return false; + else + busy = true; + } + if (!free || !busy) + return free; + src = v4l2_m2m_next_src_buf(ctx->fh.m2m_ctx); + if (!src || !src->vb2_buf.req_obj.req) + return false; + hdl = v4l2_ctrl_request_hdl_find(src->vb2_buf.req_obj.req, &ctx->ctrl_hdl); + if (!hdl) + return false; + /* + * The queued request is immutable and cannot complete while its OUTPUT + * is still on the ready queue. Read p_req, never the previous picture's + * shared p_cur, without taking a sleeping control mutex under queue_lock. + */ + list_for_each_entry(ref, &hdl->ctrl_refs, node) + if (ref->ctrl->id == V4L2_CID_STATELESS_AV1_FRAME && ref->p_req_valid) { + frame = ref->p_req.p; + break; + } + if (frame) { + ready = true; + for (unsigned int i = 0; i < av1_ctx->nbanks; i++) + if (READ_ONCE(av1_ctx->bank[i].busy) && + avd_av1_depends(frame, &av1_ctx->bank[i])) + ready = false; + } + v4l2_ctrl_request_hdl_put(hdl); + return ready; +} + +static struct avd_av1_bank *avd_av1_ref_bank(struct avd_av1_stream *av1_ctx, + struct avd_decoded_buffer *ref) +{ + if (av1_ctx->nbanks > 1 && ref->bank && ref->bank <= av1_ctx->nbanks) { + struct avd_av1_bank *bank = &av1_ctx->bank[ref->bank - 1]; + + if (bank->busy && bank->prepared && bank->dst == ref) + return bank; + } + return NULL; +} + /* * from verisilicon/rockchip_vpu981_hw_av1_dec.c */ @@ -1160,6 +1286,71 @@ static void avd_av1_set_seg(struct avd_ctx *ctx, struct avd_av1_run *run) memcpy(seg, prev_seg, dst->av1.seg_size); } +/* Preserve the legacy tail preparation; only queued immutable inputs differ. */ +static void avd_av1_prepare_prob(struct avd_ctx *ctx, struct avd_av1_run *run) +{ + struct avd_av1_ctx *codec = ctx->priv; + struct avd_av1_stream *stream = codec->stream; + struct avd_decoded_buffer *dst, *ref; + struct avd_av1_bank *bank = NULL; + + dst = vb2_to_avd_decoded_buf(&run->base.bufs.dst->vb2_buf); + if (stream->nbanks > 1 && !avd_av1_default_cdf(run->frame)) { + ref = avd_get_ref_buf(ctx, &dst->base.vb, + avd_av1_ref_ts(run->frame, + run->frame->ref_frame_idx[run->frame->primary_ref_frame])); + bank = avd_av1_ref_bank(stream, ref); + } + if (!bank) { + avd_av1_set_prob(ctx, run); + return; + } + memcpy(codec->bufs.probs.cpu, bank->codec.bufs.probs.cpu, AVD_CDFS_SIZE); + memcpy(vb2_plane_vaddr(&dst->base.vb.vb2_buf, 0) + + AVD_AV1_CDFS_OFFSET(dst->base.vb.vb2_buf.planes[0].length, + dst->av1.color_size), + codec->bufs.probs.cpu, AVD_CDFS_SIZE); +} + +static void avd_av1_prepare_seg(struct avd_ctx *ctx, struct avd_av1_run *run) +{ + struct avd_av1_ctx *codec = ctx->priv; + struct avd_av1_stream *stream = codec->stream; + const struct v4l2_ctrl_av1_frame *frame = run->frame; + struct avd_decoded_buffer *dst, *ref; + struct avd_av1_bank *bank = NULL; + void *seg; + + dst = vb2_to_avd_decoded_buf(&run->base.bufs.dst->vb2_buf); + seg = vb2_plane_vaddr(&dst->base.vb.vb2_buf, 0) + + AVD_AV1_SEG_OFFSET(dst->base.vb.vb2_buf.planes[0].length, + dst->av1.color_size, dst->av1.seg_size); + if (stream->nbanks > 1 && frame->primary_ref_frame != AV1_PRIMARY_REF_NONE) { + ref = avd_get_ref_buf(ctx, &dst->base.vb, + avd_av1_ref_ts(frame, frame->ref_frame_idx[frame->primary_ref_frame])); + if (ref != dst && + DIV_ROUND_UP(ref->av1.width + 1, 8) == + DIV_ROUND_UP(frame->frame_width_minus_1 + 1, 8) && + DIV_ROUND_UP(ref->av1.height + 1, 8) == + DIV_ROUND_UP(frame->frame_height_minus_1 + 1, 8)) + bank = avd_av1_ref_bank(stream, ref); + } + if (bank && !avd_av1_writes_seg(bank->seg_flags)) { + /* Copy into our bank: producer PP can retire during our VP. */ + memcpy(codec->bufs.prev_seg.cpu, bank->codec.bufs.saved_seg.cpu, + dst->av1.seg_size); + run->addresses.prev_seg = codec->bufs.prev_seg.addr; + if (!(frame->segmentation.flags & V4L2_AV1_SEGMENTATION_FLAG_ENABLED)) + memset(seg, 0, dst->av1.seg_size); + else if (!(frame->segmentation.flags & V4L2_AV1_SEGMENTATION_FLAG_UPDATE_MAP)) + memcpy(seg, codec->bufs.prev_seg.cpu, dst->av1.seg_size); + } else { + avd_av1_set_seg(ctx, run); + } + if (stream->nbanks > 1 && !avd_av1_writes_seg(frame->segmentation.flags)) + memcpy(codec->bufs.saved_seg.cpu, seg, dst->av1.seg_size); +} + static int avd_av1_seg_size(int w, int h) { /* 3 bits per 4x4 block, in rows of 128x128 superblocks (64x64 fit too) */ @@ -1280,6 +1471,17 @@ static int avd_av1_alloc_work_bufs(struct avd_ctx *ctx, struct avd_av1_run *run) int h = frame->frame_height_minus_1 + 1; int bit_depth = seq->bit_depth; + if (av1_ctx->stream->nbanks > 1) { + ret = avd_buf_alloc(avd, &av1_ctx->bufs.saved_seg, + avd_av1_seg_size(w, h)); + if (ret) + return ret; + ret = avd_buf_alloc(avd, &av1_ctx->bufs.prev_seg, + avd_av1_seg_size(w, h)); + if (ret) + return ret; + } + for (i = 0; i < tile_info->tile_cols; i++) { sb = tile_info->width_in_sbs_minus_1[i] + 1; max_sb_col = sb > max_sb_col ? sb : max_sb_col; @@ -1380,6 +1582,7 @@ static int avd_av1_alloc_work_bufs(struct avd_ctx *ctx, struct avd_av1_run *run) static int avd_av1_run(struct avd_ctx *ctx) { struct avd_av1_run run; + struct avd_av1_stream *av1_ctx = ((struct avd_av1_ctx *)ctx->priv)->stream; struct avd_decoded_buffer *dst, *ref; int ret; @@ -1402,6 +1605,28 @@ static int avd_av1_run(struct avd_ctx *ctx) avd_run_postamble(ctx, &run.base); return -ENOMEM; } + if (av1_ctx->nbanks > 1) { + struct avd_av1_bank *bank = NULL; + + for (unsigned int i = 0; i < av1_ctx->nbanks; i++) + if (!av1_ctx->bank[i].busy) { + bank = &av1_ctx->bank[i]; + break; + } + if (!bank) { + avd_run_postamble(ctx, &run.base); + return -EBUSY; + } + av1_ctx->b = bank; + smp_store_release(&ctx->priv, &bank->codec); + WRITE_ONCE(bank->prepared, false); + WRITE_ONCE(bank->busy, true); + bank->dst = dst; + bank->timestamp = run.base.bufs.src->vb2_buf.timestamp; + bank->flags = run.frame->flags; + bank->seg_flags = run.frame->segmentation.flags; + dst->bank = bank - av1_ctx->bank + 1; + } ret = avd_init_job(ctx, AVD_CODEC_AV1, run.frame->tile_info.tile_cols * @@ -1420,33 +1645,51 @@ static int avd_av1_run(struct avd_ctx *ctx) update_dec_buf_info(dst, run.seq, run.frame); - avd_av1_set_prob(ctx, &run); - avd_av1_set_seg(ctx, &run); + avd_av1_prepare_prob(ctx, &run); + avd_av1_prepare_seg(ctx, &run); set_header(ctx, &run); set_tiles(ctx, &run); + if (av1_ctx->nbanks > 1) { + ctx->job_fifo = av1_ctx->b->codec.bufs.inst.addr; + smp_store_release(&av1_ctx->b->prepared, true); + } avd_run_postamble(ctx, &run.base); return avd_submit_job(ctx); } -static int avd_av1_alloc_bufs(struct avd_ctx *ctx) +static void avd_av1_done(struct avd_ctx *ctx, struct vb2_v4l2_buffer *src, + struct vb2_v4l2_buffer *dst, enum vb2_buffer_state result) +{ + struct avd_av1_stream *av1_ctx = ((struct avd_av1_ctx *)ctx->priv)->stream; + struct avd_decoded_buffer *buf = vb2_to_avd_decoded_buf(&dst->vb2_buf); + + if (buf->bank && buf->bank <= av1_ctx->nbanks) { + struct avd_av1_bank *bank = &av1_ctx->bank[buf->bank - 1]; + + if (bank->dst == buf) + WRITE_ONCE(bank->busy, false); + buf->bank = 0; + } +} + +static int avd_av1_alloc_bufs(struct avd_ctx *ctx, struct avd_av1_bank *bank) { struct avd_dev *avd = ctx->dev; - struct avd_av1_ctx *av1_ctx = ctx->priv; int ret; - ret = avd_buf_alloc(avd, &av1_ctx->bufs.inst, fifo_size()); + ret = avd_buf_alloc(avd, &bank->codec.bufs.inst, fifo_size()); if (ret) return ret; /* no m1 to worry about :) */ - ret = avd_buf_alloc(avd, &av1_ctx->bufs.pipe_state, 0x200); + ret = avd_buf_alloc(avd, &bank->codec.bufs.pipe_state, 0x200); if (ret) return ret; - ret = avd_buf_alloc(avd, &av1_ctx->bufs.probs, + ret = avd_buf_alloc(avd, &bank->codec.bufs.probs, sizeof(struct avd_av1_cdfs)); if (ret) return ret; @@ -1454,51 +1697,66 @@ static int avd_av1_alloc_bufs(struct avd_ctx *ctx) return 0; } -static void avd_av1_stop(struct avd_ctx *ctx) +static void avd_av1_free(struct avd_ctx *ctx, struct avd_av1_stream *av1_ctx) { - struct avd_av1_ctx *av1_ctx = ctx->priv; struct avd_dev *avd = ctx->dev; - avd_buf_free(avd, &av1_ctx->bufs.above_info); - avd_buf_free(avd, &av1_ctx->bufs.rf_above_info); - avd_buf_free(avd, &av1_ctx->bufs.az_above); - avd_buf_free(avd, &av1_ctx->bufs.ip_above); - avd_buf_free(avd, &av1_ctx->bufs.lf_above); - avd_buf_free(avd, &av1_ctx->bufs.lf_above_info); - avd_buf_free(avd, &av1_ctx->bufs.lf_left); - avd_buf_free(avd, &av1_ctx->bufs.lf_left_info); - avd_buf_free(avd, &av1_ctx->bufs.sr_left); - avd_buf_free(avd, &av1_ctx->bufs.rf_left); - avd_buf_free(avd, &av1_ctx->bufs.rf_left_info); - avd_buf_free(avd, &av1_ctx->bufs.no_prev_seg); - avd_buf_free(avd, &av1_ctx->bufs.mv_above_info); - - avd_buf_free(avd, &av1_ctx->bufs.pipe_state); - avd_buf_free(avd, &av1_ctx->bufs.inst); - avd_buf_free(avd, &av1_ctx->bufs.probs); - + for (unsigned int i = 0; i < av1_ctx->nbanks; i++) { + struct avd_av1_bank *bank = &av1_ctx->bank[i]; + + avd_buf_free(avd, &bank->codec.bufs.above_info); + avd_buf_free(avd, &bank->codec.bufs.rf_above_info); + avd_buf_free(avd, &bank->codec.bufs.az_above); + avd_buf_free(avd, &bank->codec.bufs.ip_above); + avd_buf_free(avd, &bank->codec.bufs.lf_above); + avd_buf_free(avd, &bank->codec.bufs.lf_above_info); + avd_buf_free(avd, &bank->codec.bufs.lf_left); + avd_buf_free(avd, &bank->codec.bufs.lf_left_info); + avd_buf_free(avd, &bank->codec.bufs.sr_left); + avd_buf_free(avd, &bank->codec.bufs.rf_left); + avd_buf_free(avd, &bank->codec.bufs.rf_left_info); + avd_buf_free(avd, &bank->codec.bufs.no_prev_seg); + avd_buf_free(avd, &bank->codec.bufs.mv_above_info); + + avd_buf_free(avd, &bank->codec.bufs.pipe_state); + avd_buf_free(avd, &bank->codec.bufs.inst); + avd_buf_free(avd, &bank->codec.bufs.probs); + avd_buf_free(avd, &bank->codec.bufs.saved_seg); + avd_buf_free(avd, &bank->codec.bufs.prev_seg); + } kfree(av1_ctx); +} + +static void avd_av1_stop(struct avd_ctx *ctx) +{ + avd_av1_free(ctx, ((struct avd_av1_ctx *)ctx->priv)->stream); ctx->priv = NULL; } static int avd_av1_start(struct avd_ctx *ctx) { - struct avd_av1_ctx *av1_ctx; + struct avd_av1_stream *av1_ctx; int ret; av1_ctx = kzalloc(sizeof(*av1_ctx), GFP_KERNEL); if (!av1_ctx) return -ENOMEM; - ctx->priv = av1_ctx; - ret = avd_av1_alloc_bufs(ctx); - if (ret) - goto err_free_ctx; - + av1_ctx->nbanks = avd_qfw_has_vp_done(ctx->dev) ? AVD_AV1_BANKS : 1; + av1_ctx->b = &av1_ctx->bank[0]; + for (unsigned int i = 0; i < av1_ctx->nbanks; i++) { + av1_ctx->bank[i].codec.stream = av1_ctx; + ret = avd_av1_alloc_bufs(ctx, &av1_ctx->bank[i]); + if (ret) + goto err_free_ctx; + } + ctx->max_frames = av1_ctx->nbanks; + /* Readiness must never see partially allocated bank storage. */ + smp_store_release(&ctx->priv, &av1_ctx->bank[0].codec); return 0; err_free_ctx: - avd_av1_stop(ctx); + avd_av1_free(ctx, av1_ctx); return ret; } @@ -1586,6 +1844,8 @@ const struct avd_coded_fmt_ops avd_av1_fmt_ops = { .start = avd_av1_start, .stop = avd_av1_stop, .run = avd_av1_run, + .done = avd_av1_done, + .ready = avd_av1_ready, .submit = avd_av1_submit, .pp_kicks = avd_av1_pp_kicks, .try_ctrl = avd_av1_try_ctrl, diff --git a/drivers/media/platform/apple/avd/tests/README.rst b/drivers/media/platform/apple/avd/tests/README.rst index 1d4f3917f63c4..daa70bb23fc51 100644 --- a/drivers/media/platform/apple/avd/tests/README.rst +++ b/drivers/media/platform/apple/avd/tests/README.rst @@ -200,37 +200,97 @@ This is a backwards-compatible optional ABI 2 feature: bank reuse. Only the original tagged PP completion does that. A missing VP event remains bounded by the original per-picture watchdog. -AV1 analysis -~~~~~~~~~~~~ - -AV1's current ``avd_av1_set_prob()`` reads the primary reference's CDF tail on -the CPU, and seeds the current frame's output CDF because hardware leaves it -untouched when DISABLE_FRAME_END_UPDATE_CDF is set. ``avd_av1_set_seg()`` can -also copy the primary reference's segmentation map on the CPU. These are -separate dependencies; removing only the CDF wait is insufficient. - -=================================== ========================================= -Case CDF source dependency -=================================== ========================================= -Intra/error-resilient/PRIMARY_NONE Defaults; no primary-reference CDF read -Primary ref with frame-end disabled Its initialized CDF, available at prepare -Primary ref with frame-end enabled Its hardware-written CDF tail -=================================== ========================================= - -``disable_frame_end_update_cdf`` on the *consumer* does not remove its primary -reference input dependency. It controls that consumer's future CDF output. -Likewise, a default CDF does not necessarily remove the segmentation-map input -selected by primary_ref_frame. - -The available implementation does not establish whether the CDF tail *and* -segmentation map are fully visible at VP DONE, or which CPU DMA synchronization -is required for an in-flight CAPTURE allocation while PP writes another part. -This differs from VP9's separate coherent count buffer. AV1 therefore retains -its one-picture path. Enabling it needs evidence for both tail writes and a -safe visibility/synchronization rule, then two scratch banks and a dependency -on the actual primary reference (which need not be the preceding picture). -No AV1 or VP9 pixel correctness or throughput claim follows from these host -and emulator tests; device qualification remains necessary. +AV1 conservative picture overlap +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``av1-test.py`` compiles the production AV1 codec, entropy defaults, open +program builder and queued descriptor writer with ASan/UBSan. It is also +called by the registered ``avd-codec-tail-tests.py`` host check. Run:: + + AVD_AV1_BASELINE_REF=a1b76856 python3 tests/av1-test.py /tmp/avd-av1 + python3 tests/unicorn-av1-test.py /path/to/avd-fw \ + /path/to/avd-fw/build/avd-fw-v5-t2.bin /tmp/avd-av1 + +The independent legacy comparison builds before/after executables and compares +complete emitted programs plus initialized CDF/map tails for eight header cases. +The flag table covers primary-reference/default CDF cases, both producer and +consumer disable_frame_end_update_cdf, segmentation enabled/update_map on both +pictures, and all 256 refresh masks. Pending request p_req values are used for +readiness; p_cur still describes an earlier picture and must not be used. + +Visibility evidence and limit +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The public AV1 specification sections 7.7, 7.19 and 7.20 define frame CDF, +motion-field and segmentation storage semantics: +https://aomediacodec.github.io/av1-spec/ . They do not define this decoder's +VP/PP DMA boundary. In our driver ``set_header()`` supplies probs_out, seg and +w_color output addresses to the VP program; ``avd_av1_set_prob()`` seeds CDF +output and ``avd_av1_set_seg()`` initializes non-updated maps. The open firmware +``avdq_vp_done()`` changes VP to PP_READY and ``notify_vp()`` publishes a tag +with barriers. Neither reads the AV1 output memory nor supplies an AV1-specific +DMA visibility guarantee. A CM3 DMB orders its descriptor stores; it does not +prove the decoder's prior external tail writes are CPU-visible. + +There is therefore **no evidence here proving that a hardware-written AV1 CDF, +segmentation map or motion field is visible at VP DONE**. There is also no +proof that they are written only at PP DONE. Unicorn injects DONE and cannot +establish either assertion. The implementation keeps those dependencies until +PP retirement instead of inferring a visibility contract from their operands. +No CPU DMA sync is attempted on a CAPTURE allocation still owned by PP. + +Safe subset +^^^^^^^^^^^ + +Queue firmware advertising VP-DONE feature bit 2 enables two scratch banks; +legacy transport and older queue firmware retain one bank and identical +programs. The CDF output, segmentation output and motion-field/color output +already live in each picture's distinct CAPTURE tail. Each scratch bank owns +its FIFO, CDF input, pipe/filter/tile scratch, a saved initialized map and a +separate map input snapshot. Only PP retirement releases its bank. VP DONE +never releases storage or publishes hardware-written tails for AV1. + +For a pending consumer, each busy producer is tested by the exact timestamp +named through ref_frame_idx/primary_ref_frame, rather than the preceding frame +or a global refresh-context flag. refresh_frame_flags controls which slots +userspace subsequently names; it does not create a dependency by itself. + +=============================== ============================================== +Consumer / producer condition Prepare dependency while producer is in flight +=============================== ============================================== +PRIMARY_REF_NONE No primary CDF or map dependency +Intra or error resilient Default CDF; map dependency still checked +Primary slot names retired ref None on an in-flight picture +Primary CDF, producer end update Enabled: wait for producer PP retirement +Primary CDF, producer end update Disabled: use immutable prepared CDF input copy +Consumer end update disabled Does not remove its CDF input dependency +Consumer segmentation disabled No previous-map read +Consumer segmentation enabled Updated producer map: wait for PP (same Mi size) +Producer map not updated Copy immutable saved map into consumer bank +Different reference Mi size Zero map; no map dependency +USE_REF_FRAME_MVS Wait for PP of any named in-flight reference +refresh_frame_flags 0..255 No additional dependency; named slot decides +=============================== ============================================== + +Both consumer update_map values retain the previous-map input when segmentation +is enabled, as the existing program supplies it in either case. A disabled +producer segmentation map is saved as zeroes. Producer map snapshots are copied +into consumer-owned coherent storage, so producer PP retirement and bank reuse +cannot invalidate a live successor's VP input. CDF snapshots are similarly +copied into the consumer's CDF input. No hardware-written tail is sampled by +these copies. Busy banks publish fully prepared metadata with release/acquire; +allocation failures and PP error/reset retirement release the saved CAPTURE's +bank, independent of the current builder. + +This subset needs no extra VP token: firmware already waits for VP(N) DONE +before feeding VP(N+1). AV1 descriptors retain their ordinary completion-only +flags. The Unicorn run boots the v5-t2 CM3 reset vector, routes AV1 and PP IRQs, +and checks production AV1 program words/descriptors/configuration. It shows +VP(1) issued during PP(0), PP serialization and blocked completion mailboxes. +Host tests poison in-flight CAPTURE tails to ensure snapshot consumers never +read them. Pixels, physical DMA visibility and throughput remain hardware +qualification work for the lead. Full CDF/MV dependency release at VP DONE is +still a separate evidence requirement. H.264 conformance fixes ---------------------- diff --git a/drivers/media/platform/apple/avd/tests/av1-test.py b/drivers/media/platform/apple/avd/tests/av1-test.py new file mode 100644 index 0000000000000..a02070c85ffe7 --- /dev/null +++ b/drivers/media/platform/apple/avd/tests/av1-test.py @@ -0,0 +1,330 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Production AV1 bank/dependency/emitters, with explicit host DMA boundaries. + +Run in a controller build slot: av1-test.py OUT [AVD_DIR [KERNEL_ROOT]]. +AVD_AV1_BASELINE_REF optionally compares complete legacy programs/CDF/map bytes. +No decoder arithmetic or DMA visibility is modeled by these host fixtures. +""" +import hashlib +import importlib.util +import os +from pathlib import Path +import re +import subprocess +import sys + +from host_source import function + +HERE = Path(__file__).resolve().parent +AVD = Path(sys.argv[2]).resolve() if len(sys.argv) > 2 else HERE.parent +KERNEL = Path(sys.argv[3]).resolve() if len(sys.argv) > 3 else AVD.parents[4] +OUT = Path(sys.argv[1]).resolve() +OUT.mkdir(parents=True, exist_ok=True) +spec = importlib.util.spec_from_file_location('program_fixture', HERE / 'program-test.py') +fixture = importlib.util.module_from_spec(spec) +spec.loader.exec_module(fixture) +pre = fixture.PRELUDE +pre = pre.replace('typedef uint8_t u8;', 'typedef uint8_t u8; typedef uint16_t u16; typedef int8_t s8; typedef int16_t s16; typedef int64_t s64;') +pre = pre.replace('#define AVD_MAX_INST 512', '#define AVD_MAX_INST 512\n#define AVD_CODEC_HEVC 0') +pre = pre.replace('struct vb2_buffer { struct { size_t length; } planes[1]; dma_addr_t dma; };', + 'struct vb2_buffer { struct { size_t length; } planes[1]; dma_addr_t dma; void *cpu; size_t payload; u64 timestamp; struct {void *req;} req_obj; };') +header = (AVD / 'avd.h').read_text() +a = header.index('struct avd_av1_decoded_buffer_info {') +b = header.index('\n};', a) + 3 +pre = pre.replace('struct avd_decoded_buffer {', header[a:b] + '\nstruct avd_decoded_buffer {') +pre = pre.replace('struct avd_comp comp; unsigned int bank;', 'struct avd_comp comp; unsigned int bank; struct avd_av1_decoded_buffer_info av1;') +pre = pre.replace('struct vb2_v4l2_buffer *dst; } bufs;', 'struct vb2_v4l2_buffer *src,*dst; } bufs;') +pre = pre.replace('unsigned int fifo_idx, pp_total;', 'unsigned int fifo_idx, pp_total, max_frames; int ctrl_hdl;') +pre = pre.replace('dma_addr_t comp_out, y_out, uv_out;', 'dma_addr_t comp_out, y_out, uv_out, coded_in;') +pre = pre.replace('struct { u32 bytesperline; } plane_fmt[1];', 'u32 width,height; struct { u32 bytesperline; } plane_fmt[1];') +pre = pre.replace('static struct avd_decoded_buffer reference;', 'static struct avd_decoded_buffer reference; static struct avd_decoded_buffer *refs[3];') +pre = pre.replace('(void)c; (void)b; (void)t; return &reference;', '(void)c;for(unsigned int i=0;i<3;i++)if(refs[i]&&refs[i]->base.vb.vb2_buf.timestamp==t)return refs[i];return (void *)b;') +pre = pre.replace('static int avd_buf_alloc(struct avd_dev *d, struct avd_buf *b, size_t n)', + 'static struct avd_ctx *allocating_ctx;\nstatic int fail_alloc_after;\nstatic bool avd_av1_ready(struct avd_ctx *);\nstatic int avd_buf_alloc(struct avd_dev *d, struct avd_buf *b, size_t n)') +pre = pre.replace('{ (void)d; free(b->cpu); b->cpu = calloc(1,n);', + '{ if(allocating_ctx){assert(!avd_av1_ready(allocating_ctx));if(fail_alloc_after&&!--fail_alloc_after)return -ENOMEM;} (void)d; free(b->cpu); b->cpu = calloc(1,n);') +pre += r''' +#define AVD_ALIGN 256 +#define S16_MIN INT16_MIN +#define S16_MAX INT16_MAX +#define clamp_val(x,l,h) clamp(x,l,h) +#define U64_MAX UINT64_MAX +#define BIT_ULL(n) (1ULL<<(n)) +#define ALIGN(x,a) (((x)+(a)-1)&~((__typeof__(x))(a)-1)) +#define ALIGN_DOWN(x,a) ((x)&~((__typeof__(x))(a)-1)) +#define max_t(t,a,b) ((t)(a)>(t)(b)?(t)(a):(t)(b)) +#define min(a,b) ((a)<(b)?(a):(b)) +#define max(a,b) ((a)>(b)?(a):(b)) +#define clamp(x,l,h) ((x)<(l)?(l):(x)>(h)?(h):(x)) +#define WARN_ON(v) (v) +#define GFP_KERNEL 0 +#define smp_load_acquire(p) (*(p)) +#define smp_store_release(p,v) (*(p)=(v)) +#define READ_ONCE(x) (x) +#define WRITE_ONCE(x,v) ((x)=(v)) +#define static_assert _Static_assert +#define VB2_BUF_STATE_DONE 0 +#define VB2_BUF_STATE_ERROR 1 +enum vb2_buffer_state { STATE_DUMMY }; +#define kzalloc(n,f) calloc(1,n) +#define kfree free +static bool vp_feature=true; +static bool avd_qfw_has_vp_done(const struct avd_dev *d){return d->queued&&vp_feature;} +static void *vb2_plane_vaddr(struct vb2_buffer *b,int p){(void)p;return b->cpu;} +static struct vb2_v4l2_buffer *current_src,*current_dst; +static struct vb2_v4l2_buffer *v4l2_m2m_next_src_buf(void *m){(void)m;return current_src;} +struct v4l2_ctrl {unsigned int id; union {void *p;} p_cur;}; +struct v4l2_ctrl_ref {struct v4l2_ctrl_ref *next; struct v4l2_ctrl *ctrl; bool p_req_valid; union {void *p;} p_req;}; +struct v4l2_ctrl_handler {struct v4l2_ctrl_ref *ctrl_refs;}; +#define list_for_each_entry(ref,head,node) for((ref)=*(head);(ref);(ref)=(ref)->next) +static struct v4l2_ctrl_av1_frame params,request_params; +static struct v4l2_ctrl_av1_sequence seq; +static struct v4l2_ctrl_av1_tile_group_entry tile; +static struct v4l2_ctrl_av1_film_grain grain; +static struct v4l2_ctrl controls[4]; +static struct v4l2_ctrl_ref request_ref; +static struct v4l2_ctrl_handler request_hdl; +static unsigned int request_gets,request_puts; +static struct v4l2_ctrl_handler *v4l2_ctrl_request_hdl_find(void *req,void *h){(void)h;assert(req==&request_hdl);request_gets++;return &request_hdl;} +static void v4l2_ctrl_request_hdl_put(struct v4l2_ctrl_handler *h){assert(h==&request_hdl);request_puts++;} +static struct v4l2_ctrl *v4l2_ctrl_find(void *h,u32 id){ + (void)h;controls[0].p_cur.p=&seq;controls[1].p_cur.p=¶ms;controls[2].p_cur.p=&tile;controls[3].p_cur.p=&grain; + if(id==V4L2_CID_STATELESS_AV1_SEQUENCE)return &controls[0]; + if(id==V4L2_CID_STATELESS_AV1_FRAME)return &controls[1]; + if(id==V4L2_CID_STATELESS_AV1_TILE_GROUP_ENTRY)return &controls[2]; + return &controls[3]; +} +static void avd_run_preamble(struct avd_ctx *ctx,struct avd_run *run){ + (void)ctx;memset(run,0,sizeof(*run));run->bufs.src=current_src;run->bufs.dst=current_dst; + run->coded_in=current_src->vb2_buf.dma;run->comp_out=current_dst->vb2_buf.dma; + run->y_out=run->comp_out+0x100000;run->uv_out=run->comp_out+0x300000; +} +static void avd_run_postamble(struct avd_ctx *ctx,struct avd_run *run){(void)ctx;(void)run;} +static int avd_init_job(struct avd_ctx *ctx,int codec,size_t segments){ + free(ctx->job.segments);ctx->job.codec=codec;ctx->job.num=0; + ctx->job.segments=calloc(segments,sizeof(*ctx->job.segments));return ctx->job.segments?0:-ENOMEM; +} +static int avd_submit_job(struct avd_ctx *ctx); +''' + + +def without_includes(s): + return re.sub(r'^#include[^\n]*\n', '', s, flags=re.M) + + +av1 = (AVD / 'avd-av1.c').read_text() +common = pre + without_includes((AVD / 'avd-av1-entropymode.h').read_text()) +common += without_includes((AVD / 'avd-av1-entropymode.c').read_text()) +common += without_includes((AVD / 'avd-inst.h').read_text()) +qfw = (AVD / 'avd-backend-queue.c').read_text() +hw = (AVD / 'avd-hw.c').read_text() +post = function((AVD / 'avd-backend-open.c').read_text(), 'avd_open_build_program') +post += qfw[qfw.index('#define AVDQ_MAGIC'):qfw.index('/* Arguments of dev_dbg()')] +post += hw[hw.index('#define AVD_V3_'):hw.index('int avd_boot(')] +post += '#define w32(r,v) writel((v),avd->ctrl+(r))\n#define m32(r,v) w32(r,(v)|readl(avd->ctrl+(r)))\n' +post += function(hw, 't8112_configure_stream') + function(hw, 't8122_configure_stream') +post += function(qfw, 'avd_qfw_stream') + function(qfw, 'avd_qfw_submit') +post += '\nstatic int avd_submit_job(struct avd_ctx *ctx){return avd_open_build_program(ctx);}\n' + + +def codec(s): + return without_includes(s[s.index('#define V4L2_AV1_SEG_LVL_ALT_LF_Y_H'):s.index('static enum avd_image_fmt avd_av1_get_image_fmt')]) + function(s, 'avd_av1_pp_kicks') + + +MAIN = r''' +static void write_program(const char *dir,const char *name,struct avd_frame *f){ + char path[4096];snprintf(path,sizeof(path),"%s/%s.bin",dir,name);FILE *out=fopen(path,"wb");assert(out); + assert(fwrite(f->prog->cpu,4,f->prog_words,out)==f->prog_words);fclose(out); + struct avd_dev *dev=f->ctx->dev;struct avd_qfw *q=dev->qfw; + q->wp=0;memset(q->sram,0,0x10000);f->tag=0x10|f->idx;assert(!avd_qfw_submit(dev,f)); + snprintf(path,sizeof(path),"%s/%s-desc.bin",dir,name);out=fopen(path,"wb");assert(out); + assert(fwrite(q->sram+0x8000+f->idx*64,1,64,out)==64);fclose(out); + snprintf(path,sizeof(path),"%s/%s-mmio.bin",dir,name);out=fopen(path,"wb");assert(out); + assert(fwrite(dev->ctrl,1,0x10000,out)==0x10000);fclose(out); +} +static void queue_request(struct vb2_v4l2_buffer *src){ + request_params=params;controls[1].id=V4L2_CID_STATELESS_AV1_FRAME; + request_ref=(struct v4l2_ctrl_ref){.ctrl=&controls[1],.p_req_valid=true,.p_req.p=&request_params}; + request_hdl.ctrl_refs=&request_ref;src->vb2_buf.req_obj.req=&request_hdl; +} +static void flag_table(struct avd_av1_bank *b){ + /* Actual producer timestamp is slot 5, not the immediately prior slot 0. */ + struct v4l2_ctrl_av1_frame f={.frame_type=V4L2_AV1_INTER_FRAME,.primary_ref_frame=2, + .frame_width_minus_1=1919,.frame_height_minus_1=1087}; + for(unsigned int i=0;i<7;i++)f.ref_frame_idx[i]=i; + f.ref_frame_idx[2]=5;f.reference_frame_ts[5]=b->timestamp; + for(unsigned int defaults=0;defaults<4;defaults++) + for(unsigned int output_disabled=0;output_disabled<2;output_disabled++) + for(unsigned int input_disabled=0;input_disabled<2;input_disabled++) + for(unsigned int producer_seg=0;producer_seg<4;producer_seg++) + for(unsigned int consumer_seg=0;consumer_seg<4;consumer_seg++) + for(unsigned int refresh=0;refresh<256;refresh++){ + f.primary_ref_frame=defaults==1?7:2; + f.frame_type=defaults==2?V4L2_AV1_INTRA_ONLY_FRAME:V4L2_AV1_INTER_FRAME; + f.flags=(defaults==3?V4L2_AV1_FRAME_FLAG_ERROR_RESILIENT_MODE:0)| + (output_disabled?V4L2_AV1_FRAME_FLAG_DISABLE_FRAME_END_UPDATE_CDF:0); + f.refresh_frame_flags=refresh; + f.segmentation.flags=(consumer_seg&1?V4L2_AV1_SEGMENTATION_FLAG_ENABLED:0)| + (consumer_seg&2?V4L2_AV1_SEGMENTATION_FLAG_UPDATE_MAP:0); + b->flags=input_disabled?V4L2_AV1_FRAME_FLAG_DISABLE_FRAME_END_UPDATE_CDF:0; + b->seg_flags=(producer_seg&1?V4L2_AV1_SEGMENTATION_FLAG_ENABLED:0)| + (producer_seg&2?V4L2_AV1_SEGMENTATION_FLAG_UPDATE_MAP:0); + bool expect=defaults!=1&&((!defaults&&!input_disabled)||((consumer_seg&1)&&producer_seg==3)); + assert(avd_av1_depends(&f,b)==expect); + } + for(unsigned int primary=0;primary<7;primary++){ + f.primary_ref_frame=primary;f.flags=0;f.frame_type=V4L2_AV1_INTER_FRAME;f.segmentation.flags=0; + b->flags=0;f.ref_frame_idx[primary]=5;assert(avd_av1_depends(&f,b)); + f.reference_frame_ts[5]++;assert(!avd_av1_depends(&f,b));f.reference_frame_ts[5]--; + } + f.primary_ref_frame=2;f.flags=V4L2_AV1_FRAME_FLAG_USE_REF_FRAME_MVS;f.reference_frame_ts[5]=b->timestamp; + assert(avd_av1_depends(&f,b));f.primary_ref_frame=7;assert(avd_av1_depends(&f,b)); + f.reference_frame_ts[5]++;assert(!avd_av1_depends(&f,b)); + puts("PASS: AV1 flag table (primary, consumer/producer CDF flags, segmentation enabled/update_map, every refresh mask), actual slot and MV dependencies"); +} +int main(int argc,char **argv){ + assert(argc==2); + struct avd_variant variant={.revision=4,.quirks=0,.fifo_slots=15,.vp_slots={4,4,1,3}}; + struct avd_dev dev={.variant=&variant,.queued=true,.capabilities=15}; + struct avd_qfw queue={.sram=calloc(1,0x10000),.aperture=0x1094000,.ring_words=0x1800,.fifo_slots=15,.vp_done=true}; + dev.qfw=&queue;dev.ctrl=calloc(1,0x10000);dev.mbox=calloc(1,0x100);variant.configure_stream=t8122_configure_stream; + writel(0x20001,dev.mbox+0x50); + static const struct avd_coded_fmt_ops ops={.pp_kicks=avd_av1_pp_kicks}; + static const struct fmt_desc fmt={.ops=&ops}; + struct avd_ctx ctx={.dev=&dev,.coded_fmt_desc=&fmt,.pp_total=1}; + struct avd_buf program={};struct avd_frame frame={.prog=&program,.ctx=&ctx};dev.building=&frame; + struct avd_decoded_buffer dst[3]={};struct vb2_v4l2_buffer src={};u8 coded[512]={}; + src.vb2_buf=(struct vb2_buffer){.dma=0x220000000ULL,.cpu=coded,.payload=512,.timestamp=1000};current_src=&src; + for(unsigned int i=0;i<3;i++){ + dst[i].base.vb.vb2_buf=(struct vb2_buffer){.dma=0x230000000ULL+i*0x1000000ULL,.cpu=calloc(1,0x800000),.planes[0].length=0x800000,.timestamp=1000+i};refs[i]=&dst[i]; + } + ctx.decoded_fmt.fmt.pix_mp.width=1920;ctx.decoded_fmt.fmt.pix_mp.height=1088; + ctx.decoded_fmt.fmt.pix_mp.plane_fmt[0].bytesperline=1920; + assert(!avd_av1_ready(&ctx));allocating_ctx=&ctx;assert(!avd_av1_start(&ctx));allocating_ctx=NULL; + struct avd_av1_stream *p=((struct avd_av1_ctx *)ctx.priv)->stream;assert(p->nbanks==2&&ctx.max_frames==2); + assert(p->bank[0].codec.bufs.probs.addr!=p->bank[1].codec.bufs.probs.addr); + seq=(struct v4l2_ctrl_av1_sequence){.bit_depth=8,.flags=V4L2_AV1_SEQUENCE_FLAG_SUBSAMPLING_X|V4L2_AV1_SEQUENCE_FLAG_SUBSAMPLING_Y}; + params=(struct v4l2_ctrl_av1_frame){.frame_type=V4L2_AV1_KEY_FRAME,.primary_ref_frame=7, + .flags=V4L2_AV1_FRAME_FLAG_DISABLE_FRAME_END_UPDATE_CDF,.frame_width_minus_1=1919,.frame_height_minus_1=1087,.upscaled_width=1920}; + params.tile_info.tile_cols=params.tile_info.tile_rows=1;params.tile_info.width_in_sbs_minus_1[0]=29;params.tile_info.height_in_sbs_minus_1[0]=16; + tile.tile_size=512;tile.tile_col=tile.tile_row=0; + current_dst=&dst[0].base.vb;frame.idx=0;assert(avd_av1_ready(&ctx));assert(!avd_av1_run(&ctx)); + assert(dst[0].bank==1&&p->bank[0].busy&&p->bank[0].prepared); + assert(frame.fifo_iova==p->bank[0].codec.bufs.inst.addr); + write_program(argv[1],"av1-overlap-0",&frame);flag_table(&p->bank[0]); + p->bank[0].flags=params.flags;p->bank[0].seg_flags=0; + params.frame_type=V4L2_AV1_INTER_FRAME;params.primary_ref_frame=0;params.ref_frame_idx[0]=0;params.reference_frame_ts[0]=1000; + params.segmentation.flags=V4L2_AV1_SEGMENTATION_FLAG_ENABLED; + queue_request(&src); + /* p_cur is deliberately stale and different from the pending request. */ + params.flags=0;request_params.flags=V4L2_AV1_FRAME_FLAG_DISABLE_FRAME_END_UPDATE_CDF; + assert(avd_av1_ready(&ctx)); + p->bank[0].flags=0;assert(!avd_av1_ready(&ctx));p->bank[0].flags=V4L2_AV1_FRAME_FLAG_DISABLE_FRAME_END_UPDATE_CDF; + p->bank[0].seg_flags=V4L2_AV1_SEGMENTATION_FLAG_ENABLED|V4L2_AV1_SEGMENTATION_FLAG_UPDATE_MAP; + assert(!avd_av1_ready(&ctx));p->bank[0].seg_flags=0; + request_params.flags|=V4L2_AV1_FRAME_FLAG_USE_REF_FRAME_MVS;assert(!avd_av1_ready(&ctx));request_params.flags&=~V4L2_AV1_FRAME_FLAG_USE_REF_FRAME_MVS; + assert(avd_av1_ready(&ctx));params=request_params; + /* CAPTURE tails are poisoned: successor must consume preparation snapshots. */ + size_t tail=AVD_AV1_CDFS_OFFSET(0x800000,dst[0].av1.color_size); + memset((u8 *)dst[0].base.vb.vb2_buf.cpu+tail,0xee,sizeof(struct avd_av1_cdfs)); + memset(p->bank[0].codec.bufs.saved_seg.cpu,0x35,dst[0].av1.seg_size); + current_dst=&dst[1].base.vb;src.vb2_buf.timestamp=1001;frame.idx=1; + assert(!avd_av1_run(&ctx));assert(dst[1].bank==2&&p->bank[0].busy&&p->bank[1].busy); + assert(!memcmp(p->bank[0].codec.bufs.probs.cpu,p->bank[1].codec.bufs.probs.cpu,sizeof(struct avd_av1_cdfs))); + assert(!memcmp(p->bank[0].codec.bufs.saved_seg.cpu,p->bank[1].codec.bufs.prev_seg.cpu,dst[1].av1.seg_size)); + assert(p->bank[1].codec.bufs.prev_seg.addr!=p->bank[0].codec.bufs.saved_seg.addr); + write_program(argv[1],"av1-overlap-1",&frame);assert(!avd_av1_ready(&ctx)); + current_dst=&dst[2].base.vb;assert(avd_av1_run(&ctx)==-EBUSY);assert(!dst[2].bank); + avd_av1_done(&ctx,&src,&dst[0].base.vb,VB2_BUF_STATE_DONE); + assert(!p->bank[0].busy&&p->bank[1].busy&&dst[1].bank==2); + /* Producer bank reuse must not invalidate the consumer's VP map input. */ + memset(p->bank[0].codec.bufs.saved_seg.cpu,0x98,dst[0].av1.seg_size); + assert(((u8 *)p->bank[1].codec.bufs.prev_seg.cpu)[0]==0x35); + avd_av1_done(&ctx,&src,&dst[0].base.vb,VB2_BUF_STATE_DONE);assert(p->bank[1].busy); + avd_av1_done(&ctx,&src,&dst[1].base.vb,VB2_BUF_STATE_ERROR);assert(!p->bank[1].busy&&avd_av1_ready(&ctx)); + assert(request_gets==request_puts); + avd_av1_stop(&ctx);assert(!avd_av1_ready(&ctx)); + for(unsigned int mode=0;mode<2;mode++){ + dev.queued=mode;vp_feature=false;assert(!avd_av1_start(&ctx));p=((struct avd_av1_ctx *)ctx.priv)->stream; + assert(p->nbanks==1&&ctx.max_frames==1);avd_av1_stop(&ctx); + } + dev.queued=true;vp_feature=true; + for(int n=1;n<=6;n++){ + fail_alloc_after=n;allocating_ctx=&ctx;assert(avd_av1_start(&ctx)==-ENOMEM);allocating_ctx=NULL; + assert(!ctx.priv&&!avd_av1_ready(&ctx)); + } + for(unsigned int i=0;i<3;i++)free(dst[i].base.vb.vb2_buf.cpu); + free(program.cpu);free(queue.sram);free(dev.ctrl);free(dev.mbox); + puts("PASS: AV1 snapshot inheritance, poisoned CAPTURE, request lookahead, banks, PP retirement, exhaustion, failures and legacy fallback"); +} +''' + + +def build_run(name, source): + (OUT / f'{name}.c').write_text(source) + cmd = [os.environ.get('CC', 'cc'), '-std=gnu11', '-g', '-O1', '-Wall', '-Wextra', '-Werror', + '-Wno-unused-function', '-Wno-unused-variable', '-Wno-unused-parameter', '-Wno-sign-compare', '-Wno-pointer-sign', + '-fsanitize=address,undefined', '-fno-omit-frame-pointer', str(OUT / f'{name}.c'), '-o', str(OUT / name)] + subprocess.run(cmd, check=True) + subprocess.run([str(OUT / name), str(OUT)], check=True) + + +build_run('av1-host', common + codec(av1) + post + MAIN) +(OUT / 'source.sha256').write_text('\n'.join(hashlib.sha256(p.read_bytes()).hexdigest() + ' ' + str(p) for p in ( + AVD / 'avd-av1.c', AVD / 'avd-av1-entropymode.c', AVD / 'avd-backend-open.c', AVD / 'avd-backend-queue.c')) + '\n') + +base_ref = os.environ.get('AVD_AV1_BASELINE_REF') +if base_ref: + old = subprocess.check_output(['git', '-C', str(KERNEL), 'show', + base_ref + ':drivers/media/platform/apple/avd/avd-av1.c'], text=True) + legacy = r''' +int main(int argc,char **argv){ + assert(argc==2); + struct avd_variant variant={.revision=4,.fifo_slots=15}; + struct avd_dev dev={.variant=&variant,.queued=false}; + struct avd_coded_fmt_ops ops={};struct fmt_desc fmt={.ops=&ops}; + struct avd_ctx ctx={.dev=&dev,.coded_fmt_desc=&fmt}; + struct avd_buf program={};struct avd_frame f={.prog=&program,.ctx=&ctx};dev.building=&f; + struct avd_decoded_buffer dst[2]={};struct vb2_v4l2_buffer src={};u8 coded[512]={}; + current_src=&src;src.vb2_buf=(struct vb2_buffer){.dma=0x220000000ULL,.cpu=coded,.payload=512}; + for(unsigned int i=0;i<2;i++){ + dst[i].base.vb.vb2_buf=(struct vb2_buffer){.dma=0x230000000ULL+i*0x1000000ULL,.cpu=calloc(1,0x800000),.planes[0].length=0x800000,.timestamp=1000+i};refs[i]=&dst[i]; + } + ctx.inst.addr=0x205000000ULL; + ctx.decoded_fmt.fmt.pix_mp.width=1920;ctx.decoded_fmt.fmt.pix_mp.height=1088; + ctx.decoded_fmt.fmt.pix_mp.plane_fmt[0].bytesperline=1920; + seq=(struct v4l2_ctrl_av1_sequence){.bit_depth=8,.flags=V4L2_AV1_SEQUENCE_FLAG_SUBSAMPLING_X|V4L2_AV1_SEQUENCE_FLAG_SUBSAMPLING_Y}; + params=(struct v4l2_ctrl_av1_frame){.frame_type=V4L2_AV1_KEY_FRAME,.primary_ref_frame=7, + .frame_width_minus_1=1919,.frame_height_minus_1=1087,.upscaled_width=1920}; + params.tile_info.tile_cols=params.tile_info.tile_rows=1;params.tile_info.width_in_sbs_minus_1[0]=29;params.tile_info.height_in_sbs_minus_1[0]=16;tile.tile_size=512; + assert(!avd_av1_start(&ctx)); + for(unsigned int mode=0;mode<8;mode++){ + current_dst=&dst[!!mode].base.vb; + params.frame_type=mode==0?V4L2_AV1_KEY_FRAME:mode==2?V4L2_AV1_INTRA_ONLY_FRAME:V4L2_AV1_INTER_FRAME; + params.primary_ref_frame=mode==0||mode==4?7:0; + params.ref_frame_idx[0]=0;params.reference_frame_ts[0]=1000; + params.flags=(mode==3?V4L2_AV1_FRAME_FLAG_ERROR_RESILIENT_MODE:0)|(mode==5?V4L2_AV1_FRAME_FLAG_DISABLE_FRAME_END_UPDATE_CDF:0); + params.segmentation.flags=mode>=6?V4L2_AV1_SEGMENTATION_FLAG_ENABLED:0; + if(mode==7)params.segmentation.flags|=V4L2_AV1_SEGMENTATION_FLAG_UPDATE_MAP; + assert(!avd_av1_run(&ctx)); + char path[4096];snprintf(path,sizeof(path),"%s/@NAME@-%u.bin",argv[1],mode);FILE *out=fopen(path,"wb");assert(out); + assert(fwrite(program.cpu,4,f.prog_words,out)==f.prog_words); + struct avd_decoded_buffer *d=&dst[!!mode];size_t off=AVD_AV1_SEG_OFFSET(0x800000,d->av1.color_size,d->av1.seg_size); + assert(fwrite((u8 *)d->base.vb.vb2_buf.cpu+off,1,d->av1.seg_size+ALIGN(AVD_CDFS_SIZE,AVD_ALIGN),out)==d->av1.seg_size+ALIGN(AVD_CDFS_SIZE,AVD_ALIGN));fclose(out); + if(mode==0){ + size_t cdf=AVD_AV1_CDFS_OFFSET(0x800000,dst[0].av1.color_size); + memset((u8 *)dst[0].base.vb.vb2_buf.cpu+cdf,0x57,AVD_CDFS_SIZE); + memset((u8 *)dst[0].base.vb.vb2_buf.cpu+cdf-dst[0].av1.seg_size,0x36,dst[0].av1.seg_size); + } + } + avd_av1_stop(&ctx);free(program.cpu); + for(unsigned int i=0;i<2;i++)free(dst[i].base.vb.vb2_buf.cpu); +} +''' + for name, body in [('legacy-before', old), ('legacy-after', av1)]: + shim = 'static bool avd_av1_ready(struct avd_ctx *ctx){(void)ctx;return false;}\n' if name == 'legacy-before' else '' + build_run(name, common + codec(body) + shim + post + legacy.replace('@NAME@', name)) + for mode in range(8): + assert (OUT / f'legacy-before-{mode}.bin').read_bytes() == (OUT / f'legacy-after-{mode}.bin').read_bytes(), mode + print('PASS: separate baseline/current executables emit byte-identical legacy AV1 programs and CDF/map tails for eight header cases') diff --git a/drivers/media/platform/apple/avd/tests/unicorn-av1-test.py b/drivers/media/platform/apple/avd/tests/unicorn-av1-test.py new file mode 100644 index 0000000000000..93f4651686776 --- /dev/null +++ b/drivers/media/platform/apple/avd/tests/unicorn-av1-test.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""AV1 production programs: VP(N+1) starts after VP(N) DONE, during PP(N). + +Usage: unicorn-av1-test.py FIRMWARE_TREE v5-t2_IMAGE AV1_HOST_OUTPUT +CDF/map input snapshots are host fixtures. CM3 code, descriptors, program words, +PP commands and configure_stream writes are production. No pixels are decoded. +""" +import importlib.util +from pathlib import Path +import struct +import sys +from unicorn.arm_const import UC_ARM_REG_SP + +fw, image, programs = map(lambda p: Path(p).resolve(), sys.argv[1:4]) +spec = importlib.util.spec_from_file_location('transport', fw / 'tests/transport-test.py') +transport = importlib.util.module_from_spec(spec) +spec.loader.exec_module(transport) + + +class AV1(transport.Transport): + def __init__(self): + self.booting = False + self.expected = [] + self.events = [] + self.live = self.pp_owner = None + self.feed = self.word = 0 + super().__init__(image) + assert self.version == 5 and self.tier == 2 + self.booting = True + self.uc.reg_write(UC_ARM_REG_SP, self.u32(0)) + self.uc.emu_start(self.u32(4), 0, count=500000) + assert not self.booting + + def route(self, irq): + if irq < 14: + router = self.u32(0x50010010) & (1 << irq) + else: + off = irq - 14 + router = self.u32(0x50010014 + off // 32 * 4) & (1 << (off % 32)) + nvic = self.u32(0xe000e100 + irq // 32 * 4) & (1 << (irq % 32)) + assert router and nvic, f'IRQ {irq} masked' + self.call(f'irq{irq}') + + def write(self, uc, access, addr, size, value, user): + if self.booting and addr == 0x50010090 and value == 1: + self.booting = False + uc.emu_stop() + if addr == self.decode + self.fifo + 9 * 4: + idx, words = self.expected[self.feed] + assert value == words[self.word], (idx, self.word, value) + if not self.word: + assert self.live is None, 'successor issued before VP DONE' + self.live = idx + self.events.append(f'AV1 VP START {idx}; PP owner={self.pp_owner}') + self.word += 1 + if self.word == len(words): + self.feed += 1 + self.word = 0 + if addr == self.decode + self.pp: + idx = (value >> 4) & 15 + assert self.pp_owner is None, 'successor PP issued before predecessor PP DONE' + assert value == 0x2b00020f | idx << 4 + self.pp_owner = idx + self.events.append(f'PP START {idx}') + if addr == 0x50010060: + self.events.append(f'notification {value:#x}') + super().write(uc, access, addr, size, value, user) + + def submit_host(self, idx, start): + stem = f'av1-overlap-{idx}' + data = (programs / f'{stem}.bin').read_bytes() + words = list(struct.unpack('<' + 'I' * (len(data) // 4), data)) + desc = list(struct.unpack('<16I', (programs / f'{stem}-desc.bin').read_bytes())) + assert desc[:4] == [0x41565131, 0x10 | idx, 3, 9] + assert desc[4:6] == [0, 0] and desc[6] == len(words) + assert desc[8:16] == [1, 0, 0, 1, 0, 0, 0, 0] + assert words[0] == 0x2b000200 | idx << 4 + assert words[-1] == 0x2b000400 | idx << 4 + mmio = (programs / f'{stem}-mmio.bin').read_bytes() + for offset in (0x1d0, 0x20c, 0x248, 0x284, 0x2c0): + at = offset + idx * 4 + self.put(self.decode + at, struct.unpack_from(' Date: Sun, 1 Nov 2026 23:32:50 +1100 Subject: [PATCH 2/2] media: apple: avd: size AV1 banks from codec pools and fence named VPs Let independent AV1 pictures fill the firmware-advertised VP pool, with one scratch bank per engine and one retained by PP. Keep the two-bank old-queue and one-bank legacy fallbacks, and leave VP9's bank limit unchanged. A different VP no longer supplies the implicit same-engine ordering of the two-bank path. Track tagged VP completion in each saved AV1 CAPTURE bank and block a consumer naming that producer until its VP callback. Duplicate callbacks never release storage. Updated CDF, segmentation and motion-field inputs still require PP retirement; immutable CDF/map copies stay private to the consumer bank. Do not infer CAPTURE DMA visibility from a CM3 DMB. Extend production host and routed firmware tests for pool sizing, all three AV1 engines, reverse VP completion, reference barriers, outbox pressure, allocation failure, saved-bank error retirement and reset. Compare legacy programs and tails with the pre-overlap driver including film grain control values and show flags at 8/10 bits. Device pixel and throughput qualification remains required. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/media/platform/apple/avd/avd-av1.c | 31 ++++- .../platform/apple/avd/avd-backend-queue.c | 4 +- .../media/platform/apple/avd/tests/README.rst | 47 +++++-- .../platform/apple/avd/tests/av1-test.py | 95 +++++++++++-- .../apple/avd/tests/unicorn-av1-test.py | 131 +++++++++++++++--- 5 files changed, 258 insertions(+), 50 deletions(-) diff --git a/drivers/media/platform/apple/avd/avd-av1.c b/drivers/media/platform/apple/avd/avd-av1.c index 6a87a4de6fe9b..9a62f1bca92c4 100644 --- a/drivers/media/platform/apple/avd/avd-av1.c +++ b/drivers/media/platform/apple/avd/avd-av1.c @@ -249,12 +249,13 @@ struct avd_av1_ctx { } bufs; }; -#define AVD_AV1_BANKS 2 +#define AVD_AV1_BANKS AVD_MAX_FRAMES struct avd_av1_bank { struct avd_av1_ctx codec; bool busy; bool prepared; + bool vp_done; struct avd_decoded_buffer *dst; u64 timestamp; u32 flags; @@ -303,10 +304,12 @@ static bool avd_av1_writes_seg(u8 flags) static bool avd_av1_depends(const struct v4l2_ctrl_av1_frame *frame, const struct avd_av1_bank *bank) { - if (frame->flags & V4L2_AV1_FRAME_FLAG_USE_REF_FRAME_MVS) - for (unsigned int i = 0; i < V4L2_AV1_REFS_PER_FRAME; i++) - if (avd_av1_ref_ts(frame, frame->ref_frame_idx[i]) == bank->timestamp) - return true; + /* Preserve same-VP ordering for named references when engines differ. */ + for (unsigned int i = 0; i < V4L2_AV1_REFS_PER_FRAME; i++) + if (avd_av1_ref_ts(frame, frame->ref_frame_idx[i]) == bank->timestamp && + (!smp_load_acquire(&bank->vp_done) || + (frame->flags & V4L2_AV1_FRAME_FLAG_USE_REF_FRAME_MVS))) + return true; if (frame->primary_ref_frame >= AV1_PRIMARY_REF_NONE || avd_av1_ref_ts(frame, frame->ref_frame_idx[frame->primary_ref_frame]) != @@ -1620,6 +1623,7 @@ static int avd_av1_run(struct avd_ctx *ctx) av1_ctx->b = bank; smp_store_release(&ctx->priv, &bank->codec); WRITE_ONCE(bank->prepared, false); + WRITE_ONCE(bank->vp_done, false); WRITE_ONCE(bank->busy, true); bank->dst = dst; bank->timestamp = run.base.bufs.src->vb2_buf.timestamp; @@ -1660,6 +1664,18 @@ static int avd_av1_run(struct avd_ctx *ctx) return avd_submit_job(ctx); } + +/* Tagged engine completion releases ordering, never CAPTURE tail visibility. */ +static void avd_av1_vp_done(struct avd_ctx *ctx, struct vb2_v4l2_buffer *dst) +{ + struct avd_av1_stream *stream = ((struct avd_av1_ctx *)ctx->priv)->stream; + struct avd_decoded_buffer *buf = vb2_to_avd_decoded_buf(&dst->vb2_buf); + struct avd_av1_bank *bank = avd_av1_ref_bank(stream, buf); + + if (bank) + smp_store_release(&bank->vp_done, true); +} + static void avd_av1_done(struct avd_ctx *ctx, struct vb2_v4l2_buffer *src, struct vb2_v4l2_buffer *dst, enum vb2_buffer_state result) { @@ -1731,6 +1747,7 @@ static void avd_av1_stop(struct avd_ctx *ctx) { avd_av1_free(ctx, ((struct avd_av1_ctx *)ctx->priv)->stream); ctx->priv = NULL; + ctx->max_frames = 1; } static int avd_av1_start(struct avd_ctx *ctx) @@ -1742,7 +1759,8 @@ static int avd_av1_start(struct avd_ctx *ctx) if (!av1_ctx) return -ENOMEM; - av1_ctx->nbanks = avd_qfw_has_vp_done(ctx->dev) ? AVD_AV1_BANKS : 1; + av1_ctx->nbanks = avd_qfw_has_vp_done(ctx->dev) ? + avd_qfw_picture_banks(ctx->dev, AVD_CODEC_AV1) : 1; av1_ctx->b = &av1_ctx->bank[0]; for (unsigned int i = 0; i < av1_ctx->nbanks; i++) { av1_ctx->bank[i].codec.stream = av1_ctx; @@ -1845,6 +1863,7 @@ const struct avd_coded_fmt_ops avd_av1_fmt_ops = { .stop = avd_av1_stop, .run = avd_av1_run, .done = avd_av1_done, + .vp_done = avd_av1_vp_done, .ready = avd_av1_ready, .submit = avd_av1_submit, .pp_kicks = avd_av1_pp_kicks, diff --git a/drivers/media/platform/apple/avd/avd-backend-queue.c b/drivers/media/platform/apple/avd/avd-backend-queue.c index 5853d9d050a93..178572cb2e5d5 100644 --- a/drivers/media/platform/apple/avd/avd-backend-queue.c +++ b/drivers/media/platform/apple/avd/avd-backend-queue.c @@ -117,8 +117,8 @@ static unsigned int avd_qfw_bank_limit(struct avd_qfw *q, unsigned int codec) unsigned int banks = 1; u32 pool; - /* k4m keeps two banks with old queue firmware. */ - if (!q->multi_vp || codec > AVD_CODEC_H264) + /* Old queue firmware and VP9 retain the two-bank overlap path. */ + if (!q->multi_vp || codec == AVD_CODEC_VP9 || codec > AVD_CODEC_AV1) return 2; /* One bank per VP plus one retained by PP. No SoC counts in the host. */ for (pool = q->vp_pool[codec]; pool; pool &= pool - 1) diff --git a/drivers/media/platform/apple/avd/tests/README.rst b/drivers/media/platform/apple/avd/tests/README.rst index daa70bb23fc51..9b17575f8d7f5 100644 --- a/drivers/media/platform/apple/avd/tests/README.rst +++ b/drivers/media/platform/apple/avd/tests/README.rst @@ -200,19 +200,19 @@ This is a backwards-compatible optional ABI 2 feature: bank reuse. Only the original tagged PP completion does that. A missing VP event remains bounded by the original per-picture watchdog. -AV1 conservative picture overlap -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +AV1 picture overlap and codec VP pools +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``av1-test.py`` compiles the production AV1 codec, entropy defaults, open program builder and queued descriptor writer with ASan/UBSan. It is also called by the registered ``avd-codec-tail-tests.py`` host check. Run:: - AVD_AV1_BASELINE_REF=a1b76856 python3 tests/av1-test.py /tmp/avd-av1 + AVD_AV1_BASELINE_REF=d62b4894 python3 tests/av1-test.py /tmp/avd-av1 python3 tests/unicorn-av1-test.py /path/to/avd-fw \ /path/to/avd-fw/build/avd-fw-v5-t2.bin /tmp/avd-av1 The independent legacy comparison builds before/after executables and compares -complete emitted programs plus initialized CDF/map tails for eight header cases. +complete emitted programs plus initialized CDF/map tails for twelve header cases. The flag table covers primary-reference/default CDF cases, both producer and consumer disable_frame_end_update_cdf, segmentation enabled/update_map on both pictures, and all 256 refresh masks. Pending request p_req values are used for @@ -243,12 +243,17 @@ Safe subset ^^^^^^^^^^^ Queue firmware advertising VP-DONE feature bit 2 enables two scratch banks; -legacy transport and older queue firmware retain one bank and identical +with a validated codec pool (feature bit 3), AV1 instead allocates one bank per +advertised AV1 VP plus one retained by PP, bounded by physical FIFO slots. +There are no host SoC counts. VP9 retains its existing two-bank limit. +Legacy transport and queue firmware without tagged VP DONE retain one bank and identical programs. The CDF output, segmentation output and motion-field/color output already live in each picture's distinct CAPTURE tail. Each scratch bank owns its FIFO, CDF input, pipe/filter/tile scratch, a saved initialized map and a separate map input snapshot. Only PP retirement releases its bank. VP DONE -never releases storage or publishes hardware-written tails for AV1. +never releases storage or publishes hardware-written tails for AV1. It releases +only the producer VP ordering barrier for a named reference. Each saved CAPTURE +bank records its own tagged VP event, independently of the current builder. For a pending consumer, each busy producer is tested by the exact timestamp named through ref_frame_idx/primary_ref_frame, rather than the preceding frame @@ -258,6 +263,7 @@ userspace subsequently names; it does not create a dependency by itself. =============================== ============================================== Consumer / producer condition Prepare dependency while producer is in flight =============================== ============================================== +Named live reference Wait for its tagged VP DONE on any engine PRIMARY_REF_NONE No primary CDF or map dependency Intra or error resilient Default CDF; map dependency still checked Primary slot names retired ref None on an in-flight picture @@ -282,11 +288,23 @@ these copies. Busy banks publish fully prepared metadata with release/acquire; allocation failures and PP error/reset retirement release the saved CAPTURE's bank, independent of the current builder. -This subset needs no extra VP token: firmware already waits for VP(N) DONE -before feeding VP(N+1). AV1 descriptors retain their ordinary completion-only -flags. The Unicorn run boots the v5-t2 CM3 reset vector, routes AV1 and PP IRQs, +The former single-engine subset relied on firmware waiting for VP(N) DONE +before feeding VP(N+1) on the same engine. Across engines that ordering is +explicit: a consumer naming any live reference waits for that producer's tagged +VP event. The callback publishes only ordering, without sampling CAPTURE tails. +The CDF/map snapshot exception then applies; hardware-written CDF/map/MV inputs +still wait for PP retirement. Independent pictures can start on different VPs +before either completes. PP consumes reference reconstruction in submission +order, as in the multi-VP H.264/HEVC path. + +The Unicorn run boots the v5-t2 CM3 reset vector, routes AV1 and PP IRQs, and checks production AV1 program words/descriptors/configuration. It shows -VP(1) issued during PP(0), PP serialization and blocked completion mailboxes. +VP(1) issued during PP(0), concurrent VPs 9/10/11, a fourth picture waiting for +VP9 DONE, reverse engine completion, per-picture VP tokens, ordered PP and +blocked completion mailboxes. A routed error on each AV1 engine drains every +owner; reset boots all engines again. Host tests cover pool sizes, sparse masks, +FIFO bounds, each producer's CDF/map/MV and VP barriers, duplicate VP callbacks, +all-bank allocation failures, exhaustion and saved-bank error retirement. Host tests poison in-flight CAPTURE tails to ensure snapshot consumers never read them. Pixels, physical DMA visibility and throughput remain hardware qualification work for the lead. Full CDF/MV dependency release at VP DONE is @@ -341,3 +359,12 @@ preamble. Across all four revision/address variants, weight modes and first/continuation slices, its complete program must equal the baseline IDR program exactly. Pictures with references retain their original header. The baseline comparison retains all 32 before/after empty-DPB cases. + +Film grain controls, frame flags and the legacy syntax/program emitters are +unchanged. The independent baseline comparison includes film grain control +values and SHOW_FRAME/SHOWABLE_FRAME combinations at 8 and 10 bits. It checks +program/tail parity, not grain synthesis. The stateless AV1 frame UAPI has no +show_existing_frame decode control: userspace presents the existing CAPTURE +buffer without a new tile decode. No VP event retires that CAPTURE buffer or +its media request early. Hardware qualification must include those presentation +paths as well as pixel hashes; this change makes no new grain-support claim. diff --git a/drivers/media/platform/apple/avd/tests/av1-test.py b/drivers/media/platform/apple/avd/tests/av1-test.py index a02070c85ffe7..217b433329978 100644 --- a/drivers/media/platform/apple/avd/tests/av1-test.py +++ b/drivers/media/platform/apple/avd/tests/av1-test.py @@ -38,8 +38,8 @@ pre = pre.replace('unsigned int fifo_idx, pp_total;', 'unsigned int fifo_idx, pp_total, max_frames; int ctrl_hdl;') pre = pre.replace('dma_addr_t comp_out, y_out, uv_out;', 'dma_addr_t comp_out, y_out, uv_out, coded_in;') pre = pre.replace('struct { u32 bytesperline; } plane_fmt[1];', 'u32 width,height; struct { u32 bytesperline; } plane_fmt[1];') -pre = pre.replace('static struct avd_decoded_buffer reference;', 'static struct avd_decoded_buffer reference; static struct avd_decoded_buffer *refs[3];') -pre = pre.replace('(void)c; (void)b; (void)t; return &reference;', '(void)c;for(unsigned int i=0;i<3;i++)if(refs[i]&&refs[i]->base.vb.vb2_buf.timestamp==t)return refs[i];return (void *)b;') +pre = pre.replace('static struct avd_decoded_buffer reference;', 'static struct avd_decoded_buffer reference; static struct avd_decoded_buffer *refs[5];') +pre = pre.replace('(void)c; (void)b; (void)t; return &reference;', '(void)c;for(unsigned int i=0;i<5;i++)if(refs[i]&&refs[i]->base.vb.vb2_buf.timestamp==t)return refs[i];return (void *)b;') pre = pre.replace('static int avd_buf_alloc(struct avd_dev *d, struct avd_buf *b, size_t n)', 'static struct avd_ctx *allocating_ctx;\nstatic int fail_alloc_after;\nstatic bool avd_av1_ready(struct avd_ctx *);\nstatic int avd_buf_alloc(struct avd_dev *d, struct avd_buf *b, size_t n)') pre = pre.replace('{ (void)d; free(b->cpu); b->cpu = calloc(1,n);', @@ -69,6 +69,7 @@ enum vb2_buffer_state { STATE_DUMMY }; #define kzalloc(n,f) calloc(1,n) #define kfree free +static unsigned int avd_qfw_picture_banks(const struct avd_dev *, unsigned int); static bool vp_feature=true; static bool avd_qfw_has_vp_done(const struct avd_dev *d){return d->queued&&vp_feature;} static void *vb2_plane_vaddr(struct vb2_buffer *b,int p){(void)p;return b->cpu;} @@ -124,6 +125,7 @@ def without_includes(s): post += hw[hw.index('#define AVD_V3_'):hw.index('int avd_boot(')] post += '#define w32(r,v) writel((v),avd->ctrl+(r))\n#define m32(r,v) w32(r,(v)|readl(avd->ctrl+(r)))\n' post += function(hw, 't8112_configure_stream') + function(hw, 't8122_configure_stream') +post += function(qfw, 'avd_qfw_picture_banks') post += function(qfw, 'avd_qfw_stream') + function(qfw, 'avd_qfw_submit') post += '\nstatic int avd_submit_job(struct avd_ctx *ctx){return avd_open_build_program(ctx);}\n' @@ -149,6 +151,7 @@ def codec(s): request_hdl.ctrl_refs=&request_ref;src->vb2_buf.req_obj.req=&request_hdl; } static void flag_table(struct avd_av1_bank *b){ + b->vp_done=true; /* Actual producer timestamp is slot 5, not the immediately prior slot 0. */ struct v4l2_ctrl_av1_frame f={.frame_type=V4L2_AV1_INTER_FRAME,.primary_ref_frame=2, .frame_width_minus_1=1919,.frame_height_minus_1=1087}; @@ -190,13 +193,13 @@ def codec(s): struct avd_qfw queue={.sram=calloc(1,0x10000),.aperture=0x1094000,.ring_words=0x1800,.fifo_slots=15,.vp_done=true}; dev.qfw=&queue;dev.ctrl=calloc(1,0x10000);dev.mbox=calloc(1,0x100);variant.configure_stream=t8122_configure_stream; writel(0x20001,dev.mbox+0x50); - static const struct avd_coded_fmt_ops ops={.pp_kicks=avd_av1_pp_kicks}; + static const struct avd_coded_fmt_ops ops={.pp_kicks=avd_av1_pp_kicks,.vp_done=avd_av1_vp_done}; static const struct fmt_desc fmt={.ops=&ops}; struct avd_ctx ctx={.dev=&dev,.coded_fmt_desc=&fmt,.pp_total=1}; struct avd_buf program={};struct avd_frame frame={.prog=&program,.ctx=&ctx};dev.building=&frame; - struct avd_decoded_buffer dst[3]={};struct vb2_v4l2_buffer src={};u8 coded[512]={}; + struct avd_decoded_buffer dst[5]={};struct vb2_v4l2_buffer src={};u8 coded[512]={}; src.vb2_buf=(struct vb2_buffer){.dma=0x220000000ULL,.cpu=coded,.payload=512,.timestamp=1000};current_src=&src; - for(unsigned int i=0;i<3;i++){ + for(unsigned int i=0;i<5;i++){ dst[i].base.vb.vb2_buf=(struct vb2_buffer){.dma=0x230000000ULL+i*0x1000000ULL,.cpu=calloc(1,0x800000),.planes[0].length=0x800000,.timestamp=1000+i};refs[i]=&dst[i]; } ctx.decoded_fmt.fmt.pix_mp.width=1920;ctx.decoded_fmt.fmt.pix_mp.height=1088; @@ -219,7 +222,9 @@ def codec(s): queue_request(&src); /* p_cur is deliberately stale and different from the pending request. */ params.flags=0;request_params.flags=V4L2_AV1_FRAME_FLAG_DISABLE_FRAME_END_UPDATE_CDF; - assert(avd_av1_ready(&ctx)); + p->bank[0].vp_done=false;assert(!avd_av1_ready(&ctx)); + avd_av1_vp_done(&ctx,&dst[0].base.vb);assert(p->bank[0].busy&&avd_av1_ready(&ctx)); + avd_av1_vp_done(&ctx,&dst[0].base.vb);assert(p->bank[0].busy); p->bank[0].flags=0;assert(!avd_av1_ready(&ctx));p->bank[0].flags=V4L2_AV1_FRAME_FLAG_DISABLE_FRAME_END_UPDATE_CDF; p->bank[0].seg_flags=V4L2_AV1_SEGMENTATION_FLAG_ENABLED|V4L2_AV1_SEGMENTATION_FLAG_UPDATE_MAP; assert(!avd_av1_ready(&ctx));p->bank[0].seg_flags=0; @@ -254,7 +259,70 @@ def codec(s): fail_alloc_after=n;allocating_ctx=&ctx;assert(avd_av1_start(&ctx)==-ENOMEM);allocating_ctx=NULL; assert(!ctx.priv&&!avd_av1_ready(&ctx)); } - for(unsigned int i=0;i<3;i++)free(dst[i].base.vb.vb2_buf.cpu); + + /* AV1 pool size is firmware-owned, including sparse masks and FIFO bounds. */ + for(unsigned int count=1;count<=3;count++){ + queue.multi_vp=true;queue.vp_pool[3]=((1u<stream; + assert(p->nbanks==count+1&&ctx.max_frames==count+1); + avd_av1_stop(&ctx);assert(ctx.max_frames==1); + } + queue.vp_pool[3]=BIT(9)|BIT(11);queue.fifo_slots=2; + assert(!avd_av1_start(&ctx));p=((struct avd_av1_ctx *)ctx.priv)->stream; + assert(p->nbanks==2);avd_av1_stop(&ctx); + queue.fifo_slots=15;queue.vp_pool[3]=BIT(9)|BIT(10)|BIT(11);queue.next_vp[3]=0; + assert(!avd_av1_start(&ctx));p=((struct avd_av1_ctx *)ctx.priv)->stream; + assert(p->nbanks==4&&ctx.max_frames==4); + params.primary_ref_frame=7;params.flags=V4L2_AV1_FRAME_FLAG_DISABLE_FRAME_END_UPDATE_CDF; + params.segmentation.flags=0; + for(unsigned int i=0;i<7;i++)params.ref_frame_idx[i]=-1; + for(unsigned int n=0;n<4;n++){ + src.vb2_buf.timestamp=1000+n;current_dst=&dst[n].base.vb;frame.idx=n; + queue_request(&src);assert(avd_av1_ready(&ctx));assert(!avd_av1_run(&ctx)); + char stem[64];snprintf(stem,sizeof(stem),"av1-multi-%u",n);write_program(argv[1],stem,&frame); + assert(dst[n].bank==n+1&&p->bank[n].busy&&p->bank[n].prepared); + assert(readl(queue.sram+0x8000+n*64+D_VP)==9+n%3); + for(unsigned int prev=0;prevbank[prev].codec.bufs.inst.addr!=p->bank[n].codec.bufs.inst.addr); + assert(p->bank[prev].codec.bufs.probs.addr!=p->bank[n].codec.bufs.probs.addr); + } + } + assert(!avd_av1_ready(&ctx));current_dst=&dst[4].base.vb; + assert(avd_av1_run(&ctx)==-EBUSY&&!dst[4].bank); + /* Free a bank, then reference each of the other engines' live producers. */ + avd_av1_done(&ctx,&src,&dst[3].base.vb,VB2_BUF_STATE_DONE); + for(unsigned int n=0;n<3;n++){ + request_params.primary_ref_frame=0;request_params.ref_frame_idx[0]=n; + request_params.reference_frame_ts[n]=1000+n; + p->bank[n].flags=V4L2_AV1_FRAME_FLAG_DISABLE_FRAME_END_UPDATE_CDF; + assert(!avd_av1_ready(&ctx));avd_av1_vp_done(&ctx,&dst[n].base.vb); + assert(p->bank[n].busy&&p->bank[n].vp_done&&avd_av1_ready(&ctx)); + p->bank[n].flags=0;assert(!avd_av1_ready(&ctx)); + p->bank[n].flags=V4L2_AV1_FRAME_FLAG_DISABLE_FRAME_END_UPDATE_CDF; + assert(avd_av1_ready(&ctx)); + p->bank[n].seg_flags=V4L2_AV1_SEGMENTATION_FLAG_ENABLED|V4L2_AV1_SEGMENTATION_FLAG_UPDATE_MAP; + request_params.segmentation.flags=V4L2_AV1_SEGMENTATION_FLAG_ENABLED; + assert(!avd_av1_ready(&ctx));request_params.segmentation.flags=0;p->bank[n].seg_flags=0; + request_params.flags|=V4L2_AV1_FRAME_FLAG_USE_REF_FRAME_MVS; + assert(!avd_av1_ready(&ctx));request_params.flags&=~V4L2_AV1_FRAME_FLAG_USE_REF_FRAME_MVS; + } + /* Reset/error retirement must release the saved bank, irrespective of builder. */ + for(unsigned int n=3;n>0;n--){ + avd_av1_done(&ctx,&src,&dst[n-1].base.vb,VB2_BUF_STATE_ERROR); + assert(!p->bank[n-1].busy&&!dst[n-1].bank); + } + assert(avd_av1_ready(&ctx)); + params.primary_ref_frame=7;params.segmentation.flags=0; + current_dst=&dst[0].base.vb;frame.idx=0;assert(!avd_av1_run(&ctx)); + assert(dst[0].bank==1&&p->bank[0].busy); + avd_av1_done(&ctx,&src,&dst[0].base.vb,VB2_BUF_STATE_DONE);avd_av1_stop(&ctx); + /* Fail allocation in every bank, including after earlier banks succeeded. */ + for(int n=1;n<=12;n++){ + fail_alloc_after=n;allocating_ctx=&ctx;assert(avd_av1_start(&ctx)==-ENOMEM);allocating_ctx=NULL; + assert(!ctx.priv&&!avd_av1_ready(&ctx)); + } + puts("PASS: AV1 firmware pool sizes, sparse masks, FIFO bounds, per-engine dispatch, all producer barriers, exhaustion, reset/error and reuse"); + for(unsigned int i=0;i<5;i++)free(dst[i].base.vb.vb2_buf.cpu); free(program.cpu);free(queue.sram);free(dev.ctrl);free(dev.mbox); puts("PASS: AV1 snapshot inheritance, poisoned CAPTURE, request lookahead, banks, PP retirement, exhaustion, failures and legacy fallback"); } @@ -299,12 +367,19 @@ def build_run(name, source): .frame_width_minus_1=1919,.frame_height_minus_1=1087,.upscaled_width=1920}; params.tile_info.tile_cols=params.tile_info.tile_rows=1;params.tile_info.width_in_sbs_minus_1[0]=29;params.tile_info.height_in_sbs_minus_1[0]=16;tile.tile_size=512; assert(!avd_av1_start(&ctx)); - for(unsigned int mode=0;mode<8;mode++){ + for(unsigned int mode=0;mode<12;mode++){ current_dst=&dst[!!mode].base.vb; params.frame_type=mode==0?V4L2_AV1_KEY_FRAME:mode==2?V4L2_AV1_INTRA_ONLY_FRAME:V4L2_AV1_INTER_FRAME; params.primary_ref_frame=mode==0||mode==4?7:0; params.ref_frame_idx[0]=0;params.reference_frame_ts[0]=1000; params.flags=(mode==3?V4L2_AV1_FRAME_FLAG_ERROR_RESILIENT_MODE:0)|(mode==5?V4L2_AV1_FRAME_FLAG_DISABLE_FRAME_END_UPDATE_CDF:0); + seq.bit_depth=mode>=10?10:8; + if(mode>=8){ + seq.flags|=V4L2_AV1_SEQUENCE_FLAG_FILM_GRAIN_PARAMS_PRESENT; + grain.flags=V4L2_AV1_FILM_GRAIN_FLAG_APPLY_GRAIN|V4L2_AV1_FILM_GRAIN_FLAG_UPDATE_GRAIN; + grain.grain_seed=1234+mode; + params.flags|=mode&1?V4L2_AV1_FRAME_FLAG_SHOW_FRAME:V4L2_AV1_FRAME_FLAG_SHOWABLE_FRAME; + } params.segmentation.flags=mode>=6?V4L2_AV1_SEGMENTATION_FLAG_ENABLED:0; if(mode==7)params.segmentation.flags|=V4L2_AV1_SEGMENTATION_FLAG_UPDATE_MAP; assert(!avd_av1_run(&ctx)); @@ -325,6 +400,6 @@ def build_run(name, source): for name, body in [('legacy-before', old), ('legacy-after', av1)]: shim = 'static bool avd_av1_ready(struct avd_ctx *ctx){(void)ctx;return false;}\n' if name == 'legacy-before' else '' build_run(name, common + codec(body) + shim + post + legacy.replace('@NAME@', name)) - for mode in range(8): + for mode in range(12): assert (OUT / f'legacy-before-{mode}.bin').read_bytes() == (OUT / f'legacy-after-{mode}.bin').read_bytes(), mode - print('PASS: separate baseline/current executables emit byte-identical legacy AV1 programs and CDF/map tails for eight header cases') + print('PASS: separate baseline/current executables emit byte-identical legacy AV1 programs and CDF/map tails for twelve header cases including film grain/show flags at 8/10 bits') diff --git a/drivers/media/platform/apple/avd/tests/unicorn-av1-test.py b/drivers/media/platform/apple/avd/tests/unicorn-av1-test.py index 93f4651686776..c635e3c2452e7 100644 --- a/drivers/media/platform/apple/avd/tests/unicorn-av1-test.py +++ b/drivers/media/platform/apple/avd/tests/unicorn-av1-test.py @@ -19,7 +19,10 @@ class AV1(transport.Transport): - def __init__(self): + def __init__(self, multi=False): + self.multi = multi + self.engines = {} + self.live_engines = {} self.booting = False self.expected = [] self.events = [] @@ -32,6 +35,13 @@ def __init__(self): self.uc.emu_start(self.u32(4), 0, count=500000) assert not self.booting + def drain(self, n): + # Simulate a host consuming each available token; self.busy separately + # models a deliberately blocked outbox in the poll-only sections. + for _ in range(n): + self.busy = False + self.poll() + def route(self, irq): if irq < 14: router = self.u32(0x50010010) & (1 << irq) @@ -46,10 +56,15 @@ def write(self, uc, access, addr, size, value, user): if self.booting and addr == 0x50010090 and value == 1: self.booting = False uc.emu_stop() - if addr == self.decode + self.fifo + 9 * 4: + if self.decode + self.fifo <= addr < self.decode + self.fifo + self.vps * 4: idx, words = self.expected[self.feed] assert value == words[self.word], (idx, self.word, value) - if not self.word: + vp = self.engines[idx] + assert addr == self.decode + self.fifo + vp * 4 + if not self.word and self.multi: + assert vp not in self.live_engines, "VP reused before its DONE" + self.live_engines[vp] = idx + if not self.word and not self.multi: assert self.live is None, 'successor issued before VP DONE' self.live = idx self.events.append(f'AV1 VP START {idx}; PP owner={self.pp_owner}') @@ -68,20 +83,23 @@ def write(self, uc, access, addr, size, value, user): super().write(uc, access, addr, size, value, user) def submit_host(self, idx, start): - stem = f'av1-overlap-{idx}' + stem = f"av1-{'multi' if self.multi else 'overlap'}-{idx}" data = (programs / f'{stem}.bin').read_bytes() words = list(struct.unpack('<' + 'I' * (len(data) // 4), data)) desc = list(struct.unpack('<16I', (programs / f'{stem}-desc.bin').read_bytes())) - assert desc[:4] == [0x41565131, 0x10 | idx, 3, 9] + vp = 9 + idx % 3 if self.multi else 9 + assert self.pool[3] & (1 << vp) + assert desc[:4] == [0x41565131, 0x10 | idx, 3, vp] + self.engines[idx] = vp assert desc[4:6] == [0, 0] and desc[6] == len(words) - assert desc[8:16] == [1, 0, 0, 1, 0, 0, 0, 0] + assert desc[8:16] == [1, 0, 0, 3, 0, 0, 0, 0] assert words[0] == 0x2b000200 | idx << 4 assert words[-1] == 0x2b000400 | idx << 4 mmio = (programs / f'{stem}-mmio.bin').read_bytes() for offset in (0x1d0, 0x20c, 0x248, 0x284, 0x2c0): at = offset + idx * 4 self.put(self.decode + at, struct.unpack_from('> 16 == 0x5453] + + for blocked in (False, True): t = AV1() wp = t.submit_host(0, 0) - t.poll(20) - assert t.live == 0 and t.pp_owner is None and not t.notifications + t.drain(20) + assert t.live == 0 and t.pp_owner is None and not pp_tokens(t) t.finish_vp(0) - t.poll(20) - assert t.pp_owner == 0 and t.live is None and not t.notifications - # The host fixture proved that the second picture reads only immutable - # preparation snapshots. No VP notification is needed for this subset. + t.drain(20) + assert t.pp_owner == 0 and t.live is None and not pp_tokens(t) + # The host consumes the tagged VP barrier before dispatching this named + # reference. Its CDF/map inputs come from immutable preparation snapshots. wp = t.submit_host(1, wp) - t.poll(20) + t.drain(20) assert t.live == 1 and t.pp_owner == 0, 'no AV1 VP/PP overlap' t.finish_vp(1) - t.poll(20) - assert t.pp_owner == 0 and not t.notifications + t.drain(20) + assert t.pp_owner == 0 and not pp_tokens(t) t.busy = blocked t.finish_pp(0) t.poll(20) if blocked: - assert not t.notifications and t.result(0) == [1, 0, 0] + assert not pp_tokens(t) and t.result(0) == [1, 0, 0] t.busy = False - t.poll(20) - assert t.result(0) == [2, 0, 0x10] and t.notifications == [0x54530010] + t.drain(20) + assert t.result(0) == [2, 0, 0x10] and pp_tokens(t) == [0x54530010] assert t.pp_owner == 1 t.busy = False t.finish_pp(1) - t.poll(20) + t.drain(20) assert t.result(1) == [2, 0, 0x11] - assert t.notifications == [0x54530010, 0x54530011] + assert t.notifications == [0x54540010, 0x54540011, 0x54530010, 0x54530011] assert t.u32(0x10007ff8) == t.u32(0x10007ffc) == wp assert not t.fetches and not t.ring_writes - assert not t.u32(0x1000803c) and not t.u32(0x1000807c) + assert t.u32(0x1000803c) == t.u32(0x1000807c) == 1 print('\n'.join('TRACE ' + e for e in t.events)) print(f'PASS: AV1 snapshot picture overlap, blocked PP mailbox={blocked}; VP(1) issued before PP(0) DONE') + + +for blocked in (False, True): + t = AV1(multi=True) + wp = 0 + for idx in range(4): + wp = t.submit_host(idx, wp) + t.drain(30) + assert t.live_engines == {9: 0, 10: 1, 11: 2} + assert t.feed == 3 and t.pp_owner is None + # DONE on a later engine never starts its PP before predecessor VP DONE. + for idx in (2, 1): + t.finish_vp(idx) + t.drain(10) + assert t.pp_owner is None and not pp_tokens(t) + t.finish_vp(0) + t.drain(20) + assert t.pp_owner == 0 and t.live_engines == {9: 3} + t.finish_vp(3) + t.drain(10) + assert not t.live_engines and t.pp_owner == 0 + for idx in range(4): + assert t.pp_owner == idx + t.busy = blocked + t.finish_pp(idx) + t.poll(10) + if blocked: + assert t.result(idx) == [1, 0, 0] + t.busy = False + t.drain(10) + assert t.result(idx) == [2, 0, 0x10 | idx] + assert pp_tokens(t) == [0x54530010 | idx for idx in range(4)] + assert [word for word in t.notifications if word >> 16 == 0x5454] == [0x54540012, 0x54540011, 0x54540010, 0x54540013] + assert t.u32(0x10007ff8) == t.u32(0x10007ffc) == wp + assert not t.fetches and not t.ring_writes + print(f'PASS: AV1 three-engine dispatch, fourth waits for VP9, reverse VP DONE, ordered PP, blocked mailbox={blocked}') + +# A routed error on each advertised AV1 engine must fault every owner; restart +# from reset then proves the next set of production descriptors can run again. +for failed_vp in (9, 10, 11): + t = AV1(multi=True) + wp = 0 + for idx in range(3): + wp = t.submit_host(idx, wp) + t.drain(20) + assert len(t.live_engines) == 3 + t.live_engines.clear() + t.route(79 + failed_vp * 5) # engine-specific error, not DONE + t.drain(30) + assert t.pp_owner is None + assert all(t.result(idx) == [2, 2, 0x10 | idx] for idx in range(3)) + assert t.notifications == [0x54530010 | idx for idx in range(3)] + # A fresh CM3 reset boots all three engines after the halt. + restarted = AV1(multi=True) + wp = 0 + for idx in range(3): + wp = restarted.submit_host(idx, wp) + restarted.drain(20) + assert restarted.live_engines == {9: 0, 10: 1, 11: 2} + print(f'PASS: AV1 VP{failed_vp} routed error drains all owners; reset restores all engines')