From b6c937df90f12e00b1004ee7ccad6b5f6bd038f6 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sun, 1 Nov 2026 00:05:26 +1100 Subject: [PATCH 1/4] media: v4l2-ctrls: reject invalid AV1 context tiles and VP9 tile rows Complete AV1 tile validation with the context-update tile index bound, which also rejects empty tile grids. Keep the existing descriptor-count limit. Reject VP9 tile_rows_log2 above two before drivers use it in shifts or tile loops. These checks match the public AVD v3 series by Sofus Forstreuter. Link: https://lore.kernel.org/linux-media/20261020-avd-v3-4-85cbaa2125d8@icloud.com/ Link: https://lore.kernel.org/linux-media/20261020-avd-v3-5-85cbaa2125d8@icloud.com/ Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/media/v4l2-core/v4l2-ctrls-core.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/drivers/media/v4l2-core/v4l2-ctrls-core.c b/drivers/media/v4l2-core/v4l2-ctrls-core.c index 648b88c868bc6..0db4d34f1fb62 100644 --- a/drivers/media/v4l2-core/v4l2-ctrls-core.c +++ b/drivers/media/v4l2-core/v4l2-ctrls-core.c @@ -629,9 +629,9 @@ validate_vp9_frame(struct v4l2_ctrl_vp9_frame *frame) /* * According to the spec, tile_cols_log2 shall be less than or equal - * to 6. + * to 6, and tile_rows_log2 shall be less than or equal to 2. */ - if (frame->tile_cols_log2 > 6) + if (frame->tile_cols_log2 > 6 || frame->tile_rows_log2 > 2) return -EINVAL; if (frame->reference_mode > V4L2_VP9_REFERENCE_MODE_SELECT) @@ -804,7 +804,8 @@ static int validate_av1_tile_info(struct v4l2_av1_tile_info *t) t->tile_rows > V4L2_AV1_MAX_TILE_ROWS) return -EINVAL; - if ((u32)t->tile_cols * t->tile_rows > V4L2_AV1_MAX_TILE_COUNT) + if ((u32)t->tile_cols * t->tile_rows > V4L2_AV1_MAX_TILE_COUNT || + t->context_update_tile_id >= (u32)t->tile_cols * t->tile_rows) return -EINVAL; return 0; From 8f09f9f6f7a4e8ade114e03a4aa4f3ff42315334 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sun, 1 Nov 2026 00:09:10 +1100 Subject: [PATCH 2/4] iommu: apple-dart: make live paging-domain switches flush translation state Stop translation before replacing an ordinary stream's TTBRs and propagate TLB invalidation failures. Restore the preceding domain if attachment fails, or leave DMA disabled if restoration also fails. Balance runtime PM on acquisition failures and move stream membership out of the old domain only after a successful switch. AVD recovery needs to attach an empty paging domain and detach back to the DMA domain after resetting the decoder. Keep firmware-owned and managed streams outside this recovery mechanism. Add KUnit coverage for two-stream membership transfer without removing another client's stream. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/iommu/apple-dart.c | 116 ++++++++++++++++++++++++++++++++++--- 1 file changed, 107 insertions(+), 9 deletions(-) diff --git a/drivers/iommu/apple-dart.c b/drivers/iommu/apple-dart.c index 384e243d5d0a2..f235f9503cbc9 100644 --- a/drivers/iommu/apple-dart.c +++ b/drivers/iommu/apple-dart.c @@ -2543,18 +2543,69 @@ static int apple_dart_domain_add_streams(struct apple_dart_domain *domain, true); } +static void apple_dart_domain_remove_device(struct iommu_domain *domain, + struct apple_dart_master_cfg *cfg) +{ + if (domain && (domain->type & __IOMMU_DOMAIN_PAGING)) + apple_dart_mod_streams(to_dart_domain(domain)->stream_maps, + cfg->stream_maps, false); +} + +/* The IOMMU core does not roll back the device whose attach failed. */ +static void apple_dart_restore_domain(struct iommu_domain *old, + struct apple_dart_stream_map *stream_map) +{ + struct apple_dart *dart = stream_map->dart; + int ret; + + /* Firmware-owned roots and managed SIDs have their own unwind paths. */ + if (dart->locked || dart->managed_sids || apple_dart_has_borrowed(dart) || + dart->fw_mirror || dart->fw_handoff) + return; + apple_dart_hw_disable_dma(stream_map); + if (old && (old->type & __IOMMU_DOMAIN_PAGING)) { + ret = apple_dart_setup_translation(to_dart_domain(old), stream_map); + } else { + apple_dart_hw_clear_all_ttbrs(stream_map); + ret = dart->hw->invalidate_tlb(stream_map); + if (!ret && old && old->type == IOMMU_DOMAIN_IDENTITY) + apple_dart_hw_enable_bypass(stream_map); + } + if (ret) { + /* Do not leave DMA pointing at a domain the caller may now free. */ + apple_dart_hw_disable_dma(stream_map); + apple_dart_hw_clear_all_ttbrs(stream_map); + dev_err(dart->dev, "failed to restore translation after attach error: %d\n", ret); + } +} + static int apple_dart_attach_dev_paging(struct iommu_domain *domain, struct device *dev, struct iommu_domain *old) { - int ret, i; + int ret, i, powered = 0; struct apple_dart_stream_map *stream_map; struct apple_dart_master_cfg *cfg = dev_iommu_priv_get(dev); struct apple_dart_domain *dart_domain = to_dart_domain(domain); bool streams_added = false; - for_each_stream_map(i, cfg, stream_map) - WARN_ON(pm_runtime_get_sync(stream_map->dart->dev) < 0); + /* Only ordinary paging domains may be replaced during live recovery. */ + if (old && old != domain && (old->type & __IOMMU_DOMAIN_PAGING)) { + for_each_stream_map(i, cfg, stream_map) { + struct apple_dart *dart = stream_map->dart; + + if (dart->locked || dart->managed_sids || apple_dart_has_borrowed(dart) || + dart->fw_mirror || dart->fw_handoff) + return -EOPNOTSUPP; + } + } + + for_each_stream_map(i, cfg, stream_map) { + ret = pm_runtime_resume_and_get(stream_map->dart->dev); + if (ret < 0) + goto err_pm; + powered++; + } ret = apple_dart_finalize_domain(dart_domain, cfg); if (ret) @@ -2584,20 +2635,35 @@ static int apple_dart_attach_dev_paging(struct iommu_domain *domain, spin_unlock_irqrestore(&dart->managed_lock, flags); if (ret) goto err; - } else if (!stream_map->dart->locked) - apple_dart_setup_translation(dart_domain, stream_map); - else { + } else if (!stream_map->dart->locked) { + /* Stop translation before replacing a live domain's TTBRs. */ + apple_dart_hw_disable_dma(stream_map); + ret = apple_dart_setup_translation(dart_domain, stream_map); + if (ret) + goto err; + } else { ret = apple_dart_setup_translation_locked(dart_domain, stream_map); if (ret) goto err; } } + /* The old domain must no longer flush or free this device's streams. */ + if (old != domain) + apple_dart_domain_remove_device(old, cfg); err: - if (ret && streams_added && cfg->stream_maps[0].dart->managed_sids) - apple_dart_mod_streams(dart_domain->stream_maps, cfg->stream_maps, false); - for_each_stream_map(i, cfg, stream_map) + if (ret && streams_added) { + for_each_stream_map(i, cfg, stream_map) + apple_dart_restore_domain(old, stream_map); + if (old != domain) + apple_dart_mod_streams(dart_domain->stream_maps, cfg->stream_maps, false); + } +err_pm: + for_each_stream_map(i, cfg, stream_map) { + if (i == powered) + break; pm_runtime_put(stream_map->dart->dev); + } return ret; } @@ -4850,8 +4916,40 @@ static void apple_dart_fw_leaf_test(struct kunit *test) KUNIT_EXPECT_FALSE(test, matched); } +static void apple_dart_domain_switch_test(struct kunit *test) +{ + struct apple_dart *dart = kunit_kzalloc(test, sizeof(*dart), GFP_KERNEL); + struct apple_dart_master_cfg *cfg = kunit_kzalloc(test, sizeof(*cfg), GFP_KERNEL); + struct apple_dart_domain *dma = kunit_kzalloc(test, sizeof(*dma), GFP_KERNEL); + struct apple_dart_domain *empty = kunit_kzalloc(test, sizeof(*empty), GFP_KERNEL); + + KUNIT_ASSERT_NOT_NULL(test, dart); + KUNIT_ASSERT_NOT_NULL(test, cfg); + KUNIT_ASSERT_NOT_NULL(test, dma); + KUNIT_ASSERT_NOT_NULL(test, empty); + dart->num_streams = 16; + cfg->stream_maps[0].dart = dart; + set_bit(0, cfg->stream_maps[0].sidmap); + set_bit(1, cfg->stream_maps[0].sidmap); + dma->stream_maps[0].dart = dart; + empty->stream_maps[0].dart = dart; + dma->domain.type = IOMMU_DOMAIN_DMA; + empty->domain.type = IOMMU_DOMAIN_UNMANAGED; + /* A second client in the DMA domain must survive the switch. */ + atomic_long_set(&dma->stream_maps[0].sidmap[0], BIT(0) | BIT(1) | BIT(2)); + KUNIT_ASSERT_EQ(test, apple_dart_domain_add_streams(empty, cfg), 0); + apple_dart_domain_remove_device(&dma->domain, cfg); + KUNIT_EXPECT_EQ(test, atomic_long_read(&dma->stream_maps[0].sidmap[0]), (long)BIT(2)); + KUNIT_ASSERT_EQ(test, apple_dart_domain_add_streams(dma, cfg), 0); + apple_dart_domain_remove_device(&empty->domain, cfg); + KUNIT_EXPECT_EQ(test, atomic_long_read(&empty->stream_maps[0].sidmap[0]), 0L); + KUNIT_EXPECT_EQ(test, atomic_long_read(&dma->stream_maps[0].sidmap[0]), + (long)(BIT(0) | BIT(1) | BIT(2))); +} + static struct kunit_case apple_dart_fw_cases[] = { KUNIT_CASE(apple_dart_fw_leaf_test), + KUNIT_CASE(apple_dart_domain_switch_test), {} }; From 5d0f2fe9394b7836e15babaf73ad1133c67e0858 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sun, 1 Nov 2026 00:11:22 +1100 Subject: [PATCH 3/4] media: apple: avd: validate requests and flush DART during recovery Reject H.264 MBAFF/PAFF and field pictures, multiple slice groups and coded sizes above 4096x4096 with rate-limited diagnostics. Validate component formats across codecs and check AV1 tile coordinates and payload ranges. Apply and validate effective request controls before either frame_ops or the legacy builder can prepare a hardware program. Complete requests at core retirement, including preparation failures and held-slice requests. Recover by resetting the decoder, attaching and detaching an empty paging domain, then rebooting the backend. Preserve completed results and prepared slots that firmware never owned after successful recovery. Fail prepared slots too if recovery fails, and cancel only the draining context's pending slots during streamoff. An inbox retry timeout needs no global reset. Require a recovery domain at probe and manage its cleanup on error paths. Add validation KUnit cases, reset ownership tests and a portable host runner that compiles the production V4L2 validation helpers under host shims. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/media/platform/apple/avd/Kconfig | 3 +- drivers/media/platform/apple/avd/Makefile | 3 +- drivers/media/platform/apple/avd/avd-drv.c | 30 ++- .../platform/apple/avd/avd-queue-internal.h | 13 ++ .../media/platform/apple/avd/avd-queue-test.c | 26 +++ drivers/media/platform/apple/avd/avd-queue.c | 58 ++++-- drivers/media/platform/apple/avd/avd-v4l2.c | 75 +++++++- .../media/platform/apple/avd/avd-validate.h | 101 +++++++++++ .../platform/apple/avd/avd-validation-test.c | 171 ++++++++++++++++++ drivers/media/platform/apple/avd/avd.h | 1 + .../platform/apple/avd/tests/.kunitconfig | 3 + .../apple/avd/tests/include/linux/const.h | 6 + .../apple/avd/tests/include/linux/errno.h | 2 +- .../apple/avd/tests/include/linux/stddef.h | 2 + .../apple/avd/tests/include/linux/types.h | 16 +- .../platform/apple/avd/tests/queue-host.c | 18 ++ .../apple/avd/tests/run-validation-host.py | 108 +++++++++++ 17 files changed, 598 insertions(+), 38 deletions(-) create mode 100644 drivers/media/platform/apple/avd/avd-validate.h create mode 100644 drivers/media/platform/apple/avd/avd-validation-test.c create mode 100644 drivers/media/platform/apple/avd/tests/include/linux/const.h create mode 100644 drivers/media/platform/apple/avd/tests/include/linux/stddef.h create mode 100644 drivers/media/platform/apple/avd/tests/run-validation-host.py diff --git a/drivers/media/platform/apple/avd/Kconfig b/drivers/media/platform/apple/avd/Kconfig index ed5ad44276c31..a8e15f401165a 100644 --- a/drivers/media/platform/apple/avd/Kconfig +++ b/drivers/media/platform/apple/avd/Kconfig @@ -26,6 +26,7 @@ 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. Also exercise unsupported stream + rejection, V4L2 AV1/VP9 tile controls and recovery slot ownership. If unsure, say N. diff --git a/drivers/media/platform/apple/avd/Makefile b/drivers/media/platform/apple/avd/Makefile index 64095b4242133..d720656643c72 100644 --- a/drivers/media/platform/apple/avd/Makefile +++ b/drivers/media/platform/apple/avd/Makefile @@ -3,5 +3,6 @@ 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 avd-queue.o avd-backend-open.o \ avd-backend-apple.o -apple-avd-$(CONFIG_VIDEO_APPLE_AVD_KUNIT_TEST) += avd-h264-test.o avd-queue-test.o +apple-avd-$(CONFIG_VIDEO_APPLE_AVD_KUNIT_TEST) += avd-h264-test.o avd-queue-test.o \ + avd-validation-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 4abdc1de5f032..6128c550586c0 100644 --- a/drivers/media/platform/apple/avd/avd-drv.c +++ b/drivers/media/platform/apple/avd/avd-drv.c @@ -587,6 +587,17 @@ static void avd_release_open_firmware(void *data) release_firmware(avd->fw); } +static void avd_free_empty_domain(void *data) +{ + struct avd_dev *avd = data; + + if (iommu_get_domain_for_dev(avd->dev) == avd->empty_domain) { + dev_err(avd->dev, "retaining attached recovery domain\n"); + return; + } + iommu_domain_free(avd->empty_domain); +} + static int avd_probe(struct platform_device *pdev) { struct avd_dev *avd; @@ -624,13 +635,15 @@ static int avd_probe(struct platform_device *pdev) return PTR_ERR(avd->ctrl); avd->domain = iommu_get_domain_for_dev(avd->dev); - if (avd->domain) { - avd->empty_domain = iommu_paging_domain_alloc(avd->dev); - if (IS_ERR(avd->empty_domain)) { - avd->empty_domain = NULL; - dev_warn(avd->dev, "cannot alloc new empty domain"); - } - } + if (!avd->domain) + return dev_err_probe(avd->dev, -ENODEV, "missing DMA domain for recovery\n"); + avd->empty_domain = iommu_paging_domain_alloc(avd->dev); + if (IS_ERR(avd->empty_domain)) + return dev_err_probe(avd->dev, PTR_ERR(avd->empty_domain), + "cannot allocate recovery domain\n"); + ret = devm_add_action_or_reset(avd->dev, avd_free_empty_domain, avd); + if (ret) + return ret; ret = avd_backend_select(avd); if (ret) @@ -683,9 +696,6 @@ static void avd_remove(struct platform_device *pdev) avd_v4l2_cleanup(avd); - if (avd->empty_domain) - iommu_domain_free(avd->empty_domain); - pm_runtime_disable(avd->dev); pm_runtime_dont_use_autosuspend(avd->dev); } diff --git a/drivers/media/platform/apple/avd/avd-queue-internal.h b/drivers/media/platform/apple/avd/avd-queue-internal.h index 0e2a668d5a62f..45721b939f1bc 100644 --- a/drivers/media/platform/apple/avd/avd-queue-internal.h +++ b/drivers/media/platform/apple/avd/avd-queue-internal.h @@ -65,6 +65,19 @@ static inline bool avd_slot_expired(const struct avd_frame *f, unsigned long now f->state == AVD_FRAME_RESERVED) && time_after_eq(now, f->deadline); } +/* Completed results survive recovery; firmware never owned RESERVED slots. */ +static inline void avd_slot_reset(struct avd_frame *f, int status, + bool failed, unsigned long deadline) +{ + if (f->state == AVD_FRAME_SUBMITTED || + (failed && f->state == AVD_FRAME_RESERVED)) { + f->status = status; + f->state = AVD_FRAME_DONE; + } else if (f->state == AVD_FRAME_RESERVED) { + f->deadline = deadline; + } +} + static inline bool avd_inbox_full(unsigned int producer, unsigned int consumer) { return ((producer + 1) & 7) == consumer; diff --git a/drivers/media/platform/apple/avd/avd-queue-test.c b/drivers/media/platform/apple/avd/avd-queue-test.c index eec4761f850e9..17d9325c9f691 100644 --- a/drivers/media/platform/apple/avd/avd-queue-test.c +++ b/drivers/media/platform/apple/avd/avd-queue-test.c @@ -187,8 +187,34 @@ static void avd_dispatch_reservation(struct kunit *test) KUNIT_EXPECT_EQ(test, avd_queue_ready(&ctx[1]), 1); } +static void avd_reset_ownership(struct kunit *test) +{ + struct avd_frame f = { .state = AVD_FRAME_FREE, .tag = 123 }; + + avd_slot_reset(&f, -ETIMEDOUT, false, 100); + KUNIT_EXPECT_EQ(test, f.state, AVD_FRAME_FREE); + f.state = AVD_FRAME_RESERVED; + avd_slot_reset(&f, -ETIMEDOUT, false, 100); + KUNIT_EXPECT_EQ(test, f.state, AVD_FRAME_RESERVED); + KUNIT_EXPECT_EQ(test, f.deadline, 100UL); + KUNIT_EXPECT_EQ(test, f.tag, 123U); + f.state = AVD_FRAME_SUBMITTED; + avd_slot_reset(&f, -ETIMEDOUT, false, 100); + KUNIT_EXPECT_EQ(test, f.state, AVD_FRAME_DONE); + KUNIT_EXPECT_EQ(test, f.status, -ETIMEDOUT); + KUNIT_EXPECT_FALSE(test, avd_slot_complete(&f, 123, 0)); + f.status = 0; + avd_slot_reset(&f, -EIO, true, 100); + KUNIT_EXPECT_EQ(test, f.status, 0); + f.state = AVD_FRAME_RESERVED; + avd_slot_reset(&f, -EIO, true, 100); + KUNIT_EXPECT_EQ(test, f.state, AVD_FRAME_DONE); + KUNIT_EXPECT_EQ(test, f.status, -EIO); +} + static struct kunit_case avd_queue_cases[] = { KUNIT_CASE(avd_slots_and_tags), + KUNIT_CASE(avd_reset_ownership), KUNIT_CASE(avd_retirement_order), KUNIT_CASE(avd_watchdog_and_retry), KUNIT_CASE(avd_completion_parser), diff --git a/drivers/media/platform/apple/avd/avd-queue.c b/drivers/media/platform/apple/avd/avd-queue.c index 4991b5a38bac7..94604510092cf 100644 --- a/drivers/media/platform/apple/avd/avd-queue.c +++ b/drivers/media/platform/apple/avd/avd-queue.c @@ -1,5 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-only OR MIT #include +#include #include #include #include @@ -47,7 +48,7 @@ static void avd_retire(struct avd_dev *avd) } else if (ctx->coded_fmt_desc->ops->done) { ctx->coded_fmt_desc->ops->done(ctx, f->src, f->dst, state); } - if (ctx->coded_fmt_desc->frame_ops && f->src->vb2_buf.req_obj.req) + if (f->src->vb2_buf.req_obj.req) v4l2_ctrl_request_complete(f->src->vb2_buf.req_obj.req, &ctx->ctrl_hdl); /* CAPTURE first: OUTPUT completion signals the media request. */ @@ -82,6 +83,15 @@ static void avd_reset_queue(struct avd_dev *avd, int status) avd->backend->shutdown(avd); ret = reset_control_reset(avd->rstc); + if (!ret) { + /* Reset alone leaves DART translations cached by the decoder. */ + ret = iommu_attach_device(avd->empty_domain, avd->dev); + if (!ret) { + iommu_detach_device(avd->empty_domain, avd->dev); + if (iommu_get_domain_for_dev(avd->dev) != avd->domain) + ret = -EIO; + } + } if (!ret) ret = avd_backend_boot(avd); WRITE_ONCE(avd->faulted, !!ret); @@ -92,10 +102,8 @@ static void avd_reset_queue(struct avd_dev *avd, int status) ctx->session_started = false; spin_lock_irqsave(&avd->queue_lock, flags); for (i = 0; i < AVD_MAX_FRAMES; i++) { - if (avd->frames[i].state == AVD_FRAME_FREE) - continue; - avd->frames[i].status = status; - avd->frames[i].state = AVD_FRAME_DONE; + avd_slot_reset(&avd->frames[i], status, !!ret, + jiffies + AVD_FRAME_TIMEOUT); } spin_unlock_irqrestore(&avd->queue_lock, flags); } @@ -149,8 +157,15 @@ static void avd_queue_worker(struct work_struct *work) spin_unlock_irqrestore(&avd->queue_lock, flags); if (expired) { dev_err(avd->dev, "frame tag %#x timed out\n", avd->frames[i].tag); - avd_reset_queue(avd, -ETIMEDOUT); - break; + if (avd->frames[i].state == AVD_FRAME_SUBMITTED) { + avd_reset_queue(avd, -ETIMEDOUT); + break; + } + /* An inbox retry timeout has no DMA to recover. */ + spin_lock_irqsave(&avd->queue_lock, flags); + avd->frames[i].status = -ETIMEDOUT; + avd->frames[i].state = AVD_FRAME_DONE; + spin_unlock_irqrestore(&avd->queue_lock, flags); } } avd_retire(avd); @@ -274,6 +289,14 @@ static void avd_queue_submit_work(struct work_struct *work) f->frame_num = f->tag; f->deadline = jiffies + AVD_FRAME_TIMEOUT; memset(f->cmd, 0, sizeof(f->cmd)); + if (f->src->vb2_buf.req_obj.req) { + ret = v4l2_ctrl_request_setup(f->src->vb2_buf.req_obj.req, &ctx->ctrl_hdl); + if (ret) + goto fail; + } + ret = avd_validate_request(ctx); + if (ret) + goto fail; if (ops) { if (!f->prog->cpu) { ret = avd_buf_alloc(avd, f->prog, AVD_PROGRAM_SIZE); @@ -300,11 +323,6 @@ static void avd_queue_submit_work(struct work_struct *work) goto fail; } } - if (f->src->vb2_buf.req_obj.req) { - ret = v4l2_ctrl_request_setup(f->src->vb2_buf.req_obj.req, &ctx->ctrl_hdl); - if (ret) - goto fail; - } ret = ops->prepare(ctx, f); } else { avd->building = f; @@ -321,7 +339,7 @@ static void avd_queue_submit_work(struct work_struct *work) ops->retire(ctx, f, VB2_BUF_STATE_DONE); else if (!ops && ctx->coded_fmt_desc->ops->done) ctx->coded_fmt_desc->ops->done(ctx, f->src, f->dst, VB2_BUF_STATE_DONE); - if (ops && f->src->vb2_buf.req_obj.req) + if (f->src->vb2_buf.req_obj.req) v4l2_ctrl_request_complete(f->src->vb2_buf.req_obj.req, &ctx->ctrl_hdl); pm_runtime_put_autosuspend(avd->dev); spin_lock_irqsave(&avd->queue_lock, flags); @@ -398,13 +416,23 @@ void avd_queue_drain(struct avd_ctx *ctx) avd->queued_ctx = NULL; spin_unlock_irqrestore(&avd->queue_lock, flags); for (i = 0; i < AVD_MAX_FRAMES; i++) { - if (avd->frames[i].state != AVD_FRAME_FREE && avd->frames[i].ctx == ctx) { + if (avd->frames[i].state == AVD_FRAME_SUBMITTED && avd->frames[i].ctx == ctx) { if (avd->backend->abort) avd->backend->abort(ctx); avd_reset_queue(avd, -ECANCELED); break; } } + spin_lock_irqsave(&avd->queue_lock, flags); + for (i = 0; i < AVD_MAX_FRAMES; i++) { + struct avd_frame *f = &avd->frames[i]; + + if (f->ctx == ctx && f->state == AVD_FRAME_RESERVED) { + f->status = -ECANCELED; + f->state = AVD_FRAME_DONE; + } + } + spin_unlock_irqrestore(&avd->queue_lock, flags); avd_retire(avd); if (ctx->session_started && avd->backend->session_stop) { int ret = pm_runtime_resume_and_get(avd->dev); @@ -482,6 +510,8 @@ void avd_queue_cleanup(struct avd_dev *avd) WRITE_ONCE(avd->stopping, true); if (avd->inflight) { avd_reset_queue(avd, -ESHUTDOWN); + for (i = 0; i < AVD_MAX_FRAMES; i++) + avd_slot_reset(&avd->frames[i], -ESHUTDOWN, true, 0); avd_retire(avd); } mutex_unlock(&avd->queue_mutex); diff --git a/drivers/media/platform/apple/avd/avd-v4l2.c b/drivers/media/platform/apple/avd/avd-v4l2.c index 661100c2365b7..7df24574c336a 100644 --- a/drivers/media/platform/apple/avd/avd-v4l2.c +++ b/drivers/media/platform/apple/avd/avd-v4l2.c @@ -18,6 +18,7 @@ #include #include "avd.h" +#include "avd-validate.h" static bool avd_image_fmt_match(enum avd_image_fmt fmt1, enum avd_image_fmt fmt2) @@ -111,12 +112,74 @@ void avd_reset_decoded_fmt(struct avd_ctx *ctx) avd_fill_decoded_pixfmt(ctx, &f->fmt.pix_mp); } +static const char *avd_ctrl_unsupported(struct v4l2_ctrl *ctrl, bool current) +{ + union v4l2_ctrl_ptr p = current ? ctrl->p_cur : ctrl->p_new; + + switch (ctrl->id) { + case V4L2_CID_STATELESS_H264_SPS: + return avd_validate_h264_sps(p.p_h264_sps); + case V4L2_CID_STATELESS_H264_PPS: + return avd_validate_h264_pps(p.p_h264_pps); + case V4L2_CID_STATELESS_H264_DECODE_PARAMS: + return avd_validate_h264_decode(p.p_h264_decode_params); + case V4L2_CID_STATELESS_HEVC_SPS: + return avd_validate_hevc_sps(p.p_hevc_sps); + case V4L2_CID_STATELESS_VP9_FRAME: + return avd_validate_vp9(p.p_vp9_frame); + case V4L2_CID_STATELESS_AV1_SEQUENCE: + return avd_validate_av1_sequence(p.p_av1_sequence); + default: + return NULL; + } +} + +/* Check the effective request, including controls inherited from earlier jobs. */ +int avd_validate_request(struct avd_ctx *ctx) +{ + const struct avd_ctrls *ctrls = ctx->coded_fmt_desc->ctrls; + unsigned int i; + + for (i = 0; i < ctrls->num_ctrls; i++) { + struct v4l2_ctrl *ctrl = v4l2_ctrl_find(&ctx->ctrl_hdl, ctrls->ctrls[i].cfg.id); + const char *reason; + + if (!ctrl) + return -EINVAL; + reason = avd_ctrl_unsupported(ctrl, true); + if (!reason && ctrl->id == V4L2_CID_STATELESS_AV1_FRAME) { + const struct v4l2_ctrl_av1_frame *frame = ctrl->p_cur.p_av1_frame; + u32 payload; + struct v4l2_ctrl *tiles = v4l2_ctrl_find(&ctx->ctrl_hdl, + V4L2_CID_STATELESS_AV1_TILE_GROUP_ENTRY); + struct vb2_v4l2_buffer *src = v4l2_m2m_next_src_buf(ctx->fh.m2m_ctx); + + if (!tiles || !src) + return -EINVAL; + payload = vb2_get_plane_payload(&src->vb2_buf, 0); + reason = avd_validate_av1_tiles(&frame->tile_info, + tiles->p_cur.p_av1_tile_group_entry, + tiles->elems, payload); + } + if (reason) { + dev_err_ratelimited(ctx->dev->dev, "%s\n", reason); + return -EINVAL; + } + } + return 0; +} + static int avd_try_ctrl(struct v4l2_ctrl *ctrl) { struct avd_ctx *ctx = container_of(ctrl->handler, struct avd_ctx, ctrl_hdl); const struct avd_coded_fmt_desc *desc = ctx->coded_fmt_desc; + const char *reason = avd_ctrl_unsupported(ctrl, false); + if (reason) { + dev_err_ratelimited(ctx->dev->dev, "%s\n", reason); + return -EINVAL; + } if (desc->ops->try_ctrl) return desc->ops->try_ctrl(ctx, ctrl); @@ -233,6 +296,7 @@ static const struct avd_decoded_fmt_desc avd_hevc_decoded_fmts[] = { static const struct avd_ctrl_desc avd_h264_ctrl_descs[] = { { .cfg.id = V4L2_CID_STATELESS_H264_DECODE_PARAMS, + .cfg.ops = &avd_ctrl_ops, }, { .cfg.id = V4L2_CID_STATELESS_H264_SPS, @@ -988,7 +1052,6 @@ 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) { - struct media_request *src_req; struct avd_decoded_buffer *dst; memset(run, 0, sizeof(*run)); @@ -1009,20 +1072,12 @@ void avd_run_preamble(struct avd_ctx *ctx, struct avd_run *run) dst = vb2_to_avd_decoded_buf(&run->bufs.dst->vb2_buf); memcpy(&dst->comp, &ctx->comp, sizeof(ctx->comp)); - /* Apply request(s) controls if needed. */ - src_req = run->bufs.src->vb2_buf.req_obj.req; - if (src_req) - v4l2_ctrl_request_setup(src_req, &ctx->ctrl_hdl); - v4l2_m2m_buf_copy_metadata(run->bufs.src, run->bufs.dst); } void avd_run_postamble(struct avd_ctx *ctx, struct avd_run *run) { - struct media_request *src_req = run->bufs.src->vb2_buf.req_obj.req; - - if (src_req) - v4l2_ctrl_request_complete(src_req, &ctx->ctrl_hdl); + /* Request controls remain owned by the frame until core retirement. */ } static int avd_add_ctrls(struct avd_ctx *ctx, const struct avd_ctrls *ctrls) diff --git a/drivers/media/platform/apple/avd/avd-validate.h b/drivers/media/platform/apple/avd/avd-validate.h new file mode 100644 index 0000000000000..fe9a9dcd20876 --- /dev/null +++ b/drivers/media/platform/apple/avd/avd-validate.h @@ -0,0 +1,101 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +#ifndef AVD_VALIDATE_H_ +#define AVD_VALIDATE_H_ + +#include +#include + +/* Return a diagnostic so control rejection and request rejection agree. */ +static inline const char *avd_validate_h264_sps(const struct v4l2_ctrl_h264_sps *sps) +{ + if (!(sps->flags & V4L2_H264_SPS_FLAG_FRAME_MBS_ONLY) || + (sps->flags & V4L2_H264_SPS_FLAG_MB_ADAPTIVE_FRAME_FIELD)) + return "H.264 MBAFF/PAFF is unsupported"; + if (sps->pic_width_in_mbs_minus1 >= 256 || + sps->pic_height_in_map_units_minus1 >= 256) + return "H.264 dimensions exceed 4096x4096"; + if (sps->chroma_format_idc > 2) + return "H.264 4:4:4 is unsupported"; + if (sps->bit_depth_luma_minus8 != sps->bit_depth_chroma_minus8 || + (sps->bit_depth_luma_minus8 != 0 && sps->bit_depth_luma_minus8 != 2)) + return "H.264 requires matching 8-bit or 10-bit components"; + if (sps->chroma_format_idc == 2 && sps->bit_depth_luma_minus8 == 2) + return "H.264 10-bit 4:2:2 requires unavailable P210 capture"; + return NULL; +} + +static inline const char *avd_validate_h264_pps(const struct v4l2_ctrl_h264_pps *pps) +{ + return pps->num_slice_groups_minus1 ? "H.264 multiple slice groups are unsupported" : NULL; +} + +static inline const char *avd_validate_h264_decode(const struct v4l2_ctrl_h264_decode_params *p) +{ + return p->flags & (V4L2_H264_DECODE_PARAM_FLAG_FIELD_PIC | + V4L2_H264_DECODE_PARAM_FLAG_BOTTOM_FIELD) ? + "H.264 field pictures are unsupported" : NULL; +} + +static inline const char *avd_validate_hevc_sps(const struct v4l2_ctrl_hevc_sps *sps) +{ + if (sps->chroma_format_idc > 2) + return "HEVC 4:4:4 is unsupported"; + if (sps->bit_depth_luma_minus8 != sps->bit_depth_chroma_minus8 || + (sps->bit_depth_luma_minus8 != 0 && sps->bit_depth_luma_minus8 != 2)) + return "HEVC requires matching 8-bit or 10-bit components"; + if (sps->chroma_format_idc == 2 && sps->bit_depth_luma_minus8 == 2) + return "HEVC 10-bit 4:2:2 requires unavailable P210 capture"; + return NULL; +} + +static inline const char *avd_validate_vp9(const struct v4l2_ctrl_vp9_frame *f) +{ + if (f->bit_depth != 8 && f->bit_depth != 10) + return "VP9 requires 8-bit or 10-bit components"; + if (!(f->flags & V4L2_VP9_FRAME_FLAG_X_SUBSAMPLING)) + return "VP9 4:4:4 is unsupported"; + if (!(f->flags & V4L2_VP9_FRAME_FLAG_Y_SUBSAMPLING) && f->bit_depth == 10) + return "VP9 10-bit 4:2:2 requires unavailable P210 capture"; + if (f->tile_cols_log2 > 6 || f->tile_rows_log2 > 2) + return "VP9 tile log2 dimensions are invalid"; + return NULL; +} + +static inline const char *avd_validate_av1_sequence(const struct v4l2_ctrl_av1_sequence *s) +{ + if (s->bit_depth != 8 && s->bit_depth != 10) + return "AV1 requires 8-bit or 10-bit components"; + if (s->flags & V4L2_AV1_SEQUENCE_FLAG_MONO_CHROME) + return "AV1 monochrome is unsupported"; + if (!(s->flags & V4L2_AV1_SEQUENCE_FLAG_SUBSAMPLING_X)) + return "AV1 4:4:4 is unsupported"; + if (!(s->flags & V4L2_AV1_SEQUENCE_FLAG_SUBSAMPLING_Y) && s->bit_depth == 10) + return "AV1 10-bit 4:2:2 requires unavailable P210 capture"; + return NULL; +} + +/* Validate every tile before set_tiles() indexes arrays or emits DMA ranges. */ +static inline const char * +avd_validate_av1_tiles(const struct v4l2_av1_tile_info *ti, + const struct v4l2_ctrl_av1_tile_group_entry *tiles, + unsigned int entries, unsigned int payload) +{ + unsigned int i, count = ti->tile_cols * ti->tile_rows; + + if (!ti->tile_cols || !ti->tile_rows || + ti->tile_cols > V4L2_AV1_MAX_TILE_COLS || + ti->tile_rows > V4L2_AV1_MAX_TILE_ROWS || count > entries || + ti->context_update_tile_id >= count) + return "AV1 tile grid is invalid"; + for (i = 0; i < count; i++) { + if (tiles[i].tile_row != i / ti->tile_cols || + tiles[i].tile_col != i % ti->tile_cols) + return "AV1 tile coordinates do not match the grid"; + if (!tiles[i].tile_size || tiles[i].tile_offset > payload || + tiles[i].tile_size > payload - tiles[i].tile_offset) + return "AV1 tile data exceeds the OUTPUT payload"; + } + return NULL; +} + +#endif /* AVD_VALIDATE_H_ */ diff --git a/drivers/media/platform/apple/avd/avd-validation-test.c b/drivers/media/platform/apple/avd/avd-validation-test.c new file mode 100644 index 0000000000000..c85fcf54d3829 --- /dev/null +++ b/drivers/media/platform/apple/avd/avd-validation-test.c @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +#include +#include + +#include "avd-validate.h" + +static void avd_h264_capabilities(struct kunit *test) +{ + struct v4l2_ctrl_h264_sps sps = { + .flags = V4L2_H264_SPS_FLAG_FRAME_MBS_ONLY, + .pic_width_in_mbs_minus1 = 255, + .pic_height_in_map_units_minus1 = 255, + .chroma_format_idc = 1, + }; + struct v4l2_ctrl_h264_pps pps = {}; + struct v4l2_ctrl_h264_decode_params decode = {}; + + KUNIT_EXPECT_NULL(test, avd_validate_h264_sps(&sps)); + sps.pic_width_in_mbs_minus1++; + KUNIT_EXPECT_NOT_NULL(test, avd_validate_h264_sps(&sps)); + sps.pic_width_in_mbs_minus1--; + sps.pic_height_in_map_units_minus1++; + KUNIT_EXPECT_NOT_NULL(test, avd_validate_h264_sps(&sps)); + sps.pic_height_in_map_units_minus1--; + sps.flags = 0; /* PAFF, even if this particular picture is a frame. */ + KUNIT_EXPECT_NOT_NULL(test, avd_validate_h264_sps(&sps)); + sps.flags = V4L2_H264_SPS_FLAG_FRAME_MBS_ONLY | + V4L2_H264_SPS_FLAG_MB_ADAPTIVE_FRAME_FIELD; + KUNIT_EXPECT_NOT_NULL(test, avd_validate_h264_sps(&sps)); + sps.flags = V4L2_H264_SPS_FLAG_FRAME_MBS_ONLY; + sps.chroma_format_idc = 3; + KUNIT_EXPECT_NOT_NULL(test, avd_validate_h264_sps(&sps)); + sps.chroma_format_idc = 2; + sps.bit_depth_luma_minus8 = 2; + sps.bit_depth_chroma_minus8 = 2; + KUNIT_EXPECT_NOT_NULL(test, avd_validate_h264_sps(&sps)); + sps.chroma_format_idc = 1; + KUNIT_EXPECT_NULL(test, avd_validate_h264_sps(&sps)); + sps.bit_depth_chroma_minus8 = 0; + KUNIT_EXPECT_NOT_NULL(test, avd_validate_h264_sps(&sps)); + sps.bit_depth_luma_minus8 = 4; + sps.bit_depth_chroma_minus8 = 4; + KUNIT_EXPECT_NOT_NULL(test, avd_validate_h264_sps(&sps)); + KUNIT_EXPECT_NULL(test, avd_validate_h264_pps(&pps)); + pps.num_slice_groups_minus1 = 1; + KUNIT_EXPECT_NOT_NULL(test, avd_validate_h264_pps(&pps)); + KUNIT_EXPECT_NULL(test, avd_validate_h264_decode(&decode)); + decode.flags = V4L2_H264_DECODE_PARAM_FLAG_FIELD_PIC; + KUNIT_EXPECT_NOT_NULL(test, avd_validate_h264_decode(&decode)); + decode.flags = V4L2_H264_DECODE_PARAM_FLAG_BOTTOM_FIELD; + KUNIT_EXPECT_NOT_NULL(test, avd_validate_h264_decode(&decode)); +} + +static void avd_component_formats(struct kunit *test) +{ + struct v4l2_ctrl_hevc_sps hevc = { .chroma_format_idc = 1 }; + struct v4l2_ctrl_av1_sequence av1 = { + .flags = V4L2_AV1_SEQUENCE_FLAG_SUBSAMPLING_X | + V4L2_AV1_SEQUENCE_FLAG_SUBSAMPLING_Y, + .bit_depth = 8, + }; + struct v4l2_ctrl_vp9_frame vp9 = { + .flags = V4L2_VP9_FRAME_FLAG_X_SUBSAMPLING | + V4L2_VP9_FRAME_FLAG_Y_SUBSAMPLING, + .bit_depth = 8, + }; + + KUNIT_EXPECT_NULL(test, avd_validate_hevc_sps(&hevc)); + hevc.chroma_format_idc = 3; + KUNIT_EXPECT_NOT_NULL(test, avd_validate_hevc_sps(&hevc)); + hevc.chroma_format_idc = 1; + hevc.bit_depth_luma_minus8 = 4; + hevc.bit_depth_chroma_minus8 = 4; + KUNIT_EXPECT_NOT_NULL(test, avd_validate_hevc_sps(&hevc)); + KUNIT_EXPECT_NULL(test, avd_validate_av1_sequence(&av1)); + av1.bit_depth = 10; + KUNIT_EXPECT_NULL(test, avd_validate_av1_sequence(&av1)); + av1.bit_depth = 12; + KUNIT_EXPECT_NOT_NULL(test, avd_validate_av1_sequence(&av1)); + av1.bit_depth = 8; + av1.flags = 0; + KUNIT_EXPECT_NOT_NULL(test, avd_validate_av1_sequence(&av1)); + KUNIT_EXPECT_NULL(test, avd_validate_vp9(&vp9)); + vp9.bit_depth = 12; + KUNIT_EXPECT_NOT_NULL(test, avd_validate_vp9(&vp9)); + vp9.bit_depth = 10; + KUNIT_EXPECT_NULL(test, avd_validate_vp9(&vp9)); + vp9.flags = 0; + KUNIT_EXPECT_NOT_NULL(test, avd_validate_vp9(&vp9)); +} + +static void avd_av1_tile_payload(struct kunit *test) +{ + struct v4l2_av1_tile_info ti = { .tile_cols = 2, .tile_rows = 1 }; + struct v4l2_ctrl_av1_tile_group_entry tiles[] = { + { .tile_size = 8 }, + { .tile_col = 1, .tile_offset = 8, .tile_size = 8 }, + }; + + KUNIT_EXPECT_NULL(test, avd_validate_av1_tiles(&ti, tiles, 2, 16)); + KUNIT_EXPECT_NOT_NULL(test, avd_validate_av1_tiles(&ti, tiles, 1, 16)); + KUNIT_EXPECT_NOT_NULL(test, avd_validate_av1_tiles(&ti, tiles, 2, 15)); + tiles[1].tile_offset = U32_MAX; + KUNIT_EXPECT_NOT_NULL(test, avd_validate_av1_tiles(&ti, tiles, 2, 16)); + tiles[1].tile_offset = 8; + tiles[1].tile_row = V4L2_AV1_MAX_TILE_ROWS; + KUNIT_EXPECT_NOT_NULL(test, avd_validate_av1_tiles(&ti, tiles, 2, 16)); + tiles[1].tile_row = 0; + ti.context_update_tile_id = 2; + KUNIT_EXPECT_NOT_NULL(test, avd_validate_av1_tiles(&ti, tiles, 2, 16)); + ti.context_update_tile_id = 0; + ti.tile_cols = 0; + KUNIT_EXPECT_NOT_NULL(test, avd_validate_av1_tiles(&ti, tiles, 2, 16)); +} + +/* Exercise the public V4L2 type validator, before any driver callback. */ +static void avd_v4l2_tile_controls(struct kunit *test) +{ + struct v4l2_ctrl ctrl = { .type = V4L2_CTRL_TYPE_AV1_FRAME, .new_elems = 1 }; + struct v4l2_ctrl_av1_frame *av1 = kunit_kzalloc(test, sizeof(*av1), GFP_KERNEL); + struct v4l2_ctrl_vp9_frame vp9 = { + .bit_depth = 8, + .flags = V4L2_VP9_FRAME_FLAG_X_SUBSAMPLING | + V4L2_VP9_FRAME_FLAG_Y_SUBSAMPLING, + }; + unsigned int cols, rows; + int ret; + + KUNIT_ASSERT_NOT_NULL(test, av1); + for (cols = 0; cols <= V4L2_AV1_MAX_TILE_COLS + 1; cols++) { + for (rows = 0; rows <= V4L2_AV1_MAX_TILE_ROWS + 1; rows++) { + bool valid = cols && rows && cols <= V4L2_AV1_MAX_TILE_COLS && + rows <= V4L2_AV1_MAX_TILE_ROWS && + cols * rows <= V4L2_AV1_MAX_TILE_COUNT; + + av1->tile_info.tile_cols = cols; + av1->tile_info.tile_rows = rows; + av1->tile_info.context_update_tile_id = 0; + ret = v4l2_ctrl_type_op_validate(&ctrl, v4l2_ctrl_ptr_create(av1)); + KUNIT_EXPECT_EQ(test, ret, valid ? 0 : -EINVAL); + } + } + av1->tile_info.tile_cols = 2; + av1->tile_info.tile_rows = 2; + av1->tile_info.context_update_tile_id = 3; + KUNIT_EXPECT_EQ(test, v4l2_ctrl_type_op_validate(&ctrl, v4l2_ctrl_ptr_create(av1)), 0); + av1->tile_info.context_update_tile_id = 4; + ret = v4l2_ctrl_type_op_validate(&ctrl, v4l2_ctrl_ptr_create(av1)); + KUNIT_EXPECT_EQ(test, ret, -EINVAL); + ctrl.type = V4L2_CTRL_TYPE_VP9_FRAME; + for (rows = 0; rows <= 255; rows++) { + vp9.tile_rows_log2 = rows; + KUNIT_EXPECT_EQ(test, v4l2_ctrl_type_op_validate(&ctrl, v4l2_ctrl_ptr_create(&vp9)), + rows <= 2 ? 0 : -EINVAL); + } +} + +static struct kunit_case avd_validation_cases[] = { + KUNIT_CASE(avd_h264_capabilities), + KUNIT_CASE(avd_component_formats), + KUNIT_CASE(avd_av1_tile_payload), + KUNIT_CASE(avd_v4l2_tile_controls), + {} +}; + +static struct kunit_suite avd_validation_suite = { + .name = "apple-avd-validation", + .test_cases = avd_validation_cases, +}; + +kunit_test_suite(avd_validation_suite); diff --git a/drivers/media/platform/apple/avd/avd.h b/drivers/media/platform/apple/avd/avd.h index 69787064c4eec..6f40f2377f764 100644 --- a/drivers/media/platform/apple/avd/avd.h +++ b/drivers/media/platform/apple/avd/avd.h @@ -309,6 +309,7 @@ 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_job_finish(struct avd_ctx *ctx, enum vb2_buffer_state result); +int avd_validate_request(struct avd_ctx *ctx); void avd_run_preamble(struct avd_ctx *ctx, struct avd_run *run); void avd_run_postamble(struct avd_ctx *ctx, struct avd_run *run); diff --git a/drivers/media/platform/apple/avd/tests/.kunitconfig b/drivers/media/platform/apple/avd/tests/.kunitconfig index c079bc44af531..b95ab6dfe9330 100644 --- a/drivers/media/platform/apple/avd/tests/.kunitconfig +++ b/drivers/media/platform/apple/avd/tests/.kunitconfig @@ -11,3 +11,6 @@ CONFIG_OF=y CONFIG_VIDEO_APPLE_AVD=y CONFIG_VIDEO_APPLE_AVD_KUNIT_TEST=y CONFIG_WERROR=y +CONFIG_ARCH_APPLE=y +CONFIG_IOMMU_SUPPORT=y +CONFIG_APPLE_DART=y diff --git a/drivers/media/platform/apple/avd/tests/include/linux/const.h b/drivers/media/platform/apple/avd/tests/include/linux/const.h new file mode 100644 index 0000000000000..7efa976d198f6 --- /dev/null +++ b/drivers/media/platform/apple/avd/tests/include/linux/const.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +#ifndef AVD_HOST_CONST_H_ +#define AVD_HOST_CONST_H_ +#define _BITUL(x) (1UL << (x)) +#define _BITULL(x) (1ULL << (x)) +#endif diff --git a/drivers/media/platform/apple/avd/tests/include/linux/errno.h b/drivers/media/platform/apple/avd/tests/include/linux/errno.h index 32a2df3ba8d20..56c61b35451d2 100644 --- a/drivers/media/platform/apple/avd/tests/include/linux/errno.h +++ b/drivers/media/platform/apple/avd/tests/include/linux/errno.h @@ -1,2 +1,2 @@ /* SPDX-License-Identifier: GPL-2.0-only OR MIT */ -#include_next +#include diff --git a/drivers/media/platform/apple/avd/tests/include/linux/stddef.h b/drivers/media/platform/apple/avd/tests/include/linux/stddef.h new file mode 100644 index 0000000000000..14e0586e96f81 --- /dev/null +++ b/drivers/media/platform/apple/avd/tests/include/linux/stddef.h @@ -0,0 +1,2 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +#include diff --git a/drivers/media/platform/apple/avd/tests/include/linux/types.h b/drivers/media/platform/apple/avd/tests/include/linux/types.h index 3d29df0e18138..d201e93a7d30c 100644 --- a/drivers/media/platform/apple/avd/tests/include/linux/types.h +++ b/drivers/media/platform/apple/avd/tests/include/linux/types.h @@ -3,8 +3,22 @@ #define AVD_HOST_TYPES_H_ #include #include -#include_next +#include +/* Host-only UAPI types use the C library instead of kernel headers. */ +typedef uint8_t __u8; +typedef int8_t __s8; +typedef uint16_t __u16; +typedef int16_t __s16; +typedef uint32_t __u32; +typedef int32_t __s32; +typedef uint64_t __u64; +typedef int64_t __s64; typedef __u8 u8; +typedef __s8 s8; +typedef __u16 u16; +typedef __s16 s16; typedef __u32 u32; +typedef __s32 s32; typedef __u64 u64; +typedef __s64 s64; #endif diff --git a/drivers/media/platform/apple/avd/tests/queue-host.c b/drivers/media/platform/apple/avd/tests/queue-host.c index 5983d7ad5d6a3..a8f09120eebb6 100644 --- a/drivers/media/platform/apple/avd/tests/queue-host.c +++ b/drivers/media/platform/apple/avd/tests/queue-host.c @@ -15,6 +15,24 @@ int main(void) u32 rng = 0x3851, old_tag[AVD_MAX_FRAMES] = {}; unsigned int i, step, completed = 0; + /* Mixed clients: recovery preserves DONE results and unsubmitted work. */ + for (i = AVD_FRAME_FREE; i <= AVD_FRAME_DONE; i++) { + struct avd_frame f = { .state = i, .tag = 0x123, .status = 0 }; + + avd_slot_reset(&f, -ETIMEDOUT, false, 100); + assert(f.tag == 0x123); + if (i == AVD_FRAME_SUBMITTED) { + assert(f.state == AVD_FRAME_DONE && f.status == -ETIMEDOUT); + assert(!avd_slot_complete(&f, f.tag, 0)); + } else { + assert(f.state == i && !f.status); + } + if (i == AVD_FRAME_RESERVED) { + assert(f.deadline == 100); + avd_slot_reset(&f, -EIO, true, 0); + assert(f.state == AVD_FRAME_DONE && f.status == -EIO); + } + } for (i = 0; i < AVD_MAX_FRAMES; i++) pool[i].idx = i; for (step = 0; step < 100000; step++) { diff --git a/drivers/media/platform/apple/avd/tests/run-validation-host.py b/drivers/media/platform/apple/avd/tests/run-validation-host.py new file mode 100644 index 0000000000000..77e91b4c94fec --- /dev/null +++ b/drivers/media/platform/apple/avd/tests/run-validation-host.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Run the validation KUnit cases with host shims and extracted core validators. + +This does not replace an in-kernel KUnit run. No validator implementation is +copied: the static functions are compiled from the selected kernel source. +""" +import argparse +import os +from pathlib import Path +import re +import shlex +import subprocess +import tempfile + + +def extract(source, name): + match = re.search(r"static int\s+" + name + r"\s*\(", source) + if not match: + raise ValueError(f"missing production validator: {name}") + start = source.index("{", match.start()) + depth = 1 + end = start + 1 + while depth: + depth += (source[end] == "{") - (source[end] == "}") + end += 1 + return source[match.start():end] + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--kernel", type=Path) + args = parser.parse_args() + args.kernel = args.kernel or Path(__file__).resolve().parents[6] + tests = Path(__file__).resolve().parent + source = (args.kernel / "drivers/media/v4l2-core/v4l2-ctrls-core.c").read_text() + names = ["validate_vp9_lf_params", "validate_vp9_quant_params", "validate_vp9_seg_params", + "validate_vp9_frame", "validate_av1_quantization", "validate_av1_segmentation", + "validate_av1_loop_filter", "validate_av1_cdef", "validate_av1_loop_restauration", + "validate_av1_tile_info", "validate_av1_frame"] + with tempfile.TemporaryDirectory(prefix="avd-validation-") as temp: + out = Path(temp) + (out / "kunit").mkdir() + (out / "media").mkdir() + (out / "kunit/test.h").write_text(''' +#include +#include +#include +#include +#include +#include +#define U32_MAX UINT32_MAX +#define GFP_KERNEL 0 +struct kunit { int unused; }; +struct kunit_case { void (*run)(struct kunit *); }; +struct kunit_suite { const char *name; struct kunit_case *test_cases; }; +#define kunit_kzalloc(test, size, flags) calloc(1, size) +#define KUNIT_CASE(fn) { fn } +#define KUNIT_ASSERT_NOT_NULL(test, value) assert((value) != NULL) +#define KUNIT_EXPECT_NOT_NULL(test, value) assert((value) != NULL) +#define KUNIT_EXPECT_NULL(test, value) assert((value) == NULL) +#define KUNIT_EXPECT_EQ(test, a, b) assert((a) == (b)) +#define kunit_test_suite(suite) \\ +int main(void) { struct kunit test = {0}; unsigned int i; \\ +for (i = 0; suite.test_cases[i].run; i++) suite.test_cases[i].run(&test); \\ +printf("PASS: %s (%u KUnit cases under host shims)\\n", suite.name, i); return 0; } +''') + (out / "media/v4l2-ctrls.h").write_text(''' +#include +#define V4L2_CTRL_TYPE_AV1_FRAME 1 +#define V4L2_CTRL_TYPE_VP9_FRAME 2 +struct v4l2_ctrl { unsigned int type, new_elems; }; +union v4l2_ctrl_ptr { void *p; }; +static inline union v4l2_ctrl_ptr v4l2_ctrl_ptr_create(void *p) +{ return (union v4l2_ctrl_ptr){ .p = p }; } +int v4l2_ctrl_type_op_validate(const struct v4l2_ctrl *, union v4l2_ctrl_ptr); +''') + validators = '''#include +#include +#include +#include +#define zero_reserved(s) memset(&(s).reserved, 0, sizeof((s).reserved)) +#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) +#define GENMASK(h, l) ((~0U << (l)) & (~0U >> (31 - (h)))) +''' + validators += "\n\n".join(extract(source, name) for name in names) + validators += ''' +int v4l2_ctrl_type_op_validate(const struct v4l2_ctrl *ctrl, union v4l2_ctrl_ptr ptr) +{ + if (ctrl->new_elems != 1) abort(); + return ctrl->type == V4L2_CTRL_TYPE_AV1_FRAME ? + validate_av1_frame(ptr.p) : validate_vp9_frame(ptr.p); +} +''' + validators = "#include \n" + validators + (out / "validators.c").write_text(validators) + command = shlex.split(os.environ.get("CC", "cc")) + [ + "-std=gnu11", "-Wall", "-Wextra", "-Werror", "-Wno-unused-parameter", "-O2", + "-I" + str(out), "-I" + str(tests / "include"), + "-I" + str(args.kernel / "include/uapi"), + str(tests.parent / "avd-validation-test.c"), str(out / "validators.c"), + "-o", str(out / "validate")] + subprocess.run(command, check=True) + subprocess.run([str(out / "validate")], check=True) + + +if __name__ == "__main__": + main() From 024a71d99f0afcb254f9aa628a13acb600a911a1 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sun, 1 Nov 2026 00:12:21 +1100 Subject: [PATCH 4/4] media: apple: avd: add a repeatable six-suite fluster conformance target Record the public upstream reference scores and annotate known unsupported vectors without skipping them or changing raw scores. Include 14 interlaced AVC vectors verified from checksum-matched public ITU archives and format limits from pinned Fluster catalogues. Run each suite after preceding failures, preserve logs and decoded output, record catalogue hashes and return codes, and refuse to overwrite evidence. Provide explicit V4L2 stateless decoder commands, optional ffprobe annotation, a plan mode and host tests for failure continuation and evidence preservation. Link: https://lore.kernel.org/linux-media/20261020-avd-v3-0-85cbaa2125d8@icloud.com/ Assisted-by: LLM Signed-off-by: Ryan Murray --- .../apple/avd/tests/conformance/README.md | 86 ++++ .../apple/avd/tests/conformance/baseline.json | 371 ++++++++++++++++++ .../avd/tests/conformance/run-fluster.py | 166 ++++++++ .../avd/tests/conformance/test_fluster.py | 78 ++++ 4 files changed, 701 insertions(+) create mode 100644 drivers/media/platform/apple/avd/tests/conformance/README.md create mode 100644 drivers/media/platform/apple/avd/tests/conformance/baseline.json create mode 100644 drivers/media/platform/apple/avd/tests/conformance/run-fluster.py create mode 100644 drivers/media/platform/apple/avd/tests/conformance/test_fluster.py diff --git a/drivers/media/platform/apple/avd/tests/conformance/README.md b/drivers/media/platform/apple/avd/tests/conformance/README.md new file mode 100644 index 0000000000000..66630462309ad --- /dev/null +++ b/drivers/media/platform/apple/avd/tests/conformance/README.md @@ -0,0 +1,86 @@ +# AVD conformance + +The baseline records the six scores reported in the public AVD v3 cover letter: +77/135, 42/69, 216/305, 1/6, 145/147 and 238/242, in suite order below. +These are upstream reference scores, not results measured with this driver. + +`baseline.json` records per-vector limitations separately from scores. Fourteen +AVC vectors have an interlaced SPS verified from the public ITU archives, with +archive checksums and SPS flags recorded. Format marks come from the pinned +Fluster catalogues. The list is incomplete: an unmarked vector is unclassified. +4:4:4, 12-bit, AV1 monochrome and unavailable P210 capture are driver limits; +only the documented H.264 interlace/size limits use `hw-unsupported` marks. + +No vector is skipped. A rejected unsupported vector still counts in the raw +Fluster result and denominator. Each suite runs after a preceding failure, with +one job by default and a 30-second per-vector timeout. `run.json` records commands, +return codes and catalogue hashes. CSV summaries, decoder logs and decoded outputs +are retained. Use a new result directory for every run. + +Use Fluster revision `5d9d2bc676d97a17900e98fc69216d6d2c8f1ad0` and a GStreamer +build with the four `v4l2sl*dec` elements. Explicit V4L2 stateless decoder names +avoid automatic software decoder selection. Decoder overrides use +`--decoder 'H.264=NAME'`, `H.265`, `VP9` or `AV1`. + +Download the public vectors once: + +```sh +python3 /opt/fluster/fluster.py --resources /srv/avd-fluster/resources download \ + JVT-AVC_V1 JVT-FR-EXT VP9-TEST-VECTORS VP9-TEST-VECTORS-HIGH \ + JCT-VC-HEVC_V1 AV1-TEST-VECTORS +``` + +From the kernel checkout, print the exact six commands without opening a device: + +```sh +python3 drivers/media/platform/apple/avd/tests/conformance/run-fluster.py \ + --fluster /opt/fluster --resources /srv/avd-fluster/resources \ + --results /srv/avd-fluster/results/conformance-001 --plan +``` + +Run them on the AVD target: + +```sh +python3 drivers/media/platform/apple/avd/tests/conformance/run-fluster.py \ + --fluster /opt/fluster --resources /srv/avd-fluster/resources \ + --results /srv/avd-fluster/results/conformance-001 --probe +``` + +`--probe` uses ffprobe to annotate the first stream header, retaining probe errors. +It cannot certify that later parameter sets are supported, or identify every +multiple-slice-group vector. Its failure does not prevent decoding. To exercise +several contexts after the serial baseline, repeat with `--jobs 4` and a new result +directory. Check that valid vectors after a timeout still decode and that the +kernel log has no new DART faults. A device without AV1 will report that suite's +failure; do not treat it as an AV1 measurement. + +Local checks, without a decoder device: + +```sh +sh drivers/media/platform/apple/avd/tests/run-host.sh /tmp/avd-queue-host +python3 drivers/media/platform/apple/avd/tests/run-validation-host.py +python3 drivers/media/platform/apple/avd/tests/conformance/test_fluster.py +``` + +The validation host runner compiles the production static V4L2 validators and +executes the validation KUnit cases through host shims. It does not exercise the +kernel control framework, MMIO or IOMMU. An in-kernel KUnit run must also enable +`CONFIG_VIDEO_APPLE_AVD_KUNIT_TEST`, `CONFIG_VIDEO_APPLE_AVD` and `CONFIG_KUNIT`. +For an arm64 QEMU KUnit run from a full source tree with the cross compiler: + +```sh +python3 tools/testing/kunit/kunit.py run --arch=arm64 \ + --cross_compile=aarch64-linux-gnu- --build_dir=/tmp/avd-kunit \ + --kunitconfig=drivers/media/platform/apple/avd/tests/.kunitconfig 'apple-*' +``` + +The `apple-avd-validation`, `apple-avd-queue` and `apple-dart-firmware-leaf` suites +cover validation and recovery bookkeeping; live DART switching still needs a +target run. + +Public sources: + +- [AVD v3 cover letter and reference scores](https://lore.kernel.org/linux-media/20261020-avd-v3-0-85cbaa2125d8@icloud.com/) +- [AV1 tile validation](https://lore.kernel.org/linux-media/20261020-avd-v3-4-85cbaa2125d8@icloud.com/) +- [VP9 tile row validation](https://lore.kernel.org/linux-media/20261020-avd-v3-5-85cbaa2125d8@icloud.com/) +- [Pinned Fluster catalogues](https://github.com/fluendo/fluster/tree/5d9d2bc676d97a17900e98fc69216d6d2c8f1ad0/test_suites) diff --git a/drivers/media/platform/apple/avd/tests/conformance/baseline.json b/drivers/media/platform/apple/avd/tests/conformance/baseline.json new file mode 100644 index 0000000000000..8cef5899a4ddf --- /dev/null +++ b/drivers/media/platform/apple/avd/tests/conformance/baseline.json @@ -0,0 +1,371 @@ +{ + "schema_version": 1, + "score_source": "https://lore.kernel.org/linux-media/20261020-avd-v3-0-85cbaa2125d8@icloud.com/", + "catalogue_revision": "5d9d2bc676d97a17900e98fc69216d6d2c8f1ad0", + "notes": [ + "Public upstream scores, not measurements of this driver.", + "Marks annotate raw results; no marked vector is skipped or removed from a denominator.", + "The hardware-unsupported list is incomplete. Unmarked means unclassified, not supported.", + "4:4:4, 12-bit, monochrome AV1 and P210 capture are implementation limits, not claims about physical hardware." + ], + "suites": { + "JVT-AVC_V1": { + "passed": 77, + "total": 135, + "catalogue_sha256": "602cb6495b77514d11812c9a957682dc583172b24524ea29bc83bfa1d5803bb0" + }, + "JVT-FR-EXT": { + "passed": 42, + "total": 69, + "catalogue_sha256": "4adeeb82abbc258c7f7247f1bcf7e9d70788bf0ea28ee209e498ab9c31746a51" + }, + "VP9-TEST-VECTORS": { + "passed": 216, + "total": 305, + "catalogue_sha256": "37f79fb4b3c529f54f8f54b0e36040b410d75a2bf71d022f9726e5545bd129b0" + }, + "VP9-TEST-VECTORS-HIGH": { + "passed": 1, + "total": 6, + "catalogue_sha256": "0f0bbd89d3001522d8aa7b86f3a0c1b5ed3d323767a22ed4d504e1b67c4d2df5" + }, + "JCT-VC-HEVC_V1": { + "passed": 145, + "total": 147, + "catalogue_sha256": "778c3cf8724b7f3a9f9ad59d4633531f110e3d660d27f47721b96e148224d04f" + }, + "AV1-TEST-VECTORS": { + "passed": 238, + "total": 242, + "catalogue_sha256": "4ed6a2dcd63b1abaac88636a66df8bc1f062e325cb72dbc853c82175196286d9" + } + }, + "vector_marks": { + "JVT-FR-EXT/Hi422FR13_SONY_A": { + "marks": [ + "driver-unsupported: P210 capture format is not exposed" + ], + "evidence": { + "output_format": "yuv422p10le", + "source": "https://github.com/fluendo/fluster/blob/5d9d2bc676d97a17900e98fc69216d6d2c8f1ad0/test_suites/h.264/JVT-FR-EXT.json" + } + }, + "JVT-FR-EXT/Hi422FR14_SONY_A": { + "marks": [ + "driver-unsupported: P210 capture format is not exposed" + ], + "evidence": { + "output_format": "yuv422p10le", + "source": "https://github.com/fluendo/fluster/blob/5d9d2bc676d97a17900e98fc69216d6d2c8f1ad0/test_suites/h.264/JVT-FR-EXT.json" + } + }, + "JVT-FR-EXT/Hi422FR15_SONY_A": { + "marks": [ + "driver-unsupported: P210 capture format is not exposed" + ], + "evidence": { + "output_format": "yuv422p10le", + "source": "https://github.com/fluendo/fluster/blob/5d9d2bc676d97a17900e98fc69216d6d2c8f1ad0/test_suites/h.264/JVT-FR-EXT.json" + } + }, + "JVT-FR-EXT/Hi422FR6_SONY_A": { + "marks": [ + "driver-unsupported: P210 capture format is not exposed" + ], + "evidence": { + "output_format": "yuv422p10le", + "source": "https://github.com/fluendo/fluster/blob/5d9d2bc676d97a17900e98fc69216d6d2c8f1ad0/test_suites/h.264/JVT-FR-EXT.json" + } + }, + "JVT-FR-EXT/Hi422FR7_SONY_A": { + "marks": [ + "driver-unsupported: P210 capture format is not exposed" + ], + "evidence": { + "output_format": "yuv422p10le", + "source": "https://github.com/fluendo/fluster/blob/5d9d2bc676d97a17900e98fc69216d6d2c8f1ad0/test_suites/h.264/JVT-FR-EXT.json" + } + }, + "JVT-FR-EXT/Hi422FR8_SONY_A": { + "marks": [ + "driver-unsupported: P210 capture format is not exposed" + ], + "evidence": { + "output_format": "yuv422p10le", + "source": "https://github.com/fluendo/fluster/blob/5d9d2bc676d97a17900e98fc69216d6d2c8f1ad0/test_suites/h.264/JVT-FR-EXT.json" + } + }, + "JVT-FR-EXT/Hi422FR9_SONY_A": { + "marks": [ + "driver-unsupported: P210 capture format is not exposed" + ], + "evidence": { + "output_format": "yuv422p10le", + "source": "https://github.com/fluendo/fluster/blob/5d9d2bc676d97a17900e98fc69216d6d2c8f1ad0/test_suites/h.264/JVT-FR-EXT.json" + } + }, + "JVT-FR-EXT/Hi422FREXT16_SONY_A": { + "marks": [ + "driver-unsupported: P210 capture format is not exposed" + ], + "evidence": { + "output_format": "yuv422p10le", + "source": "https://github.com/fluendo/fluster/blob/5d9d2bc676d97a17900e98fc69216d6d2c8f1ad0/test_suites/h.264/JVT-FR-EXT.json" + } + }, + "JVT-FR-EXT/Hi422FREXT17_SONY_A": { + "marks": [ + "driver-unsupported: P210 capture format is not exposed" + ], + "evidence": { + "output_format": "yuv422p10le", + "source": "https://github.com/fluendo/fluster/blob/5d9d2bc676d97a17900e98fc69216d6d2c8f1ad0/test_suites/h.264/JVT-FR-EXT.json" + } + }, + "JVT-FR-EXT/Hi422FREXT18_SONY_A": { + "marks": [ + "driver-unsupported: P210 capture format is not exposed" + ], + "evidence": { + "output_format": "yuv422p10le", + "source": "https://github.com/fluendo/fluster/blob/5d9d2bc676d97a17900e98fc69216d6d2c8f1ad0/test_suites/h.264/JVT-FR-EXT.json" + } + }, + "JVT-FR-EXT/Hi422FREXT19_SONY_A": { + "marks": [ + "driver-unsupported: P210 capture format is not exposed" + ], + "evidence": { + "output_format": "yuv422p10le", + "source": "https://github.com/fluendo/fluster/blob/5d9d2bc676d97a17900e98fc69216d6d2c8f1ad0/test_suites/h.264/JVT-FR-EXT.json" + } + }, + "VP9-TEST-VECTORS/vp91-2-04-yuv444.webm": { + "marks": [ + "driver-unsupported: 4:4:4" + ], + "evidence": { + "output_format": "yuv444p", + "source": "https://github.com/fluendo/fluster/blob/5d9d2bc676d97a17900e98fc69216d6d2c8f1ad0/test_suites/vp9/VP9-TEST-VECTORS.json" + } + }, + "VP9-TEST-VECTORS-HIGH/vp92-2-20-12bit-yuv420.webm": { + "marks": [ + "driver-unsupported: 12-bit" + ], + "evidence": { + "output_format": "yuv420p12le", + "source": "https://github.com/fluendo/fluster/blob/5d9d2bc676d97a17900e98fc69216d6d2c8f1ad0/test_suites/vp9/VP9-TEST-VECTORS-HIGH.json" + } + }, + "VP9-TEST-VECTORS-HIGH/vp93-2-20-10bit-yuv422.webm": { + "marks": [ + "driver-unsupported: P210 capture format is not exposed" + ], + "evidence": { + "output_format": "yuv422p10le", + "source": "https://github.com/fluendo/fluster/blob/5d9d2bc676d97a17900e98fc69216d6d2c8f1ad0/test_suites/vp9/VP9-TEST-VECTORS-HIGH.json" + } + }, + "VP9-TEST-VECTORS-HIGH/vp93-2-20-12bit-yuv422.webm": { + "marks": [ + "driver-unsupported: 12-bit" + ], + "evidence": { + "output_format": "yuv422p12le", + "source": "https://github.com/fluendo/fluster/blob/5d9d2bc676d97a17900e98fc69216d6d2c8f1ad0/test_suites/vp9/VP9-TEST-VECTORS-HIGH.json" + } + }, + "VP9-TEST-VECTORS-HIGH/vp93-2-20-10bit-yuv444.webm": { + "marks": [ + "driver-unsupported: 4:4:4" + ], + "evidence": { + "output_format": "yuv444p10le", + "source": "https://github.com/fluendo/fluster/blob/5d9d2bc676d97a17900e98fc69216d6d2c8f1ad0/test_suites/vp9/VP9-TEST-VECTORS-HIGH.json" + } + }, + "VP9-TEST-VECTORS-HIGH/vp93-2-20-12bit-yuv444.webm": { + "marks": [ + "driver-unsupported: 4:4:4", + "driver-unsupported: 12-bit" + ], + "evidence": { + "output_format": "yuv444p12le", + "source": "https://github.com/fluendo/fluster/blob/5d9d2bc676d97a17900e98fc69216d6d2c8f1ad0/test_suites/vp9/VP9-TEST-VECTORS-HIGH.json" + } + }, + "AV1-TEST-VECTORS/av1-1-b10-24-monochrome": { + "marks": [ + "driver-unsupported: AV1 monochrome" + ], + "evidence": { + "output_format": "gray10le", + "source": "https://github.com/fluendo/fluster/blob/5d9d2bc676d97a17900e98fc69216d6d2c8f1ad0/test_suites/av1/AV1-TEST-VECTORS.json" + } + }, + "AV1-TEST-VECTORS/av1-1-b8-24-monochrome": { + "marks": [ + "driver-unsupported: AV1 monochrome" + ], + "evidence": { + "output_format": "gray", + "source": "https://github.com/fluendo/fluster/blob/5d9d2bc676d97a17900e98fc69216d6d2c8f1ad0/test_suites/av1/AV1-TEST-VECTORS.json" + } + }, + "JVT-AVC_V1/cabac_mot_fld0_full": { + "marks": [ + "hw-unsupported: H.264 interlace" + ], + "evidence": { + "source": "https://www.itu.int/wftp3/av-arch/jvt-site/draft_conformance/AVCv1/cabac_mot_fld0_full.zip", + "archive_md5": "14fc0427e00bfeb617b9ce9213833a55", + "frame_mbs_only_flag": 0, + "mb_adaptive_frame_field_flag": 0 + } + }, + "JVT-AVC_V1/cabac_mot_mbaff0_full": { + "marks": [ + "hw-unsupported: H.264 interlace" + ], + "evidence": { + "source": "https://www.itu.int/wftp3/av-arch/jvt-site/draft_conformance/AVCv1/cabac_mot_mbaff0_full.zip", + "archive_md5": "c2abf195c9a792757643f52bb2ddc220", + "frame_mbs_only_flag": 0, + "mb_adaptive_frame_field_flag": 1 + } + }, + "JVT-AVC_V1/cabac_mot_picaff0_full": { + "marks": [ + "hw-unsupported: H.264 interlace" + ], + "evidence": { + "source": "https://www.itu.int/wftp3/av-arch/jvt-site/draft_conformance/AVCv1/cabac_mot_picaff0_full.zip", + "archive_md5": "c003d30c0f2313349b2723d482cb3831", + "frame_mbs_only_flag": 0, + "mb_adaptive_frame_field_flag": 0 + } + }, + "JVT-AVC_V1/CAMP_MOT_MBAFF_L30": { + "marks": [ + "hw-unsupported: H.264 interlace" + ], + "evidence": { + "source": "https://www.itu.int/wftp3/av-arch/jvt-site/draft_conformance/AVCv1/CAMP_MOT_MBAFF_L30.zip", + "archive_md5": "6f03e6e0fbb4e89ce8c6ddb62804f58b", + "frame_mbs_only_flag": 0, + "mb_adaptive_frame_field_flag": 1 + } + }, + "JVT-AVC_V1/CAMP_MOT_MBAFF_L31": { + "marks": [ + "hw-unsupported: H.264 interlace" + ], + "evidence": { + "source": "https://www.itu.int/wftp3/av-arch/jvt-site/draft_conformance/AVCv1/CAMP_MOT_MBAFF_L31.zip", + "archive_md5": "58875831f97a414bde478a15cac5c595", + "frame_mbs_only_flag": 0, + "mb_adaptive_frame_field_flag": 1 + } + }, + "JVT-AVC_V1/cavlc_mot_fld0_full_B": { + "marks": [ + "hw-unsupported: H.264 interlace" + ], + "evidence": { + "source": "https://www.itu.int/wftp3/av-arch/jvt-site/draft_conformance/AVCv1/cavlc_mot_fld0_full_B.zip", + "archive_md5": "3991b434ca939b8c4b638724dd8af77b", + "frame_mbs_only_flag": 0, + "mb_adaptive_frame_field_flag": 0 + } + }, + "JVT-AVC_V1/cavlc_mot_mbaff0_full_B": { + "marks": [ + "hw-unsupported: H.264 interlace" + ], + "evidence": { + "source": "https://www.itu.int/wftp3/av-arch/jvt-site/draft_conformance/AVCv1/cavlc_mot_mbaff0_full_B.zip", + "archive_md5": "ce6a2e2ed9d0ab5d3094ad7cf8f41b4f", + "frame_mbs_only_flag": 0, + "mb_adaptive_frame_field_flag": 1 + } + }, + "JVT-AVC_V1/cavlc_mot_picaff0_full_B": { + "marks": [ + "hw-unsupported: H.264 interlace" + ], + "evidence": { + "source": "https://www.itu.int/wftp3/av-arch/jvt-site/draft_conformance/AVCv1/cavlc_mot_picaff0_full_B.zip", + "archive_md5": "4373f1bdde46f56ca1a35a837a000923", + "frame_mbs_only_flag": 0, + "mb_adaptive_frame_field_flag": 0 + } + }, + "JVT-AVC_V1/CVMP_MOT_FLD_L30_B": { + "marks": [ + "hw-unsupported: H.264 interlace" + ], + "evidence": { + "source": "https://www.itu.int/wftp3/av-arch/jvt-site/draft_conformance/AVCv1/CVMP_MOT_FLD_L30_B.zip", + "archive_md5": "b77be505335f504b854a0c6b8e3e1974", + "frame_mbs_only_flag": 0, + "mb_adaptive_frame_field_flag": 0 + } + }, + "JVT-AVC_V1/Sharp_MP_Field_1_B": { + "marks": [ + "hw-unsupported: H.264 interlace" + ], + "evidence": { + "source": "https://www.itu.int/wftp3/av-arch/jvt-site/draft_conformance/AVCv1/Sharp_MP_Field_1_B.zip", + "archive_md5": "b1095ff29ad6a3feeef73e20757c3f76", + "frame_mbs_only_flag": 0, + "mb_adaptive_frame_field_flag": 0 + } + }, + "JVT-AVC_V1/Sharp_MP_Field_2_B": { + "marks": [ + "hw-unsupported: H.264 interlace" + ], + "evidence": { + "source": "https://www.itu.int/wftp3/av-arch/jvt-site/draft_conformance/AVCv1/Sharp_MP_Field_2_B.zip", + "archive_md5": "b992383697fd0e34de9f374dc661443e", + "frame_mbs_only_flag": 0, + "mb_adaptive_frame_field_flag": 0 + } + }, + "JVT-AVC_V1/Sharp_MP_Field_3_B": { + "marks": [ + "hw-unsupported: H.264 interlace" + ], + "evidence": { + "source": "https://www.itu.int/wftp3/av-arch/jvt-site/draft_conformance/AVCv1/Sharp_MP_Field_3_B.zip", + "archive_md5": "3f26557fda6e85e7e0f04f4b926ed800", + "frame_mbs_only_flag": 0, + "mb_adaptive_frame_field_flag": 0 + } + }, + "JVT-AVC_V1/Sharp_MP_PAFF_1r2": { + "marks": [ + "hw-unsupported: H.264 interlace" + ], + "evidence": { + "source": "https://www.itu.int/wftp3/av-arch/jvt-site/draft_conformance/AVCv1/Sharp_MP_PAFF_1r2.zip", + "archive_md5": "1422bbedf0c98ceb703d6ca514260783", + "frame_mbs_only_flag": 0, + "mb_adaptive_frame_field_flag": 0 + } + }, + "JVT-AVC_V1/Sharp_MP_PAFF_2r": { + "marks": [ + "hw-unsupported: H.264 interlace" + ], + "evidence": { + "source": "https://www.itu.int/wftp3/av-arch/jvt-site/draft_conformance/AVCv1/Sharp_MP_PAFF_2r.zip", + "archive_md5": "b9a2f52d0fc6c45e8040dc5bf654826f", + "frame_mbs_only_flag": 0, + "mb_adaptive_frame_field_flag": 0 + } + } + } +} diff --git a/drivers/media/platform/apple/avd/tests/conformance/run-fluster.py b/drivers/media/platform/apple/avd/tests/conformance/run-fluster.py new file mode 100644 index 0000000000000..f6d7377291e01 --- /dev/null +++ b/drivers/media/platform/apple/avd/tests/conformance/run-fluster.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Run all six AVD conformance suites, preserving failures and vector annotations.""" +import argparse +import csv +import hashlib +import json +from pathlib import Path +import shlex +import subprocess +import sys + +HERE = Path(__file__).resolve().parent +DECODERS = {"H.264": "GStreamer-H.264-V4L2SL", "H.265": "GStreamer-H.265-V4L2SL", + "VP9": "GStreamer-VP9-V4L2SL", "AV1": "GStreamer-AV1-V4L2SL"} + + +def format_marks(codec, output_format): + """Implementation limits, distinct from proven hardware limits.""" + marks = [] + if "444" in output_format: + marks.append("driver-unsupported: 4:4:4") + if "12" in output_format: + marks.append("driver-unsupported: 12-bit") + if output_format == "yuv422p10le": + marks.append("driver-unsupported: P210 capture format is not exposed") + if codec == "AV1" and output_format.startswith("gray"): + marks.append("driver-unsupported: AV1 monochrome") + return marks + + +def load_suites(directory, baseline): + found = {} + for path in sorted(directory.rglob("*.json")): + suite = json.loads(path.read_text()) + name = suite.get("name") + if name in baseline["suites"]: + if name in found: + raise ValueError(f"duplicate suite: {name}") + found[name] = (path, suite) + missing = set(baseline["suites"]) - found.keys() + if missing: + raise ValueError(f"missing suites: {', '.join(sorted(missing))}") + for name, (_, suite) in found.items(): + expected = baseline["suites"][name]["total"] + digest = baseline["suites"][name].get("catalogue_sha256") + if digest and hashlib.sha256(found[name][0].read_bytes()).hexdigest() != digest: + raise ValueError(f"{name}: catalogue hash changed; review baseline") + if len(suite["test_vectors"]) != expected: + raise ValueError(f"{name}: catalogue changed from {expected} vectors; review baseline") + return found + + +def command(args, suite, codec, results): + decoder = args.decoder.get(codec, DECODERS[codec]) + return [sys.executable, str(args.fluster / "fluster.py"), + "--test-suites-dir", str(args.fluster / "test_suites"), + "--resources", str(args.resources), "--output", str(results / suite / "decoded"), + "run", "--jobs", str(args.jobs), "--timeout", str(args.timeout), + "--testsuites", suite, "--decoders", decoder, "--keep", "--verbose", + "--format", "csv", "--summary-output", str(results / suite / "results.csv")] + + +def probe(path, ffprobe, timeout): + cmd = [ffprobe, "-v", "error", "-select_streams", "v:0", "-show_entries", + "stream=width,height,pix_fmt,field_order", "-of", "json", str(path)] + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, check=False) + if result.returncode: + return {"error": result.stderr.strip(), "rc": result.returncode} + return json.loads(result.stdout) + except (OSError, subprocess.TimeoutExpired, json.JSONDecodeError) as error: + return {"error": str(error)} + + +def annotations(args, suites, baseline, results): + rows = [] + evidence = {} + for name, (_, suite) in suites.items(): + for vector in suite["test_vectors"]: + key = name + "/" + vector["name"] + marks = format_marks(suite["codec"], vector["output_format"]) + marks += baseline.get("vector_marks", {}).get(key, {}).get("marks", []) + state = "catalogue only; interlace, slice groups and size not inspected" + if args.probe: + path = args.resources / name / vector["name"] / vector["input_file"] + info = probe(path, args.ffprobe, args.timeout) + evidence[key] = info + if "error" in info or not info.get("streams"): + state = "probe-error" + else: + state = "first stream header inspected; later parameter sets and slice groups unclassified" + stream = info["streams"][0] + marks += format_marks(suite["codec"], stream.get("pix_fmt", "")) + if suite["codec"] == "H.264": + if stream.get("field_order") in ("tt", "bb", "tb", "bt"): + marks.append("hw-unsupported: H.264 interlace") + if max(stream.get("width", 0), stream.get("height", 0)) > 4096: + marks.append("hw-unsupported: H.264 above 4096x4096") + rows.append([name, vector["name"], vector["output_format"], + "; ".join(sorted(set(marks))), state]) + with (results / "vectors.csv").open("w", newline="") as output: + writer = csv.writer(output) + writer.writerow(["suite", "vector", "output_format", "unsupported_marks", "inspection"]) + writer.writerows(rows) + if args.probe: + (results / "probes.json").write_text(json.dumps(evidence, indent=2) + "\n") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--fluster", type=Path, required=True, help="Fluster checkout") + parser.add_argument("--resources", type=Path, required=True, help="already downloaded Fluster resources") + parser.add_argument("--results", type=Path, required=True, help="new directory, never overwritten") + parser.add_argument("--baseline", type=Path, default=HERE / "baseline.json") + parser.add_argument("--jobs", type=int, default=1) + parser.add_argument("--timeout", type=int, default=30, help="per-vector seconds") + parser.add_argument("--decoder", action="append", default=[], metavar="CODEC=NAME") + parser.add_argument("--probe", action="store_true", help="annotate local streams using ffprobe") + parser.add_argument("--ffprobe", default="ffprobe") + parser.add_argument("--plan", action="store_true", help="print commands without running tests") + args = parser.parse_args() + if args.jobs < 1 or args.timeout < 1: + parser.error("jobs and timeout must be positive") + try: + args.decoder = dict(item.split("=", 1) for item in args.decoder) + except ValueError: + parser.error("decoder must be CODEC=NAME") + if args.decoder.keys() - DECODERS.keys(): + parser.error("unknown codec in decoder override") + args.fluster = args.fluster.resolve() + args.resources = args.resources.resolve() + args.results = args.results.resolve() + baseline = json.loads(args.baseline.read_text()) + suites = load_suites(args.fluster / "test_suites", baseline) + if not (args.fluster / "fluster.py").is_file(): + parser.error("missing fluster.py") + if args.plan: + for name, (_, suite) in suites.items(): + print(shlex.join(command(args, name, suite["codec"], args.results))) + return 0 + args.results.mkdir(parents=True, exist_ok=False) + annotations(args, suites, baseline, args.results) + manifest = {"baseline": baseline, "jobs": args.jobs, "timeout": args.timeout, "runs": []} + failed = False + for name, (path, suite) in suites.items(): + directory = args.results / name + directory.mkdir() + cmd = command(args, name, suite["codec"], args.results) + print(shlex.join(cmd), flush=True) + with (directory / "run.log").open("w") as log: + try: + rc = subprocess.run(cmd, stdout=log, stderr=subprocess.STDOUT, check=False).returncode + except OSError as error: + log.write(str(error) + "\n") + rc = 127 + failed |= rc != 0 + manifest["runs"].append({"suite": name, "command": cmd, "rc": rc, + "catalogue_sha256": hashlib.sha256(path.read_bytes()).hexdigest()}) + # Persist after each suite, even when a decoder fails or times out. + (args.results / "run.json").write_text(json.dumps(manifest, indent=2) + "\n") + return int(failed) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/drivers/media/platform/apple/avd/tests/conformance/test_fluster.py b/drivers/media/platform/apple/avd/tests/conformance/test_fluster.py new file mode 100644 index 0000000000000..70513bf8e7cc0 --- /dev/null +++ b/drivers/media/platform/apple/avd/tests/conformance/test_fluster.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Exercise orchestration with fake decoders. No video device is opened.""" +import importlib.util +import json +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + +RUNNER = Path(__file__).with_name("run-fluster.py") +spec = importlib.util.spec_from_file_location("runner", RUNNER) +runner = importlib.util.module_from_spec(spec) +spec.loader.exec_module(runner) + + +class FlusterTest(unittest.TestCase): + def test_format_marks(self): + self.assertEqual(runner.format_marks("VP9", "yuv420p10le"), []) + self.assertEqual(len(runner.format_marks("VP9", "yuv444p12le")), 2) + self.assertIn("P210", runner.format_marks("H.264", "yuv422p10le")[0]) + self.assertTrue(runner.format_marks("AV1", "gray")) + + def test_continue_and_preserve(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + fluster = root / "fluster" + suites = fluster / "test_suites" + suites.mkdir(parents=True) + baseline = {"suites": {}, "vector_marks": { + "suite0/vector": {"marks": ["hw-unsupported: H.264 interlace"]}}} + for i in range(6): + name = f"suite{i}" + baseline["suites"][name] = {"passed": 0, "total": 1} + (suites / f"{name}.json").write_text(json.dumps({ + "name": name, "codec": "H.264", "test_vectors": [ + {"name": "vector", "input_file": "input.264", "output_format": "yuv420p"}]})) + (fluster / "fluster.py").write_text(''' +import pathlib, sys +args = sys.argv +name = args[args.index('--testsuites') + 1] +path = pathlib.Path(args[args.index('--summary-output') + 1]) +path.write_text('vector,result\\nvector,FAIL\\n') +print('fake decoder ' + name) +sys.exit(1 if name == 'suite1' else 0) +''') + baseline_path = root / "baseline.json" + baseline_path.write_text(json.dumps(baseline)) + results = root / "results" + cmd = [sys.executable, str(RUNNER), "--fluster", str(fluster), + "--resources", str(root / "resources"), "--results", str(results), + "--baseline", str(baseline_path)] + plan = subprocess.run(cmd + ["--plan"], capture_output=True, text=True, check=True) + self.assertEqual(len(plan.stdout.splitlines()), 6) + self.assertFalse(results.exists()) + run = subprocess.run(cmd, capture_output=True, text=True, check=False) + self.assertEqual(run.returncode, 1, run.stderr) + manifest = json.loads((results / "run.json").read_text()) + self.assertEqual([r["rc"] for r in manifest["runs"]], [0, 1, 0, 0, 0, 0]) + self.assertEqual(manifest["baseline"], baseline) + self.assertIn("hw-unsupported", (results / "vectors.csv").read_text()) + self.assertIn("not inspected", (results / "vectors.csv").read_text()) + final = (results / "suite5/results.csv").read_bytes() + rerun = subprocess.run(cmd, capture_output=True, text=True, check=False) + self.assertNotEqual(rerun.returncode, 0) + self.assertEqual(final, (results / "suite5/results.csv").read_bytes()) + # Changed vector counts cannot silently reuse the old denominator. + fixture = suites / "suite0.json" + changed = json.loads(fixture.read_text()) + changed["test_vectors"] = [] + fixture.write_text(json.dumps(changed)) + with self.assertRaisesRegex(ValueError, "catalogue changed"): + runner.load_suites(suites, baseline) + + +if __name__ == "__main__": + unittest.main()