Skip to content
Merged
18 changes: 18 additions & 0 deletions drivers/gpu/drm/apple/afk-service-test.c
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,23 @@ static void afk_service_completed_reader_test(struct kunit *test)
KUNIT_EXPECT_TRUE(test, afk_service_reinit(service, NULL, &reusable_ops, 2));
}

static void afk_service_close_pin_test(struct kunit *test)
{
struct apple_epic_service *service = test->priv;

KUNIT_EXPECT_PTR_EQ(test, afk_service_pin(NULL), NULL);
KUNIT_ASSERT_TRUE(test, afk_service_reinit(service, NULL, &reusable_ops, 1));
KUNIT_ASSERT_PTR_EQ(test, afk_service_get(service), service); /* owner */
afk_test_retire(service);
/* A close issued after teardown still pins the owner's service. */
KUNIT_ASSERT_PTR_EQ(test, afk_service_pin(service), service);
afk_service_put(service); /* owner detaches */
KUNIT_EXPECT_FALSE(test, afk_service_reinit(service, NULL, &reusable_ops, 2));
KUNIT_EXPECT_EQ(test, service->channel, 1U);
afk_service_put(service);
KUNIT_EXPECT_TRUE(test, afk_service_reinit(service, NULL, &reusable_ops, 2));
}

