From 26460cdc487009b739832457e0c0eb8d8bd3545a Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sat, 31 Oct 2026 21:51:16 +1100 Subject: [PATCH 1/3] media: apple: avd: pipeline CPU preparation with retained request ownership Let an independent context prepare its next decode request while one context owns the hardware. Retain explicit buffers and per-context codec state after releasing the m2m scheduler, route IRQs through the hardware owner, and drain released ownership before streamoff/free. Keep one hardware owner because PP completion is untagged, one request per context for VP9/AV1 dependencies, and the existing H.264 held-capture scheduling contract. Serialize retirement with watchdog cancellation and reset/IRQ/mailbox drain. Fail closed after reset failure and fail partial submission explicitly. Include the empty-mailbox guard from the public fix proposed in #640 so a repeated PP word cannot retire a new owner. Preserve PM references until retirement and pause/drain requests across system suspend. Enable HEVC in T6040 match data with its existing v5 header, requiring firmware CODECS:h265 support for HEVC while retaining legacy admission of other codecs. No pixel, firmware-overlap or throughput qualification is claimed. Add KUnit ownership transitions and host ASan/UBSan tests executing production decode paths, OF match data and firmware capability parsing with negative controls. Hardware validation remains a separate step. Link: https://github.com/aurora-silicon/linux/pull/640 Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/media/platform/apple/avd/Kconfig | 4 +- drivers/media/platform/apple/avd/Makefile | 2 +- drivers/media/platform/apple/avd/avd-drv.c | 366 ++++++------ drivers/media/platform/apple/avd/avd-regs.h | 2 + .../platform/apple/avd/avd-request-test.c | 105 ++++ .../media/platform/apple/avd/avd-request.h | 94 ++++ drivers/media/platform/apple/avd/avd-v4l2.c | 49 +- drivers/media/platform/apple/avd/avd.h | 35 +- tools/asahi/avd-codec-tail-tests.py | 29 +- tools/asahi/avd-request-tests.py | 164 ++++++ tools/asahi/avd-request/README.md | 44 ++ tools/asahi/avd-request/host.c | 519 ++++++++++++++++++ 12 files changed, 1188 insertions(+), 225 deletions(-) create mode 100644 drivers/media/platform/apple/avd/avd-request-test.c create mode 100644 drivers/media/platform/apple/avd/avd-request.h create mode 100644 tools/asahi/avd-request-tests.py create mode 100644 tools/asahi/avd-request/README.md create mode 100644 tools/asahi/avd-request/host.c diff --git a/drivers/media/platform/apple/avd/Kconfig b/drivers/media/platform/apple/avd/Kconfig index ed5ad44276c31..792ea42331df1 100644 --- a/drivers/media/platform/apple/avd/Kconfig +++ b/drivers/media/platform/apple/avd/Kconfig @@ -26,6 +26,8 @@ config VIDEO_APPLE_AVD_KUNIT_TEST Build KUnit tests for the Apple video decoder. They exercise the checks that the H.264 path makes on the slices that userspace queues, such as finding the slice data after the slice header, - at the limits and one past them. + at the limits and one past them. Request ownership tests also cover + submission-time IRQs, VP routing, duplicate completions and watchdog + races, without accessing decoder hardware. If unsure, say N. diff --git a/drivers/media/platform/apple/avd/Makefile b/drivers/media/platform/apple/avd/Makefile index 4d1877c414d41..0d2c540165829 100644 --- a/drivers/media/platform/apple/avd/Makefile +++ b/drivers/media/platform/apple/avd/Makefile @@ -2,5 +2,5 @@ apple-avd-y := avd-drv.o avd-v4l2.o avd-hw.o avd-h264.o avd-vp9.o avd-hevc.o \ avd-av1.o avd-av1-entropymode.o -apple-avd-$(CONFIG_VIDEO_APPLE_AVD_KUNIT_TEST) += avd-h264-test.o +apple-avd-$(CONFIG_VIDEO_APPLE_AVD_KUNIT_TEST) += avd-h264-test.o avd-request-test.o obj-$(CONFIG_VIDEO_APPLE_AVD) += apple-avd.o diff --git a/drivers/media/platform/apple/avd/avd-drv.c b/drivers/media/platform/apple/avd/avd-drv.c index bf5bf96f4c62a..e02ff6a5a7ee6 100644 --- a/drivers/media/platform/apple/avd/avd-drv.c +++ b/drivers/media/platform/apple/avd/avd-drv.c @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -170,12 +171,28 @@ int avd_submit_job(struct avd_ctx *ctx) AVD_OP_EXEC_FLAG_START_REV4(avd->variant->revision == 4) | AVD_OP_EXEC_FLAG_START_REV3(avd->variant->revision == 3); + /* + * Another context may still own VP/PP while this context builds its + * instructions. No MMIO or shared FIFO reuse until that owner retires. + * m2m still owns this builder, so there can be only one waiter. + */ + wait_event(avd->hw_wait, !READ_ONCE(avd->job_ctx)); + spin_lock_irqsave(&avd->job_lock, flags); + if (avd->broken) { + spin_unlock_irqrestore(&avd->job_lock, flags); + return -EIO; + } + avd->job_ctx = ctx; + spin_unlock_irqrestore(&avd->job_lock, flags); + ctx->fifo_idx = 0; for (i = 0; i < sub->codec; i++) vp += avd->variant->vp_slots[i]; if (avd->variant->vp_slot_base[sub->codec]) vp = avd->variant->vp_slot_base[sub->codec]; + ctx->request.vp = vp; + /* * avd_device_run() keeps this job from being finished (and the next one * from starting) until it returns, and arms the watchdog itself once the @@ -204,11 +221,11 @@ int avd_submit_job(struct avd_ctx *ctx) if ((!(avd->variant->quirks & AVD_QUIRK_V5_QUEUE_PACING) || sub->codec == AVD_CODEC_HEVC) && avd_wait_submission_queue(ctx, vp)) - break; + return -EIO; if (idx == num) { /* A completion from here on belongs to this job. */ spin_lock_irqsave(&avd->job_lock, flags); - avd->job_end_sent = true; + ctx->request.end_sent = true; spin_unlock_irqrestore(&avd->job_lock, flags); } writel(AVD_OP_EXEC | exec_mask | @@ -224,218 +241,197 @@ int avd_submit_job(struct avd_ctx *ctx) static int avd_reset(struct avd_dev *avd) { - int ret = 0; - - ret = pm_runtime_resume_and_get(avd->dev); - if (ret < 0) - return ret; + unsigned int i; + int ret; + /* Retain the failed request's PM reference until reset and IRQ drain. */ + writel(0, avd->mbox + AVD_REG_MBOX_IRQ_ENABLE); + synchronize_irq(avd->irq); + avd_shutdown(avd); ret = reset_control_reset(avd->rstc); if (ret) - dev_err(avd->dev, "reset: failed: %d", ret); + return ret; if (avd->empty_domain) { - iommu_attach_device(avd->empty_domain, avd->dev); + ret = iommu_attach_device(avd->empty_domain, avd->dev); + if (ret) + return ret; iommu_detach_device(avd->empty_domain, avd->dev); } - ret = avd_boot(avd); - if (ret) - dev_err(avd->dev, "reset: failed to boot"); - - pm_runtime_put_autosuspend(avd->dev); - - return ret; + /* Do not route a pre-reset mailbox word to the next hardware owner. */ + for (i = 0; i < 64; i++) { + if (readl(avd->mbox + AVD_REG_MBOX1_STATUS) & AVD_MBOX1_STATUS_EMPTY) + break; + readl(avd->mbox + AVD_REG_MBOX1_RETRIEVE); + } + if (i == 64) + return -EIO; + writel(AVD_MBOX1_NOT_EMPTY, avd->mbox + AVD_REG_MBOX_IRQ_CLR); + return avd_boot(avd); } -static void avd_watchdog_func(struct work_struct *work) +static void avd_finish_work(struct work_struct *work) { - struct avd_dev *avd; - struct avd_ctx *ctx; + struct avd_ctx *ctx = container_of(work, struct avd_ctx, finish_work); + struct avd_dev *avd = ctx->dev; unsigned long flags; - bool claimed; int ret; - ctx = container_of(to_delayed_work(work), struct avd_ctx, - watchdog_work); - if (!ctx) - return; - avd = ctx->dev; + /* Reset also drains an IRQ that may still be kicking PP. */ + cancel_delayed_work_sync(&ctx->watchdog_work); + if (ctx->request.reset_needed) { + ret = avd_reset(avd); + if (ret) { + dev_err(avd->dev, "failed to reset: %d\n", ret); + avd->broken = true; + writel(0, avd->mbox + AVD_REG_MBOX_IRQ_ENABLE); + } + } + + avd_finish_buffers(ctx, ctx->request.failed ? VB2_BUF_STATE_ERROR : + VB2_BUF_STATE_DONE); + if (ctx->pm_ref) + pm_runtime_put_autosuspend(avd->dev); - /* - * Time out only the job this watchdog was armed for, and only while the - * hardware still owns it. Otherwise that job already finished, and - * resetting the hardware would break whichever job is running now. - */ spin_lock_irqsave(&avd->job_lock, flags); - claimed = avd->job_ctx == ctx && avd->job_seq == ctx->wd_seq && - avd->job_state == AVD_JOB_SUBMITTED; - if (claimed) - avd->job_state = AVD_JOB_FINISHING; + if (avd->job_ctx == ctx) + avd->job_ctx = NULL; + ctx->request.state = AVD_JOB_IDLE; spin_unlock_irqrestore(&avd->job_lock, flags); - dev_dbg(avd->dev, "avd-job: watchdog seq %llu: %s\n", ctx->wd_seq, - claimed ? "timeout" : "stale, ignored"); - if (!claimed) - return; - - dev_err(avd->dev, "Frame processing timed out!"); + wake_up_all(&avd->hw_wait); - writel(0, avd->mbox + AVD_REG_MBOX_IRQ_ENABLE); - ret = avd_reset(avd); - if (ret) - dev_err(avd->dev, "failed to reset: %d", ret); - - avd_job_finish(ctx, VB2_BUF_STATE_ERROR); + /* Last state update: a subsequent run may now reuse this context. */ + smp_store_release(&ctx->active, false); + wake_up_all(&ctx->request_wait); + v4l2_m2m_try_schedule(ctx->fh.m2m_ctx); } -static irqreturn_t avd_irq_handler(int irq, void *data) +void avd_drain_context(struct avd_ctx *ctx) { - struct avd_dev *avd = data; - struct avd_ctx *ctx = v4l2_m2m_get_curr_priv(avd->m2m_dev); + wait_event(ctx->request_wait, !READ_ONCE(ctx->active)); + flush_work(&ctx->finish_work); + cancel_delayed_work_sync(&ctx->watchdog_work); +} - enum vb2_buffer_state state; +/* Held H.264 slices complete without a hardware submission. */ +void avd_job_finish(struct avd_ctx *ctx, enum vb2_buffer_state result) +{ unsigned long flags; - bool finish = false, stale = false; - u32 status; - if (!ctx) - return IRQ_HANDLED; - status = readl(avd->mbox + AVD_REG_MBOX1_RETRIEVE); + spin_lock_irqsave(&ctx->dev->job_lock, flags); + ctx->request.pending = true; + ctx->request.failed = result == VB2_BUF_STATE_ERROR; + spin_unlock_irqrestore(&ctx->dev->job_lock, flags); +} - writel(AVD_MBOX1_NOT_EMPTY, avd->mbox + AVD_REG_MBOX_IRQ_CLR); +static void avd_watchdog_func(struct work_struct *work) +{ + struct avd_ctx *ctx = container_of(to_delayed_work(work), + struct avd_ctx, watchdog_work); + struct avd_dev *avd = ctx->dev; + unsigned long flags; + bool claimed; - if (status & 0x10000) { /* dbg */ - dev_warn(avd->dev, "no handler for IRQ: %3d", status &~0x10000); - writel_relaxed(0, avd->mbox + AVD_REG_MBOX_IRQ_ENABLE); - return IRQ_HANDLED; + spin_lock_irqsave(&avd->job_lock, flags); + claimed = avd->job_ctx == ctx && + avd_request_timeout(&ctx->request, ctx->wd_seq); + spin_unlock_irqrestore(&avd->job_lock, flags); + if (claimed) { + dev_err(avd->dev, "Frame processing timed out!\n"); + schedule_work(&ctx->finish_work); } +} - if (status & 0x1000) { - /* pp is done ! we are done */ - state = VB2_BUF_STATE_DONE; - } else if (status & 0x100) { - /* a vp is done, kick the pp and hope for the best */ - if(ctx->coded_fmt_desc->ops->submit) - ctx->coded_fmt_desc->ops->submit(ctx); - goto done; - } else { - dev_err(avd->dev, "H%d error", status); - /* - * avd-fw err(n): VP n raised its error interrupt and stopped, - * and the PP only starts on a VP done, so the job cannot - * complete any more. Run the watchdog now instead of after its - * timeout: it resets the AVD and fails only this frame, and it - * ignores a job the hardware does not own yet. - */ - mod_delayed_work(system_wq, &ctx->watchdog_work, 0); - goto done; - } +static irqreturn_t avd_irq_handler(int irq, void *data) +{ + struct avd_dev *avd = data; + struct avd_ctx *ctx; + unsigned long flags; + enum avd_request_event event = AVD_EVENT_NONE; + u32 status; /* - * Finish only a job the hardware owns. While avd_device_run() is still - * setting up or submitting, a completion is left over from an older job - * unless this job's END command already went out; then remember it and - * let avd_device_run() finish the job when it returns. + * An empty pop repeats the previous word, including PP done. Allow the + * interrupt to precede its word briefly, then acknowledge a spurious IRQ. */ - spin_lock_irqsave(&avd->job_lock, flags); - if (avd->job_ctx != ctx) { - stale = true; - } else if (avd->job_state == AVD_JOB_SUBMITTED) { - avd->job_state = AVD_JOB_FINISHING; - finish = true; - } else if (avd->job_state == AVD_JOB_RUNNING && avd->job_end_sent) { - avd->job_pending = true; - avd->job_pending_result = state; - } else { - stale = true; + if (readl(avd->mbox + AVD_REG_MBOX1_STATUS) & AVD_MBOX1_STATUS_EMPTY) { + u32 mb; + + if (readl_poll_timeout_atomic(avd->mbox + AVD_REG_MBOX1_STATUS, mb, + !(mb & AVD_MBOX1_STATUS_EMPTY), 1, 10)) { + writel(AVD_MBOX1_NOT_EMPTY, avd->mbox + AVD_REG_MBOX_IRQ_CLR); + return IRQ_HANDLED; + } } + /* Always acknowledge, even when the m2m scheduler has no current job. */ + status = readl(avd->mbox + AVD_REG_MBOX1_RETRIEVE); + writel(AVD_MBOX1_NOT_EMPTY, avd->mbox + AVD_REG_MBOX_IRQ_CLR); + + spin_lock_irqsave(&avd->job_lock, flags); + ctx = avd->job_ctx; + if (ctx) + event = avd_request_irq(&ctx->request, status); spin_unlock_irqrestore(&avd->job_lock, flags); - dev_dbg(avd->dev, "avd-job: irq %#x: %s\n", status, - finish ? "finish" : stale ? "stale, ignored" : "pending"); - if (stale) - dev_warn_ratelimited(avd->dev, "ignoring stale completion"); - if (finish) { - cancel_delayed_work(&ctx->watchdog_work); - avd_job_finish(ctx, state); - } -done: + if (event == AVD_EVENT_KICK_PP && ctx->coded_fmt_desc->ops->submit) + ctx->coded_fmt_desc->ops->submit(ctx); + else if (event == AVD_EVENT_FINISH) + schedule_work(&ctx->finish_work); return IRQ_HANDLED; } +static int avd_job_ready(void *priv) +{ + struct avd_ctx *ctx = priv; + + /* Never overwrite state still used by this context's VP/PP or done(). */ + return !smp_load_acquire(&ctx->active); +} + static void avd_device_run(void *priv) { struct avd_ctx *ctx = priv; struct avd_dev *avd = ctx->dev; - const struct avd_coded_fmt_desc *desc = ctx->coded_fmt_desc; - enum vb2_buffer_state result = VB2_BUF_STATE_ERROR; - const char *outcome = "already finished"; unsigned long flags; - bool finish = false; - u64 seq; + bool finish; int ret; - if (WARN_ON(!desc)) - return; - - /* - * IRQ completion cannot wait for an already running watchdog. Drain it - * here, before reusing wd_seq, so an old callback cannot time out this - * context's next job. device_run is sleepable; hold no job_lock here. - */ - cancel_delayed_work_sync(&ctx->watchdog_work); + WRITE_ONCE(ctx->active, true); + ctx->src_buf = v4l2_m2m_next_src_buf(ctx->fh.m2m_ctx); + ctx->dst_buf = v4l2_m2m_next_dst_buf(ctx->fh.m2m_ctx); + ctx->scheduler_released = false; + avd_request_begin(&ctx->request, ++avd->job_seq); ret = pm_runtime_resume_and_get(avd->dev); - if (ret < 0) { - avd_job_finish_no_pm(ctx, VB2_BUF_STATE_ERROR); - return; - } + ctx->pm_ref = ret >= 0; + if (ret >= 0) + ret = ctx->coded_fmt_desc->ops->run(ctx); - /* - * While the job is RUNNING neither completion IRQs nor the watchdog may - * finish it: v4l2-mem2mem would start the next job on a kworker while - * this one still builds and submits ctx->job. - */ spin_lock_irqsave(&avd->job_lock, flags); - seq = ++avd->job_seq; - avd->job_ctx = ctx; - avd->job_state = AVD_JOB_RUNNING; - avd->job_end_sent = false; - avd->job_pending = false; - spin_unlock_irqrestore(&avd->job_lock, flags); - dev_dbg(avd->dev, "avd-job: run seq %llu\n", seq); - - ret = desc->ops->run(ctx); - if (ret) { - dev_dbg(avd->dev, "avd-job: run seq %llu failed: %d\n", seq, ret); - avd_job_finish(ctx, VB2_BUF_STATE_ERROR); - return; - } - - /* - * The run either finished the job itself (held capture buffer) or handed - * it to the hardware: arm the watchdog, or finish right away if the - * completion already arrived. - */ - spin_lock_irqsave(&avd->job_lock, flags); - if (avd->job_seq == seq && avd->job_state == AVD_JOB_RUNNING) { - if (avd->job_pending) { - avd->job_state = AVD_JOB_FINISHING; - result = avd->job_pending_result; - finish = true; - outcome = "completed during submission"; - } else { - avd->job_state = AVD_JOB_SUBMITTED; - ctx->wd_seq = seq; - schedule_delayed_work(&ctx->watchdog_work, msecs_to_jiffies(2000)); - outcome = "submitted"; + finish = avd_request_submitted(&ctx->request, ret != 0, + avd->job_ctx == ctx); + if (!finish) { + ctx->wd_seq = ctx->request.seq; + schedule_delayed_work(&ctx->watchdog_work, msecs_to_jiffies(2000)); + /* + * A non-held request can leave the scheduler while retaining both + * buffers and all codec storage. The next independent context can + * prepare on the CPU, but must wait in avd_submit_job() for PP. + * H.264 keeps the core's held-capture scheduling contract. + * job_ready does not take job_lock (m2m takes its own spinlock). + */ + if (!(ctx->fh.m2m_ctx->out_q_ctx.q.subsystem_flags & + VB2_V4L2_FL_SUPPORTS_M2M_HOLD_CAPTURE_BUF)) { + v4l2_m2m_src_buf_remove(ctx->fh.m2m_ctx); + v4l2_m2m_dst_buf_remove(ctx->fh.m2m_ctx); + ctx->scheduler_released = true; + v4l2_m2m_job_finish(avd->m2m_dev, ctx->fh.m2m_ctx); } } spin_unlock_irqrestore(&avd->job_lock, flags); - dev_dbg(avd->dev, "avd-job: run seq %llu: %s\n", seq, outcome); - if (finish) - avd_job_finish(ctx, result); + schedule_work(&ctx->finish_work); } /* @@ -508,6 +504,8 @@ static int avd_open(struct file *filp) goto err_free_ctx; INIT_DELAYED_WORK(&ctx->watchdog_work, avd_watchdog_func); + INIT_WORK(&ctx->finish_work, avd_finish_work); + init_waitqueue_head(&ctx->request_wait); avd_reset_coded_fmt(ctx); avd_reset_decoded_fmt(ctx); @@ -544,7 +542,7 @@ static int avd_release(struct file *filp) v4l2_fh_del(&ctx->fh, filp); v4l2_m2m_ctx_release(ctx->fh.m2m_ctx); /* The job has finished; its watchdog must not run on the freed ctx. */ - cancel_delayed_work_sync(&ctx->watchdog_work); + avd_drain_context(ctx); v4l2_ctrl_handler_free(&ctx->ctrl_hdl); v4l2_fh_exit(&ctx->fh); avd_buf_free(ctx->dev, &ctx->inst); @@ -565,6 +563,7 @@ static const struct v4l2_file_operations avd_fops = { static const struct v4l2_m2m_ops avd_m2m_ops = { .device_run = avd_device_run, + .job_ready = avd_job_ready, }; static const struct media_device_ops avd_media_ops = { @@ -807,8 +806,8 @@ static const struct avd_variant avd_t8132_variant = { * T6040 (M4 Pro): the T8132 decoder. Its first HEVC job stalled the VP when * it was sent the pre-v5 header (no DMA configuration words, no reserved * scratch slot); T6040 runs the same v5 firmware generation as T8140, so it - * gets the v5 header. HEVC stays hidden from V4L2 clients until that is - * verified on the hardware. + * gets the v5 header. Advertise HEVC only when the loaded firmware also + * supplies a CODECS trailer containing h265. */ static const struct avd_variant avd_t6040_variant = { .vp_slots = { @@ -819,7 +818,8 @@ static const struct avd_variant avd_t6040_variant = { }, .fifo_slots = 15, .configure_stream = t8122_configure_stream, - .capabilities = AVD_CAPABILITY_H264 | + .capabilities = AVD_CAPABILITY_HEVC | + AVD_CAPABILITY_H264 | AVD_CAPABILITY_VP9 | AVD_CAPABILITY_AV1, .fw_name = "apple/avd-fw-v5-t1.bin", @@ -829,6 +829,7 @@ static const struct avd_variant avd_t6040_variant = { .submit_queue_max_offset = 0x44, .submit_queue_status_offset = 0x7c, .quirks = AVD_QUIRK_HEVC_V5_HEADER, + .fw_caps_required = AVD_CAPABILITY_HEVC, }; /* can also be derived from a version register */ @@ -887,6 +888,7 @@ static int avd_probe(struct platform_device *pdev) mutex_init(&avd->vdev_lock); spin_lock_init(&avd->job_lock); + init_waitqueue_head(&avd->hw_wait); match = of_match_node(avd_of_match, pdev->dev.of_node); avd->variant = match->data; @@ -940,7 +942,8 @@ static int avd_probe(struct platform_device *pdev) avd->capabilities = caps; } else { /* Backward compatibility to old firmware, do not upstream. */ - avd->capabilities = avd->variant->capabilities; + avd->capabilities = avd->variant->capabilities & + ~avd->variant->fw_caps_required; } ret = dma_set_mask_and_coherent(avd->dev, @@ -954,6 +957,7 @@ static int avd_probe(struct platform_device *pdev) irq = platform_get_irq(pdev, 1); if (irq < 0) return irq; + avd->irq = irq; ret = devm_request_threaded_irq(&pdev->dev, irq, NULL, avd_irq_handler, IRQF_ONESHOT, dev_name(&pdev->dev), avd); @@ -1012,9 +1016,33 @@ static __maybe_unused int avd_runtime_suspend(struct device *dev) return 0; } +static __maybe_unused int avd_suspend(struct device *dev) +{ + struct avd_dev *avd = dev_get_drvdata(dev); + int ret; + + v4l2_m2m_suspend(avd->m2m_dev); + wait_event(avd->hw_wait, !READ_ONCE(avd->job_ctx)); + ret = pm_runtime_force_suspend(dev); + if (ret) + v4l2_m2m_resume(avd->m2m_dev); + return ret; +} + +static __maybe_unused int avd_resume(struct device *dev) +{ + struct avd_dev *avd = dev_get_drvdata(dev); + int ret; + + ret = pm_runtime_force_resume(dev); + if (!ret) + v4l2_m2m_resume(avd->m2m_dev); + return ret; +} + static const struct dev_pm_ops avd_pm_ops = { SET_SYSTEM_SLEEP_PM_OPS( - pm_runtime_force_suspend, - pm_runtime_force_resume) SET_RUNTIME_PM_OPS(avd_runtime_suspend, + avd_suspend, + avd_resume) SET_RUNTIME_PM_OPS(avd_runtime_suspend, avd_runtime_resume, NULL) }; static struct platform_driver avd_driver = { diff --git a/drivers/media/platform/apple/avd/avd-regs.h b/drivers/media/platform/apple/avd/avd-regs.h index 388f3e64895f5..ace0e4c48f40a 100644 --- a/drivers/media/platform/apple/avd/avd-regs.h +++ b/drivers/media/platform/apple/avd/avd-regs.h @@ -13,6 +13,8 @@ #define AVD_MBOX1_NOT_EMPTY BIT(3) #define AVD_REG_MBOX1_STATUS 0x5c +/* Read side: bits 8-11 write pointer, bits 12-15 read pointer, bit 17 empty. */ +#define AVD_MBOX1_STATUS_EMPTY BIT(17) #define AVD_REG_MBOX1_RETRIEVE 0x64 #define AVD_MBOX_ENABLE BIT(0) diff --git a/drivers/media/platform/apple/avd/avd-request-test.c b/drivers/media/platform/apple/avd/avd-request-test.c new file mode 100644 index 0000000000000..fba752f3d9d18 --- /dev/null +++ b/drivers/media/platform/apple/avd/avd-request-test.c @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-2.0 +#include + +#include "avd-request.h" + +static void avd_early_completion_test(struct kunit *test) +{ + struct avd_request req; + + avd_request_begin(&req, 1); + req.vp = 4; + KUNIT_EXPECT_EQ(test, avd_request_irq(&req, 0x104), AVD_EVENT_NONE); + req.end_sent = true; + KUNIT_EXPECT_EQ(test, avd_request_irq(&req, 0x105), AVD_EVENT_NONE); + KUNIT_EXPECT_EQ(test, avd_request_irq(&req, 0x1000), AVD_EVENT_NONE); + KUNIT_EXPECT_EQ(test, avd_request_irq(&req, 0x104), AVD_EVENT_KICK_PP); + KUNIT_EXPECT_EQ(test, avd_request_irq(&req, 0x104), AVD_EVENT_NONE); + KUNIT_EXPECT_EQ(test, avd_request_irq(&req, 0x1000), AVD_EVENT_NONE); + KUNIT_EXPECT_TRUE(test, req.pending); + KUNIT_EXPECT_EQ(test, req.state, AVD_JOB_RUNNING); + KUNIT_EXPECT_TRUE(test, avd_request_submitted(&req, false, true)); + KUNIT_EXPECT_FALSE(test, req.failed); + KUNIT_EXPECT_FALSE(test, avd_request_timeout(&req, 1)); + KUNIT_EXPECT_EQ(test, avd_request_irq(&req, 0x1000), AVD_EVENT_NONE); +} + +static void avd_timeout_race_test(struct kunit *test) +{ + struct avd_request req; + + avd_request_begin(&req, 7); + req.end_sent = true; + KUNIT_EXPECT_FALSE(test, avd_request_submitted(&req, false, true)); + KUNIT_EXPECT_FALSE(test, avd_request_timeout(&req, 6)); + KUNIT_EXPECT_EQ(test, avd_request_irq(&req, 0x100), AVD_EVENT_KICK_PP); + KUNIT_EXPECT_TRUE(test, avd_request_timeout(&req, 7)); + KUNIT_EXPECT_TRUE(test, req.failed); + KUNIT_EXPECT_TRUE(test, req.reset_needed); + KUNIT_EXPECT_EQ(test, avd_request_irq(&req, 0x1000), AVD_EVENT_NONE); + KUNIT_EXPECT_FALSE(test, avd_request_timeout(&req, 7)); + + avd_request_begin(&req, 8); + req.end_sent = true; + KUNIT_EXPECT_FALSE(test, avd_request_submitted(&req, false, true)); + KUNIT_EXPECT_EQ(test, avd_request_irq(&req, 0x100), AVD_EVENT_KICK_PP); + KUNIT_EXPECT_EQ(test, avd_request_irq(&req, 0x1000), AVD_EVENT_FINISH); + KUNIT_EXPECT_FALSE(test, avd_request_timeout(&req, 8)); + KUNIT_EXPECT_FALSE(test, req.failed); +} + +static void avd_submission_failure_test(struct kunit *test) +{ + struct avd_request req; + + /* A validation/allocation failure must not reset another context. */ + avd_request_begin(&req, 1); + KUNIT_EXPECT_TRUE(test, avd_request_submitted(&req, true, false)); + KUNIT_EXPECT_TRUE(test, req.failed); + KUNIT_EXPECT_FALSE(test, req.reset_needed); + + /* A partially submitted frame must be reset before FIFO reuse. */ + avd_request_begin(&req, 2); + KUNIT_EXPECT_TRUE(test, avd_request_submitted(&req, true, true)); + KUNIT_EXPECT_TRUE(test, req.reset_needed); + + /* A late PP word cannot turn a submission-time error into success. */ + avd_request_begin(&req, 3); + req.end_sent = true; + KUNIT_EXPECT_EQ(test, avd_request_irq(&req, 0x100), AVD_EVENT_KICK_PP); + KUNIT_EXPECT_EQ(test, avd_request_irq(&req, 0), AVD_EVENT_NONE); + KUNIT_EXPECT_EQ(test, avd_request_irq(&req, 0x1000), AVD_EVENT_NONE); + KUNIT_EXPECT_TRUE(test, avd_request_submitted(&req, false, true)); + KUNIT_EXPECT_TRUE(test, req.failed); + KUNIT_EXPECT_TRUE(test, req.reset_needed); +} + +static void avd_independent_contexts_test(struct kunit *test) +{ + struct avd_request hardware, preparing; + + avd_request_begin(&hardware, 10); + hardware.end_sent = true; + KUNIT_EXPECT_FALSE(test, avd_request_submitted(&hardware, false, true)); + avd_request_begin(&preparing, 11); + KUNIT_EXPECT_EQ(test, avd_request_irq(&hardware, 0x100), AVD_EVENT_KICK_PP); + KUNIT_EXPECT_EQ(test, preparing.state, AVD_JOB_RUNNING); + KUNIT_EXPECT_EQ(test, avd_request_irq(&hardware, 0x1000), AVD_EVENT_FINISH); + KUNIT_EXPECT_FALSE(test, preparing.pending); + KUNIT_EXPECT_FALSE(test, avd_request_timeout(&hardware, 10)); +} + +static struct kunit_case avd_request_cases[] = { + KUNIT_CASE(avd_early_completion_test), + KUNIT_CASE(avd_timeout_race_test), + KUNIT_CASE(avd_submission_failure_test), + KUNIT_CASE(avd_independent_contexts_test), + {} +}; + +static struct kunit_suite avd_request_suite = { + .name = "apple-avd-request", + .test_cases = avd_request_cases, +}; + +kunit_test_suite(avd_request_suite); diff --git a/drivers/media/platform/apple/avd/avd-request.h b/drivers/media/platform/apple/avd/avd-request.h new file mode 100644 index 0000000000000..130bc23d68036 --- /dev/null +++ b/drivers/media/platform/apple/avd/avd-request.h @@ -0,0 +1,94 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef AVD_REQUEST_H_ +#define AVD_REQUEST_H_ + +#include + +/* All transitions are serialized by avd_dev.job_lock. */ +enum avd_job_state { + AVD_JOB_IDLE, + AVD_JOB_RUNNING, + AVD_JOB_SUBMITTED, + AVD_JOB_FINISHING, +}; + +struct avd_request { + enum avd_job_state state; + u64 seq; + unsigned int vp; + bool end_sent; + bool pending; + bool vp_done; + bool failed; + bool reset_needed; +}; + +enum avd_request_event { + AVD_EVENT_NONE, + AVD_EVENT_KICK_PP, + AVD_EVENT_FINISH, +}; + +static inline void avd_request_begin(struct avd_request *req, u64 seq) +{ + *req = (struct avd_request) { + .state = AVD_JOB_RUNNING, + .seq = seq, + }; +} + +static inline enum avd_request_event +avd_request_irq(struct avd_request *req, u32 status) +{ + if (req->state != AVD_JOB_RUNNING && req->state != AVD_JOB_SUBMITTED) + return AVD_EVENT_NONE; + + if (!(status & 0x11000) && (status & 0x100)) { + if (!req->end_sent || (status & 0xff) != req->vp || req->vp_done) + return AVD_EVENT_NONE; + req->vp_done = true; + return AVD_EVENT_KICK_PP; + } + + if (status == 0x1000) { + if (!req->end_sent || !req->vp_done) + return AVD_EVENT_NONE; + } else { + req->failed = true; + req->reset_needed = true; + } + if (req->state == AVD_JOB_RUNNING) { + req->pending = true; + return AVD_EVENT_NONE; + } + req->state = AVD_JOB_FINISHING; + return AVD_EVENT_FINISH; +} + +/* Called only after the CPU has stopped touching the codec's run state. */ +static inline bool avd_request_submitted(struct avd_request *req, bool failed, + bool owns_hardware) +{ + if (failed) { + req->failed = true; + req->reset_needed = owns_hardware; + } + if (failed || req->pending) { + req->state = AVD_JOB_FINISHING; + return true; + } + req->state = AVD_JOB_SUBMITTED; + return false; +} + +static inline bool avd_request_timeout(struct avd_request *req, u64 seq) +{ + if (req->seq != seq || req->state != AVD_JOB_SUBMITTED) + return false; + req->state = AVD_JOB_FINISHING; + req->failed = true; + req->reset_needed = true; + return true; +} + +#endif /* AVD_REQUEST_H_ */ diff --git a/drivers/media/platform/apple/avd/avd-v4l2.c b/drivers/media/platform/apple/avd/avd-v4l2.c index 4622e92055486..5cd833a620272 100644 --- a/drivers/media/platform/apple/avd/avd-v4l2.c +++ b/drivers/media/platform/apple/avd/avd-v4l2.c @@ -942,6 +942,9 @@ static void avd_stop_streaming(struct vb2_queue *q) { struct avd_ctx *ctx = vb2_get_drv_priv(q); + /* m2m cancellation alone cannot see a scheduler-released request. */ + avd_drain_context(ctx); + if (V4L2_TYPE_IS_OUTPUT(q->type)) { const struct avd_coded_fmt_desc *desc = ctx->coded_fmt_desc; @@ -970,37 +973,21 @@ const struct vb2_ops avd_queue_ops = { .stop_streaming = avd_stop_streaming, }; -void avd_job_finish_no_pm(struct avd_ctx *ctx, enum vb2_buffer_state result) +void avd_finish_buffers(struct avd_ctx *ctx, enum vb2_buffer_state result) { - struct avd_dev *avd = ctx->dev; - unsigned long flags; - - spin_lock_irqsave(&avd->job_lock, flags); - if (avd->job_ctx == ctx) { - avd->job_state = AVD_JOB_IDLE; - avd->job_ctx = NULL; - avd->job_pending = false; - } - spin_unlock_irqrestore(&avd->job_lock, flags); - - if (ctx->coded_fmt_desc->ops->done) { - struct vb2_v4l2_buffer *src_buf, *dst_buf; - - src_buf = v4l2_m2m_next_src_buf(ctx->fh.m2m_ctx); - dst_buf = v4l2_m2m_next_dst_buf(ctx->fh.m2m_ctx); - ctx->coded_fmt_desc->ops->done(ctx, src_buf, dst_buf, result); + if (ctx->coded_fmt_desc->ops->done) + ctx->coded_fmt_desc->ops->done(ctx, ctx->src_buf, + ctx->dst_buf, result); + + if (ctx->scheduler_released) { + /* Return CAPTURE before OUTPUT signals the media request. */ + v4l2_m2m_buf_done(ctx->dst_buf, result); + v4l2_m2m_buf_done(ctx->src_buf, result); + } else { + /* Includes H.264's held-capture and unsubmitted error paths. */ + v4l2_m2m_buf_done_and_job_finish(ctx->dev->m2m_dev, + ctx->fh.m2m_ctx, result); } - - v4l2_m2m_buf_done_and_job_finish(ctx->dev->m2m_dev, ctx->fh.m2m_ctx, - result); -} - -void avd_job_finish(struct avd_ctx *ctx, enum vb2_buffer_state result) -{ - struct avd_dev *avd = ctx->dev; - - pm_runtime_put_autosuspend(avd->dev); - avd_job_finish_no_pm(ctx, result); } void avd_run_preamble(struct avd_ctx *ctx, struct avd_run *run) @@ -1010,8 +997,8 @@ void avd_run_preamble(struct avd_ctx *ctx, struct avd_run *run) memset(run, 0, sizeof(*run)); - run->bufs.src = v4l2_m2m_next_src_buf(ctx->fh.m2m_ctx); - run->bufs.dst = v4l2_m2m_next_dst_buf(ctx->fh.m2m_ctx); + run->bufs.src = ctx->src_buf; + run->bufs.dst = ctx->dst_buf; run->coded_in = vb2_dma_contig_plane_dma_addr(&run->bufs.src->vb2_buf, 0); diff --git a/drivers/media/platform/apple/avd/avd.h b/drivers/media/platform/apple/avd/avd.h index ad7accb6e9615..58950286e8c98 100644 --- a/drivers/media/platform/apple/avd/avd.h +++ b/drivers/media/platform/apple/avd/avd.h @@ -18,6 +18,10 @@ #include #include #include +#include +#include + +#include "avd-request.h" #include #include @@ -194,14 +198,7 @@ struct avd_variant { u32 submit_queue_max_offset; u32 submit_queue_status_offset; unsigned int quirks; -}; - -/* Lifecycle of the one m2m job the device runs at a time. */ -enum avd_job_state { - AVD_JOB_IDLE, - AVD_JOB_RUNNING, /* inside avd_device_run() */ - AVD_JOB_SUBMITTED, /* handed to the hardware, watchdog armed */ - AVD_JOB_FINISHING, /* claimed by the IRQ handler or the watchdog */ + unsigned int fw_caps_required; }; struct avd_dev { @@ -225,12 +222,11 @@ struct avd_dev { /* The current job; see avd_device_run(). Protected by job_lock. */ spinlock_t job_lock; - enum avd_job_state job_state; - struct avd_ctx *job_ctx; + struct avd_ctx *job_ctx; /* Hardware owner, independent of m2m curr_ctx. */ u64 job_seq; - bool job_end_sent; - bool job_pending; - enum vb2_buffer_state job_pending_result; + wait_queue_head_t hw_wait; + int irq; + bool broken; struct reset_control *rstc; @@ -268,8 +264,16 @@ struct avd_ctx { bool decomp; struct delayed_work watchdog_work; - /* job_seq of the job watchdog_work was armed for */ + struct work_struct finish_work; + wait_queue_head_t request_wait; + /* One request per context keeps codec scratch/reference state private. */ + bool active; + bool scheduler_released; + bool pm_ref; + struct avd_request request; u64 wd_seq; + struct vb2_v4l2_buffer *src_buf; + struct vb2_v4l2_buffer *dst_buf; void *priv; @@ -289,7 +293,8 @@ void avd_reset_coded_fmt(struct avd_ctx *ctx); void avd_reset_decoded_fmt(struct avd_ctx *ctx); int avd_init_ctrls(struct avd_ctx *ctx); -void avd_job_finish_no_pm(struct avd_ctx *ctx, enum vb2_buffer_state result); +void avd_finish_buffers(struct avd_ctx *ctx, enum vb2_buffer_state result); +void avd_drain_context(struct avd_ctx *ctx); void avd_job_finish(struct avd_ctx *ctx, enum vb2_buffer_state result); void avd_run_preamble(struct avd_ctx *ctx, struct avd_run *run); diff --git a/tools/asahi/avd-codec-tail-tests.py b/tools/asahi/avd-codec-tail-tests.py index 31616efc2651c..b7547b364f8b3 100755 --- a/tools/asahi/avd-codec-tail-tests.py +++ b/tools/asahi/avd-codec-tail-tests.py @@ -31,14 +31,16 @@ def read(path, ref): def build_source(body, defines): return r''' +#include #include #include #include #define BIT(n) (1u << (n)) #define dev_info(...) ((void)0) +#define strnstr host_strnstr ''' + defines + r''' struct firmware { const unsigned char *data; size_t size; }; -struct variant { unsigned int capabilities; }; +struct variant { unsigned int capabilities; unsigned int fw_caps_required; }; struct avd_dev { struct firmware *fw; const struct variant *variant; @@ -60,14 +62,14 @@ def build_source(body, defines): } int main(int argc, char **argv) { - if (argc != 5) + if (argc != 6) return 2; size_t size = strtoul(argv[1], NULL, 0); unsigned char *data = calloc(size ? size : 1, 1); if (!data) return 2; struct firmware fw = {data, size}; - struct variant variant = {strtoul(argv[3], NULL, 0)}; + struct variant variant = {strtoul(argv[3], NULL, 0), strtoul(argv[5], NULL, 0)}; struct avd_dev avd = {&fw, &variant, 0}; size_t len = strlen(argv[2]); if (size >= AVD_CODEC_INFO_MAX_SIZE && len <= AVD_CODEC_INFO_MAX_SIZE) @@ -97,14 +99,15 @@ def run_variant(out, name, body, defines, cases): (dest / "build.log").write_text(build.stdout + build.stderr) rows = [] if build.returncode == 0: - for index, (size, tail, mask, expected) in enumerate(cases): + for index, (size, tail, mask, expected, require_info) in enumerate(cases): result = subprocess.run( - [str(dest / "actual-tail"), str(size), tail, str(mask), str(expected)], + [str(dest / "actual-tail"), str(size), tail, str(mask), str(expected), str(int(require_info))], capture_output=True, text=True, ) (dest / f"case-{index:02d}.log").write_text(result.stdout + result.stderr) rows.append({"size": size, "tail": tail, "variant": mask, - "expected": expected, "returncode": result.returncode, + "expected": expected, "fw_caps_required": int(require_info), + "returncode": result.returncode, "sanitizer_diagnostic": bool(re.search( r"AddressSanitizer|runtime error:", result.stderr))}) return {"name": name, "build_command": cmd, @@ -146,6 +149,17 @@ def main(): (64, "CODECS:" + "x" * 57, 15, 0), (64, "CODECSx:av1", 5, 5), (128, "legacy", 0, 0), (128, "legacy", 5, 5)] + cases = [(*case, False) for case in cases] + if "fw_caps_required" in body: + cases += [(n, "legacy", 15, 14, True) for n in (0, 1, 63, 64, 128)] + cases += [(64, "CODECS:h264 vp9 av1", 15, 14, True), + (64, "CODECS:h265", 15, 1, True), + (64, all_codecs, 15, 15, True), + (64, "CODECS:h265", 14, 0, True)] + legacy_fallback = ("avd->capabilities = avd->variant->capabilities &\n" + "\t\t\t~avd->variant->fw_caps_required;") + if legacy_fallback not in body: + legacy_fallback = "avd->capabilities = avd->variant->capabilities;" variants = [("control-before", body)] if args.mutations: anchor = "\tif (avd->fw->size >= AVD_CODEC_INFO_MAX_SIZE)\n" @@ -156,8 +170,7 @@ def main(): "\t\tunsigned int variant_caps = avd->variant->capabilities;\n", "" ).replace("caps |= variant_caps & ", "caps |= ")), ("no-legacy-fallback", body.replace( - "avd->capabilities = avd->variant->capabilities;", - "avd->capabilities = 0;"))] + legacy_fallback, "avd->capabilities = 0;"))] variants.append(("control-after", body)) results = [run_variant(args.output, name, text, defines, cases) for name, text in variants] diff --git a/tools/asahi/avd-request-tests.py b/tools/asahi/avd-request-tests.py new file mode 100644 index 0000000000000..4cc157d564e4c --- /dev/null +++ b/tools/asahi/avd-request-tests.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Run AVD KUnit cases and extracted production decode paths on the host. + +MMIO, PM, workqueues and the m2m scheduler are deterministic mocks. This checks +ownership and failure ordering, not hardware pixels, concurrency or throughput. +""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import re +import shlex +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parents[2] +AVD = ROOT / "drivers/media/platform/apple/avd" + + +def function(source, name): + match = re.search(r"^(?:static )?(?:void|int|irqreturn_t) " + name + + r"\([^;]*?\)\n\{", source, re.M) + if not match: + raise ValueError(f"missing production function: {name}") + start = match.start() + depth = 1 + end = match.end() + while depth: + depth += (source[end] == "{") - (source[end] == "}") + end += 1 + return source[start:end] + "\n" + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path) + parser.add_argument("--mutations", action="store_true") + args = parser.parse_args() + temporary = tempfile.TemporaryDirectory() if args.output is None else None + out = Path(temporary.name) if temporary else args.output + out.mkdir(parents=True, exist_ok=bool(temporary)) + (out / "linux").mkdir() + (out / "kunit").mkdir() + (out / "linux/types.h").write_text( + "#include \n#include \n" + "typedef uint8_t u8; typedef uint32_t u32; typedef uint64_t u64;\n") + (out / "kunit/test.h").write_text(r''' +#include +#include +struct kunit { int unused; }; +struct kunit_case { void (*run)(struct kunit *); const char *name; }; +struct kunit_suite { const char *name; struct kunit_case *test_cases; }; +#define KUNIT_EXPECT_EQ(t, a, b) do { (void)(t); assert((a) == (b)); } while (0) +#define KUNIT_EXPECT_TRUE(t, a) do { (void)(t); assert(a); } while (0) +#define KUNIT_EXPECT_FALSE(t, a) do { (void)(t); assert(!(a)); } while (0) +#define KUNIT_CASE(f) { f, #f } +#define kunit_test_suite(s) \ +int main(void) { struct kunit t = {0}; \ +for (struct kunit_case *c = (s).test_cases; c->run; c++) { \ +c->run(&t); printf("PASS: %s/%s\n", (s).name, c->name); } return 0; } +''') + driver = (AVD / "avd-drv.c").read_text() + v4l2 = (AVD / "avd-v4l2.c").read_text() + names = ("avd_submit_job", "avd_reset", "avd_finish_work", "avd_drain_context", + "avd_job_finish", "avd_watchdog_func", "avd_irq_handler", + "avd_job_ready", "avd_device_run") + body = function(v4l2, "avd_finish_buffers") + "\n".join( + function(driver, name) for name in names) + fixture = (ROOT / "tools/asahi/avd-request/host.c").read_text() + variants = [("decode-path", body)] + if args.mutations: + mutations = { + "wrong-irq-owner": ("ctx = avd->job_ctx;", "ctx = current;"), + "reuse-active-context": ("return !smp_load_acquire(&ctx->active);", "(void)ctx; return 1;"), + "complete-queue-head": ("v4l2_m2m_buf_done(ctx->dst_buf, result);", + "v4l2_m2m_buf_done(v4l2_m2m_next_dst_buf(ctx->fh.m2m_ctx), result);"), + "premature-buffer-return": ("ctx->scheduler_released = true;", + "ctx->scheduler_released = true; v4l2_m2m_buf_done(ctx->dst_buf, VB2_BUF_STATE_DONE);"), + } + for name, (old, new) in mutations.items(): + if body.count(old) != 1: + raise ValueError(f"mutation {name} matched {body.count(old)} times") + variants.append((name, body.replace(old, new))) + cc = shlex.split(os.environ.get("CC", "cc")) + common = cc + ["-std=gnu11", "-Wall", "-Wextra", "-Werror", "-Wno-sign-compare", + "-Wno-unused-parameter", "-g", "-O1", "-fsanitize=address,undefined", + "-fno-omit-frame-pointer", "-I" + str(out), "-I" + str(AVD)] + results = [] + + def run(name, source, expect_failure=False): + dest = out / name + dest.mkdir() + compile_result = subprocess.run(common + [str(source), "-o", str(dest / "test")], + capture_output=True, text=True) + (dest / "build.log").write_text(compile_result.stdout + compile_result.stderr) + rc = None + if compile_result.returncode == 0: + proc = subprocess.run([str(dest / "test")], capture_output=True, text=True, + timeout=30) + rc = proc.returncode + (dest / "test.log").write_text(proc.stdout + proc.stderr) + passed = compile_result.returncode == 0 and ((rc != 0) if expect_failure else (rc == 0)) + results.append({"name": name, "build_rc": compile_result.returncode, + "test_rc": rc, "passed": passed}) + print(f"{'PASS' if passed else 'FAIL'}: {name}") + if not passed: + print((dest / "build.log").read_text()) + if (dest / "test.log").exists(): + print((dest / "test.log").read_text()) + + run("kunit-cases", AVD / "avd-request-test.c") + header = (AVD / "avd.h").read_text() + definitions = "\n".join(re.findall(r"^#define AVD_(?:CAPABILITY|QUIRK)_[^\n]+", header, re.M)) + variant_struct = re.search(r"struct avd_variant \{.*?\n\};", header, re.S).group() + codec_enum = re.search(r"enum avd_codec \{.*?\n\};", header, re.S).group() + matches = driver[driver.index("static const struct avd_variant avd_t8103_variant"): + driver.index("MODULE_DEVICE_TABLE(of, avd_of_match);")] + match_source = out / "match-data.c" + match_source.write_text(""" +#include +#include +#include +#include +#define BIT(n) (1U << (n)) +#define t8103_configure_stream NULL +#define t8112_configure_stream NULL +#define t8122_configure_stream NULL +struct avd_dev; +typedef unsigned long long dma_addr_t; +struct of_device_id { const char *compatible; const void *data; }; +""" + definitions + "\n" + codec_enum + "\n" + variant_struct + "\n" + matches + """ +int main(void) +{ + const struct avd_variant *matched = NULL; + for (size_t i = 0; avd_of_match[i].compatible; i++) + if (!strcmp(avd_of_match[i].compatible, "apple,t6041-avd")) + matched = avd_of_match[i].data; + assert(matched == &avd_t6040_variant); + assert(matched->capabilities == 15); + assert(matched->fw_caps_required == AVD_CAPABILITY_HEVC); + assert(matched->quirks & AVD_QUIRK_HEVC_V5_HEADER); + assert(!strcmp(matched->fw_name, "apple/avd-fw-v5-t1.bin")); + assert(avd_t8132_variant.capabilities == 15); + assert(avd_t8140_variant.capabilities == 15); + return 0; +} +""") + run("match-data", match_source) + (out / "fixture.c").write_text(fixture) + for name, actual in variants: + (out / "actual-driver.h").write_text(actual) + run(name, out / "fixture.c", name != "decode-path") + passed = all(x["passed"] for x in results) + receipt = {"passed": passed, "compiler": subprocess.check_output(cc + ["--version"], text=True).splitlines()[0], + "extracted_sha256": hashlib.sha256(body.encode()).hexdigest(), + "results": results} + (out / "receipt.json").write_text(json.dumps(receipt, indent=2) + "\n") + return 0 if passed else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/asahi/avd-request/README.md b/tools/asahi/avd-request/README.md new file mode 100644 index 0000000000000..b6cdb44ba9ab3 --- /dev/null +++ b/tools/asahi/avd-request/README.md @@ -0,0 +1,44 @@ +# AVD request ownership tests + +From the kernel source root: + +```sh +python3 tools/asahi/avd-request-tests.py --mutations --output /tmp/avd-requests +python3 tools/asahi/avd-codec-tail-tests.py --mutations --output /tmp/avd-codecs +``` + +Each output directory must be new. The runners compile with the host C compiler, +ASan and UBSan, retain build/test logs and write a JSON receipt. `CC` selects a +different compiler. No decoder, firmware or root access is needed. + +The request runner executes the same cases as the `apple-avd-request` KUnit +suite, compiles the actual OF match table and variants, then extracts production +submission, IRQ, watchdog, retirement and buffer-completion functions. The C +fixture supplies deterministic MMIO, m2m, PM and workqueue mocks. It drives two +contexts through CPU preparation and serialized hardware ownership, submission +IRQs, duplicate/wrong-slot VP events, timeout recovery, PM/validation failures, +held capture, streamoff drain and failed-reset admission. Four mutations must +compile and fail at runtime; a failed control or mutant build fails the runner. + +The capability runner executes the production firmware-tail parser, including +short/absent tails, variant intersection, and T6040's requirement for explicit +firmware HEVC support. Missing legacy metadata retains the other codecs. + +These tests do not emulate the kernel scheduler or hardware memory ordering. +They establish host-side ownership behavior, not decoded pixel correctness, +firmware concurrency, runtime performance or hardware recovery. The pipeline +allows one CPU builder alongside one hardware request across independent +contexts. Each context retains one request, and H.264 retains the m2m core's +held-capture scheduling path. No concurrent VP/PP ownership or single-stream +throughput gain is claimed. + +For an arm64 compile with the KUnit cases included: + +```sh +make O=out ARCH=arm64 LLVM=1 defconfig +scripts/config --file out/.config --enable MEDIA_SUPPORT --enable MEDIA_CONTROLLER \ + --enable VIDEO_DEV --enable MEDIA_PLATFORM_SUPPORT --enable V4L_PLATFORM_DRIVERS \ + --enable VIDEO_APPLE_AVD --enable KUNIT --enable VIDEO_APPLE_AVD_KUNIT_TEST +make O=out ARCH=arm64 LLVM=1 olddefconfig +make O=out ARCH=arm64 LLVM=1 W=1 -j8 drivers/media/platform/apple/avd/ +``` diff --git a/tools/asahi/avd-request/host.c b/tools/asahi/avd-request/host.c new file mode 100644 index 0000000000000..26e3ea6cb737f --- /dev/null +++ b/tools/asahi/avd-request/host.c @@ -0,0 +1,519 @@ +// SPDX-License-Identifier: GPL-2.0 +/* Deterministic host fixture; scheduler, MMIO, PM and workqueues are mocks. */ +#include +#include +#include +#include +#include +#include +#include +#include "avd-request.h" + +#define __iomem +#define BIT(n) (1U << (n)) +#define READ_ONCE(v) (v) +#define WRITE_ONCE(v, x) ((v) = (x)) +/* Deterministic ordering only; these mocks do not model SMP barriers. */ +#define smp_store_release(p, x) (*(p) = (x)) +#define smp_load_acquire(p) (*(p)) +#define container_of(p, t, m) ((t *)((char *)(p) - offsetof(t, m))) +#define spin_lock_irqsave(l, f) \ + do { \ + (void)(l); \ + (f) = 0; \ + } while (0) +#define spin_unlock_irqrestore(l, f) \ + do { \ + (void)(l); \ + (void)(f); \ + } while (0) +#define dev_err(...) ((void)0) +#define WARN_ON_ONCE(x) (x) +#define kvfree(p) free(p) +#define wait_event(q, cond) \ + do { \ + (void)(q); \ + if (!(cond)) \ + wait_hook(); \ + assert(cond); \ + } while (0) +#define wake_up_all(q) ((void)(q)) +#define msecs_to_jiffies(n) (n) +#define IRQ_HANDLED 1 +#define VB2_V4L2_FL_SUPPORTS_M2M_HOLD_CAPTURE_BUF 1 +#define AVD_QUIRK_V5_QUEUE_PACING BIT(3) +#define AVD_OP_EXEC 0x2b000000U +#define AVD_OP_EXEC_REV3_VP9_MASK 0x00fff000U +#define AVD_OP_EXEC_FIFO_IDX(n) ((n) << 4) +#define AVD_OP_EXEC_FLAG_START_REV3(n) ((n) << 8) +#define AVD_OP_EXEC_FLAG_START_REV4(n) ((n) << 9) +#define AVD_OP_EXEC_FLAG_END(n) ((n) << 10) +#include "avd-regs.h" + +typedef int irqreturn_t; +typedef uintptr_t dma_addr_t; +enum vb2_buffer_state { VB2_BUF_STATE_DONE, VB2_BUF_STATE_ERROR }; +enum avd_codec { AVD_CODEC_HEVC, AVD_CODEC_H264, AVD_CODEC_VP9, AVD_CODEC_AV1 }; +struct work_struct { + bool queued; +}; +struct delayed_work { + struct work_struct work; +}; +#define to_delayed_work(w) container_of(w, struct delayed_work, work) +struct vb2_v4l2_buffer { + int id, done, result; + bool held; +}; +struct v4l2_m2m_ctx { + struct { + struct { + unsigned int subsystem_flags; + } q; + } out_q_ctx; + struct vb2_v4l2_buffer *src[4], *dst[4]; + int ns, nd, finishes; +}; +struct avd_ctx; +struct avd_dev; +struct avd_variant { + unsigned int revision, quirks, vp_slots[4], vp_slot_base[4]; + unsigned int vp_slot_offset; + void (*configure_stream)(struct avd_dev *dev, dma_addr_t addr, u8 fifo, + u32 vp); +}; +struct avd_dev { + struct avd_ctx *job_ctx; + u64 job_seq; + int job_lock, hw_wait, irq; + bool broken; + void *dev, *m2m_dev, *rstc, *empty_domain; + unsigned char *ctrl, *mbox; + const struct avd_variant *variant; +}; +struct avd_coded_fmt_ops { + int (*run)(struct avd_ctx *ctx); + void (*submit)(struct avd_ctx *ctx); + void (*done)(struct avd_ctx *ctx, struct vb2_v4l2_buffer *src, + struct vb2_v4l2_buffer *dst, enum vb2_buffer_state state); +}; +struct avd_coded_fmt_desc { + const struct avd_coded_fmt_ops *ops; +}; +struct avd_segment { + size_t num; + u32 instructions[512]; +}; +struct avd_job { + enum avd_codec codec; + size_t num; + struct avd_segment *segments; +}; +struct avd_ctx { + struct { + struct v4l2_m2m_ctx *m2m_ctx; + } fh; + struct avd_dev *dev; + const struct avd_coded_fmt_desc *coded_fmt_desc; + struct delayed_work watchdog_work; + struct work_struct finish_work; + int request_wait; + bool active, scheduler_released, pm_ref; + struct avd_request request; + u64 wd_seq; + struct vb2_v4l2_buffer *src_buf, *dst_buf; + struct avd_job job; + int fifo_idx; + struct { + dma_addr_t addr; + } inst; + int kicks, callbacks, run_mode; +}; +static struct avd_ctx *current, *waiting_owner, *draining; +static int pm_refs, pm_error, resets, reset_error, writes, configures; +static int queue_full, early_irq, seq_log[16], nlog, wait_calls; +static u32 irq_status; +static unsigned char *mmio_base; +static int mailbox_words, pops; +#define readl_poll_timeout_atomic(reg, v, cond, delay, timeout) \ + ({ \ + int rc = -EIO; \ + for (int j = 0; j < (timeout); j++) { \ + (v) = readl(reg); \ + if (cond) { \ + rc = 0; \ + break; \ + } \ + } \ + rc; \ + }) +static void wait_hook(void); +static void inject(struct avd_dev *, u32); +static void pump(struct avd_ctx *); +static void writel(u32 value, void *reg) +{ + (void)reg; + writes++; + if (early_irq && (value & 0xff000000U) == AVD_OP_EXEC && + (value & BIT(10))) { + early_irq = 0; + inject(current->dev, 0x100 | current->request.vp); + inject(current->dev, 0x1000); + } +} +static u32 readl(void *reg) +{ + if (reg == mmio_base + AVD_REG_MBOX1_STATUS) + return mailbox_words ? 0 : AVD_MBOX1_STATUS_EMPTY; + assert(reg == mmio_base + AVD_REG_MBOX1_RETRIEVE && mailbox_words > 0); + mailbox_words--; + pops++; + return irq_status; +} +static int schedule_work(struct work_struct *w) +{ + assert(!w->queued); + w->queued = true; + return 1; +} +static int schedule_delayed_work(struct delayed_work *w, int delay) +{ + (void)delay; + assert(!w->work.queued); + w->work.queued = true; + return 1; +} +static void cancel_delayed_work_sync(struct delayed_work *w) +{ + w->work.queued = false; +} +static void synchronize_irq(int irq) +{ + (void)irq; +} +static int reset_control_reset(void *r) +{ + (void)r; + resets++; + return reset_error; +} +static int iommu_attach_device(void *domain, void *dev) +{ + (void)domain; + (void)dev; + return 0; +} +static void iommu_detach_device(void *domain, void *dev) +{ + (void)domain; + (void)dev; +} +static void avd_shutdown(struct avd_dev *d) +{ + (void)d; +} +static int avd_boot(struct avd_dev *d) +{ + (void)d; + return 0; +} +static int pm_runtime_resume_and_get(void *d) +{ + (void)d; + if (pm_error) + return -EIO; + return ++pm_refs; +} +static void pm_runtime_put_autosuspend(void *d) +{ + (void)d; + assert(pm_refs > 0); + pm_refs--; +} +static struct vb2_v4l2_buffer *v4l2_m2m_next_src_buf(struct v4l2_m2m_ctx *m) +{ + return m->ns ? m->src[0] : NULL; +} +static struct vb2_v4l2_buffer *v4l2_m2m_next_dst_buf(struct v4l2_m2m_ctx *m) +{ + return m->nd ? m->dst[0] : NULL; +} +static struct vb2_v4l2_buffer *remove_buf(struct vb2_v4l2_buffer **a, int *n) +{ + assert(*n > 0); + struct vb2_v4l2_buffer *b = a[0]; + memmove(a, a + 1, --*n * sizeof(*a)); + return b; +} +static struct vb2_v4l2_buffer *v4l2_m2m_src_buf_remove(struct v4l2_m2m_ctx *m) +{ + return remove_buf(m->src, &m->ns); +} +static struct vb2_v4l2_buffer *v4l2_m2m_dst_buf_remove(struct v4l2_m2m_ctx *m) +{ + return remove_buf(m->dst, &m->nd); +} +static void v4l2_m2m_buf_done(struct vb2_v4l2_buffer *b, + enum vb2_buffer_state r) +{ + assert(!b->done); + b->done++; + b->result = r; + seq_log[nlog++] = b->id; +} +static void v4l2_m2m_job_finish(void *d, struct v4l2_m2m_ctx *m) +{ + (void)d; + assert(current && current->fh.m2m_ctx == m); + m->finishes++; + current = NULL; +} +static void v4l2_m2m_buf_done_and_job_finish(void *d, struct v4l2_m2m_ctx *m, + enum vb2_buffer_state r) +{ + struct vb2_v4l2_buffer *src = v4l2_m2m_src_buf_remove(m); + if (!src->held) + v4l2_m2m_buf_done(v4l2_m2m_dst_buf_remove(m), r); + v4l2_m2m_buf_done(src, r); + v4l2_m2m_job_finish(d, m); +} +static void v4l2_m2m_try_schedule(struct v4l2_m2m_ctx *m) +{ + (void)m; +} +static int avd_wait_submission_queue(struct avd_ctx *c, int vp) +{ + (void)c; + (void)vp; + return queue_full; +} +static void flush_work(struct work_struct *w) +{ + if (w->queued) + pump(container_of(w, struct avd_ctx, finish_work)); +} +#include "actual-driver.h" + +static void configure(struct avd_dev *d, dma_addr_t addr, u8 fifo, u32 vp) +{ + (void)addr; + (void)vp; + assert(d->job_ctx == current && fifo == 0); + configures++; +} +static void pp_submit(struct avd_ctx *ctx) +{ + ctx->kicks++; +} +static void codec_done(struct avd_ctx *ctx, struct vb2_v4l2_buffer *src, + struct vb2_v4l2_buffer *dst, + enum vb2_buffer_state result) +{ + (void)result; + assert(src == ctx->src_buf && dst == ctx->dst_buf); + assert(!src->done && !dst->done); + ctx->callbacks++; +} +static int codec_run(struct avd_ctx *ctx) +{ + if (ctx->run_mode == 1) + return -EINVAL; + if (ctx->run_mode == 2) { + avd_job_finish(ctx, VB2_BUF_STATE_DONE); + return 0; + } + ctx->job.codec = AVD_CODEC_HEVC; + ctx->job.num = 1; + ctx->job.segments = calloc(2, sizeof(*ctx->job.segments)); + assert(ctx->job.segments); + return avd_submit_job(ctx); +} +static void pump(struct avd_ctx *ctx) +{ + assert(ctx->finish_work.queued); + ctx->finish_work.queued = false; + avd_finish_work(&ctx->finish_work); + /* avd_submit_job retains its CPU table on partial submission error. */ + free(ctx->job.segments); + ctx->job.segments = NULL; +} +static void inject(struct avd_dev *d, u32 status) +{ + irq_status = status; + mailbox_words = 1; + avd_irq_handler(0, d); +} +static void complete(struct avd_ctx *ctx) +{ + inject(ctx->dev, 0x100 | ctx->request.vp); + inject(ctx->dev, 0x1000); + pump(ctx); +} +static void wait_hook(void) +{ + if (draining) { + pump(draining); + draining = NULL; + return; + } + assert(waiting_owner && current && current != waiting_owner); + assert(waiting_owner->active && current->active); + assert(!waiting_owner->src_buf->done && !waiting_owner->dst_buf->done); + assert(configures == + 1); /* CPU prepared the second job, no second MMIO. */ + wait_calls++; + complete(waiting_owner); + waiting_owner = NULL; +} +static void init_ctx(struct avd_ctx *ctx, struct avd_dev *d, + struct v4l2_m2m_ctx *m, struct vb2_v4l2_buffer *b) +{ + static const struct avd_coded_fmt_ops ops = { codec_run, pp_submit, + codec_done }; + static const struct avd_coded_fmt_desc desc = { &ops }; + nlog = 0; + memset(ctx, 0, sizeof(*ctx)); + memset(m, 0, sizeof(*m)); + memset(b, 0, 4 * sizeof(*b)); + ctx->dev = d; + ctx->fh.m2m_ctx = m; + ctx->coded_fmt_desc = &desc; + for (int i = 0; i < 4; i++) + b[i].id = i; + m->src[0] = b; + m->src[1] = b + 2; + m->ns = 2; + m->dst[0] = b + 1; + m->dst[1] = b + 3; + m->nd = 2; +} +static void start(struct avd_ctx *ctx) +{ + assert(!current && avd_job_ready(ctx)); + current = ctx; + avd_device_run(ctx); +} +int main(void) +{ + unsigned char regs[256] = { 0 }; + mmio_base = regs; + struct avd_variant variant = { .revision = 4, + .configure_stream = configure }; + struct avd_dev dev = { .ctrl = regs, + .mbox = regs, + .variant = &variant }; + struct avd_ctx a, b; + struct v4l2_m2m_ctx ma, mb; + struct vb2_v4l2_buffer ba[4], bb[4]; + + init_ctx(&a, &dev, &ma, ba); + init_ctx(&b, &dev, &mb, bb); + start(&a); + assert(a.active && !avd_job_ready(&a) && a.scheduler_released); + assert(!ba[0].done && !ba[1].done && ma.ns == 1 && ma.nd == 1); + assert(dev.job_ctx == &a && !current && pm_refs == 1); + waiting_owner = &a; + start(&b); /* Run B's real submission path while A owns the hardware. */ + assert(wait_calls == 1 && !a.active && dev.job_ctx == &b && + pm_refs == 1); + assert(a.kicks == 1 && b.kicks == 0 && a.callbacks == 1); + assert(ba[0].done == 1 && ba[1].done == 1 && !ba[2].done && + !ba[3].done); + assert(!bb[0].done && !bb[1].done && seq_log[0] == 1 && + seq_log[1] == 0); + complete(&b); + assert(!dev.job_ctx && pm_refs == 0 && !resets && b.callbacks == 1); + int prior_pops = pops; + avd_irq_handler(0, &dev); + assert(pops == prior_pops); + inject(&dev, 0x1000); /* No owner: still acknowledge, no completion. */ + assert(!a.finish_work.queued && !b.finish_work.queued); + puts("PASS: two-context CPU pipeline, owner routing, retained buffers and PM"); + + init_ctx(&a, &dev, &ma, ba); + early_irq = 1; + start(&a); + assert(a.request.state == AVD_JOB_FINISHING && !a.scheduler_released); + assert(!ba[0].done && a.finish_work.queued && current == &a); + pump(&a); + assert(!current && pm_refs == 0); + puts("PASS: IRQ during submission is deferred until run returns"); + + init_ctx(&a, &dev, &ma, ba); + start(&a); + inject(&dev, 0x101); + assert(!a.kicks); + inject(&dev, 0x100); + inject(&dev, 0x100); + assert(a.kicks == 1); + avd_watchdog_func(&a.watchdog_work.work); + inject(&dev, 0x1000); + pump(&a); + assert(resets == 1 && ba[0].result == VB2_BUF_STATE_ERROR && + pm_refs == 0); + init_ctx(&a, &dev, &ma, ba); + start(&a); + complete(&a); + assert(ba[0].result == VB2_BUF_STATE_DONE && pm_refs == 0); + puts("PASS: wrong/duplicate VP, watchdog wins PP race, next decode recovers"); + + init_ctx(&a, &dev, &ma, ba); + a.run_mode = 1; + start(&a); + pump(&a); + assert(resets == 1 && ba[0].result == VB2_BUF_STATE_ERROR && + pm_refs == 0); + init_ctx(&a, &dev, &ma, ba); + pm_error = 1; + start(&a); + pump(&a); + pm_error = 0; + assert(resets == 1 && pm_refs == 0); + puts("PASS: pre-submit and PM failures do not reset another owner or put PM"); + + init_ctx(&a, &dev, &ma, ba); + ma.out_q_ctx.q.subsystem_flags = 1; + ba[0].held = true; + a.run_mode = 2; + start(&a); + pump(&a); + assert(ba[0].done == 1 && !ba[1].done && ma.nd == 2 && + !a.scheduler_released); + init_ctx(&a, &dev, &ma, ba); + ma.out_q_ctx.q.subsystem_flags = 1; + start(&a); + assert(current == &a && !a.scheduler_released); + complete(&a); + assert(!current && ba[1].done && pm_refs == 0); + puts("PASS: held-capture path retains m2m scheduler and capture buffer"); + + init_ctx(&a, &dev, &ma, ba); + start(&a); + inject(&dev, 0x100); + inject(&dev, 0x1000); + draining = &a; + avd_drain_context(&a); + assert(!a.active && !a.watchdog_work.work.queued); + puts("PASS: streamoff drains released ownership and watchdog"); + + init_ctx(&a, &dev, &ma, ba); + queue_full = 1; + start(&a); + pump(&a); + queue_full = 0; + assert(resets == 2 && ba[0].result == VB2_BUF_STATE_ERROR); + init_ctx(&a, &dev, &ma, ba); + reset_error = -EIO; + start(&a); + avd_watchdog_func(&a.watchdog_work.work); + pump(&a); + assert(dev.broken); + int before = configures; + init_ctx(&a, &dev, &ma, ba); + start(&a); + pump(&a); + assert(configures == before && ba[0].result == VB2_BUF_STATE_ERROR && + pm_refs == 0); + puts("PASS: partial submission resets; failed reset prevents FIFO reuse"); + return 0; +} From aadf60d3851c9c452dcfe3c9133026f203385622 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sat, 31 Oct 2026 21:58:13 +1100 Subject: [PATCH 2/3] media: apple: avd: keep failed recovery closed across runtime resume Do not boot firmware on runtime resume after reset failure has marked the device broken. Submission admission already rejects this state; keep the same boundary when an autosuspended device is accessed again. Execute the production runtime-resume function in the host fixture and check that broken admission performs no firmware boot, while normal resume retains its boot path. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/media/platform/apple/avd/avd-drv.c | 3 +++ tools/asahi/avd-request-tests.py | 4 ++-- tools/asahi/avd-request/host.c | 17 +++++++++++++++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/drivers/media/platform/apple/avd/avd-drv.c b/drivers/media/platform/apple/avd/avd-drv.c index e02ff6a5a7ee6..8527f47523208 100644 --- a/drivers/media/platform/apple/avd/avd-drv.c +++ b/drivers/media/platform/apple/avd/avd-drv.c @@ -1002,6 +1002,9 @@ static __maybe_unused int avd_runtime_resume(struct device *dev) int ret; struct avd_dev *avd = platform_get_drvdata(to_platform_device(dev)); + if (READ_ONCE(avd->broken)) + return -EIO; + ret = avd_boot(avd); if (ret) dev_err(dev, "failed to boot"); diff --git a/tools/asahi/avd-request-tests.py b/tools/asahi/avd-request-tests.py index 4cc157d564e4c..f6d2b897f49b8 100644 --- a/tools/asahi/avd-request-tests.py +++ b/tools/asahi/avd-request-tests.py @@ -20,7 +20,7 @@ def function(source, name): - match = re.search(r"^(?:static )?(?:void|int|irqreturn_t) " + name + + match = re.search(r"^(?:static )?(?:__maybe_unused )?(?:void|int|irqreturn_t) " + name + r"\([^;]*?\)\n\{", source, re.M) if not match: raise ValueError(f"missing production function: {name}") @@ -65,7 +65,7 @@ def main(): v4l2 = (AVD / "avd-v4l2.c").read_text() names = ("avd_submit_job", "avd_reset", "avd_finish_work", "avd_drain_context", "avd_job_finish", "avd_watchdog_func", "avd_irq_handler", - "avd_job_ready", "avd_device_run") + "avd_job_ready", "avd_device_run", "avd_runtime_resume") body = function(v4l2, "avd_finish_buffers") + "\n".join( function(driver, name) for name in names) fixture = (ROOT / "tools/asahi/avd-request/host.c").read_text() diff --git a/tools/asahi/avd-request/host.c b/tools/asahi/avd-request/host.c index 26e3ea6cb737f..d4e1bf9368ec2 100644 --- a/tools/asahi/avd-request/host.c +++ b/tools/asahi/avd-request/host.c @@ -9,6 +9,7 @@ #include #include "avd-request.h" +#define __maybe_unused #define __iomem #define BIT(n) (1U << (n)) #define READ_ONCE(v) (v) @@ -91,6 +92,11 @@ struct avd_dev { unsigned char *ctrl, *mbox; const struct avd_variant *variant; }; +struct device { + struct avd_dev *data; +}; +#define to_platform_device(d) (d) +#define platform_get_drvdata(d) ((d)->data) struct avd_coded_fmt_ops { int (*run)(struct avd_ctx *ctx); void (*submit)(struct avd_ctx *ctx); @@ -131,7 +137,7 @@ struct avd_ctx { }; static struct avd_ctx *current, *waiting_owner, *draining; static int pm_refs, pm_error, resets, reset_error, writes, configures; -static int queue_full, early_irq, seq_log[16], nlog, wait_calls; +static int queue_full, early_irq, seq_log[16], nlog, wait_calls, boots; static u32 irq_status; static unsigned char *mmio_base; static int mailbox_words, pops; @@ -215,6 +221,7 @@ static void avd_shutdown(struct avd_dev *d) static int avd_boot(struct avd_dev *d) { (void)d; + boots++; return 0; } static int pm_runtime_resume_and_get(void *d) @@ -514,6 +521,12 @@ int main(void) pump(&a); assert(configures == before && ba[0].result == VB2_BUF_STATE_ERROR && pm_refs == 0); - puts("PASS: partial submission resets; failed reset prevents FIFO reuse"); + struct device device = { .data = &dev }; + int before_boots = boots; + + assert(avd_runtime_resume(&device) == -EIO && boots == before_boots); + dev.broken = false; + assert(avd_runtime_resume(&device) == 0 && boots == before_boots + 1); + puts("PASS: partial submission resets; failed reset prevents FIFO reuse and firmware resume"); return 0; } From c7edee0359b6d9cb127232dfb7ef5615ca9ced41 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sat, 31 Oct 2026 22:40:14 +1100 Subject: [PATCH 3/3] media: apple: avd: discard instruction tables on request failure A submission error can leave a CPU instruction table referencing buffers that the codec's error completion then drops. Free and clear that table before error completion so a following H.264 continuation cannot reuse stale instructions. Successful held-capture requests keep their table. Remove the fixture's implicit table cleanup so it exercises production retirement. Check validation and partial-submission failures, successful held capture, and a mutation that retains the failed table. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/media/platform/apple/avd/avd-drv.c | 6 ++++++ tools/asahi/avd-request-tests.py | 2 ++ tools/asahi/avd-request/README.md | 2 +- tools/asahi/avd-request/host.c | 11 ++++++++--- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/drivers/media/platform/apple/avd/avd-drv.c b/drivers/media/platform/apple/avd/avd-drv.c index 8527f47523208..0bce84759b523 100644 --- a/drivers/media/platform/apple/avd/avd-drv.c +++ b/drivers/media/platform/apple/avd/avd-drv.c @@ -289,6 +289,12 @@ static void avd_finish_work(struct work_struct *work) } } + /* Failed pictures must not leave instructions referencing dropped buffers. */ + if (ctx->request.failed) { + kvfree(ctx->job.segments); + ctx->job.segments = NULL; + } + avd_finish_buffers(ctx, ctx->request.failed ? VB2_BUF_STATE_ERROR : VB2_BUF_STATE_DONE); if (ctx->pm_ref) diff --git a/tools/asahi/avd-request-tests.py b/tools/asahi/avd-request-tests.py index f6d2b897f49b8..e2641e5909c76 100644 --- a/tools/asahi/avd-request-tests.py +++ b/tools/asahi/avd-request-tests.py @@ -78,6 +78,8 @@ def main(): "v4l2_m2m_buf_done(v4l2_m2m_next_dst_buf(ctx->fh.m2m_ctx), result);"), "premature-buffer-return": ("ctx->scheduler_released = true;", "ctx->scheduler_released = true; v4l2_m2m_buf_done(ctx->dst_buf, VB2_BUF_STATE_DONE);"), + "retain-failed-table": ("kvfree(ctx->job.segments);\n\t\tctx->job.segments = NULL;", + "/* Retained stale instructions: must be detected. */"), } for name, (old, new) in mutations.items(): if body.count(old) != 1: diff --git a/tools/asahi/avd-request/README.md b/tools/asahi/avd-request/README.md index b6cdb44ba9ab3..051fefc7d4297 100644 --- a/tools/asahi/avd-request/README.md +++ b/tools/asahi/avd-request/README.md @@ -17,7 +17,7 @@ submission, IRQ, watchdog, retirement and buffer-completion functions. The C fixture supplies deterministic MMIO, m2m, PM and workqueue mocks. It drives two contexts through CPU preparation and serialized hardware ownership, submission IRQs, duplicate/wrong-slot VP events, timeout recovery, PM/validation failures, -held capture, streamoff drain and failed-reset admission. Four mutations must +held capture, streamoff drain failed-reset admission and failed-table cleanup. Five mutations must compile and fail at runtime; a failed control or mutant build fails the runner. The capability runner executes the production firmware-tail parser, including diff --git a/tools/asahi/avd-request/host.c b/tools/asahi/avd-request/host.c index d4e1bf9368ec2..ad670c37dbeae 100644 --- a/tools/asahi/avd-request/host.c +++ b/tools/asahi/avd-request/host.c @@ -340,9 +340,6 @@ static void pump(struct avd_ctx *ctx) assert(ctx->finish_work.queued); ctx->finish_work.queued = false; avd_finish_work(&ctx->finish_work); - /* avd_submit_job retains its CPU table on partial submission error. */ - free(ctx->job.segments); - ctx->job.segments = NULL; } static void inject(struct avd_dev *d, u32 status) { @@ -458,6 +455,7 @@ int main(void) pump(&a); assert(resets == 1 && ba[0].result == VB2_BUF_STATE_ERROR && pm_refs == 0); + assert(!a.job.segments); init_ctx(&a, &dev, &ma, ba); start(&a); complete(&a); @@ -466,10 +464,12 @@ int main(void) init_ctx(&a, &dev, &ma, ba); a.run_mode = 1; + a.job.segments = calloc(2, sizeof(*a.job.segments)); start(&a); pump(&a); assert(resets == 1 && ba[0].result == VB2_BUF_STATE_ERROR && pm_refs == 0); + assert(!a.job.segments); init_ctx(&a, &dev, &ma, ba); pm_error = 1; start(&a); @@ -482,10 +482,14 @@ int main(void) ma.out_q_ctx.q.subsystem_flags = 1; ba[0].held = true; a.run_mode = 2; + a.job.segments = calloc(2, sizeof(*a.job.segments)); start(&a); pump(&a); assert(ba[0].done == 1 && !ba[1].done && ma.nd == 2 && !a.scheduler_released); + assert(a.job.segments); + free(a.job.segments); + a.job.segments = NULL; init_ctx(&a, &dev, &ma, ba); ma.out_q_ctx.q.subsystem_flags = 1; start(&a); @@ -509,6 +513,7 @@ int main(void) pump(&a); queue_full = 0; assert(resets == 2 && ba[0].result == VB2_BUF_STATE_ERROR); + assert(!a.job.segments); init_ctx(&a, &dev, &ma, ba); reset_error = -EIO; start(&a);