From 217ad61304310d20f9641354214336e852fd7b74 Mon Sep 17 00:00:00 2001 From: nekolyanich Date: Sun, 11 Oct 2026 18:01:28 +0300 Subject: [PATCH 1/4] drm: apple: audio: ask for up to 256 KiB of audio elements, as macOS When the audio elements do not fit the buffer of the get_elements call, DCP returns them cut off at the buffer's end and reports a used size of 0. A Samsung TV behind a USB-C to HDMI adapter has 17312 bytes of elements, so with the 16 KiB the legacy path asked for, everything past 16 KiB was lost. macOS 13 asks for up to 256 KiB (max_size 0x40000 in the request, 0x43a0 used in the reply for this TV), as the native 14.7 path already does: use that capacity on every firmware. The native path no longer needs a second buffer of its own. Allocate the elements buffer and the call buffers with kvzalloc(), including the ones in afk_service_call_timeout_len() and afk_service_call_with_reply_len(), which every AFK service call goes through; they are only copied into the DMA buffers. Fixes: cb24343242a7 ("gpu: drm: apple: Add DCP audio driver") Link: https://github.com/aurora-silicon/linux/pull/276 Signed-off-by: nekolyanich Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/afk.c | 8 +++--- drivers/gpu/drm/apple/audio.c | 53 ++++++++++++++++++----------------- drivers/gpu/drm/apple/av.c | 4 +-- 3 files changed, 34 insertions(+), 31 deletions(-) diff --git a/drivers/gpu/drm/apple/afk.c b/drivers/gpu/drm/apple/afk.c index 0296cb9b41bba..1a631596a7e68 100644 --- a/drivers/gpu/drm/apple/afk.c +++ b/drivers/gpu/drm/apple/afk.c @@ -3186,7 +3186,7 @@ int afk_service_call_timeout_len(struct apple_epic_service *service, u16 group, if (!service) return -ENODEV; - bfr = kzalloc(bfr_len, GFP_KERNEL); + bfr = kvzalloc(bfr_len, GFP_KERNEL); if (!bfr) return -ENOMEM; @@ -3256,7 +3256,7 @@ int afk_service_call_timeout_len(struct apple_epic_service *service, u16 group, } out: - kfree(bfr); + kvfree(bfr); return ret; } @@ -3294,7 +3294,7 @@ int afk_service_call_with_reply_len(struct apple_epic_service *service, &bfr_len) || bfr_len > U32_MAX) return -EOVERFLOW; - bfr = kzalloc(bfr_len, GFP_KERNEL); + bfr = kvzalloc(bfr_len, GFP_KERNEL); if (!bfr) return -ENOMEM; @@ -3339,7 +3339,7 @@ int afk_service_call_with_reply_len(struct apple_epic_service *service, } out: - kfree(bfr); + kvfree(bfr); return ret; } diff --git a/drivers/gpu/drm/apple/audio.c b/drivers/gpu/drm/apple/audio.c index 9088a022ba9f3..6aa099ca6eb81 100644 --- a/drivers/gpu/drm/apple/audio.c +++ b/drivers/gpu/drm/apple/audio.c @@ -21,6 +21,8 @@ #include #include #include +#include +#include #include #include #include @@ -37,11 +39,15 @@ #include "audio.h" #include "parser.h" -#define DCPAUD_ELEMENTS_MAXSIZE 16384 +/* + * The element catalog capacity, on every firmware. Elements that do not + * fit come back cut off at the buffer's end with a used size of 0: a TV + * behind a USB-C to HDMI adapter has 17312 bytes of them. macOS 13 asks + * for up to 256 KiB. + */ +#define DCPAUD_ELEMENTS_MAXSIZE SZ_256K #define DCPAUD_PRODUCTATTRS_MAXSIZE 1024 #define DCPAUD_ELD_SIZE MAX_ELD_BYTES -/* The native element catalog capacity (14.7 external role). */ -#define DCPAUD_ELEMENTS_V14_7 SZ_256K /* * SIO takes one of its 64 descriptor slots per period, shared by all its @@ -140,11 +146,6 @@ static inline bool dcpaud_native_link(struct dcp_audio *dcpaud) #endif } -static inline size_t dcpaud_elements_size(struct dcp_audio *dcpaud) -{ - return dcpaud_native_link(dcpaud) ? DCPAUD_ELEMENTS_V14_7 : DCPAUD_ELEMENTS_MAXSIZE; -} - static const struct snd_pcm_hardware dcp_pcm_hw = { .info = SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_MMAP_VALID | SNDRV_PCM_INFO_INTERLEAVED, @@ -193,7 +194,7 @@ static int dcpaud_read_remote_info(struct dcp_audio *dcpaud) int ret; ret = dcp_audiosrv_get_elements(dcpaud->dcp_dev, dcpaud->elements, - dcpaud_elements_size(dcpaud)); + DCPAUD_ELEMENTS_MAXSIZE); if (ret < 0) return ret; @@ -250,7 +251,7 @@ static void dcpaud_consult_elements(struct dcp_audio *dcpaud, struct dcp_parse_ctx elements = { .dcp = dev_get_drvdata(dcpaud->dcp_dev), .blob = dcpaud->elements + 4, - .len = dcpaud_elements_size(dcpaud) - 4, + .len = DCPAUD_ELEMENTS_MAXSIZE - 4, .pos = 0, #if IS_ENABLED(CONFIG_DRM_APPLE_DCP_T6030_AUDIO) .v14_7_audio = dcpaud->native_link, @@ -270,7 +271,7 @@ static int dcpaud_select_cookie(struct dcp_audio *dcpaud, struct dcp_parse_ctx elements = { .dcp = dev_get_drvdata(dcpaud->dcp_dev), .blob = dcpaud->elements + 4, - .len = dcpaud_elements_size(dcpaud) - 4, + .len = DCPAUD_ELEMENTS_MAXSIZE - 4, .pos = 0, #if IS_ENABLED(CONFIG_DRM_APPLE_DCP_T6030_AUDIO) .v14_7_audio = dcpaud->native_link, @@ -779,7 +780,7 @@ static int dcpaud_pin_rate_format(struct dcp_audio *dcpaud, struct dcp_parse_ctx elements = { .dcp = dev_get_drvdata(dcpaud->dcp_dev), .blob = dcpaud->elements + 4, - .len = dcpaud_elements_size(dcpaud) - 4, + .len = DCPAUD_ELEMENTS_MAXSIZE - 4, .pos = 0, .v14_7_audio = dcpaud->native_link, }; @@ -1292,7 +1293,7 @@ static int dcpaud_select_default_cookie(struct dcp_audio *dcpaud) struct dcp_parse_ctx elements = { .dcp = dev_get_drvdata(dcpaud->dcp_dev), .blob = dcpaud->elements + 4, - .len = dcpaud_elements_size(dcpaud) - 4, + .len = DCPAUD_ELEMENTS_MAXSIZE - 4, .pos = 0, .v14_7_audio = dcpaud->native_link, }; @@ -1838,16 +1839,9 @@ static int dcpaud_comp_bind(struct device *dev, struct device *main, void *data) dcpaud->dcp_dev = &dcp_pdev->dev; #if IS_ENABLED(CONFIG_DRM_APPLE_DCP_T6030_AUDIO) - /* Native external roles share the link lifecycle and element catalog. */ - if (dcp_audiosrv_native_link(dcpaud->dcp_dev)) { - void *elements = devm_kzalloc(dev, DCPAUD_ELEMENTS_V14_7, GFP_KERNEL); - - if (!elements) - goto rpm_put; - devm_kfree(dev, dcpaud->elements); - dcpaud->elements = elements; + /* Native external roles share the link lifecycle. */ + if (dcp_audiosrv_native_link(dcpaud->dcp_dev)) dcpaud->native_link = true; - } #endif dma_pdev = of_find_device_by_node(dma_spec.np); @@ -1870,7 +1864,7 @@ static int dcpaud_comp_bind(struct device *dev, struct device *main, void *data) dcpaud_expose_debugfs_blob(dcpaud, "selected_cookie", &dcpaud->selected_cookie, sizeof(dcpaud->selected_cookie)); dcpaud_expose_debugfs_blob(dcpaud, "elements", dcpaud->elements, - dcpaud_elements_size(dcpaud)); + DCPAUD_ELEMENTS_MAXSIZE); dcpaud_expose_debugfs_blob(dcpaud, "product_attrs", dcpaud->productattrs, DCPAUD_PRODUCTATTRS_MAXSIZE); } @@ -1922,18 +1916,27 @@ static const struct component_ops dcpaud_comp_ops = { .unbind = dcpaud_comp_unbind, }; +static void dcpaud_free_elements(void *elements) +{ + kvfree(elements); +} + static int dcpaud_probe(struct platform_device *pdev) { struct dcp_audio *dcpaud; + int ret; dcpaud = devm_kzalloc(&pdev->dev, sizeof(*dcpaud), GFP_KERNEL); if (!dcpaud) return -ENOMEM; - dcpaud->elements = devm_kzalloc(&pdev->dev, DCPAUD_ELEMENTS_MAXSIZE, - GFP_KERNEL); + dcpaud->elements = kvzalloc(DCPAUD_ELEMENTS_MAXSIZE, GFP_KERNEL); if (!dcpaud->elements) return -ENOMEM; + ret = devm_add_action_or_reset(&pdev->dev, dcpaud_free_elements, + dcpaud->elements); + if (ret) + return ret; dcpaud->productattrs = devm_kzalloc(&pdev->dev, DCPAUD_PRODUCTATTRS_MAXSIZE, GFP_KERNEL); diff --git a/drivers/gpu/drm/apple/av.c b/drivers/gpu/drm/apple/av.c index 6b350cb0b9414..ede5d86a1073f 100644 --- a/drivers/gpu/drm/apple/av.c +++ b/drivers/gpu/drm/apple/av.c @@ -564,12 +564,12 @@ dcp_audiosrv_osobject_call(struct apple_epic_service *service, u16 group, static_assert(sizeof(*hdr) == 48); size_t received = 0, used; size_t bfr_len = output_maxsize + sizeof(*hdr); - void *bfr __free(kfree) = NULL; + void *bfr __free(kvfree) = NULL; int ret; KUNIT_STATIC_STUB_REDIRECT(dcp_audiosrv_osobject_call, service, group, command, output, output_maxsize, output_size); - bfr = kzalloc(bfr_len, GFP_KERNEL); + bfr = kvzalloc(bfr_len, GFP_KERNEL); if (!bfr) return -ENOMEM; From 36d7929ca6576c81829d4440effeaae96de6d31e Mon Sep 17 00:00:00 2001 From: nekolyanich Date: Sun, 11 Oct 2026 18:01:42 +0300 Subject: [PATCH 2/4] drm: apple: audio: drop the runtime PM reference only after the DMA stopped Trigger STOP put the runtime PM reference right after starting the asynchronous DMA terminate. Without autosuspend that suspends the device at once and powers its domain (ps_dpa1/ps_dpa2) down while SIO may still run DMA towards the DPA. SIO then crashed with "Shim DMA unavailable" and the TERMINATE timed out after it, taking DP audio on all outputs down until a reboot. On an M2 Pro with two DP sinks an earlier version of this change on a downstream tree took it from 4 of 4 boots to none in 11. Put the reference in sync_stop, after snd_dmaengine_pcm_sync_stop() waited for the terminate. ALSA calls sync_stop at the next hw_params, prepare or hw_free, at close, at system suspend and at disconnect, not right after the STOP, so after a drain, an xrun or a stop with the stream left open the DPA stays powered until then. Waiting inside trigger would also work (the PCM is nonatomic), but sync_stop is the hook ALSA has for it, as in the AOP audio driver. The stream remembers whether it holds the reference. sync_stop runs outside the stream lock: test and clear it under the lock. START takes the reference only once, since a START can follow a failed one before sync_stop ran. A stream that plays into the void without a sink holds none, as before. Fixes: f25cb501c5fc ("drm: apple: audio: Implement runtime PM support") Link: https://github.com/aurora-silicon/linux/pull/276 Signed-off-by: nekolyanich Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/audio.c | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/drivers/gpu/drm/apple/audio.c b/drivers/gpu/drm/apple/audio.c index 6aa099ca6eb81..6e1681d279766 100644 --- a/drivers/gpu/drm/apple/audio.c +++ b/drivers/gpu/drm/apple/audio.c @@ -103,6 +103,8 @@ struct dcp_audio { ktime_t void_start; u64 void_periods; struct delayed_work void_work; + /* The stream holds a runtime PM reference, put in sync_stop; stream lock */ + bool pm_held; void *elements; void *productattrs; @@ -648,7 +650,11 @@ static int dcp_pcm_trigger(struct snd_pcm_substream *substream, int cmd) if (dcpaud->open_unplugged || !dcpaud_connection_up(dcpaud)) return dcpaud_void_start(substream); - WARN_ON(pm_runtime_get_sync(dcpaud->dev) < 0); + /* A START can follow a failed one before sync_stop ran */ + if (!dcpaud->pm_held) { + WARN_ON(pm_runtime_get_sync(dcpaud->dev) < 0); + dcpaud->pm_held = true; + } ret = dcp_audiosrv_startlink(dcpaud->dcp_dev, &dcpaud->selected_cookie); if (ret < 0) @@ -681,9 +687,8 @@ static int dcp_pcm_trigger(struct snd_pcm_substream *substream, int cmd) case SNDRV_PCM_TRIGGER_STOP: case SNDRV_PCM_TRIGGER_SUSPEND: + /* The runtime PM reference goes in sync_stop */ ret = dcp_audiosrv_stoplink(dcpaud->dcp_dev); - pm_runtime_mark_last_busy(dcpaud->dev); - __pm_runtime_put_autosuspend(dcpaud->dev); if (ret < 0) return ret; break; @@ -693,14 +698,32 @@ static int dcp_pcm_trigger(struct snd_pcm_substream *substream, int cmd) } /* - * ALSA calls this once a stream has stopped, outside the stream lock, before - * the next hw_params, prepare or hw_free and at close. + * ALSA calls this once a stream has stopped (STOP, SUSPEND, or the STOP that + * undoes a failed START), outside the stream lock, before the next + * hw_params, prepare or hw_free, at close, at system suspend and at + * disconnect. The DMA terminate STOP started is asynchronous: putting the + * runtime PM reference in STOP powered the DPA's domain (ps_dpa*) down while + * SIO still ran DMA towards it, and SIO crashed ("Shim DMA unavailable", the + * TERMINATE timing out after it), taking DP audio down on every output until + * a reboot. Wait for the terminate first. */ static int dcp_pcm_sync_stop(struct snd_pcm_substream *substream) { struct dcp_audio *dcpaud = substream->pcm->private_data; + bool held; cancel_delayed_work_sync(&dcpaud->void_work); + snd_dmaengine_pcm_sync_stop(substream); + + snd_pcm_stream_lock_irq(substream); + held = dcpaud->pm_held; + dcpaud->pm_held = false; + snd_pcm_stream_unlock_irq(substream); + + if (held) { + pm_runtime_mark_last_busy(dcpaud->dev); + __pm_runtime_put_autosuspend(dcpaud->dev); + } return 0; } From d060023713e1441c643f4fc0814b3a33888ad72d Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 13 Nov 2026 00:41:28 +1100 Subject: [PATCH 3/4] drm: apple: audio: test the legacy stream's PM reference and element capacity KUnit cases for the legacy (pre-14.7) audio service: a stopped or suspended stream keeps its runtime PM reference until sync_stop has waited for the DMA terminate, a failed START and a retried one before sync_stop leave one reference, a stream without a sink takes none, and the element catalog is asked for with 256 KiB so that a 17312 byte catalog arrives whole. The audio service's link and catalog calls get static stubs for this. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/audio-legacy-test.c | 302 ++++++++++++++++++++++ drivers/gpu/drm/apple/audio.c | 1 + drivers/gpu/drm/apple/av.c | 28 +- 3 files changed, 323 insertions(+), 8 deletions(-) create mode 100644 drivers/gpu/drm/apple/audio-legacy-test.c diff --git a/drivers/gpu/drm/apple/audio-legacy-test.c b/drivers/gpu/drm/apple/audio-legacy-test.c new file mode 100644 index 0000000000000..fb082c244ee2b --- /dev/null +++ b/drivers/gpu/drm/apple/audio-legacy-test.c @@ -0,0 +1,302 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* + * Included by audio.c: on the legacy (pre-14.7) audio service a stopped + * stream keeps its runtime PM reference until sync_stop has waited for the + * DMA terminate, a failed START leaves exactly one reference to put, and the + * element catalog is asked for with the full 256 KiB capacity. + * The production trigger and sync_stop run; the audio service's link calls + * and the DMA channel are stubbed. + */ +#include +#include +#include + +struct dcpaud_legacy_test { + struct dma_device dma; + struct dma_chan channel; + struct dma_async_tx_descriptor desc; + char sequence[32]; + unsigned int nsequence; + /* The runtime PM usage count when the terminate was waited for */ + int usage_at_drain; + int startlink_err; + size_t elements_asked; + size_t catalog; + struct device *dev; +}; + +static struct dcpaud_legacy_test *dcpaud_legacy_current; + +static void dcpaud_legacy_record(char step) +{ + struct dcpaud_legacy_test *t = dcpaud_legacy_current; + + if (t->nsequence < sizeof(t->sequence) - 1) + t->sequence[t->nsequence++] = step; +} + +static int dcpaud_legacy_terminate(struct dma_chan *chan) +{ + dcpaud_legacy_record('X'); + return 0; +} + +static void dcpaud_legacy_drain(struct dma_chan *chan) +{ + struct dcpaud_legacy_test *t = dcpaud_legacy_current; + + t->usage_at_drain = atomic_read(&t->dev->power.usage_count); + dcpaud_legacy_record('D'); +} + +static dma_cookie_t dcpaud_legacy_submit(struct dma_async_tx_descriptor *tx) +{ + return 1; +} + +static struct dma_async_tx_descriptor * +dcpaud_legacy_prep(struct dma_chan *chan, dma_addr_t buf, size_t len, size_t period, + enum dma_transfer_direction dir, unsigned long flags) +{ + struct dcpaud_legacy_test *t = dcpaud_legacy_current; + + dcpaud_legacy_record('Q'); + t->desc.chan = chan; + t->desc.tx_submit = dcpaud_legacy_submit; + return &t->desc; +} + +static void dcpaud_legacy_issue(struct dma_chan *chan) +{ +} + +static enum dma_status dcpaud_legacy_status(struct dma_chan *chan, dma_cookie_t cookie, + struct dma_tx_state *state) +{ + return DMA_COMPLETE; +} + +static int dcpaud_legacy_startlink(struct device *dev, struct dcp_sound_cookie *cookie) +{ + dcpaud_legacy_record('T'); + return dcpaud_legacy_current->startlink_err; +} + +static int dcpaud_legacy_stoplink(struct device *dev) +{ + dcpaud_legacy_record('S'); + return 0; +} + +static void dcpaud_legacy_rpm_disable(void *data) +{ + pm_runtime_disable(data); +} + +static void dcpaud_legacy_close(void *data) +{ + snd_dmaengine_pcm_close(data); +} + +/* A legacy service instance with a sink, and a configured stream on it */ +static struct snd_pcm_substream *dcpaud_legacy_setup(struct kunit *test, + struct dcp_audio **audio) +{ + struct dcpaud_legacy_test *t = kunit_kzalloc(test, sizeof(*t), GFP_KERNEL); + struct dcp_audio *dcpaud = kunit_kzalloc(test, sizeof(*dcpaud), GFP_KERNEL); + struct snd_pcm_substream *sub = kunit_kzalloc(test, sizeof(*sub), GFP_KERNEL); + struct snd_pcm_runtime *runtime = kunit_kzalloc(test, sizeof(*runtime), GFP_KERNEL); + struct snd_pcm *pcm = kunit_kzalloc(test, sizeof(*pcm), GFP_KERNEL); + struct platform_device *pdev; + + KUNIT_ASSERT_NOT_NULL(test, t); + KUNIT_ASSERT_NOT_NULL(test, dcpaud); + KUNIT_ASSERT_NOT_NULL(test, sub); + KUNIT_ASSERT_NOT_NULL(test, runtime); + KUNIT_ASSERT_NOT_NULL(test, pcm); + pdev = kunit_platform_device_alloc(test, "dcpaud-legacy", PLATFORM_DEVID_AUTO); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, pdev); + platform_set_drvdata(pdev, dcpaud); + dcpaud->dev = &pdev->dev; + dcpaud->dcp_dev = &pdev->dev; + mutex_init(&dcpaud->data_lock); + INIT_DELAYED_WORK(&dcpaud->void_work, dcpaud_void_work); + dcpaud->connected = true; + dcpaud->substream = sub; + + /* Runtime PM on, without callbacks: only the usage count matters */ + pm_runtime_no_callbacks(dcpaud->dev); + pm_runtime_enable(dcpaud->dev); + KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, dcpaud_legacy_rpm_disable, + dcpaud->dev), 0); + + dcpaud_legacy_current = t; + t->dev = dcpaud->dev; + t->usage_at_drain = -1; + t->channel.device = &t->dma; + t->dma.device_terminate_all = dcpaud_legacy_terminate; + t->dma.device_synchronize = dcpaud_legacy_drain; + t->dma.device_tx_status = dcpaud_legacy_status; + t->dma.device_prep_dma_cyclic = dcpaud_legacy_prep; + t->dma.device_issue_pending = dcpaud_legacy_issue; + kunit_activate_static_stub(test, dcp_audiosrv_startlink, dcpaud_legacy_startlink); + kunit_activate_static_stub(test, dcp_audiosrv_stoplink, dcpaud_legacy_stoplink); + + pcm->private_data = dcpaud; + pcm->nonatomic = true; + sub->pcm = pcm; + sub->runtime = runtime; + mutex_init(&sub->self_group.mutex); + spin_lock_init(&sub->self_group.lock); + sub->group = &sub->self_group; + runtime->frame_bits = 64; + runtime->rate = 48000; + runtime->buffer_size = 8192; + runtime->period_size = 512; + KUNIT_ASSERT_EQ(test, snd_dmaengine_pcm_open(sub, &t->channel), 0); + KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, dcpaud_legacy_close, sub), 0); + + *audio = dcpaud; + return sub; +} + +static int dcpaud_legacy_usage(struct dcp_audio *dcpaud) +{ + return atomic_read(&dcpaud->dev->power.usage_count); +} + +static void dcpaud_legacy_stop_waits_for_dma(struct kunit *test) +{ + struct dcp_audio *audio; + struct snd_pcm_substream *sub = dcpaud_legacy_setup(test, &audio); + + KUNIT_ASSERT_EQ(test, dcp_pcm_trigger(sub, SNDRV_PCM_TRIGGER_START), 0); + KUNIT_EXPECT_EQ(test, dcpaud_legacy_usage(audio), 1); + + /* The terminate is only started: the DPA stays powered */ + KUNIT_ASSERT_EQ(test, dcp_pcm_trigger(sub, SNDRV_PCM_TRIGGER_STOP), 0); + KUNIT_EXPECT_STREQ(test, dcpaud_legacy_current->sequence, "TQXS"); + KUNIT_EXPECT_EQ(test, dcpaud_legacy_usage(audio), 1); + + /* The reference goes once the terminate was waited for */ + KUNIT_ASSERT_EQ(test, dcp_pcm_sync_stop(sub), 0); + KUNIT_EXPECT_STREQ(test, dcpaud_legacy_current->sequence, "TQXSD"); + KUNIT_EXPECT_EQ(test, dcpaud_legacy_current->usage_at_drain, 1); + KUNIT_EXPECT_EQ(test, dcpaud_legacy_usage(audio), 0); + KUNIT_EXPECT_FALSE(test, audio->pm_held); + + /* A second sync_stop (close after prepare) has nothing to put */ + KUNIT_ASSERT_EQ(test, dcp_pcm_sync_stop(sub), 0); + KUNIT_EXPECT_EQ(test, dcpaud_legacy_usage(audio), 0); +} + +static void dcpaud_legacy_suspend_waits_for_dma(struct kunit *test) +{ + struct dcp_audio *audio; + struct snd_pcm_substream *sub = dcpaud_legacy_setup(test, &audio); + + KUNIT_ASSERT_EQ(test, dcp_pcm_trigger(sub, SNDRV_PCM_TRIGGER_START), 0); + KUNIT_ASSERT_EQ(test, dcp_pcm_trigger(sub, SNDRV_PCM_TRIGGER_SUSPEND), 0); + KUNIT_EXPECT_EQ(test, dcpaud_legacy_usage(audio), 1); + KUNIT_ASSERT_EQ(test, dcp_pcm_sync_stop(sub), 0); + KUNIT_EXPECT_STREQ(test, dcpaud_legacy_current->sequence, "TQXSD"); + KUNIT_EXPECT_EQ(test, dcpaud_legacy_current->usage_at_drain, 1); + KUNIT_EXPECT_EQ(test, dcpaud_legacy_usage(audio), 0); +} + +static void dcpaud_legacy_failed_start(struct kunit *test) +{ + struct dcp_audio *audio; + struct snd_pcm_substream *sub = dcpaud_legacy_setup(test, &audio); + + /* START fails at the link; ALSA undoes it with a STOP */ + dcpaud_legacy_current->startlink_err = -EIO; + KUNIT_ASSERT_EQ(test, dcp_pcm_trigger(sub, SNDRV_PCM_TRIGGER_START), -EIO); + KUNIT_ASSERT_EQ(test, dcp_pcm_trigger(sub, SNDRV_PCM_TRIGGER_STOP), 0); + KUNIT_EXPECT_EQ(test, dcpaud_legacy_usage(audio), 1); + + /* Retried before sync_stop ran: still one reference */ + dcpaud_legacy_current->startlink_err = 0; + KUNIT_ASSERT_EQ(test, dcp_pcm_trigger(sub, SNDRV_PCM_TRIGGER_START), 0); + KUNIT_EXPECT_EQ(test, dcpaud_legacy_usage(audio), 1); + KUNIT_ASSERT_EQ(test, dcp_pcm_trigger(sub, SNDRV_PCM_TRIGGER_STOP), 0); + KUNIT_ASSERT_EQ(test, dcp_pcm_sync_stop(sub), 0); + KUNIT_EXPECT_STREQ(test, dcpaud_legacy_current->sequence, "TXSTQXSD"); + KUNIT_EXPECT_EQ(test, dcpaud_legacy_usage(audio), 0); +} + +static void dcpaud_legacy_void_takes_no_reference(struct kunit *test) +{ + struct dcp_audio *audio; + struct snd_pcm_substream *sub = dcpaud_legacy_setup(test, &audio); + + /* No sink: the stream plays into the void; a 10 s period never elapses here */ + audio->open_unplugged = true; + sub->runtime->period_size = 480000; + KUNIT_ASSERT_EQ(test, dcp_pcm_trigger(sub, SNDRV_PCM_TRIGGER_START), 0); + KUNIT_EXPECT_TRUE(test, audio->void_running); + KUNIT_ASSERT_EQ(test, dcp_pcm_trigger(sub, SNDRV_PCM_TRIGGER_STOP), 0); + KUNIT_ASSERT_EQ(test, dcp_pcm_sync_stop(sub), 0); + KUNIT_EXPECT_STREQ(test, dcpaud_legacy_current->sequence, "D"); + KUNIT_EXPECT_EQ(test, dcpaud_legacy_usage(audio), 0); +} + +static int dcpaud_legacy_get_elements(struct device *dev, void *elements, size_t maxsize) +{ + struct dcpaud_legacy_test *t = dcpaud_legacy_current; + + t->elements_asked = maxsize; + /* What the service returns: cut off at the buffer's end */ + memset(elements, 0xa5, min(t->catalog, maxsize)); + return 0; +} + +static int dcpaud_legacy_get_attrs(struct device *dev, void *attrs, size_t maxsize) +{ + return 0; +} + +static void dcpaud_legacy_free(void *data) +{ + kvfree(data); +} + +static void dcpaud_legacy_elements_capacity(struct kunit *test) +{ + struct dcp_audio *audio; + u8 *elements; + + dcpaud_legacy_setup(test, &audio); + elements = kvzalloc(DCPAUD_ELEMENTS_MAXSIZE, GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, elements); + KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, dcpaud_legacy_free, elements), 0); + audio->elements = elements; + audio->productattrs = kunit_kzalloc(test, DCPAUD_PRODUCTATTRS_MAXSIZE, GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, audio->productattrs); + kunit_activate_static_stub(test, dcp_audiosrv_get_elements, dcpaud_legacy_get_elements); + kunit_activate_static_stub(test, dcp_audiosrv_get_product_attrs, dcpaud_legacy_get_attrs); + + /* A TV behind a USB-C to HDMI adapter: 17312 bytes of elements */ + dcpaud_legacy_current->catalog = 17312; + KUNIT_ASSERT_FALSE(test, dcpaud_native_link(audio)); + KUNIT_ASSERT_EQ(test, dcpaud_read_remote_info(audio), 0); + KUNIT_EXPECT_EQ(test, dcpaud_legacy_current->elements_asked, (size_t)SZ_256K); + KUNIT_EXPECT_EQ(test, elements[17311], 0xa5); + KUNIT_EXPECT_EQ(test, elements[17312], 0); +} + +static struct kunit_case dcpaud_legacy_cases[] = { + KUNIT_CASE(dcpaud_legacy_stop_waits_for_dma), + KUNIT_CASE(dcpaud_legacy_suspend_waits_for_dma), + KUNIT_CASE(dcpaud_legacy_failed_start), + KUNIT_CASE(dcpaud_legacy_void_takes_no_reference), + KUNIT_CASE(dcpaud_legacy_elements_capacity), + {} +}; + +static struct kunit_suite dcpaud_legacy_suite = { + .name = "apple-dcpaud-legacy", + .test_cases = dcpaud_legacy_cases, +}; + +kunit_test_suite(dcpaud_legacy_suite); diff --git a/drivers/gpu/drm/apple/audio.c b/drivers/gpu/drm/apple/audio.c index 6e1681d279766..bd25c0358ef2e 100644 --- a/drivers/gpu/drm/apple/audio.c +++ b/drivers/gpu/drm/apple/audio.c @@ -2074,6 +2074,7 @@ void dcp_audio_unregister(void) #if IS_ENABLED(CONFIG_DRM_APPLE_DISPLAY_KUNIT_TEST) #include "audio-no-sio-test.c" +#include "audio-legacy-test.c" #if IS_ENABLED(CONFIG_DRM_APPLE_DCP_T6030_AUDIO) #include "audio-link-test.c" #endif diff --git a/drivers/gpu/drm/apple/av.c b/drivers/gpu/drm/apple/av.c index ede5d86a1073f..7f1a45587457b 100644 --- a/drivers/gpu/drm/apple/av.c +++ b/drivers/gpu/drm/apple/av.c @@ -229,10 +229,13 @@ int dcp_audiosrv_prepare(struct device *dev, struct dcp_sound_cookie *cookie) int dcp_audiosrv_startlink(struct device *dev, struct dcp_sound_cookie *cookie) { - struct apple_dcp *dcp = dev_get_drvdata(dev); - struct audiosrv_data *asrv = dcp->audiosrv; + struct apple_dcp *dcp; + struct audiosrv_data *asrv; int ret; + KUNIT_STATIC_STUB_REDIRECT(dcp_audiosrv_startlink, dev, cookie); + dcp = dev_get_drvdata(dev); + asrv = dcp->audiosrv; down_write(&asrv->srv_rwsem); ret = afk_service_call(asrv->srv, 0, asrv->cmds.start_link, cookie, sizeof(*cookie), 64 - sizeof(*cookie), NULL, 0, @@ -244,10 +247,13 @@ int dcp_audiosrv_startlink(struct device *dev, struct dcp_sound_cookie *cookie) int dcp_audiosrv_stoplink(struct device *dev) { - struct apple_dcp *dcp = dev_get_drvdata(dev); - struct audiosrv_data *asrv = dcp->audiosrv; + struct apple_dcp *dcp; + struct audiosrv_data *asrv; int ret; + KUNIT_STATIC_STUB_REDIRECT(dcp_audiosrv_stoplink, dev); + dcp = dev_get_drvdata(dev); + asrv = dcp->audiosrv; down_write(&asrv->srv_rwsem); ret = afk_service_call(asrv->srv, 0, asrv->cmds.stop_link, NULL, 0, 64, NULL, 0, 64); @@ -633,13 +639,16 @@ static bool av_elements_need_more(int ret, size_t size, size_t cap, bool empty) int dcp_audiosrv_get_elements(struct device *dev, void *elements, size_t maxsize) { - struct apple_dcp *dcp = dev_get_drvdata(dev); - struct audiosrv_data *asrv = dcp->audiosrv; + struct apple_dcp *dcp; + struct audiosrv_data *asrv; struct apple_epic_service *srv; size_t size = 0, cap; bool compact, empty; int ret; + KUNIT_STATIC_STUB_REDIRECT(dcp_audiosrv_get_elements, dev, elements, maxsize); + dcp = dev_get_drvdata(dev); + asrv = dcp->audiosrv; if (!asrv) return -ENODEV; scoped_guard(rwsem_read, &asrv->srv_rwsem) @@ -687,12 +696,15 @@ int dcp_audiosrv_get_elements(struct device *dev, void *elements, size_t maxsize int dcp_audiosrv_get_product_attrs(struct device *dev, void *attrs, size_t maxsize) { - struct apple_dcp *dcp = dev_get_drvdata(dev); - struct audiosrv_data *asrv = dcp->audiosrv; + struct apple_dcp *dcp; + struct audiosrv_data *asrv; struct apple_epic_service *srv; size_t size; int ret; + KUNIT_STATIC_STUB_REDIRECT(dcp_audiosrv_get_product_attrs, dev, attrs, maxsize); + dcp = dev_get_drvdata(dev); + asrv = dcp->audiosrv; if (!asrv) return -ENODEV; scoped_guard(rwsem_read, &asrv->srv_rwsem) From 27f1c0dd2f7dc2e44ee79ef23615331a83849f4e Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 13 Nov 2026 01:35:40 +1100 Subject: [PATCH 4/4] selftests: apple-dcp, apple-afkv2: kvzalloc() and kvfree() for the call buffers The AFK service call and audio osobject call buffers now come from kvzalloc(); give the host harnesses that compile those functions the same calloc()/free() shims they have for kzalloc(). Assisted-by: LLM Signed-off-by: Ryan Murray --- tools/testing/selftests/drivers/gpu/apple-afkv2/extract-edid.py | 2 ++ tools/testing/selftests/drivers/gpu/apple-afkv2/extract-hpd.py | 2 ++ .../selftests/drivers/gpu/apple-afkv2/extract-service.py | 2 ++ tools/testing/selftests/drivers/gpu/apple-dcp/audio.py | 2 +- 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-edid.py b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-edid.py index bcced2f684cfa..5448555012263 100644 --- a/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-edid.py +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-edid.py @@ -27,6 +27,8 @@ def function(name): #include "epic/dpavserv-edid.h" #define GFP_KERNEL 0 #define kzalloc(n,f) calloc(1,n) +#define kvzalloc(n,f) calloc(1,n) +#define kvfree(p) free(p) #define cpu_to_le64(x) (x) #define le64_to_cpu(x) (x) #define READ_ONCE(x) (x) diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-hpd.py b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-hpd.py index b21cba6c7665c..7a9881784a816 100644 --- a/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-hpd.py +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-hpd.py @@ -36,6 +36,8 @@ def function(source, name): typedef u32 __le32; #define GFP_KERNEL 0 #define kzalloc(n,f) calloc(1,n) +#define kvzalloc(n,f) calloc(1,n) +#define kvfree(p) free(p) #define kfree(p) free(p) #define max(a,b) ((a)>(b)?(a):(b)) #define min(a,b) ((a)<(b)?(a):(b)) diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-service.py b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-service.py index 19381f919d7e8..4c0164dce1aef 100644 --- a/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-service.py +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-service.py @@ -24,6 +24,8 @@ #define GFP_KERNEL 0 #define kzalloc(n,f) calloc(1,n) #define kfree(p) free(p) +#define kvzalloc(n,f) calloc(1,n) +#define kvfree(p) free(p) #define max(a,b) ((a)>(b)?(a):(b)) #define min(a,b) ((a)<(b)?(a):(b)) #define cpu_to_le16(x) (x) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/audio.py b/tools/testing/selftests/drivers/gpu/apple-dcp/audio.py index 44e2bd9768089..8b0af0f2558e5 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/audio.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/audio.py @@ -24,7 +24,7 @@ def reply_bounds(root): #define GFP_KERNEL 0 static void free_buffer(void *p) {free(*(void **)p);} #define __free(x) __attribute__((cleanup(free_buffer))) -static void *kzalloc(size_t n, int flags) {(void)flags;return calloc(1,n);} +static void *kvzalloc(size_t n, int flags) {(void)flags;return calloc(1,n);} #define cpu_to_le64(x) (x) #define le64_to_cpu(x) (x) struct apple_dcp_afkep {bool compact;};