static void afk_service_tag_sequence_test(struct kunit *test)
{
struct apple_epic_service *service = test->priv;
Expand Down Expand Up @@ -226,6 +243,7 @@ static struct kunit_case afk_service_cases[] = {
KUNIT_CASE(afk_service_reader_pin_test),
KUNIT_CASE(afk_service_pending_reply_test),
KUNIT_CASE(afk_service_completed_reader_test),
KUNIT_CASE(afk_service_close_pin_test),
KUNIT_CASE(afk_service_tag_sequence_test),
KUNIT_CASE(afk_service_unconverted_owner_test),
{ }
Expand Down
19 changes: 19 additions & 0 deletions drivers/gpu/drm/apple/afk.h
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,25 @@ static inline void afk_service_put(struct apple_epic_service *service)
afk_service_retry(service->ep);
}

/*
* Take another pin on a service the caller already keeps pinned, e.g. an
* owner pointer read under the owner's lock. Unlike afk_service_get() this
* also succeeds after teardown, so a close can still be issued.
*/
static inline struct apple_epic_service *afk_service_pin(struct apple_epic_service *service)
{
unsigned long flags;

if (!service)
return NULL;

spin_lock_irqsave(&service->lock, flags);
service->users++;
spin_unlock_irqrestore(&service->lock, flags);

return service;
}

static inline void afk_service_disable(struct apple_epic_service *service)
{
unsigned long flags;
Expand Down
126 changes: 110 additions & 16 deletions drivers/gpu/drm/apple/audio.c
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@
#define DCPAUD_ELEMENTS_MAXSIZE 16384
#define DCPAUD_PRODUCTATTRS_MAXSIZE 1024

/*
* SIO takes one of its 64 descriptor slots per period, shared by all its
* channels, and up to three DP audio outputs use one SIO. A stream with
* more periods fails START with -ENOMEM.
*/
#define DCPAUD_PERIODS_MAX 16

struct dcp_audio {
struct device *dev;
struct device *dcp_dev;
Expand All @@ -53,6 +60,11 @@ struct dcp_audio {

struct snd_pcm_chmap_elem selected_chmap;
struct dcp_sound_cookie selected_cookie;
/* stream runs without a sink, see dcpaud_void_start() */
bool void_running;
ktime_t void_start;
u64 void_periods;
struct delayed_work void_work;
void *elements;
void *productattrs;

Expand All @@ -73,7 +85,7 @@ static const struct snd_pcm_hardware dcp_pcm_hw = {
.period_bytes_min = 4096, /* TODO */
.period_bytes_max = SIZE_MAX,
.periods_min = 2,
.periods_max = UINT_MAX,
.periods_max = DCPAUD_PERIODS_MAX,
};

/*
Expand All @@ -82,7 +94,7 @@ static const struct snd_pcm_hardware dcp_pcm_hw = {
* configured, as HDMI PCMs on other platforms can. Userspace (PipeWire,
* PulseAudio) probes the PCM once when the card appears and drops the
* output for good if that fails, so a monitor attached later would never
* show up. Starting the stream still fails until a sink is connected.
* show up. The stream then plays into the void, see dcpaud_void_start().
*/
static const struct snd_pcm_hardware dcp_pcm_hw_unplugged = {
.info = SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_MMAP_VALID |
Expand All @@ -99,7 +111,7 @@ static const struct snd_pcm_hardware dcp_pcm_hw_unplugged = {
.period_bytes_min = 4096,
.period_bytes_max = SIZE_MAX,
.periods_min = 2,
.periods_max = UINT_MAX,
.periods_max = DCPAUD_PERIODS_MAX,
};

static bool dcpaud_h17p(struct dcp_audio *dcpaud)
Expand Down Expand Up @@ -324,7 +336,7 @@ static int dcp_pcm_open(struct snd_pcm_substream *substream)
hw.info = SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_MMAP_VALID |
SNDRV_PCM_INFO_INTERLEAVED;
hw.periods_min = 2;
hw.periods_max = UINT_MAX;
hw.periods_max = DCPAUD_PERIODS_MAX;
hw.period_bytes_min = 256;
hw.period_bytes_max = SIZE_MAX; // TODO dma_get_max_seg_size(dma_dev);
hw.buffer_bytes_max = SIZE_MAX;
Expand Down Expand Up @@ -365,12 +377,10 @@ static int dcp_pcm_hw_params(struct snd_pcm_substream *substream,
struct dma_chan *chan = snd_dmaengine_pcm_get_chan(substream);
int ret;

if (dcpaud->open_unplugged)
/* no sink: the stream runs without DMA, see dcpaud_void_start() */
if (dcpaud->open_unplugged || !dcpaud_connection_up(dcpaud))
return 0;

if (!dcpaud_connection_up(dcpaud))
return -ENXIO;

ret = dcpaud_select_cookie(dcpaud, params);
if (ret < 0)
return ret;
Expand Down Expand Up @@ -410,16 +420,79 @@ static int dcp_pcm_prepare(struct snd_pcm_substream *substream)
struct dcp_audio *dcpaud = substream->pcm->private_data;

/* alsa-lib prepares right after hw_params; see dcp_pcm_hw_unplugged */
if (dcpaud->open_unplugged)
if (dcpaud->open_unplugged || !dcpaud_connection_up(dcpaud))
return 0;

if (!dcpaud_connection_up(dcpaud))
return -ENXIO;

return dcp_audiosrv_prepare(dcpaud->dcp_dev,
&dcpaud->selected_cookie);
}

static u64 dcpaud_void_period_ns(struct snd_pcm_runtime *runtime)
{
return div_u64((u64)runtime->period_size * NSEC_PER_SEC, runtime->rate);
}

static void dcpaud_void_work(struct work_struct *work)
{
struct dcp_audio *dcpaud = container_of(to_delayed_work(work),
struct dcp_audio, void_work);
struct snd_pcm_substream *substream = dcpaud->substream;
ktime_t next;
s64 delay;

snd_pcm_period_elapsed(substream);

snd_pcm_stream_lock_irq(substream);
if (dcpaud->void_running) {
/* the next period boundary, so that periods do not drift */
next = ktime_add_ns(dcpaud->void_start,
++dcpaud->void_periods *
dcpaud_void_period_ns(substream->runtime));
delay = ktime_to_ns(ktime_sub(next, ktime_get()));
schedule_delayed_work(&dcpaud->void_work,
delay > 0 ? nsecs_to_jiffies(delay) : 0);
}
snd_pcm_stream_unlock_irq(substream);
}

/*
* Without a sink, DCP has no audio link to start. Failing START does not
* stop userspace from trying: PipeWire retries it on every wakeup, and a
* stream that never starts never makes it wait, so it retries at about
* 130000 times a second for as long as something plays (a monitor
* without audio, or none attached). Play into the void instead, as HDMI
* PCMs elsewhere do: the stream runs on the clock, without DMA, and its
* samples are dropped. A sink attached later gets the next stream opened.
*/
static int dcpaud_void_start(struct snd_pcm_substream *substream)
{
struct dcp_audio *dcpaud = substream->pcm->private_data;
u64 period_ns = dcpaud_void_period_ns(substream->runtime);

dev_dbg(dcpaud->dev, "no sink, playing into the void\n");
dcpaud->void_running = true;
dcpaud->void_start = ktime_get();
dcpaud->void_periods = 0;
schedule_delayed_work(&dcpaud->void_work, nsecs_to_jiffies(period_ns));

return 0;
}

static snd_pcm_uframes_t dcp_pcm_pointer(struct snd_pcm_substream *substream)
{
struct dcp_audio *dcpaud = substream->pcm->private_data;
struct snd_pcm_runtime *runtime = substream->runtime;
u64 frames;

if (!dcpaud->void_running)
return snd_dmaengine_pcm_pointer(substream);

frames = mul_u64_u32_div(ktime_to_ns(ktime_sub(ktime_get(),
dcpaud->void_start)),
runtime->rate, NSEC_PER_SEC);
return do_div(frames, runtime->buffer_size);
}

static int dcp_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
{
struct dcp_audio *dcpaud = substream->pcm->private_data;
Expand All @@ -428,8 +501,8 @@ static int dcp_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
switch (cmd) {
case SNDRV_PCM_TRIGGER_START:
case SNDRV_PCM_TRIGGER_RESUME:
if (!dcpaud_connection_up(dcpaud))
return -ENXIO;
if (dcpaud->open_unplugged || !dcpaud_connection_up(dcpaud))
return dcpaud_void_start(substream);

WARN_ON(pm_runtime_get_sync(dcpaud->dev) < 0);
ret = dcp_audiosrv_startlink(dcpaud->dcp_dev,
Expand All @@ -440,6 +513,11 @@ static int dcp_pcm_trigger(struct snd_pcm_substream *substream, int cmd)

case SNDRV_PCM_TRIGGER_STOP:
case SNDRV_PCM_TRIGGER_SUSPEND:
if (dcpaud->void_running) {
/* sync_stop waits for the work */
dcpaud->void_running = false;
return 0;
}
break;

default:
Expand Down Expand Up @@ -468,14 +546,28 @@ static int dcp_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
return 0;
}

/*
* ALSA calls this once a stream has stopped, outside the stream lock, before
* the next hw_params, prepare or hw_free and at close.
*/
static int dcp_pcm_sync_stop(struct snd_pcm_substream *substream)
{
struct dcp_audio *dcpaud = substream->pcm->private_data;

cancel_delayed_work_sync(&dcpaud->void_work);

return 0;
}

struct snd_pcm_ops dcp_playback_ops = {
.open = dcp_pcm_open,
.close = dcp_pcm_close,
.hw_params = dcp_pcm_hw_params,
.hw_free = dcp_pcm_hw_free,
.prepare = dcp_pcm_prepare,
.trigger = dcp_pcm_trigger,
.pointer = snd_dmaengine_pcm_pointer,
.sync_stop = dcp_pcm_sync_stop,
.pointer = dcp_pcm_pointer,
};

// Transitional workaround: for the chmap control TLV, advertise options
Expand Down Expand Up @@ -557,7 +649,8 @@ static void dcpaud_report_hotplug(struct dcp_audio *dcpaud, bool connected)

if (!connected) {
snd_pcm_stream_lock(substream);
if (substream->runtime)
/* a stream without a sink plays on, see dcpaud_void_start() */
if (substream->runtime && !dcpaud->void_running)
snd_pcm_stop(substream, SNDRV_PCM_STATE_DISCONNECTED);
snd_pcm_stream_unlock(substream);
}
Expand Down Expand Up @@ -769,6 +862,7 @@ static int dcpaud_probe(struct platform_device *pdev)

dcpaud->dev = &pdev->dev;
mutex_init(&dcpaud->data_lock);
INIT_DELAYED_WORK(&dcpaud->void_work, dcpaud_void_work);
platform_set_drvdata(pdev, dcpaud);

return component_add(&pdev->dev, &dcpaud_comp_ops);
Expand Down
Loading