diff --git a/MAINTAINERS b/MAINTAINERS index 2e0aab08c5d6f..cc21f5f3ba263 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2695,6 +2695,7 @@ F: drivers/dma/apple-sio-probe-test.dtso F: drivers/dma/apple-sio-t6030-test.dtso F: drivers/dma/apple-sio-test.c F: drivers/dma/apple-sio.c +F: include/linux/dma/apple-sio.h F: sound/soc/apple/* F: sound/soc/codecs/cs42l83-i2c.c F: sound/soc/codecs/cs42l84.* diff --git a/arch/arm64/boot/dts/apple/t603x-j514s-j516s-usb4.dtsi b/arch/arm64/boot/dts/apple/t603x-j514s-j516s-usb4.dtsi index b6c397e17c905..8aae684af663a 100644 --- a/arch/arm64/boot/dts/apple/t603x-j514s-j516s-usb4.dtsi +++ b/arch/arm64/boot/dts/apple/t603x-j514s-j516s-usb4.dtsi @@ -70,3 +70,42 @@ }; }; +/* + * DPA2: the Type-C DisplayPort processor's (dcpext1) audio, SIO channel 0x68, + * as the ADT links dcpext1 to dp-audio2. + */ +&dcpext1 { + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@0 { + reg = <0>; + dcpext1_audio: endpoint { + remote-endpoint = <&dpaudio2_dcpext1>; + }; + }; + }; +}; + +/* DPA2, like DPA1, has no AP-side registers: not a child of the SoC bus. */ +/ { + dpaudio2: audio-controller-2 { + compatible = "apple,t6030-dpaudio", "apple,dpaudio"; + dmas = <&sio 0x68>; + dma-names = "tx"; + power-domains = <&ps_dpa2>; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@0 { + reg = <0>; + dpaudio2_dcpext1: endpoint { + remote-endpoint = <&dcpext1_audio>; + }; + }; + }; + }; +}; diff --git a/drivers/dma/apple-sio.c b/drivers/dma/apple-sio.c index 65ba4511501c7..5ea2209f92e7d 100644 --- a/drivers/dma/apple-sio.c +++ b/drivers/dma/apple-sio.c @@ -6,9 +6,11 @@ */ #include +#include #include #include #include +#include #include #include #include @@ -20,6 +22,7 @@ #include #include #include +#include #include "dmaengine.h" #include "virt-dma.h" @@ -102,6 +105,38 @@ struct sio_chan { struct sio_shmem_chan_config cfg; struct sio_tx *current_tx; + + /* + * Until a client sets an idle transfer, the channel keeps the old + * accounting, charging ACKs and REPORTs to the current transfer; its + * ISSUEs not yet acknowledged are only counted. + */ + unsigned int pending; + + /* + * The ring accounting, from the first idle transfer until the channel + * is quiet again (sio_ring_leave_locked()): the client's idle transfer + * (struct apple_sio_idle), issued while no other transfer is; + * transfers ended while it is set, until their issued descriptors + * completed; and the owners of the issued descriptors in issue order, + * which is the order of their reports. The last one may still wait for + * its ISSUE to be acknowledged. + */ + bool ring; + struct sio_tx *idle; + struct list_head retired; + wait_queue_head_t drained; + struct sio_tx *issued[SIO_MAX_NINFLIGHT]; + unsigned int issued_head; + unsigned int nissued; + bool issuing; + /* Nothing is issued between a TERMINATE and the ring's reset. */ + bool stopping; + /* + * Ended transfers with nothing in flight, freed once no completion + * callback can still be running (sio_free_dead()). + */ + struct list_head dead; }; #define SIO_NTAGS 16 @@ -145,6 +180,7 @@ struct sio_tx { struct completion done; bool terminated; + bool retired; size_t period_len; int nperiods; int ninflight; @@ -435,23 +471,22 @@ static void sio_tx_free(struct virt_dma_desc *vd) kfree(siotx); } -static struct dma_async_tx_descriptor *sio_prep_dma_cyclic( - struct dma_chan *chan, dma_addr_t buf_addr, size_t buf_len, - size_t period_len, enum dma_transfer_direction direction, - unsigned long flags) +static struct sio_tx *sio_alloc_tx(struct sio_chan *siochan, dma_addr_t buf_addr, + size_t buf_len, size_t period_len, gfp_t gfp) { - struct sio_chan *siochan = to_sio_chan(chan); struct sio_tx *siotx = NULL; - int i, nperiods = buf_len / period_len; + int i, nperiods; - if (direction != sio_chan_direction(siochan->no)) + if (!period_len || buf_len < period_len) return NULL; + nperiods = buf_len / period_len; - siotx = kzalloc(struct_size(siotx, siodesc, nperiods), GFP_NOWAIT); + siotx = kzalloc(struct_size(siotx, siodesc, nperiods), gfp); if (!siotx) return NULL; init_completion(&siotx->done); + siotx->vd.tx.chan = &siochan->vc.chan; siotx->period_len = period_len; siotx->nperiods = nperiods; @@ -460,7 +495,6 @@ static struct dma_async_tx_descriptor *sio_prep_dma_cyclic( siotx->siodesc[i] = d = sio_alloc_desc(siochan->host); if (!d) { - siotx->vd.tx.chan = &siochan->vc.chan; sio_tx_free(&siotx->vd); return NULL; } @@ -477,6 +511,24 @@ static struct dma_async_tx_descriptor *sio_prep_dma_cyclic( } dma_wmb(); + return siotx; +} + +static struct dma_async_tx_descriptor *sio_prep_dma_cyclic( + struct dma_chan *chan, dma_addr_t buf_addr, size_t buf_len, + size_t period_len, enum dma_transfer_direction direction, + unsigned long flags) +{ + struct sio_chan *siochan = to_sio_chan(chan); + struct sio_tx *siotx; + + if (direction != sio_chan_direction(siochan->no)) + return NULL; + + siotx = sio_alloc_tx(siochan, buf_addr, buf_len, period_len, GFP_NOWAIT); + if (!siotx) + return NULL; + return vchan_tx_prep(&siochan->vc, &siotx->vd, flags); } @@ -521,32 +573,172 @@ static enum dma_status sio_tx_status(struct dma_chan *chan, dma_cookie_t cookie, static bool sio_fill_in_locked(struct sio_chan *siochan); -static void sio_handle_issue_ack(struct sio_chan *siochan, void *cookie, bool ok) +static struct sio_tx **sio_issued_slot(struct sio_chan *siochan, unsigned int i) { - dma_cookie_t tx_cookie = (unsigned long) cookie; - unsigned long flags; - struct sio_tx *tx; + return &siochan->issued[(siochan->issued_head + i) % SIO_MAX_NINFLIGHT]; +} + +/* Neither in flight nor waiting for its ISSUE to be acknowledged */ +static bool sio_tx_quiet_locked(struct sio_chan *siochan, struct sio_tx *tx) +{ + unsigned int i; + + if (tx->ninflight) + return false; + for (i = 0; i < siochan->nissued; i++) + if (*sio_issued_slot(siochan, i) == tx) + return false; + return true; +} + +/* A transfer that is freed no longer owns issued descriptors */ +static void sio_scrub_locked(struct sio_chan *siochan, struct sio_tx *tx) +{ + unsigned int i; + + for (i = 0; i < siochan->nissued; i++) + if (*sio_issued_slot(siochan, i) == tx) + *sio_issued_slot(siochan, i) = NULL; +} + +/* Descriptors are issued from the current transfer, else from the idle one. */ +static struct sio_tx *sio_source_locked(struct sio_chan *siochan) +{ + struct sio_tx *tx = siochan->current_tx; + if (tx) + return tx->terminated ? NULL : tx; + if (siochan->idle && !siochan->idle->terminated) + return siochan->idle; + return NULL; +} + +/* + * The first idle transfer: the ring accounting starts from what the old one + * has in flight, all of it the current transfer's. It cannot place two ISSUEs + * still waiting for their acks, nor an ended transfer's descriptors. + */ +static int sio_ring_enter_locked(struct sio_chan *siochan) +{ + struct sio_tx *tx = siochan->current_tx; + unsigned int i, n = tx ? tx->ninflight : 0; + + if (siochan->pending > 1 || (siochan->pending && !tx) || (tx && tx->terminated) || + n + siochan->pending > SIO_MAX_NINFLIGHT) + return -EBUSY; + siochan->issued_head = 0; + siochan->nissued = n + siochan->pending; + for (i = 0; i < siochan->nissued; i++) + siochan->issued[i] = tx; + siochan->issuing = siochan->pending; + siochan->pending = 0; + siochan->ring = true; + return 0; +} + +/* + * The resync point: once the channel is quiet, with no idle, current or + * retired transfer and nothing issued, the old accounting resumes. + */ +static void sio_ring_leave_locked(struct sio_chan *siochan) +{ + if (!siochan->ring || siochan->idle || siochan->current_tx || + !list_empty(&siochan->retired) || siochan->nissued || siochan->issuing) + return; + siochan->ring = false; + siochan->issued_head = 0; +} + +/* + * An ended transfer with no descriptor left in flight: a retired one goes to + * be freed, the waiter of another one is woken. + */ +static void sio_settle_locked(struct sio_chan *siochan, struct sio_tx *tx) +{ + if (!tx || !tx->terminated || !sio_tx_quiet_locked(siochan, tx)) + return; + if (tx->retired) { + list_move_tail(&tx->vd.node, &siochan->dead); + wake_up(&siochan->drained); + return; + } + /* sio_terminate_work() or sio_clear_idle() */ + complete(&tx->done); +} + +/* + * The old accounting, for channels that never had an idle transfer: an ACK + * is the current transfer's, if its cookie still is. + */ +static void sio_old_issue_ack_locked(struct sio_chan *siochan, dma_cookie_t tx_cookie, bool ok) +{ + struct sio_tx *tx = siochan->current_tx; + + if (siochan->pending) + siochan->pending--; if (!ok) { dev_err(siochan->host->dev, "nacked issue on chan %d\n", siochan->no); return; } + if (!tx || tx_cookie != tx->vd.tx.cookie || tx->terminated) + return; - spin_lock_irqsave(&siochan->vc.lock, flags); - if (!siochan->current_tx || tx_cookie != siochan->current_tx->vd.tx.cookie || - siochan->current_tx->terminated) - goto out; - - tx = siochan->current_tx; tx->next = (tx->next + 1) % tx->nperiods; tx->ninflight++; sio_fill_in_locked(siochan); +} + +/* The ring accounting: an ACK is the last issued descriptor's. */ +static void sio_ring_issue_ack_locked(struct sio_chan *siochan, bool ok) +{ + struct sio_tx *tx; -out: + siochan->issuing = false; + if (!siochan->nissued) { + dev_err_ratelimited(siochan->host->dev, "chan %d: ack without an issued descriptor\n", + siochan->no); + return; + } + + tx = *sio_issued_slot(siochan, siochan->nissued - 1); + if (!ok) { + dev_err(siochan->host->dev, "nacked issue on chan %d\n", siochan->no); + siochan->nissued--; + if (!siochan->nissued) + wake_up(&siochan->drained); + /* + * During a handover this may have been the ended transfer's + * last ISSUE, and no report is left to start the next one. A + * transfer whose own ISSUE was refused is not issued again. + */ + if (sio_source_locked(siochan) != tx) + sio_fill_in_locked(siochan); + sio_settle_locked(siochan, tx); + return; + } + + /* An ended transfer's descriptor is in flight all the same. */ + if (tx) { + tx->next = (tx->next + 1) % tx->nperiods; + tx->ninflight++; + } + sio_fill_in_locked(siochan); +} + +static void sio_handle_issue_ack(struct sio_chan *siochan, void *cookie, bool ok) +{ + unsigned long flags; + + spin_lock_irqsave(&siochan->vc.lock, flags); + if (siochan->ring) + sio_ring_issue_ack_locked(siochan, ok); + else + sio_old_issue_ack_locked(siochan, (unsigned long)cookie, ok); spin_unlock_irqrestore(&siochan->vc.lock, flags); } -static bool sio_fill_in_locked(struct sio_chan *siochan) +/* The old accounting: the current transfer's next descriptor */ +static bool sio_fill_in_old_locked(struct sio_chan *siochan) { struct sio_data *sio = siochan->host; struct sio_tx *tx = siochan->current_tx; @@ -560,13 +752,50 @@ static bool sio_fill_in_locked(struct sio_chan *siochan) ret = sio_send_siomsg_atomic(sio, FIELD_PREP(SIOMSG_EP, siochan->no) | FIELD_PREP(SIOMSG_TYPE, MSG_ISSUE) | FIELD_PREP(SIOMSG_DATA, sio_coproc_desc_slot(sio, d)), - sio_handle_issue_ack, (void *) (uintptr_t) tx->vd.tx.cookie); + sio_handle_issue_ack, (void *)(uintptr_t)tx->vd.tx.cookie); if (ret < 0) dev_err_ratelimited(sio->dev, "can't issue on chan %d ninflight %d: %d\n", siochan->no, tx->ninflight, ret); + else + siochan->pending++; + return true; +} + +/* The ring accounting: one ISSUE at a time, each after the previous one's ack */ +static bool sio_fill_in_ring_locked(struct sio_chan *siochan) +{ + struct sio_data *sio = siochan->host; + struct sio_tx *tx = sio_source_locked(siochan); + struct sio_coproc_desc *d; + int ret; + + if (!tx || siochan->issuing || siochan->stopping || + siochan->nissued >= SIO_MAX_NINFLIGHT) + return false; + + d = tx->siodesc[tx->next]; + ret = sio_send_siomsg_atomic(sio, FIELD_PREP(SIOMSG_EP, siochan->no) | + FIELD_PREP(SIOMSG_TYPE, MSG_ISSUE) | + FIELD_PREP(SIOMSG_DATA, sio_coproc_desc_slot(sio, d)), + sio_handle_issue_ack, NULL); + if (ret < 0) { + dev_err_ratelimited(sio->dev, "can't issue on chan %d ninflight %d: %d\n", + siochan->no, siochan->nissued, ret); + return false; + } + /* The ack takes the channel lock, held here. */ + *sio_issued_slot(siochan, siochan->nissued++) = tx; + siochan->issuing = true; return true; } +static bool sio_fill_in_locked(struct sio_chan *siochan) +{ + if (siochan->ring) + return sio_fill_in_ring_locked(siochan); + return siochan->current_tx && sio_fill_in_old_locked(siochan); +} + static void sio_update_current_tx_locked(struct sio_chan *siochan) { struct virt_dma_desc *vd = vchan_next_desc(&siochan->vc); @@ -592,14 +821,37 @@ static void sio_issue_pending(struct dma_chan *chan) static int sio_terminate_all(struct dma_chan *chan) { struct sio_chan *siochan = to_sio_chan(chan); + struct sio_tx *tx; unsigned long flags; LIST_HEAD(to_free); spin_lock_irqsave(&siochan->vc.lock, flags); - if (siochan->current_tx && !siochan->current_tx->terminated) { - dma_cookie_complete(&siochan->current_tx->vd.tx); - siochan->current_tx->terminated = true; - schedule_work(&siochan->terminate_wq); + tx = siochan->current_tx; + if (tx && !tx->terminated) { + dma_cookie_complete(&tx->vd.tx); + tx->terminated = true; + if (siochan->ring && siochan->idle && !siochan->idle->terminated) { + /* + * No TERMINATE: the idle transfer follows the issued + * descriptors, and the peripheral runs on without a gap. + * sio_synchronize() waits for those to complete. + */ + if (siochan->vc.cyclic == &tx->vd) + siochan->vc.cyclic = NULL; + siochan->current_tx = NULL; + /* Its callback may be running: sio_free_dead() frees it. */ + if (sio_tx_quiet_locked(siochan, tx)) { + list_add_tail(&tx->vd.node, &siochan->dead); + } else { + tx->retired = true; + list_add_tail(&tx->vd.node, &siochan->retired); + } + sio_fill_in_locked(siochan); + } else { + if (siochan->ring && sio_tx_quiet_locked(siochan, tx)) + complete(&tx->done); + schedule_work(&siochan->terminate_wq); + } } vchan_get_all_descriptors(&siochan->vc, &to_free); spin_unlock_irqrestore(&siochan->vc.lock, flags); @@ -638,23 +890,223 @@ static void sio_terminate_work(struct work_struct *wq) spin_lock_irqsave(&siochan->vc.lock, flags); WARN_ON(siochan->current_tx != tx); siochan->current_tx = NULL; + sio_scrub_locked(siochan, tx); sio_update_current_tx_locked(siochan); + sio_ring_leave_locked(siochan); spin_unlock_irqrestore(&siochan->vc.lock, flags); sio_tx_free(&tx->vd); } +static bool sio_drained(struct sio_chan *siochan) +{ + unsigned long flags; + bool drained; + + spin_lock_irqsave(&siochan->vc.lock, flags); + drained = list_empty(&siochan->retired); + spin_unlock_irqrestore(&siochan->vc.lock, flags); + + return drained; +} + +static bool sio_ring_empty(struct sio_chan *siochan) +{ + unsigned long flags; + bool empty; + + spin_lock_irqsave(&siochan->vc.lock, flags); + empty = !siochan->nissued && !siochan->issuing; + spin_unlock_irqrestore(&siochan->vc.lock, flags); + + return empty; +} + +/* + * Issued descriptors that did not complete in time: stop the channel and wait + * for their reports, so that their buffers are no longer read. Nothing is + * issued from the TERMINATE until the channel is known stopped. A channel + * that acknowledged TERMINATE and still reports nothing is resynced: its ring + * is emptied, as nothing it holds is played any more. A channel that refused + * TERMINATE or did not answer cannot be known to have stopped: -EIO, and its + * transfers stay as they are. Then the current or idle transfer is issued + * again. + */ +static int sio_stop_quiet(struct sio_chan *siochan) +{ + struct device *dev = siochan->host->dev; + unsigned long flags; + struct sio_tx *tx; + unsigned int i; + int ret; + + dev_err(dev, "chan %d: issued descriptors did not complete, stopping it\n", siochan->no); + spin_lock_irqsave(&siochan->vc.lock, flags); + siochan->stopping = true; + spin_unlock_irqrestore(&siochan->vc.lock, flags); + + /* 1 on ACK, 0 on NACK, negative without an answer */ + ret = sio_call(siochan->host, FIELD_PREP(SIOMSG_EP, siochan->no) | + FIELD_PREP(SIOMSG_TYPE, MSG_TERMINATE)); + if (ret == 1) + wait_event_timeout(siochan->drained, sio_ring_empty(siochan), + msecs_to_jiffies(500)); + else if (!ret) + dev_err(dev, "chan %d: TERMINATE refused, its buffers may still be read\n", + siochan->no); + else + dev_err(dev, "chan %d: not stopped (%d), its buffers may still be read\n", + siochan->no, ret); + + spin_lock_irqsave(&siochan->vc.lock, flags); + if (ret == 1 && (siochan->nissued || siochan->issuing)) { + dev_err(dev, "chan %d: stopped with %u descriptors unreported, ring reset\n", + siochan->no, siochan->nissued); + for (i = 0; i < siochan->nissued; i++) { + tx = *sio_issued_slot(siochan, i); + if (tx) + tx->ninflight = 0; + } + siochan->nissued = 0; + siochan->issuing = false; + siochan->issued_head = 0; + } + if (ret == 1) { + list_splice_tail_init(&siochan->retired, &siochan->dead); + if (siochan->idle && siochan->idle->terminated) + complete(&siochan->idle->done); + } + siochan->stopping = false; + sio_fill_in_locked(siochan); + spin_unlock_irqrestore(&siochan->vc.lock, flags); + + return ret == 1 ? 0 : -EIO; +} + +/* Frees the ended transfers once no completion callback can still use them. */ +static void sio_free_dead(struct sio_chan *siochan) +{ + struct sio_tx *tx, *n; + unsigned long flags; + LIST_HEAD(dead); + + spin_lock_irqsave(&siochan->vc.lock, flags); + list_splice_init(&siochan->dead, &dead); + spin_unlock_irqrestore(&siochan->vc.lock, flags); + if (list_empty(&dead)) + return; + + /* vchan_complete() runs a callback with its descriptor unlocked. */ + tasklet_kill(&siochan->vc.task); + list_for_each_entry_safe(tx, n, &dead, vd.node) { + list_del(&tx->vd.node); + sio_tx_free(&tx->vd); + } +} + static void sio_synchronize(struct dma_chan *chan) { struct sio_chan *siochan = to_sio_chan(chan); flush_work(&siochan->terminate_wq); + if (READ_ONCE(siochan->ring)) { + /* The caller frees the buffer on return; nothing can stop that. */ + if (!wait_event_timeout(siochan->drained, sio_drained(siochan), + msecs_to_jiffies(500)) && + sio_stop_quiet(siochan)) + dev_WARN_ONCE(siochan->host->dev, 1, + "chan %d: synchronized without being stopped\n", + siochan->no); + /* No callback of an ended transfer runs after this. */ + tasklet_kill(&siochan->vc.task); + } + sio_free_dead(siochan); +} + +/* + * Returns once the idle transfer's descriptors completed. If the channel + * cannot be confirmed stopped, the idle transfer stays, its buffer in use. + */ +static int sio_clear_idle(struct sio_chan *siochan) +{ + struct sio_tx *idle; + unsigned long flags; + bool quiet, stop; + int ret; + + spin_lock_irqsave(&siochan->vc.lock, flags); + idle = siochan->idle; + if (!idle) { + spin_unlock_irqrestore(&siochan->vc.lock, flags); + return 0; + } + idle->terminated = true; + quiet = sio_tx_quiet_locked(siochan, idle); + /* Nothing follows the idle transfer: the channel stops. */ + stop = !quiet && !siochan->current_tx; + spin_unlock_irqrestore(&siochan->vc.lock, flags); + + if (stop) { + ret = sio_call(siochan->host, FIELD_PREP(SIOMSG_EP, siochan->no) | + FIELD_PREP(SIOMSG_TYPE, MSG_TERMINATE)); + if (ret < 0) + dev_err(siochan->host->dev, "terminate call on chan %d failed: %d\n", + siochan->no, ret); + } + if (!quiet && !wait_for_completion_timeout(&idle->done, msecs_to_jiffies(500))) { + ret = sio_stop_quiet(siochan); + if (ret) + return ret; + } + + spin_lock_irqsave(&siochan->vc.lock, flags); + siochan->idle = NULL; + sio_scrub_locked(siochan, idle); + sio_ring_leave_locked(siochan); + spin_unlock_irqrestore(&siochan->vc.lock, flags); + + sio_tx_free(&idle->vd); + sio_free_dead(siochan); + return 0; +} + +/* See struct apple_sio_idle */ +static int sio_set_idle(struct sio_chan *siochan, const struct apple_sio_idle *config) +{ + struct sio_tx *idle; + unsigned long flags; + int ret; + + if (!config->len) + return sio_clear_idle(siochan); + if (!config->period_len || config->len < config->period_len) + return -EINVAL; + + idle = sio_alloc_tx(siochan, config->addr, config->len, config->period_len, GFP_KERNEL); + if (!idle) + return -ENOMEM; + + spin_lock_irqsave(&siochan->vc.lock, flags); + ret = siochan->idle ? -EBUSY : 0; + if (!ret && !siochan->ring) + ret = sio_ring_enter_locked(siochan); + if (ret) { + spin_unlock_irqrestore(&siochan->vc.lock, flags); + sio_tx_free(&idle->vd); + return ret; + } + siochan->idle = idle; + sio_fill_in_locked(siochan); + spin_unlock_irqrestore(&siochan->vc.lock, flags); + + return 0; } static void sio_free_chan_resources(struct dma_chan *chan) { sio_terminate_all(chan); sio_synchronize(chan); + sio_clear_idle(to_sio_chan(chan)); vchan_free_chan_resources(&to_sio_chan(chan)->vc); } @@ -677,20 +1129,61 @@ static void sio_rtk_crashed(void *cookie, const void *crashlog, size_t crashlog_ dev_err(sio->dev, "SIO down (crashed)"); } -static void sio_process_report(struct sio_chan *siochan) +/* The old accounting: a report is the current transfer's. */ +static void sio_old_report_locked(struct sio_chan *siochan) { - unsigned long flags; + struct sio_tx *tx = siochan->current_tx; - spin_lock_irqsave(&siochan->vc.lock, flags); - if (siochan->current_tx) { - struct sio_tx *tx = siochan->current_tx; + if (!tx) + return; + if (tx->ninflight) + tx->ninflight--; + vchan_cyclic_callback(&tx->vd); + if (!sio_fill_in_locked(siochan) && !tx->ninflight) + complete(&tx->done); +} + +/* The ring accounting: the oldest issued descriptor completed. */ +static void sio_ring_report_locked(struct sio_chan *siochan) +{ + struct sio_tx *tx; + /* An ISSUE not yet acknowledged has no completed descriptor. */ + if (siochan->nissued <= (siochan->issuing ? 1 : 0)) { + dev_warn_ratelimited(siochan->host->dev, "chan %d: report without a descriptor\n", + siochan->no); + return; + } + + tx = *sio_issued_slot(siochan, 0); + siochan->issued_head = (siochan->issued_head + 1) % SIO_MAX_NINFLIGHT; + siochan->nissued--; + if (tx) { if (tx->ninflight) tx->ninflight--; - vchan_cyclic_callback(&tx->vd); - if (!sio_fill_in_locked(siochan) && !tx->ninflight) - complete(&tx->done); + else + dev_err_ratelimited(siochan->host->dev, + "chan %d: report for a descriptor not in flight\n", + siochan->no); + if (tx == siochan->current_tx && !tx->terminated) + vchan_cyclic_callback(&tx->vd); } + sio_fill_in_locked(siochan); + sio_settle_locked(siochan, tx); + if (!siochan->nissued) + wake_up(&siochan->drained); + sio_ring_leave_locked(siochan); +} + +static void sio_process_report(struct sio_chan *siochan) +{ + unsigned long flags; + + spin_lock_irqsave(&siochan->vc.lock, flags); + if (siochan->ring) + sio_ring_report_locked(siochan); + else + sio_old_report_locked(siochan); spin_unlock_irqrestore(&siochan->vc.lock, flags); } @@ -827,6 +1320,12 @@ static int sio_device_config(struct dma_chan *chan, struct sio_shmem_chan_config cfg; int ret; + if (config->peripheral_config) { + if (config->peripheral_size != sizeof(struct apple_sio_idle)) + return -EINVAL; + return sio_set_idle(siochan, config->peripheral_config); + } + switch (is_tx ? config->dst_addr_width : config->src_addr_width) { case DMA_SLAVE_BUSWIDTH_1_BYTE: cfg.datashape = 0; @@ -908,8 +1407,8 @@ static int sio_alloc_shmem(struct sio_data *sio) return -ENOMEM; sio->shmem_desc_base = (struct sio_coproc_desc *) (sio->shmem + 56); - sio->desc_allocated = devm_kzalloc(sio->dev, SIO_NO_DESC_SLOTS / 32, - GFP_KERNEL); + sio->desc_allocated = devm_bitmap_zalloc(sio->dev, SIO_NO_DESC_SLOTS, + GFP_KERNEL); if (!sio->desc_allocated) return -ENOMEM; @@ -1060,6 +1559,9 @@ static int sio_probe(struct platform_device *pdev) siochan->no = i; siochan->vc.desc_free = sio_tx_free; INIT_WORK(&siochan->terminate_wq, sio_terminate_work); + INIT_LIST_HEAD(&siochan->retired); + INIT_LIST_HEAD(&siochan->dead); + init_waitqueue_head(&siochan->drained); vchan_init(&siochan->vc, dma); } diff --git a/drivers/gpu/drm/apple/apple_drv.c b/drivers/gpu/drm/apple/apple_drv.c index d71c5c0370f2e..ea95084e936ba 100644 --- a/drivers/gpu/drm/apple/apple_drv.c +++ b/drivers/gpu/drm/apple/apple_drv.c @@ -1559,3 +1559,10 @@ module_exit(appledrm_unregister); MODULE_AUTHOR("Asahi Linux contributors"); MODULE_DESCRIPTION(DRIVER_DESC); MODULE_LICENSE("Dual MIT/GPL"); +/* + * The DCPs take their HDMI and Type-C route mux controls from the display + * crossbar. Without this, initramfs generators leave it out, it loads only + * from the root filesystem, and a DCP probe that waits past the deferred + * probe timeout (an encrypted root's passphrase prompt) gives up its routes. + */ +MODULE_SOFTDEP("pre: mux-apple-display-crossbar"); diff --git a/drivers/gpu/drm/apple/audio-link-test.c b/drivers/gpu/drm/apple/audio-link-test.c index a90d85f221ead..0ddd42dce072c 100644 --- a/drivers/gpu/drm/apple/audio-link-test.c +++ b/drivers/gpu/drm/apple/audio-link-test.c @@ -1,9 +1,9 @@ // SPDX-License-Identifier: GPL-2.0-only OR MIT /* * Included by audio.c: a Terminate's link release ends only the link of the - * instance it is for. The production dcpaud_release_link() runs; the audio - * service's answer to which instance the link is on, and its link calls, - * are stubbed. + * instance it is for, and a stopped stereo link is held between streams. + * The production link lifecycle runs; the audio service's answer to which + * instance the link is on, its link calls and the DMA channel are stubbed. */ #include #include @@ -11,13 +11,87 @@ struct dcpaud_link_test { bool link_on; + bool link_live; + /* The error removing the idle transfer returns */ + int idle_remove_err; u64 asked; unsigned int completes; unsigned int stops; + struct dma_device dma; + struct dma_chan channel; + struct dma_async_tx_descriptor desc; + char sequence[16]; + unsigned int nsequence; }; static struct dcpaud_link_test *dcpaud_link_current; +static void dcpaud_link_record(char step) +{ + struct dcpaud_link_test *t = dcpaud_link_current; + + if (t->nsequence < sizeof(t->sequence) - 1) + t->sequence[t->nsequence++] = step; +} + +static bool dcpaud_link_test_live(struct device *dev) +{ + return dcpaud_link_current->link_live; +} + +static int dcpaud_link_test_terminate(struct dma_chan *chan) +{ + dcpaud_link_record('X'); + return 0; +} + +/* G the channel configured; the silence as its idle transfer: I set, i removed */ +static int dcpaud_link_test_config(struct dma_chan *chan, struct dma_slave_config *config) +{ + const struct apple_sio_idle *idle = config->peripheral_config; + + if (!idle) { + dcpaud_link_record('G'); + return 0; + } + if (config->peripheral_size != sizeof(*idle)) + return -EINVAL; + dcpaud_link_record(idle->len ? 'I' : 'i'); + return idle->len ? 0 : dcpaud_link_current->idle_remove_err; +} + +static dma_cookie_t dcpaud_link_test_submit(struct dma_async_tx_descriptor *tx) +{ + return 1; +} + +static struct dma_async_tx_descriptor * +dcpaud_link_test_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_link_test *t = dcpaud_link_current; + + dcpaud_link_record('Q'); + t->desc.chan = chan; + t->desc.tx_submit = dcpaud_link_test_submit; + return &t->desc; +} + +static void dcpaud_link_test_issue(struct dma_chan *chan) +{ +} + +static void dcpaud_link_test_drain(struct dma_chan *chan) +{ + dcpaud_link_record('D'); +} + +static enum dma_status dcpaud_link_test_status(struct dma_chan *chan, + dma_cookie_t cookie, struct dma_tx_state *state) +{ + return DMA_COMPLETE; +} + static bool dcpaud_link_test_on(struct device *dev, u64 gen) { dcpaud_link_current->asked = gen; @@ -27,10 +101,17 @@ static bool dcpaud_link_test_on(struct device *dev, u64 gen) static int dcpaud_link_test_call(struct device *dev, enum dcp_audio_link op, const struct dcp_sound_cookie *cookie) { - if (op == DCP_AUDIO_LINK_COMPLETE) + if (op == DCP_AUDIO_LINK_COMPLETE) { dcpaud_link_current->completes++; - else if (op == DCP_AUDIO_LINK_STOP) + dcpaud_link_record('C'); + } else if (op == DCP_AUDIO_LINK_STOP) { dcpaud_link_current->stops++; + dcpaud_link_record('S'); + } else if (op == DCP_AUDIO_LINK_PREPARE) { + dcpaud_link_record('P'); + } else if (op == DCP_AUDIO_LINK_START) { + dcpaud_link_record('T'); + } return 0; } @@ -49,7 +130,16 @@ static struct dcp_audio *dcpaud_link_setup(struct kunit *test, struct platform_d dcpaud->native_link = true; mutex_init(&dcpaud->link_lock); dcpaud_link_current = t; + t->link_live = true; + t->channel.device = &t->dma; + t->dma.device_terminate_all = dcpaud_link_test_terminate; + t->dma.device_synchronize = dcpaud_link_test_drain; + t->dma.device_tx_status = dcpaud_link_test_status; + t->dma.device_prep_dma_cyclic = dcpaud_link_test_prep; + t->dma.device_issue_pending = dcpaud_link_test_issue; + t->dma.device_config = dcpaud_link_test_config; kunit_activate_static_stub(test, dcp_audiosrv_link_on, dcpaud_link_test_on); + kunit_activate_static_stub(test, dcp_audiosrv_link_live, dcpaud_link_test_live); kunit_activate_static_stub(test, dcp_audiosrv_link, dcpaud_link_test_call); return dcpaud; } @@ -85,8 +175,287 @@ static void dcpaud_link_release_instance(struct kunit *test) KUNIT_EXPECT_EQ(test, dcpaud_link_current->completes, 2U); } +static void dcpaud_link_test_close(void *data) +{ + snd_dmaengine_pcm_close(data); +} + +static struct snd_pcm_substream *dcpaud_link_test_open(struct kunit *test, + struct dcp_audio *dcpaud) +{ + 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); + + KUNIT_ASSERT_NOT_NULL(test, sub); + KUNIT_ASSERT_NOT_NULL(test, runtime); + KUNIT_ASSERT_NOT_NULL(test, pcm); + pcm->private_data = dcpaud; + pcm->nonatomic = true; + sub->pcm = pcm; + sub->runtime = runtime; + KUNIT_ASSERT_EQ(test, snd_dmaengine_pcm_open(sub, &dcpaud_link_current->channel), 0); + KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, dcpaud_link_test_close, sub), 0); + return sub; +} + +static void dcpaud_disconnect_sync_completes_live_owner(struct kunit *test) +{ + struct platform_device *pdev; + struct dcp_audio *audio = dcpaud_link_setup(test, &pdev); + struct snd_pcm_substream *sub = dcpaud_link_test_open(test, audio); + + /* Disconnect cleared connected; the deferred owner release has not run. */ + audio->connected = false; + audio->link_prepared = true; + audio->link_started = true; + KUNIT_ASSERT_EQ(test, dcp_pcm_native_sync_stop(sub), 0); + KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, "DSC"); + KUNIT_EXPECT_FALSE(test, audio->link_prepared); + KUNIT_EXPECT_FALSE(test, audio->link_started); + dcpaud_release_link(pdev, NULL); + KUNIT_EXPECT_EQ(test, dcpaud_link_current->stops, 1U); + KUNIT_EXPECT_EQ(test, dcpaud_link_current->completes, 1U); +} + +static void dcpaud_gone_service_drops_link(struct kunit *test) +{ + struct platform_device *pdev; + struct dcp_audio *audio = dcpaud_link_setup(test, &pdev); + struct snd_pcm_substream *sub = dcpaud_link_test_open(test, audio); + + audio->link_prepared = true; + audio->link_started = true; + dcpaud_link_current->link_live = false; + KUNIT_ASSERT_EQ(test, dcp_pcm_native_sync_stop(sub), 0); + KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, "D"); + KUNIT_EXPECT_FALSE(test, audio->link_prepared); + KUNIT_EXPECT_FALSE(test, audio->link_started); +} + +/* A stream stopped by STOP with its DMA playing on, on the PCM's channel */ +static void dcpaud_link_test_held_stop(struct kunit *test, struct dcp_audio *audio) +{ + audio->chan = &dcpaud_link_current->channel; + audio->silence = kunit_kzalloc(test, DCPAUD_SILENCE_BYTES, GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, audio->silence); + audio->link_prepared = true; + audio->link_started = true; + audio->dma_cleared = true; + /* The stream's PM reference, carried by the cleared DMA */ + pm_runtime_get_noresume(audio->dev); +} + +static void dcpaud_stop_holds_link(struct kunit *test) +{ + struct platform_device *pdev; + struct dcp_audio *audio = dcpaud_link_setup(test, &pdev); + struct snd_pcm_substream *sub = dcpaud_link_test_open(test, audio); + + dcpaud_link_test_held_stop(test, audio); + /* The silence set first, the stream's DMA ends into it; no link call */ + KUNIT_ASSERT_EQ(test, dcp_pcm_native_sync_stop(sub), 0); + KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, "IXD"); + KUNIT_EXPECT_TRUE(test, audio->silence_running); + KUNIT_EXPECT_TRUE(test, audio->idle_set); + KUNIT_EXPECT_TRUE(test, audio->link_started); + + KUNIT_ASSERT_EQ(test, dcp_pcm_native_hw_free(sub), 0); + KUNIT_EXPECT_TRUE(test, audio->link_prepared); + KUNIT_EXPECT_EQ(test, dcpaud_link_current->stops + dcpaud_link_current->completes, 0U); + + /* The display goes off: the silence ends, then the link */ + dcpaud_release_link(pdev, NULL); + KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, "IXDiDSC"); + KUNIT_EXPECT_FALSE(test, audio->silence_running); + KUNIT_EXPECT_FALSE(test, audio->idle_set); + KUNIT_EXPECT_FALSE(test, audio->link_prepared); + /* The stream's reference went with the silence */ + KUNIT_EXPECT_EQ(test, atomic_read(&audio->dev->power.usage_count), 0); +} + +static void dcpaud_stuck_silence_keeps_pm(struct kunit *test) +{ + struct platform_device *pdev; + struct dcp_audio *audio = dcpaud_link_setup(test, &pdev); + struct snd_pcm_substream *sub = dcpaud_link_test_open(test, audio); + + dcpaud_link_test_held_stop(test, audio); + KUNIT_ASSERT_EQ(test, dcp_pcm_native_sync_stop(sub), 0); + KUNIT_ASSERT_EQ(test, dcp_pcm_native_hw_free(sub), 0); + + /* The channel cannot be confirmed stopped: it may still read the silence */ + dcpaud_link_current->idle_remove_err = -EIO; + dcpaud_release_link(pdev, NULL); + KUNIT_EXPECT_FALSE(test, audio->silence_running); + KUNIT_EXPECT_FALSE(test, audio->link_prepared); + KUNIT_EXPECT_TRUE(test, audio->idle_set); + KUNIT_EXPECT_TRUE(test, audio->idle_pm); + /* The stream's reference stays with the silence */ + KUNIT_EXPECT_EQ(test, atomic_read(&audio->dev->power.usage_count), 1); + + /* Removed later, it gives the reference back */ + dcpaud_link_current->idle_remove_err = 0; + scoped_guard(mutex, &audio->link_lock) + KUNIT_EXPECT_EQ(test, dcpaud_idle(audio, false), 0); + KUNIT_EXPECT_FALSE(test, audio->idle_set); + KUNIT_EXPECT_FALSE(test, audio->idle_pm); + KUNIT_EXPECT_EQ(test, atomic_read(&audio->dev->power.usage_count), 0); +} + +static void dcpaud_held_stop_without_service(struct kunit *test) +{ + struct platform_device *pdev; + struct dcp_audio *audio = dcpaud_link_setup(test, &pdev); + struct snd_pcm_substream *sub = dcpaud_link_test_open(test, audio); + + dcpaud_link_test_held_stop(test, audio); + dcpaud_link_current->link_live = false; + /* No service to hold it for: the DMA ends and the link is dropped */ + KUNIT_ASSERT_EQ(test, dcp_pcm_native_sync_stop(sub), 0); + KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, "XD"); + KUNIT_EXPECT_FALSE(test, audio->silence_running); + KUNIT_EXPECT_FALSE(test, audio->link_prepared); + KUNIT_EXPECT_FALSE(test, audio->link_started); + KUNIT_EXPECT_EQ(test, atomic_read(&audio->dev->power.usage_count), 0); +} + +/* A held link and its silence, and a prepared stream to start on it */ +static struct snd_pcm_substream *dcpaud_link_test_held(struct kunit *test, struct dcp_audio *audio) +{ + struct snd_pcm_substream *sub = dcpaud_link_test_open(test, audio); + + mutex_init(&audio->data_lock); + audio->connected = true; + audio->chan = &dcpaud_link_current->channel; + audio->link_prepared = true; + audio->link_started = true; + audio->silence_running = true; + audio->idle_set = true; + /* The held link's PM reference, then the stream's from its START */ + pm_runtime_get_noresume(audio->dev); + pm_runtime_get_noresume(audio->dev); + sub->runtime->frame_bits = 64; + sub->runtime->buffer_size = 8192; + sub->runtime->period_size = 512; + return sub; +} + +static void dcpaud_start_on_held_link(struct kunit *test) +{ + struct platform_device *pdev; + struct dcp_audio *audio = dcpaud_link_setup(test, &pdev); + struct snd_pcm_substream *sub = dcpaud_link_test_held(test, audio); + + /* Its element: the stream's DMA follows the silence, no link call */ + KUNIT_ASSERT_EQ(test, dcp_pcm_native_start(sub, SNDRV_PCM_TRIGGER_START), 0); + KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, "Q"); + KUNIT_EXPECT_FALSE(test, audio->silence_running); + KUNIT_EXPECT_TRUE(test, audio->idle_set); + KUNIT_EXPECT_TRUE(test, audio->link_started); + pm_runtime_put_noidle(audio->dev); +} + +static void dcpaud_start_replaces_held_link(struct kunit *test) +{ + struct platform_device *pdev; + struct dcp_audio *audio = dcpaud_link_setup(test, &pdev); + struct snd_pcm_substream *sub = dcpaud_link_test_held(test, audio); + + /* Another element: the held link ends, the stream's is configured, prepared and started */ + memset(&audio->link_cookie, 1, sizeof(audio->link_cookie)); + memset(&audio->selected_cookie, 2, sizeof(audio->selected_cookie)); + KUNIT_ASSERT_EQ(test, dcp_pcm_native_start(sub, SNDRV_PCM_TRIGGER_START), 0); + KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, "iSCGPQT"); + KUNIT_EXPECT_FALSE(test, audio->idle_set); + KUNIT_EXPECT_EQ(test, memcmp(&audio->link_cookie, &audio->selected_cookie, + sizeof(audio->link_cookie)), 0); + KUNIT_EXPECT_TRUE(test, audio->link_started); + pm_runtime_put_noidle(audio->dev); +} + +static void dcpaud_start_after_held_link_went(struct kunit *test) +{ + struct platform_device *pdev; + struct dcp_audio *audio = dcpaud_link_setup(test, &pdev); + struct snd_pcm_substream *sub = dcpaud_link_test_held(test, audio); + + /* The held link's service went (unplug): the stream gets a link of its own */ + dcpaud_link_current->link_live = false; + KUNIT_ASSERT_EQ(test, dcp_pcm_native_start(sub, SNDRV_PCM_TRIGGER_START), 0); + KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, "iGPQT"); + KUNIT_EXPECT_FALSE(test, audio->silence_running); + KUNIT_EXPECT_TRUE(test, audio->link_started); + pm_runtime_put_noidle(audio->dev); + KUNIT_EXPECT_EQ(test, atomic_read(&audio->dev->power.usage_count), 0); +} + +static void dcpaud_failed_start_keeps_held_link(struct kunit *test) +{ + struct platform_device *pdev; + struct dcp_audio *audio = dcpaud_link_setup(test, &pdev); + struct snd_pcm_substream *sub = dcpaud_link_test_held(test, audio); + + /* Unplugged before the swap: START fails, its STOP leaves the silence */ + audio->connected = false; + sub->runtime->channels = 2; + sub->runtime->dma_area = kunit_kzalloc(test, 64, GFP_KERNEL); + sub->runtime->dma_bytes = 64; + KUNIT_ASSERT_EQ(test, dcp_pcm_native_start(sub, SNDRV_PCM_TRIGGER_START), -ENXIO); + KUNIT_ASSERT_EQ(test, dcp_pcm_native_trigger(sub, SNDRV_PCM_TRIGGER_STOP), 0); + KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, ""); + KUNIT_EXPECT_FALSE(test, audio->dma_cleared); + KUNIT_EXPECT_TRUE(test, audio->silence_running); + /* Only the held silence's reference is left */ + KUNIT_EXPECT_EQ(test, atomic_read(&audio->dev->power.usage_count), 1); + pm_runtime_put_noidle(audio->dev); +} + +static void dcpaud_close_keeps_held_silence(struct kunit *test) +{ + struct platform_device *pdev; + struct dcp_audio *audio = dcpaud_link_setup(test, &pdev); + 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); + + KUNIT_ASSERT_NOT_NULL(test, sub); + KUNIT_ASSERT_NOT_NULL(test, runtime); + KUNIT_ASSERT_NOT_NULL(test, pcm); + pcm->private_data = audio; + pcm->nonatomic = true; + sub->pcm = pcm; + sub->runtime = runtime; + KUNIT_ASSERT_EQ(test, snd_dmaengine_pcm_open(sub, &dcpaud_link_current->channel), 0); + audio->chan = &dcpaud_link_current->channel; + audio->link_prepared = true; + audio->link_started = true; + audio->silence_running = true; + audio->idle_set = true; + pm_runtime_get_noresume(audio->dev); + + /* The PCM's terminate leaves the idle silence: nothing queued, no link call */ + KUNIT_ASSERT_EQ(test, dcp_pcm_native_close(sub), 0); + KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, "XD"); + KUNIT_EXPECT_TRUE(test, audio->silence_running); + KUNIT_EXPECT_TRUE(test, audio->idle_set); + KUNIT_EXPECT_TRUE(test, audio->link_started); + KUNIT_EXPECT_EQ(test, atomic_read(&audio->dev->power.usage_count), 1); + pm_runtime_put_noidle(audio->dev); +} + static struct kunit_case dcpaud_link_cases[] = { KUNIT_CASE(dcpaud_link_release_instance), + KUNIT_CASE(dcpaud_disconnect_sync_completes_live_owner), + KUNIT_CASE(dcpaud_gone_service_drops_link), + KUNIT_CASE(dcpaud_stop_holds_link), + KUNIT_CASE(dcpaud_stuck_silence_keeps_pm), + KUNIT_CASE(dcpaud_held_stop_without_service), + KUNIT_CASE(dcpaud_start_on_held_link), + KUNIT_CASE(dcpaud_start_replaces_held_link), + KUNIT_CASE(dcpaud_start_after_held_link_went), + KUNIT_CASE(dcpaud_failed_start_keeps_held_link), + KUNIT_CASE(dcpaud_close_keeps_held_silence), {} }; diff --git a/drivers/gpu/drm/apple/audio.c b/drivers/gpu/drm/apple/audio.c index ccb10dcf3768c..44c228deb2a89 100644 --- a/drivers/gpu/drm/apple/audio.c +++ b/drivers/gpu/drm/apple/audio.c @@ -14,7 +14,10 @@ #include #include #include +#include #include +#include +#include #include #include #include @@ -43,6 +46,22 @@ */ #define DCPAUD_PERIODS_MAX 16 +/* + * The silence a held link plays between streams, as the SIO channel's idle + * transfer: SIO keeps up to four periods in flight, and a stream's first + * period follows those (about 85 ms). Its four descriptor slots stay taken + * while the link is held, next to a stream's sixteen at most. + */ +#define DCPAUD_SILENCE_PERIOD SZ_4K +#define DCPAUD_SILENCE_BYTES (4 * DCPAUD_SILENCE_PERIOD) + +/* + * A stream that ends into the held link's silence leaves up to four periods + * issued, and dmaengine_synchronize() waits 500 ms for them before it stops + * the channel. Streams on a port that holds links keep to 50 ms periods. + */ +#define DCPAUD_HELD_PERIOD_MAX_US 50000 + struct dcp_audio { struct device *dev; struct device *dcp_dev; @@ -59,6 +78,8 @@ struct dcp_audio { bool dcp_connected; /// dcp status keep for delayed initialization bool connected; unsigned int connection_cookie; + /* When the sink was last connected */ + ktime_t connected_at; struct snd_pcm_chmap_elem selected_chmap; struct dcp_sound_cookie selected_cookie; @@ -81,6 +102,22 @@ struct dcp_audio { struct mutex link_lock; bool link_prepared; bool link_started; + /* The element the link was prepared with */ + struct dcp_sound_cookie link_cookie; + /* START issued the stream's DMA */ + bool stream_dma; + /* STOP left the DMA playing the cleared buffer, see dcp_pcm_native_sync_stop() */ + bool dma_cleared; + /* Drops a held link whose service went, see dcpaud_disconnect() */ + struct work_struct stale_work; + /* The held link's silence, see dcpaud_silence_start() */ + void *silence; + dma_addr_t silence_addr; + bool silence_running; + /* The silence is the channel's idle transfer, see dcpaud_idle() */ + bool idle_set; + /* It holds a PM reference: its removal failed, see dcpaud_silence_stop() */ + bool idle_pm; #endif }; @@ -291,7 +328,7 @@ static int dcpaud_init_dma(struct dcp_audio *dcpaud) { struct dma_chan *chan; if (dcpaud->chan) - return 0; + goto silence; chan = of_dma_request_slave_channel(dcpaud->dev->of_node, "tx"); /* squelch dma channel request errors, the driver will try again alter */ @@ -311,6 +348,14 @@ static int dcpaud_init_dma(struct dcp_audio *dcpaud) dcpaud->chan->device->dev, 1024 * 1024, SIZE_MAX); +silence: +#if IS_ENABLED(CONFIG_DRM_APPLE_DCP_T6030_AUDIO) + /* Without it a stopped link is not held, see dcpaud_silence_start(). */ + if (dcpaud_native_link(dcpaud) && !dcpaud->silence) + dcpaud->silence = dma_alloc_coherent(dcpaud->chan->device->dev, + DCPAUD_SILENCE_BYTES, + &dcpaud->silence_addr, GFP_KERNEL); +#endif return 0; } @@ -599,14 +644,206 @@ struct snd_pcm_ops dcp_playback_ops = { * * prepare: the DMA channel configured, then the link prepared, once; * START: the cyclic DMA issued, then the link started; - * STOP: the DMA terminated; - * sync_stop: the DMA drained, then the link stopped and completed; + * STOP: the DMA terminated, or for a held link (below) left playing + * the cleared buffer; + * sync_stop: the DMA drained, then the link stopped and completed, or the + * held link's silence taking over from the DMA; * hw_free: a link prepared but never started, completed. * - * Every link method carries the selected element's link data. A link of an - * earlier connection went with its service: its state is dropped unsent. - * The audio service itself is open only while the PCM is. + * Every link method carries its element's link data. A link of an earlier + * connection went with its service: its state is dropped unsent. The audio + * service itself is open while the PCM is, or while its link is held. + */ +/* + * The sinks behind the J514S HDMI port (PS190 converter and TV) and its + * Type-C ports mute the audio of a link that stops and starts again with an + * unchanged format, until the DP link trains again: only a different channel + * layout brings it back. So a stereo link, once started, is held between + * streams, as macOS holds it: silence follows the stopped stream's DMA and + * the link keeps running until the format changes or the display goes off + * (dcpaud_release_link()). + * + * The link must not run dry either: a gap of 20 ms in its samples mutes the + * sink the same way. The silence is the SIO channel's idle transfer, which + * the channel issues whenever no stream's DMA does, in descriptor order: + * a stream's DMA takes over from it and hands back to it without a gap. + */ +/* + * A change of sample rate or sample size with an unchanged channel layout + * replaces the link with a new one, which this sink mutes just as it does a + * same-format restart. PipeWire changes the rate to follow its streams + * (44.1 kHz music, 48 kHz video). macOS runs the link at one nominal rate + * and format and converts the streams to it; so does this driver while it + * holds links: the PCM offers one rate (48 kHz, else 44.1 or 32 kHz) and one + * sample size (24 bits, else 16), both of one element that takes them in + * stereo, and userspace resamples. Channel layouts stay free: a layout + * change is heard. + * + * The pair covers every layout, not only the stereo that links are held in: + * userspace commonly sets the sample format before the channel count, and a + * pair applied only once a stream is stereo let it set a format that then + * ruled stereo out (PipeWire built no HDMI sink). + */ +static int dcpaud_pin_rate_format(struct dcp_audio *dcpaud, + struct snd_pcm_runtime *runtime) +{ + static const unsigned int rates[] = { 48000, 44100, 32000 }; + static const snd_pcm_format_t formats[] = { + SNDRV_PCM_FORMAT_S24_LE, SNDRV_PCM_FORMAT_S16_LE, + }; + struct dcp_sound_format_mask sieve = { + .formats = SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S24_LE, + .rates = GENMASK(snd_pcm_known_rates.count - 1, 0), + .nchans = BIT(2), + }; + struct dcp_sound_format_mask hits = { 0, 0, 0 }; + struct dcp_parse_ctx elements = { + .dcp = dev_get_drvdata(dcpaud->dcp_dev), + .blob = dcpaud->elements + 4, + .len = dcpaud_elements_size(dcpaud) - 4, + .pos = 0, + .v14_7_audio = dcpaud->native_link, + }; + unsigned int rate = 0, rate_bit = 0, i, j; + int ret; + + parse_sound_constraints(&elements, &sieve, &hits); + for (i = 0; i < ARRAY_SIZE(rates) && !rate; i++) + for (j = 0; j < snd_pcm_known_rates.count; j++) + if (snd_pcm_known_rates.list[j] == rates[i] && (hits.rates & BIT(j))) { + rate = rates[i]; + rate_bit = j; + } + if (!rate) + return 0; /* no stereo LPCM element: the sink decides */ + + /* The sample size of an element that takes that rate in stereo */ + sieve.rates = BIT(rate_bit); + elements.pos = 0; + parse_sound_constraints(&elements, &sieve, &hits); + for (i = 0; i < ARRAY_SIZE(formats); i++) + if (hits.formats & pcm_format_to_bits(formats[i])) + break; + if (i == ARRAY_SIZE(formats)) + return 0; + + dev_dbg(dcpaud->dev, "held links run at %u Hz, format %d\n", rate, + (__force int)formats[i]); + ret = snd_pcm_hw_constraint_single(runtime, SNDRV_PCM_HW_PARAM_RATE, rate); + if (ret < 0) + return ret; + ret = snd_pcm_hw_constraint_mask64(runtime, SNDRV_PCM_HW_PARAM_FORMAT, + pcm_format_to_bits(formats[i])); + return ret < 0 ? ret : 0; +} + +/* + * Seen once, at boot: a link started about 0.2 s after the display came on + * was not heard, and holding it kept it silent; the next connection's first + * link, started later, was heard. While links are held, the first link of a + * connection starts once the sink has been up this long, a margin rather + * than a measured bound. One link per stream needs no wait: the next stream + * starts a new link. + */ +#define DCPAUD_SINK_SETTLE_MS 1500 + +/* + * Sets or removes the silence as the channel's idle transfer. Removed, it + * stops the channel unless a stream's DMA runs, once its periods played. */ +static int dcpaud_idle(struct dcp_audio *dcpaud, bool on) +{ + struct apple_sio_idle idle = { + .addr = dcpaud->silence_addr, + .len = on ? DCPAUD_SILENCE_BYTES : 0, + .period_len = DCPAUD_SILENCE_PERIOD, + }; + struct dma_slave_config config = { + .direction = DMA_MEM_TO_DEV, + .peripheral_config = &idle, + .peripheral_size = sizeof(idle), + }; + int ret; + + ret = dmaengine_slave_config(dcpaud->chan, &config); + /* Not removed: the channel may still read the silence. */ + if (ret) + return ret; + dcpaud->idle_set = on; + if (!on && dcpaud->idle_pm) { + dcpaud->idle_pm = false; + pm_runtime_mark_last_busy(dcpaud->dev); + pm_runtime_put_autosuspend(dcpaud->dev); + } + return 0; +} + +/* + * The held link's silence carries the PM reference of the stream. It is set + * before the stream's DMA ends, which it then follows without a gap. + */ +static int dcpaud_silence_start(struct dcp_audio *dcpaud) +{ + int ret; + + lockdep_assert_held(&dcpaud->link_lock); + if (!dcpaud->silence) + return -ENOMEM; + if (!dcpaud->idle_set) { + ret = dcpaud_idle(dcpaud, true); + if (ret) + return ret; + } + dcpaud->silence_running = true; + dev_dbg(dcpaud->dev, "link held, playing silence\n"); + return 0; +} + +/* The link ends: so does its silence, which a stream may have taken over from. */ +static void dcpaud_silence_stop(struct dcp_audio *dcpaud) +{ + lockdep_assert_held(&dcpaud->link_lock); + if (dcpaud->idle_set) + dcpaud_idle(dcpaud, false); + if (!dcpaud->silence_running) + return; + dcpaud->silence_running = false; + /* A channel that may still read the silence keeps the device up. */ + if (dcpaud->idle_set && !dcpaud->idle_pm) { + dcpaud->idle_pm = true; + return; + } + pm_runtime_mark_last_busy(dcpaud->dev); + pm_runtime_put_autosuspend(dcpaud->dev); + dev_dbg(dcpaud->dev, "held link's silence stopped\n"); +} + +static void dcpaud_link_teardown(struct dcp_audio *dcpaud); + +/* + * A held link went with its audio service (sink change, unplug): its silence + * stops and its state is dropped unsent, so that a new service's stream + * prepares and starts its own link. + */ +static void dcpaud_drop_stale(struct dcp_audio *dcpaud) +{ + lockdep_assert_held(&dcpaud->link_lock); + if ((dcpaud->silence_running || dcpaud->link_prepared || dcpaud->link_started) && + !dcp_audiosrv_link_live(dcpaud->dcp_dev)) { + dev_dbg(dcpaud->dev, "held link went with its service\n"); + dcpaud_link_teardown(dcpaud); + } +} + +static void dcpaud_stale_work(struct work_struct *work) +{ + struct dcp_audio *dcpaud = container_of(work, struct dcp_audio, stale_work); + + guard(mutex)(&dcpaud->link_lock); + if (!dcpaud->dma_cleared) + dcpaud_drop_stale(dcpaud); +} + static int dcp_pcm_native_open(struct snd_pcm_substream *substream) { struct dcp_audio *dcpaud = substream->pcm->private_data; @@ -622,8 +859,27 @@ static int dcp_pcm_native_open(struct snd_pcm_substream *substream) } ret = dcp_pcm_open(substream); - if (ret && live) - dcp_audiosrv_close(dcpaud->dcp_dev); + if (!ret && live && !dcpaud->open_unplugged) { + ret = dcpaud_pin_rate_format(dcpaud, substream->runtime); + if (!ret) + ret = snd_pcm_hw_constraint_minmax(substream->runtime, + SNDRV_PCM_HW_PARAM_PERIOD_TIME, 0, + DCPAUD_HELD_PERIOD_MAX_US); + /* Positive: the constraint narrowed the period time. */ + if (ret > 0) + ret = 0; + if (ret) + dcp_pcm_close(substream); + } + if (ret && live) { + bool held; + + /* A held link keeps its service open. */ + scoped_guard(mutex, &dcpaud->link_lock) + held = dcpaud->silence_running; + if (!held) + dcp_audiosrv_close(dcpaud->dcp_dev); + } return ret; } @@ -632,45 +888,73 @@ static int dcp_pcm_native_close(struct snd_pcm_substream *substream) { struct dcp_audio *dcpaud = substream->pcm->private_data; int ret = dcp_pcm_close(substream); + bool held; - dcp_audiosrv_close(dcpaud->dcp_dev); + /* + * A held link keeps its service open. Closing the PCM terminated the + * channel's transfer, which leaves the idle one, the silence, running. + */ + scoped_guard(mutex, &dcpaud->link_lock) { + dcpaud_drop_stale(dcpaud); + held = dcpaud->silence_running; + } + if (!held) + dcp_audiosrv_close(dcpaud->dcp_dev); return ret; } /* - * Stop and complete the link held, on the open audio service (@live), or - * drop both unsent with a service that went away. + * A PCM disconnect does not end its firmware service. Complete the link on + * that instance while it is published; a removed instance took it along. + * The service can disappear after the check, so -ENODEV during teardown is normal. */ -static void dcpaud_link_teardown(struct dcp_audio *dcpaud, bool live) +static void dcpaud_link_teardown(struct dcp_audio *dcpaud) { + bool live; int ret; lockdep_assert_held(&dcpaud->link_lock); + /* A held link's silence ends with it. */ + dcpaud_silence_stop(dcpaud); + live = (dcpaud->link_prepared || dcpaud->link_started) && + dcp_audiosrv_link_live(dcpaud->dcp_dev); if (dcpaud->link_started && live) { ret = dcp_audiosrv_link(dcpaud->dcp_dev, DCP_AUDIO_LINK_STOP, - &dcpaud->selected_cookie); - if (ret) + &dcpaud->link_cookie); + if (ret && ret != -ENODEV) dev_err(dcpaud->dev, "audio link stop failed: %d\n", ret); } dcpaud->link_started = false; if (dcpaud->link_prepared && live) { ret = dcp_audiosrv_link(dcpaud->dcp_dev, DCP_AUDIO_LINK_COMPLETE, - &dcpaud->selected_cookie); - if (ret) + &dcpaud->link_cookie); + if (ret && ret != -ENODEV) dev_err(dcpaud->dev, "audio link complete failed: %d\n", ret); } dcpaud->link_prepared = false; } -/* New parameters select a new element: the link of the old one ends first. */ +/* + * New parameters select a new element: the link of the old one ends first. + * A held link stays until a stream of another element starts (it may be + * userspace probing), and its channel stays configured. + */ static int dcp_pcm_native_hw_params(struct snd_pcm_substream *substream, struct snd_pcm_hw_params *params) { struct dcp_audio *dcpaud = substream->pcm->private_data; + bool up = !dcpaud->open_unplugged && dcpaud_connection_up(dcpaud); + int ret; - scoped_guard(mutex, &dcpaud->link_lock) - dcpaud_link_teardown(dcpaud, dcpaud_connection_up(dcpaud)); + scoped_guard(mutex, &dcpaud->link_lock) { + dcpaud_drop_stale(dcpaud); + if (up && dcpaud->silence_running) { + ret = dcpaud_select_cookie(dcpaud, params); + return ret < 0 ? ret : ret ? 0 : -EINVAL; + } + dcpaud_link_teardown(dcpaud); + } return dcp_pcm_hw_params(substream, params); } @@ -680,14 +964,15 @@ static int dcp_pcm_native_hw_free(struct snd_pcm_substream *substream) struct dcp_audio *dcpaud = substream->pcm->private_data; guard(mutex)(&dcpaud->link_lock); - dcpaud_link_teardown(dcpaud, dcpaud_connection_up(dcpaud)); + if (!dcpaud->silence_running) + dcpaud_link_teardown(dcpaud); return 0; } -static int dcp_pcm_native_prepare(struct snd_pcm_substream *substream) +/* The SIO channel is configured again before every link prepare. */ +static int dcpaud_link_prepare(struct dcp_audio *dcpaud, struct snd_pcm_substream *substream) { - struct dcp_audio *dcpaud = substream->pcm->private_data; struct dma_slave_config slave_config = { .direction = DMA_MEM_TO_DEV, /* One 32-bit DPA entry per sample word, as in hw_params. */ @@ -695,6 +980,23 @@ static int dcp_pcm_native_prepare(struct snd_pcm_substream *substream) }; int ret; + lockdep_assert_held(&dcpaud->link_lock); + ret = dmaengine_slave_config(snd_dmaengine_pcm_get_chan(substream), &slave_config); + if (ret) + return ret; + ret = dcp_audiosrv_link(dcpaud->dcp_dev, DCP_AUDIO_LINK_PREPARE, + &dcpaud->selected_cookie); + if (ret) + return ret; + dcpaud->link_prepared = true; + dcpaud->link_cookie = dcpaud->selected_cookie; + return 0; +} + +static int dcp_pcm_native_prepare(struct snd_pcm_substream *substream) +{ + struct dcp_audio *dcpaud = substream->pcm->private_data; + if (dcpaud->open_unplugged) return 0; @@ -702,44 +1004,73 @@ static int dcp_pcm_native_prepare(struct snd_pcm_substream *substream) return -ENXIO; guard(mutex)(&dcpaud->link_lock); + dcpaud_drop_stale(dcpaud); if (dcpaud->link_prepared) return 0; - - /* The SIO channel is configured again before every link prepare. */ - ret = dmaengine_slave_config(snd_dmaengine_pcm_get_chan(substream), - &slave_config); - if (ret) - return ret; - - ret = dcp_audiosrv_link(dcpaud->dcp_dev, DCP_AUDIO_LINK_PREPARE, - &dcpaud->selected_cookie); - if (ret) - return ret; - - dcpaud->link_prepared = true; - return 0; + return dcpaud_link_prepare(dcpaud, substream); } static int dcp_pcm_native_start(struct snd_pcm_substream *substream, int cmd) { struct dcp_audio *dcpaud = substream->pcm->private_data; + s64 up_ms; + bool held; int ret; if (!dcpaud_connection_up(dcpaud)) return -ENXIO; + /* The first link of the connection waits for the sink to settle. */ + scoped_guard(mutex, &dcpaud->data_lock) + up_ms = ktime_ms_delta(ktime_get(), dcpaud->connected_at); + if (!READ_ONCE(dcpaud->link_started) && up_ms >= 0 && + up_ms < DCPAUD_SINK_SETTLE_MS) { + dev_dbg(dcpaud->dev, "sink up %lld ms, link waits\n", up_ms); + msleep(DCPAUD_SINK_SETTLE_MS - up_ms); + } + + guard(mutex)(&dcpaud->link_lock); + dcpaud_drop_stale(dcpaud); + /* + * The stream's DMA follows the held link's silence, which stays the + * channel's idle transfer, and takes over the PM reference. + */ + held = dcpaud->silence_running; + if (held) { + dcpaud->silence_running = false; + pm_runtime_mark_last_busy(dcpaud->dev); + pm_runtime_put_autosuspend(dcpaud->dev); + } + + /* + * A held link of another element ends, its silence with it, and the + * stream's is prepared; so is a stream's whose prepare found a link that + * has gone since. + */ + if (!dcpaud->link_prepared || + memcmp(&dcpaud->link_cookie, &dcpaud->selected_cookie, sizeof(dcpaud->link_cookie))) { + if (dcpaud->link_prepared) + dcpaud_link_teardown(dcpaud); + ret = dcpaud_link_prepare(dcpaud, substream); + if (ret) + return ret; + dev_dbg(dcpaud->dev, "link prepared at START\n"); + } else if (held) { + dev_dbg(dcpaud->dev, "stream follows the held link's silence\n"); + } + /* The first period descriptor is issued before the link starts. */ ret = snd_dmaengine_pcm_trigger(substream, cmd); if (ret) return ret; + WRITE_ONCE(dcpaud->stream_dma, true); - guard(mutex)(&dcpaud->link_lock); if (!dcpaud->link_prepared) return -EBADFD; if (dcpaud->link_started) return 0; ret = dcp_audiosrv_link(dcpaud->dcp_dev, DCP_AUDIO_LINK_START, - &dcpaud->selected_cookie); + &dcpaud->link_cookie); dcpaud->link_started = !ret; return ret; } @@ -761,8 +1092,21 @@ static int dcp_pcm_native_trigger(struct snd_pcm_substream *substream, int cmd) return dcp_pcm_native_start(substream, cmd); case SNDRV_PCM_TRIGGER_STOP: + /* + * The DMA plays the cleared buffer on, with the stream's PM + * reference, until sync_stop hands over to the held link's + * silence: the link must not run dry meanwhile. + */ + if (READ_ONCE(dcpaud->stream_dma) && substream->runtime->channels == 2 && + substream->runtime->dma_area) { + memset(substream->runtime->dma_area, 0, substream->runtime->dma_bytes); + WRITE_ONCE(dcpaud->dma_cleared, true); + return 0; + } + fallthrough; case SNDRV_PCM_TRIGGER_SUSPEND: - ret = snd_dmaengine_pcm_trigger(substream, cmd); + /* A START that issued no DMA leaves a held link's silence alone. */ + ret = READ_ONCE(dcpaud->stream_dma) ? snd_dmaengine_pcm_trigger(substream, cmd) : 0; pm_runtime_mark_last_busy(dcpaud->dev); __pm_runtime_put_autosuspend(dcpaud->dev); return ret; @@ -777,11 +1121,34 @@ static int dcp_pcm_native_sync_stop(struct snd_pcm_substream *substream) struct dcp_audio *dcpaud = substream->pcm->private_data; int ret; + WRITE_ONCE(dcpaud->stream_dma, false); + if (READ_ONCE(dcpaud->dma_cleared)) { + guard(mutex)(&dcpaud->link_lock); + WRITE_ONCE(dcpaud->dma_cleared, false); + /* + * The silence set first, the channel hands over to it in + * descriptor order when the stream's DMA ends; synchronize + * returns once the stream's issued periods have played. + */ + if (dcpaud->silence_running || !dcpaud->link_started || + !dcp_audiosrv_link_live(dcpaud->dcp_dev) || dcpaud_silence_start(dcpaud)) { + dmaengine_terminate_async(dcpaud->chan); + dmaengine_synchronize(dcpaud->chan); + dcpaud_link_teardown(dcpaud); + pm_runtime_mark_last_busy(dcpaud->dev); + pm_runtime_put_autosuspend(dcpaud->dev); + return 0; + } + dmaengine_terminate_async(dcpaud->chan); + dmaengine_synchronize(dcpaud->chan); + return 0; + } + /* SIO TERMINATE acknowledged and the issued periods reported back. */ ret = snd_dmaengine_pcm_sync_stop(substream); guard(mutex)(&dcpaud->link_lock); - dcpaud_link_teardown(dcpaud, dcpaud_connection_up(dcpaud)); + dcpaud_link_teardown(dcpaud); return ret; } @@ -808,9 +1175,13 @@ void dcpaud_release_link(struct platform_device *pdev, const u64 *gen) /* Only that instance's link: a later instance's PCM keeps its own. */ if (gen && !dcp_audiosrv_link_on(dcpaud->dcp_dev, *gen)) return; + dcpaud_silence_stop(dcpaud); + /* A stopped stream still playing its cleared buffer: sync_stop finishes it. */ + if (dcpaud->chan && READ_ONCE(dcpaud->dma_cleared)) + dmaengine_terminate_async(dcpaud->chan); if (dcpaud->chan) dmaengine_synchronize(dcpaud->chan); - dcpaud_link_teardown(dcpaud, true); + dcpaud_link_teardown(dcpaud); } static const struct snd_pcm_ops dcp_native_playback_ops = { @@ -901,8 +1272,10 @@ static void dcpaud_report_hotplug(struct dcp_audio *dcpaud, bool connected) } dcpaud->connected = connected; - if (connected) + if (connected) { dcpaud->connection_cookie++; + dcpaud->connected_at = ktime_get(); + } mutex_unlock(&dcpaud->data_lock); snd_jack_report(dcpaud->jack, connected ? SND_JACK_AVOUT : 0); @@ -1006,6 +1379,11 @@ void dcpaud_disconnect(struct platform_device *pdev) mutex_lock(&dcpaud->data_lock); dcpaud_report_hotplug(dcpaud, false); +#if IS_ENABLED(CONFIG_DRM_APPLE_DCP_T6030_AUDIO) + /* The service may go without a release, and a held link with it. */ + if (dcpaud->native_link) + schedule_work(&dcpaud->stale_work); +#endif } static int dcpaud_comp_bind(struct device *dev, struct device *main, void *data) @@ -1108,6 +1486,30 @@ static void dcpaud_comp_unbind(struct device *dev, struct device *main, /* snd_card_free_when_closed() checks for NULL */ snd_card_free_when_closed(dcpaud->card); +#if IS_ENABLED(CONFIG_DRM_APPLE_DCP_T6030_AUDIO) + if (dcpaud->native_link) { + bool held; + + cancel_work_sync(&dcpaud->stale_work); + /* A held link ends with the card; an open PCM's close finds it gone. */ + scoped_guard(mutex, &dcpaud->link_lock) { + held = dcpaud->silence_running; + if (held) + dcpaud_link_teardown(dcpaud); + else if (dcpaud->idle_set) + dcpaud_idle(dcpaud, false); + if (dcpaud->silence && dcpaud->idle_set) + dev_err(dcpaud->dev, "silence kept: its DMA channel did not stop\n"); + else if (dcpaud->silence) + dma_free_coherent(dcpaud->chan->device->dev, DCPAUD_SILENCE_BYTES, + dcpaud->silence, dcpaud->silence_addr); + dcpaud->silence = NULL; + } + if (held) + dcp_audiosrv_close(dcpaud->dcp_dev); + } +#endif + if (dcpaud->dma_link) device_link_del(dcpaud->dma_link); } @@ -1139,6 +1541,7 @@ static int dcpaud_probe(struct platform_device *pdev) mutex_init(&dcpaud->data_lock); #if IS_ENABLED(CONFIG_DRM_APPLE_DCP_T6030_AUDIO) mutex_init(&dcpaud->link_lock); + INIT_WORK(&dcpaud->stale_work, dcpaud_stale_work); #endif INIT_DELAYED_WORK(&dcpaud->void_work, dcpaud_void_work); platform_set_drvdata(pdev, dcpaud); diff --git a/drivers/gpu/drm/apple/audio.h b/drivers/gpu/drm/apple/audio.h index 88e9610363234..79bee44bcefbc 100644 --- a/drivers/gpu/drm/apple/audio.h +++ b/drivers/gpu/drm/apple/audio.h @@ -35,6 +35,8 @@ int dcp_audiosrv_link(struct device *dev, enum dcp_audio_link op, void dcpaud_release_link(struct platform_device *pdev, const u64 *gen); /* Whether the link prepared last is on instance @gen, still the open one */ bool dcp_audiosrv_link_on(struct device *dev, u64 gen); +/* The prepared link still belongs to the client of the published instance. */ +bool dcp_audiosrv_link_live(struct device *dev); #endif #endif /* __AUDIO_H__ */ diff --git a/drivers/gpu/drm/apple/av-v14-test.c b/drivers/gpu/drm/apple/av-v14-test.c index 362bb77db6465..433530763af7f 100644 --- a/drivers/gpu/drm/apple/av-v14-test.c +++ b/drivers/gpu/drm/apple/av-v14-test.c @@ -29,9 +29,11 @@ struct av_v14_test { bool successor_during_release; u32 link_status; u8 link_body[64]; + bool replace_during_prepare; }; static struct av_v14_test *av_v14_current; +static void av_v14_replace(struct av_v14_test *t); static int av_v14_call(struct apple_epic_service *srv, u32 command, const void *body, size_t body_len, size_t body_pad, void *reply, size_t reply_len) @@ -50,6 +52,10 @@ static int av_v14_call(struct apple_epic_service *srv, u32 command, const void * if (body_len == sizeof(t->link_body)) { memcpy(t->link_body, body, body_len); put_unaligned_le32(t->link_status, reply + 48); + if (command == t->asrv->cmds.prepare && t->replace_during_prepare) { + t->replace_during_prepare = false; + av_v14_replace(t); + } } return 0; } @@ -59,8 +65,6 @@ static void av_v14_card_disconnect(struct platform_device *pdev) av_v14_current->disconnects++; } -static void av_v14_replace(struct av_v14_test *t); - static void av_v14_release_link(struct platform_device *pdev, const u64 *gen) { struct av_v14_test *t = av_v14_current; @@ -304,7 +308,9 @@ static void av_v14_link_instance(struct kunit *test) old = t->asrv->gen; KUNIT_EXPECT_EQ(test, dcp_audiosrv_link(t->dcp->dev, DCP_AUDIO_LINK_PREPARE, &cookie), 0); KUNIT_EXPECT_TRUE(test, dcp_audiosrv_link_on(t->dcp->dev, old)); + KUNIT_EXPECT_TRUE(test, dcp_audiosrv_link_live(t->dcp->dev)); av_v14_replace(t); + KUNIT_EXPECT_FALSE(test, dcp_audiosrv_link_live(t->dcp->dev)); KUNIT_EXPECT_EQ(test, dcp_audiosrv_open(t->dcp->dev), 0); KUNIT_ASSERT_EQ(test, t->ncalls, 3U); KUNIT_EXPECT_EQ(test, dcp_audiosrv_link(t->dcp->dev, DCP_AUDIO_LINK_STOP, &cookie), -ENODEV); @@ -315,10 +321,68 @@ static void av_v14_link_instance(struct kunit *test) KUNIT_EXPECT_FALSE(test, dcp_audiosrv_link_on(t->dcp->dev, t->asrv->gen)); KUNIT_EXPECT_EQ(test, dcp_audiosrv_link(t->dcp->dev, DCP_AUDIO_LINK_PREPARE, &cookie), 0); KUNIT_EXPECT_TRUE(test, dcp_audiosrv_link_on(t->dcp->dev, t->asrv->gen)); + KUNIT_EXPECT_TRUE(test, dcp_audiosrv_link_live(t->dcp->dev)); KUNIT_EXPECT_EQ(test, dcp_audiosrv_link(t->dcp->dev, DCP_AUDIO_LINK_COMPLETE, &cookie), 0); KUNIT_EXPECT_EQ(test, t->ncalls, 5U); } +static void av_v14_prepare_rejects_unpublished_open(struct kunit *test) +{ + struct av_v14_test *t = av_v14_setup(test); + struct dcp_sound_cookie cookie = {}; + + KUNIT_ASSERT_EQ(test, dcp_audiosrv_open(t->dcp->dev), 0); + av_v14_replace(t); + /* Same slot and pointer, but the PCM still owns the old instance. */ + KUNIT_EXPECT_EQ(test, dcp_audiosrv_link(t->dcp->dev, DCP_AUDIO_LINK_PREPARE, &cookie), + -ENODEV); + KUNIT_EXPECT_EQ(test, t->ncalls, 1U); + KUNIT_EXPECT_EQ(test, t->asrv->link_gen, 0ULL); +} + +static void av_v14_prepare_replaced_during_reply(struct kunit *test) +{ + struct av_v14_test *t = av_v14_setup(test); + struct dcp_sound_cookie cookie = {}; + + KUNIT_ASSERT_EQ(test, dcp_audiosrv_open(t->dcp->dev), 0); + t->replace_during_prepare = true; + KUNIT_EXPECT_EQ(test, dcp_audiosrv_link(t->dcp->dev, DCP_AUDIO_LINK_PREPARE, &cookie), + -ENODEV); + KUNIT_EXPECT_EQ(test, t->ncalls, 2U); + KUNIT_EXPECT_EQ(test, t->asrv->link_gen, 0ULL); +} + +static void av_v14_failed_prepare_does_not_own_link(struct kunit *test) +{ + struct av_v14_test *t = av_v14_setup(test); + struct dcp_sound_cookie cookie = {}; + + KUNIT_ASSERT_EQ(test, dcp_audiosrv_open(t->dcp->dev), 0); + t->link_status = 0xe00002ea; + KUNIT_EXPECT_EQ(test, dcp_audiosrv_link(t->dcp->dev, DCP_AUDIO_LINK_PREPARE, &cookie), + -EIO); + KUNIT_EXPECT_EQ(test, t->ncalls, 2U); + KUNIT_EXPECT_EQ(test, t->asrv->link_gen, 0ULL); +} + +static void av_v14_failed_reprepare_drops_owner(struct kunit *test) +{ + struct av_v14_test *t = av_v14_setup(test); + struct dcp_sound_cookie cookie = {}; + u64 gen; + + KUNIT_ASSERT_EQ(test, dcp_audiosrv_open(t->dcp->dev), 0); + gen = t->asrv->gen; + KUNIT_ASSERT_EQ(test, dcp_audiosrv_link(t->dcp->dev, DCP_AUDIO_LINK_PREPARE, &cookie), 0); + KUNIT_ASSERT_EQ(test, dcp_audiosrv_link(t->dcp->dev, DCP_AUDIO_LINK_COMPLETE, &cookie), 0); + t->link_status = 0xe00002ea; + KUNIT_EXPECT_EQ(test, dcp_audiosrv_link(t->dcp->dev, DCP_AUDIO_LINK_PREPARE, &cookie), + -EIO); + KUNIT_EXPECT_FALSE(test, dcp_audiosrv_link_on(t->dcp->dev, gen)); + KUNIT_EXPECT_EQ(test, t->asrv->link_gen, 0ULL); +} + /* A Terminate from an instance that is not the published one does nothing. */ static void av_v14_terminate_unpublished(struct kunit *test) { @@ -403,6 +467,10 @@ static struct kunit_case av_v14_cases[] = { KUNIT_CASE_PARAM(av_v14_successor_during_release, av_native_gen_params), KUNIT_CASE_PARAM(av_v14_terminate_after_successor, av_native_gen_params), KUNIT_CASE_PARAM(av_v14_link_instance, av_native_gen_params), + KUNIT_CASE_PARAM(av_v14_prepare_rejects_unpublished_open, av_native_gen_params), + KUNIT_CASE_PARAM(av_v14_prepare_replaced_during_reply, av_native_gen_params), + KUNIT_CASE_PARAM(av_v14_failed_prepare_does_not_own_link, av_native_gen_params), + KUNIT_CASE_PARAM(av_v14_failed_reprepare_drops_owner, av_native_gen_params), KUNIT_CASE_PARAM(av_v14_terminate_unpublished, av_native_gen_params), KUNIT_CASE_PARAM(av_native_link_payload_status, av_native_gen_params), KUNIT_CASE_PARAM(av_desktop_service_admission, av_native_gen_params), diff --git a/drivers/gpu/drm/apple/av.c b/drivers/gpu/drm/apple/av.c index afb918fb58f89..5e11c704a86ef 100644 --- a/drivers/gpu/drm/apple/av.c +++ b/drivers/gpu/drm/apple/av.c @@ -388,6 +388,29 @@ bool dcp_audiosrv_link_on(struct device *dev, u64 gen) return asrv->open_srv && asrv->open_gen == gen && asrv->link_gen == gen; } +/* open_lock pins the client; the endpoint can replace its published instance. */ +static bool av_audiosrv_open_published(struct audiosrv_data *asrv) +{ + lockdep_assert_held(&asrv->open_lock); + guard(rwsem_read)(&asrv->srv_rwsem); + return asrv->open_srv && asrv->srv == asrv->open_srv && + asrv->gen == asrv->open_gen; +} + +bool dcp_audiosrv_link_live(struct device *dev) +{ + struct apple_dcp *dcp; + struct audiosrv_data *asrv; + + KUNIT_STATIC_STUB_REDIRECT(dcp_audiosrv_link_live, dev); + dcp = dev_get_drvdata(dev); + asrv = dcp->audiosrv; + if (!asrv) + return false; + guard(mutex)(&asrv->open_lock); + return asrv->link_gen == asrv->open_gen && av_audiosrv_open_published(asrv); +} + /* * The link commands of the 14.7 external role's audio service, the native * audio driver's: prepare, start, stop and complete all take the selected @@ -433,6 +456,11 @@ int dcp_audiosrv_link(struct device *dev, enum dcp_audio_link op, scoped_guard(mutex, &asrv->open_lock) { if (!asrv->open_srv) return -ENODEV; + if (op == DCP_AUDIO_LINK_PREPARE) { + asrv->link_gen = 0; + if (!av_audiosrv_open_published(asrv)) + return -ENODEV; + } /* A link ends on the instance it was prepared on; one gone took it along. */ if (op != DCP_AUDIO_LINK_PREPARE && asrv->link_gen != asrv->open_gen) return -ENODEV; @@ -445,8 +473,11 @@ int dcp_audiosrv_link(struct device *dev, enum dcp_audio_link op, dev_err(dev, "audiosrv: link method %u answered %#x\n", command, status); return -EIO; } - if (op == DCP_AUDIO_LINK_PREPARE) + if (op == DCP_AUDIO_LINK_PREPARE) { + if (!av_audiosrv_open_published(asrv)) + return -ENODEV; asrv->link_gen = asrv->open_gen; + } } return 0; } @@ -1010,9 +1041,28 @@ int avep_init(struct apple_dcp *dcp) #endif } + /* + * Only a processor whose port@0 links an audio device has DP/HDMI + * audio to start. Look before the firmware: a processor without one + * (an internal panel's, or one whose firmware has no audio service + * here, such as H17P) stays as it is without hdmi_audio, quietly. + */ + endpoint = of_graph_get_endpoint_by_regs(dev->of_node, 0, 0); + if (endpoint) { + audio_node = of_graph_get_remote_port_parent(endpoint); + of_node_put(endpoint); + } + if (!audio_node || !of_device_is_available(audio_node)) { + of_node_put(audio_node); + dev_dbg(dev, "No audio support\n"); + return 0; + } + audiosrv_data = devm_kzalloc(dcp->dev, sizeof(*audiosrv_data), GFP_KERNEL); - if (!audiosrv_data) + if (!audiosrv_data) { + of_node_put(audio_node); return -ENOMEM; + } init_rwsem(&audiosrv_data->srv_rwsem); mutex_init(&audiosrv_data->plug_lock); @@ -1037,6 +1087,7 @@ int avep_init(struct apple_dcp *dcp) break; } #endif + of_node_put(audio_node); dev_err(dcp->dev, "Audio not supported for firmware\n"); return -ENODEV; } @@ -1049,17 +1100,6 @@ int avep_init(struct apple_dcp *dcp) audiosrv_data->works_live = true; dcp->audiosrv = audiosrv_data; - endpoint = of_graph_get_endpoint_by_regs(dev->of_node, 0, 0); - if (endpoint) { - audio_node = of_graph_get_remote_port_parent(endpoint); - of_node_put(endpoint); - } - if (!audio_node || !of_device_is_available(audio_node)) { - of_node_put(audio_node); - dev_info(dev, "No audio support\n"); - return av_audiosrv_register_cleanup(dcp, audiosrv_data, NULL); - } - audio_pdev = of_find_device_by_node(audio_node); of_node_put(audio_node); if (!audio_pdev) { diff --git a/drivers/gpu/drm/apple/av.h b/drivers/gpu/drm/apple/av.h index 1d710b0f706f3..6d25d83d78d79 100644 --- a/drivers/gpu/drm/apple/av.h +++ b/drivers/gpu/drm/apple/av.h @@ -6,7 +6,7 @@ //int avep_audiosrv_startlink(struct apple_dcp *dcp, struct dcp_sound_cookie *cookie); //int avep_audiosrv_stoplink(struct apple_dcp *dcp); -/* dcp.c: the opt-in to DP/HDMI audio (appledrm.hdmi_audio). */ +/* dcp.c: DP/HDMI audio, on unless appledrm.hdmi_audio=0. */ extern bool hdmi_audio; #if IS_ENABLED(CONFIG_DRM_APPLE_AUDIO) diff --git a/drivers/gpu/drm/apple/connector.c b/drivers/gpu/drm/apple/connector.c index 282fc4d1295f5..2434eba0656ed 100644 --- a/drivers/gpu/drm/apple/connector.c +++ b/drivers/gpu/drm/apple/connector.c @@ -117,7 +117,14 @@ static void dcp_afk_debugfs_root(struct platform_device *pdev, int ep, struct de { #if IS_ENABLED(CONFIG_DRM_APPLE_DEBUG) struct dentry *entry = NULL; - struct apple_dcp *dcp = platform_get_drvdata(pdev); + struct apple_dcp *dcp; + + /* Type-C connectors can be registered before a pipeline is routed. */ + if (!pdev) + return; + dcp = platform_get_drvdata(pdev); + if (!dcp) + return; switch (ep) { case AV_ENDPOINT: @@ -153,7 +160,7 @@ void apple_connector_debugfs_init(struct drm_connector *connector, struct dentry switch (connector->connector_type) { case DRM_MODE_CONNECTOR_DisplayPort: case DRM_MODE_CONNECTOR_HDMIA: - dcp_afk_debugfs_root(apple_con->dcp, AV_ENDPOINT, root); + dcp_afk_debugfs_root(READ_ONCE(apple_con->dcp), AV_ENDPOINT, root); break; default: break; diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 38a117e769630..175891f8e4e6f 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -892,6 +892,7 @@ static inline void dcp_test_point(struct apple_dcp *dcp, enum dcp_test_point poi } #if IS_ENABLED(CONFIG_KUNIT) +void dcp_work_register_backlight(struct work_struct *work); void dcp_comp_unbind(struct device *dev, struct device *main, void *data); void dcp_platform_shutdown(struct platform_device *pdev); void dcp_disable_typec_work(struct apple_dcp *dcp, bool release_cable); diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 01091127a5734..64f63aefb7dc8 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -203,9 +203,9 @@ static bool show_notch; module_param(show_notch, bool, 0644); MODULE_PARM_DESC(show_notch, "Use the full display height and shows the notch"); -bool hdmi_audio; +bool hdmi_audio = true; module_param(hdmi_audio, bool, 0644); -MODULE_PARM_DESC(hdmi_audio, "Enable unstable HDMI audio support"); +MODULE_PARM_DESC(hdmi_audio, "Enable DP/HDMI audio support (default: on)"); static bool unstable_edid = true; module_param(unstable_edid, bool, 0644); @@ -3710,7 +3710,7 @@ int dcp_start(struct platform_device *pdev) /* * A native T6030 external processor: its DP/HDMI audio endpoint (EP 0x29) - * on the driver's opt-in (hdmi_audio), as the other DCPs start it, for a + * unless hdmi_audio is off, as the other DCPs start it, for a * processor whose port@0 links an audio device; once per session, and after * the first display's EDID was read: with the endpoint's interfaces * published, an EDID copy (dcpavserv_copy_edid()) has the firmware rebuild @@ -3889,7 +3889,9 @@ static void __maybe_unused dcp_sleep(struct apple_dcp *dcp) static bool dcp_uses_soft_dpms(struct apple_dcp *dcp) { - return (dcp_backlight_active(dcp) && dcp_has_panel(dcp)) || + /* The 14.7 panel has its own known-level blank and restore hooks. */ + return (!iomfb_v14_7_panel_nits(dcp) && + dcp_backlight_active(dcp) && dcp_has_panel(dcp)) || (dcp->fw_compat == DCP_FIRMWARE_H17P && dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G && dcp->connector_type == DRM_MODE_CONNECTOR_eDP); @@ -4197,7 +4199,7 @@ void dcp_backlight_takeover_registered(struct platform_device *pdev) schedule_work(&dcp->bl_register_wq); } -static void dcp_work_register_backlight(struct work_struct *work) +VISIBLE_IF_KUNIT void dcp_work_register_backlight(struct work_struct *work) { int ret; struct apple_dcp *dcp; @@ -4205,7 +4207,7 @@ static void dcp_work_register_backlight(struct work_struct *work) dcp = container_of(work, struct apple_dcp, bl_register_wq); mutex_lock(&dcp->bl_register_mutex); - if (dcp->brightness.bl_dev) + if (READ_ONCE(dcp->quiescing) || dcp->brightness.bl_dev) goto out_unlock; /* try to register backlight device, */ @@ -6306,6 +6308,7 @@ static int dcp_platform_suspend(struct device *dev) return -EBUSY; if (READ_ONCE(dcp->ext_dead)) return 0; + iomfb_v14_7_backlight_suspend(dcp); /* Serialize PM against explicit bring-up, including queued startup work. * Before a startup attempt the device remains suspendable. After any @@ -6389,6 +6392,7 @@ static int dcp_platform_resume(struct device *dev) dcp->link_recovery_tries = 0; dcp_release_after_sleep(dcp); + iomfb_v14_7_backlight_resume(dcp); dcp_enable_typec_work(dcp); /* The HDMI output's PHY and crossbar may have lost their setup in sleep. */ if (dcp->external_native) diff --git a/drivers/gpu/drm/apple/dcp_backlight.c b/drivers/gpu/drm/apple/dcp_backlight.c index 36f3ed466185d..223a462afced8 100644 --- a/drivers/gpu/drm/apple/dcp_backlight.c +++ b/drivers/gpu/drm/apple/dcp_backlight.c @@ -16,6 +16,7 @@ #include "connector.h" #include "dcp.h" #include "dcp-internal.h" +#include "iomfb_v14_7.h" #define MIN_BRIGHTNESS_PART1 2U #define MAX_BRIGHTNESS_PART1 99U @@ -93,14 +94,33 @@ static u32 brightness_part2[] = { #define DCP_BL_FADE_MS 250U #define DCP_BL_FADE_STEP_MS 16U +static bool dcp_backlight_presents_nits(struct apple_dcp *dcp) +{ + return (dcp->fw_compat == DCP_FIRMWARE_H17P && + dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G) || + iomfb_v14_7_panel_nits(dcp); +} + bool dcp_backlight_active(struct apple_dcp *dcp) { - return ((dcp->fw_compat == DCP_FIRMWARE_H17P && - dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G) || - iomfb_is_m4(dcp)) && + return (dcp_backlight_presents_nits(dcp) || iomfb_is_m4(dcp)) && READ_ONCE(dcp->backlight.state.ready); } +bool dcp_backlight_level_known(struct apple_dcp *dcp) +{ + unsigned long flags; + bool known; + + /* Only a panel running the policy initialised its lock and state. */ + if (!dcp_backlight_active(dcp)) + return false; + spin_lock_irqsave(&dcp->backlight.lock, flags); + known = dcp->backlight.state.ready && dcp->backlight.state.level_known; + spin_unlock_irqrestore(&dcp->backlight.lock, flags); + return known; +} + static bool dcp_backlight_desktop(struct apple_dcp *dcp) { return dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && @@ -228,11 +248,17 @@ static int dcp_get_brightness(struct backlight_device *bd) return -ENODEV; if (dcp_backlight_active(dcp) || dcp_backlight_desktop(dcp)) { + /* + * The 14.7 panel reports no actual level until a present + * carried one: before that it shows the boot loader's level, + * which Linux does not know. + */ spin_lock_irqsave(&dcp->backlight.lock, flags); if (dcp_backlight_desktop(dcp)) actual = dcp_bl_actual(&dcp->backlight.state); else - actual = dcp->backlight.state.actual; + actual = dcp_bl_reported_actual(&dcp->backlight.state, + iomfb_v14_7_panel_nits(dcp)); spin_unlock_irqrestore(&dcp->backlight.lock, flags); return actual; } @@ -249,7 +275,7 @@ int dcp_backlight_configure(struct apple_dcp *dcp, u32 maximum, int ret; if ((dcp->fw_compat != DCP_FIRMWARE_H17P && - !iomfb_is_m4(dcp)) || + !iomfb_is_m4(dcp) && !iomfb_v14_7_panel_nits(dcp)) || !dcp_has_panel(dcp) || !dcp->crtc || !kick) return -EINVAL; @@ -269,7 +295,7 @@ int dcp_backlight_configure(struct apple_dcp *dcp, u32 maximum, dcp_bl_jump(&dcp->backlight.state); dcp->backlight.kick = kick; } else if (!ret) { - dcp->backlight.state.fade = true; + dcp->backlight.state.fade = !iomfb_v14_7_panel_nits(dcp); dcp->backlight.kick = kick; } } @@ -655,8 +681,7 @@ int dcp_backlight_update(struct apple_dcp *dcp) dcp_backlight_kick(dcp); return 0; } - if (dcp->fw_compat == DCP_FIRMWARE_H17P && - dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G) + if (dcp_backlight_presents_nits(dcp)) return -ENODATA; /* @@ -744,6 +769,22 @@ static const struct backlight_ops dcp_backlight_ops = { .controls_device = dcp_backlight_controls_device, }; +static bool dcp_backlight_controls_device_v14(struct backlight_device *bd, + struct device *display_dev) +{ + struct apple_dcp *dcp = bl_get_data(bd); + + /* An fbdev unblank must not present the unrequested default level. */ + return dcp_backlight_level_known(dcp) && + dcp_backlight_controls_device(bd, display_dev); +} + +static const struct backlight_ops dcp_backlight_ops_v14 = { + .get_brightness = dcp_get_brightness, + .update_status = dcp_set_brightness, + .controls_device = dcp_backlight_controls_device_v14, +}; + int dcp_backlight_register(struct apple_dcp *dcp) { struct device *dev = dcp->dev; @@ -756,9 +797,7 @@ int dcp_backlight_register(struct apple_dcp *dcp) .scale = BACKLIGHT_SCALE_LINEAR, }; props.max_brightness = min(dcp->brightness.maximum, MAX_BRIGHTNESS_PART2 - 1); - if (((dcp->fw_compat == DCP_FIRMWARE_H17P && - dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G) || - iomfb_is_m4(dcp)) && + if ((dcp_backlight_presents_nits(dcp) || iomfb_is_m4(dcp)) && !dcp_backlight_active(dcp)) return -ENODATA; if (dcp_backlight_active(dcp)) { @@ -804,7 +843,9 @@ int dcp_backlight_register(struct apple_dcp *dcp) } bl_dev = devm_backlight_device_register(dev, "apple-panel-bl", dev, dcp, - &dcp_backlight_ops, &props); + iomfb_v14_7_panel_nits(dcp) ? + &dcp_backlight_ops_v14 : &dcp_backlight_ops, + &props); if (IS_ERR(bl_dev)) return PTR_ERR(bl_dev); diff --git a/drivers/gpu/drm/apple/dcp_backlight.h b/drivers/gpu/drm/apple/dcp_backlight.h index 0540c87d28108..cc2d321fb1396 100644 --- a/drivers/gpu/drm/apple/dcp_backlight.h +++ b/drivers/gpu/drm/apple/dcp_backlight.h @@ -309,6 +309,13 @@ static inline bool dcp_bl_complete(struct dcp_backlight_state *state, return true; } +static inline int dcp_bl_reported_actual(const struct dcp_backlight_state *state, bool strict) +{ + if (strict && !state->level_known) + return -ENODATA; + return state->actual; +} + /* Three deferred retries per user request; a permanent rejection stays dirty. */ static inline unsigned int dcp_bl_retry_delay(const struct dcp_backlight_state *state) { @@ -376,6 +383,7 @@ void dcp_backlight_desktop_finish(struct apple_dcp *dcp, u32 id, * Configure before registration and keep the callback valid until teardown. */ bool dcp_backlight_active(struct apple_dcp *dcp); +bool dcp_backlight_level_known(struct apple_dcp *dcp); int dcp_backlight_takeover(struct apple_dcp *dcp, u32 millinits); int dcp_backlight_configure(struct apple_dcp *dcp, u32 maximum, bool inherited_valid, u32 inherited, diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index 6f2a687007c58..dcfafc23cded9 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -2139,6 +2139,10 @@ int dcp_get_modes(struct drm_connector *connector) if (apple_connector->drm_edid) dcp_retry_placeholder_edid(dcp, apple_connector->drm_edid); + /* EDID updates clear display_info even for panels without an EDID. */ + if (published && dcp_has_panel(dcp)) + dcp_update_connector_dimensions(dcp, connector); + /* * An internal panel has no EDID, so nothing fills in the refresh range * that userspace needs before it will drive VRR. Supply the range DCP diff --git a/drivers/gpu/drm/apple/iomfb_v14_7.c b/drivers/gpu/drm/apple/iomfb_v14_7.c index 474841ff8d0ae..6940145afcc47 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7.c +++ b/drivers/gpu/drm/apple/iomfb_v14_7.c @@ -40,6 +40,7 @@ #include "dcp-lifecycle.h" #include "iomfb_internal.h" #include "iomfb_v14_7.h" +#include "iomfb_v14_7_backlight.h" #include "iomfb_v14_7_board.h" #include "iomfb_v14_7_link.h" #include "iomfb_v14_7_swap.h" @@ -102,6 +103,7 @@ static const struct dcp_v14_board dcp_v14_board_t6030 = { .firmware_uuid = DCP_V14_FIRMWARE_UUID, .handoff = "apple,t6030-handoff", .promotion = true, + .panel_nits = true, }; /* @@ -118,6 +120,7 @@ static const struct dcp_v14_board dcp_v14_board_j613 = { .panel_width = 2560, .panel_height = 1664, .promotion = false, + .panel_nits = true, .ctm_set = A(421), .ctm_get = A(420), }; @@ -159,6 +162,13 @@ static const struct dcp_v14_board dcp_v14_board_j516c = { .panel_height = 2234, .promotion = true, .log_bringup = true, + .swap_timestamps = true, + /* + * The J516S panel and this image's swap carry the level block the + * T6030 record uses (present byte +0x354, binary64 nits +0x35c, 500 nits + * at most), as the e272b67 path on this J516C presented it. + */ + .panel_nits = true, }; /* @@ -287,6 +297,19 @@ struct apple_dcp_v14 { struct drm_framebuffer *active_fb; u64 swaps; u64 swap_ns_max; + + /* Immutable probe admission: only the J514S/J516S/J516C internal panel. */ + bool panel_nits; + /* Copy of the last accepted surface; active_fb pins its DMA under lock. */ + u8 shown_surface[DCP_V14_SURFACE_SIZE]; + u64 shown_iova; + u32 shown_width, shown_height, shown_dst_y; + struct delayed_work backlight_work; + /* Serializes gate transitions with queue admission. */ + spinlock_t bl_gate_lock; + enum dcp_v14_bl_gate bl_gate; + u32 nits_sent; + u64 nits_presents, brightness_presents, nits_refused; }; /* @@ -299,6 +322,135 @@ static bool dcp_v14_panel_session; static int dcp_v14_callback(void *cookie, u32 tag, const void *input, u32 in_size, void *output, u32 out_size); +static void dcp_v14_backlight_work(struct work_struct *work); + +/* The internal panel of a board whose swap level block was validated (T6030). */ +static bool dcp_v14_board_nits(const struct apple_dcp_v14 *v14) +{ + return !v14->external && v14->panel_nits && v14->board && v14->board->panel_nits; +} + +bool iomfb_v14_7_panel_nits(struct apple_dcp *dcp) +{ + return dcp_is_v14_7_panel(dcp) && dcp->v14 && dcp_v14_board_nits(dcp->v14); +} + +static enum dcp_v14_bl_gate dcp_v14_bl_gate_get(struct apple_dcp_v14 *v14) +{ + enum dcp_v14_bl_gate gate; + unsigned long flags; + + spin_lock_irqsave(&v14->bl_gate_lock, flags); + gate = v14->bl_gate; + spin_unlock_irqrestore(&v14->bl_gate_lock, flags); + return gate; +} + +/* Queues a brightness-only present, unless the gate is shut. */ +static void dcp_v14_bl_queue(struct apple_dcp_v14 *v14, unsigned long delay) +{ + unsigned long flags; + + spin_lock_irqsave(&v14->bl_gate_lock, flags); + if (dcp_v14_bl_may_queue(v14->bl_gate)) + mod_delayed_work(system_dfl_wq, &v14->backlight_work, delay); + spin_unlock_irqrestore(&v14->bl_gate_lock, flags); +} + +/* + * Shuts the gate, until resume or (@for_good, unbind and remove) for good, + * then drains the work. The RPC lock waits for a running present; the + * gate lock excludes late queueing. Cancellation runs outside the RPC lock. + */ +static void dcp_v14_bl_shut(struct apple_dcp_v14 *v14, bool for_good) +{ + unsigned long flags; + + mutex_lock(&v14->lock); + spin_lock_irqsave(&v14->bl_gate_lock, flags); + v14->bl_gate = for_good ? DCP_V14_BL_CLOSED : dcp_v14_bl_gate_suspend(v14->bl_gate); + spin_unlock_irqrestore(&v14->bl_gate_lock, flags); + mutex_unlock(&v14->lock); + cancel_delayed_work_sync(&v14->backlight_work); +} + +/* The class device may be platform devres, outside the component bind group. */ +static void dcp_v14_stop_backlight(struct apple_dcp_v14 *v14) +{ + struct apple_dcp *dcp = READ_ONCE(v14->dcp); + + if (dcp) + WRITE_ONCE(dcp->quiescing, true); + dcp_v14_bl_shut(v14, true); + if (!dcp) + return; + dcp_backlight_stop(dcp); + cancel_work_sync(&dcp->bl_register_wq); + cancel_work_sync(&dcp->bl_update_wq); + /* Never hold this mutex while waiting for its registration worker. */ + guard(mutex)(&dcp->bl_register_mutex); + if (dcp->brightness.bl_dev) { + devm_backlight_device_unregister(dcp->dev, dcp->brightness.bl_dev); + dcp->brightness.bl_dev = NULL; + } +} + +/* The policy's kick (dcp_backlight.c): only queues; the RPC runs in the work. */ +static void dcp_v14_backlight_kick(struct apple_dcp *dcp) +{ + if (iomfb_v14_7_panel_nits(dcp) && !READ_ONCE(dcp->quiescing)) + dcp_v14_bl_queue(dcp->v14, 0); +} + +/* + * The panel's level policy, set up once the panel runs: at most 500 nits + * (less if the panel's DT record says so), and no fades. No boot loader + * level is known, so none is presented until userspace sets one; until + * then the backlight reports DCP_V14_NITS_DEFAULT as its requested level + * and no actual level (dcp_backlight.c). dcp_backlight_configure() + * registers the device from bl_register_wq. + */ +static void dcp_v14_configure_backlight(struct apple_dcp *dcp) +{ + struct apple_dcp_v14 *v14 = dcp->v14; + u32 maximum = DCP_V14_NITS_MAX; + int ret; + + if (!iomfb_v14_7_panel_nits(dcp) || READ_ONCE(dcp->quiescing) || + !v14->started || dcp_backlight_active(dcp) || + !dcp_has_panel(dcp) || !dcp->crtc) + return; + /* A described panel maximum may only lower the wire bound. */ + if (dcp->brightness.maximum) + maximum = min(maximum, dcp->brightness.maximum); + ret = dcp_backlight_configure(dcp, maximum, false, 0, true, + min_t(u32, maximum, DCP_V14_NITS_DEFAULT), + dcp_v14_backlight_kick); + if (ret && ret != -EBUSY) + dev_err_once(dcp->dev, "%s backlight unavailable: %d\n", v14->board->name, ret); +} + +void iomfb_v14_7_backlight_suspend(struct apple_dcp *dcp) +{ + if (iomfb_v14_7_panel_nits(dcp)) + dcp_v14_bl_shut(dcp->v14, false); +} + +/* Pending levels ride the next normal CRTC restore present after admission opens. */ +void iomfb_v14_7_backlight_resume(struct apple_dcp *dcp) +{ + struct apple_dcp_v14 *v14 = dcp->v14; + unsigned long flags; + + if (!iomfb_v14_7_panel_nits(dcp)) + return; + mutex_lock(&v14->lock); + spin_lock_irqsave(&v14->bl_gate_lock, flags); + v14->bl_gate = dcp_v14_bl_gate_resume(v14->bl_gate); + spin_unlock_irqrestore(&v14->bl_gate_lock, flags); + mutex_unlock(&v14->lock); +} + /* * An external session stopped on a link error, not a crash: its display is * gone until reboot. Report that as the crash path does, from process @@ -336,6 +488,8 @@ static int dcp_v14_call_guarded(struct apple_dcp_v14 *v14, u32 tag, const void * bool vetoed; int ret; + KUNIT_STATIC_STUB_REDIRECT(dcp_v14_call_guarded, v14, tag, in, in_size, + out, out_size, completion, admit, admit_cookie); if (v14->failed) return -EIO; dev_dbg(v14->dev, "call %#x in %u out %u\n", tag, in_size, out_size); @@ -1455,6 +1609,8 @@ static void dcp_v14_release(void *data) { struct apple_dcp_v14 *v14 = data; + if (dcp_v14_board_nits(v14)) + dcp_v14_stop_backlight(v14); if (!v14->rtk) kfree(v14); } @@ -1536,11 +1692,24 @@ const struct dcp_v14_board *iomfb_v14_7_board(struct device *dev) return NULL; } +static bool dcp_v14_panel_nits_admit(const struct dcp_v14_board *board, + dcp_v14_compat_fn machine_is, const void *ctx) +{ + return board && board->panel_nits && + (machine_is(ctx, "apple,j514s") || machine_is(ctx, "apple,j516s") || + machine_is(ctx, "apple,j516c") || machine_is(ctx, "apple,j613")); +} + const char *iomfb_v14_7_board_name(const struct dcp_v14_board *board) { return board->name; } +bool dcp_v14_board_stamps_swaps(const struct dcp_v14_board *board, bool external) +{ + return board && !external && board->swap_timestamps; +} + int iomfb_v14_7_probe(struct apple_dcp *dcp) { struct device *dev = dcp->dev; @@ -1589,10 +1758,13 @@ int iomfb_v14_7_probe(struct apple_dcp *dcp) return -ENOMEM; v14->dev = dev; v14->board = board; + v14->panel_nits = dcp_v14_panel_nits_admit(board, dcp_v14_machine_is, np); v14->dcp = dcp; mutex_init(&v14->lock); init_waitqueue_head(&v14->described_wait); INIT_WORK(&v14->idle_work, dcp_v14_idle); + INIT_DELAYED_WORK(&v14->backlight_work, dcp_v14_backlight_work); + spin_lock_init(&v14->bl_gate_lock); ret = devm_add_action_or_reset(dev, dcp_v14_release, v14); if (ret) return ret; @@ -2045,6 +2217,8 @@ static int dcpext_boot(struct apple_dcp *dcp) mutex_init(&v14->lock); init_waitqueue_head(&v14->described_wait); INIT_WORK(&v14->idle_work, dcp_v14_idle); + INIT_DELAYED_WORK(&v14->backlight_work, dcp_v14_backlight_work); + spin_lock_init(&v14->bl_gate_lock); INIT_WORK(&v14->stopped_work, dcp_v14_stopped_work); dcp->v14 = v14; dcp_v14_link_init(&v14->link, dev, NULL); @@ -2275,6 +2449,8 @@ void iomfb_v14_7_unbind(struct apple_dcp *dcp) flush_work(&v14->idle_work); cancel_work_sync(&v14->stopped_work); } else { + if (dcp_v14_board_nits(v14)) + dcp_v14_stop_backlight(v14); WRITE_ONCE(v14->dcp, NULL); } if (v14->rtk) @@ -2314,6 +2490,8 @@ void iomfb_v14_7_remove(struct apple_dcp *dcp) * Callbacks that already loaded the old pointer finish before the * flush returns; later ones see NULL. */ + if (dcp_v14_board_nits(v14)) + dcp_v14_stop_backlight(v14); WRITE_ONCE(v14->dcp, NULL); if (v14->rtk) apple_rtkit_flush_rx(v14->rtk); @@ -2344,6 +2522,9 @@ static int dcp_v14_status_show(struct seq_file *m, void *unused) v14->stride, v14->clock_rate); seq_printf(m, "backlight factor %#x updates %llu\n", v14->backlight_factor, v14->backlight_factor_updates); + seq_printf(m, "backlight nits_enabled %d sent %u presents %llu brightness_only %llu refused %llu\n", + dcp_v14_board_nits(v14), v14->nits_sent, v14->nits_presents, + v14->brightness_presents, v14->nits_refused); seq_printf(m, "ctm valid %d disabled %d calls %llu setter %#x getter %#x\n", v14->ctm_valid, v14->ctm_disabled, v14->ctm_calls, v14->ctm_status, v14->ctm_get_status); @@ -2466,6 +2647,7 @@ int iomfb_v14_7_start(struct apple_dcp *dcp) dcp->mode_state.valid = true; dcp->active = true; complete(&dcp->start_done); + dcp_v14_configure_backlight(dcp); mutex_lock(&dcp->modes_lock); selected = dcp->modes[0].mode; @@ -2571,20 +2753,64 @@ static bool dcp_v14_swap_admit(void *cookie) return connector && READ_ONCE(connector->connected); } +static bool dcp_v14_swap_nits(struct apple_dcp_v14 *v14, u8 *swap, + struct dcp_backlight_present *present) +{ + struct apple_dcp *dcp = READ_ONCE(v14->dcp); + + lockdep_assert_held(&v14->lock); + /* The policy exists only on the T6030 panel: nothing else is asked. */ + if (!dcp || !dcp_v14_board_nits(v14) || !dcp_backlight_active(dcp)) + return false; + if (!dcp_v14_swap_takes_nits(true, v14->external, dcp_is_typec_output(dcp), true)) + return false; + /* -EALREADY: no change; -ENODATA/-EBUSY: nothing to present now. */ + if (dcp_backlight_prepare(dcp, true, present)) + return false; + /* The policy's maximum is at most the wire bound; never above it. */ + if (WARN_ON_ONCE(dcp_v14_encode_nits(swap, true, present->nits))) { + dcp_backlight_complete(dcp, present->sequence, false); + v14->nits_refused++; + return false; + } + return true; +} + +static void dcp_v14_swap_nits_done(struct apple_dcp_v14 *v14, + const struct dcp_backlight_present *present, bool accepted) +{ + struct apple_dcp *dcp = READ_ONCE(v14->dcp); + + if (!dcp) + return; + if (!dcp_backlight_complete(dcp, present->sequence, accepted)) + return; + if (accepted) { + v14->nits_sent = present->nits; + v14->nits_presents++; + return; + } + v14->nits_refused++; + /* The internal swap failure is final: swap_locked latches failed. */ +} + /* * Swap start, then the swap; returns once the firmware completed that swap. * @ctm_state, if set, carries the colour matrix to send first. */ -static int dcp_v14_swap(struct apple_dcp_v14 *v14, const u8 *surface, u64 iova, - u32 width, u32 height, u32 dst_y, - const struct drm_crtc_state *ctm_state) +static int dcp_v14_swap_locked(struct apple_dcp_v14 *v14, const u8 *surface, u64 iova, + u32 width, u32 height, u32 dst_y, + const struct drm_crtc_state *ctm_state) { struct dcp_v14_swap_admission admission = { .v14 = v14 }; __le32 start[4] = {}, started[2] = {}, result[3]; + struct dcp_backlight_present present; + bool nits; u8 *swap; u32 id; int ret; + lockdep_assert_held(&v14->lock); if (surface && (!iova || !width || width > v14->panel_width || !height || dst_y > v14->panel_height || height > v14->panel_height - dst_y)) return -EINVAL; @@ -2592,7 +2818,6 @@ static int dcp_v14_swap(struct apple_dcp_v14 *v14, const u8 *surface, u64 iova, if (!swap) return -ENOMEM; - mutex_lock(&v14->lock); admission.generation = v14->description_generation; /* * A colour matrix the firmware refuses costs night light, not the @@ -2609,11 +2834,16 @@ static int dcp_v14_swap(struct apple_dcp_v14 *v14, const u8 *surface, u64 iova, if (!ret) { dcp_v14_encode_swap(swap, id, DCP_V14_BLACK, surface, iova, width, height, dst_y, DCP_V14_LAYER); + if (dcp_v14_board_stamps_swaps(v14->board, v14->external)) + dcp_v14_swap_timestamps(swap); + nits = dcp_v14_swap_nits(v14, swap, &present); ret = dcp_v14_call_guarded(v14, A(407), swap, DCP_V14_SWAP_SIZE, result, sizeof(result), id, dcp_v14_swap_admit, &admission); /* The swap status sits at byte 5 of the reply. */ if (!ret && get_unaligned_le32((u8 *)result + 5)) ret = -EIO; + if (nits) + dcp_v14_swap_nits_done(v14, &present, !ret); } /* * An external display can go away under a swap: the firmware then @@ -2622,26 +2852,50 @@ static int dcp_v14_swap(struct apple_dcp_v14 *v14, const u8 *surface, u64 iova, */ if (ret && !v14->external) v14->failed = true; - mutex_unlock(&v14->lock); kfree(swap); return ret; } +/* Publish a successful swap while the RPC lock still pins both surfaces. */ +static struct drm_framebuffer *dcp_v14_publish_surface(struct apple_dcp_v14 *v14, + struct drm_framebuffer *fb, + const u8 *surface, u64 iova, + u32 width, u32 height, u32 dst_y) +{ + struct drm_framebuffer *old; + + KUNIT_STATIC_STUB_REDIRECT(dcp_v14_publish_surface, v14, fb, surface, iova, + width, height, dst_y); + lockdep_assert_held(&v14->lock); + old = v14->active_fb; + v14->active_fb = fb; + if (fb) { + memcpy(v14->shown_surface, surface, sizeof(v14->shown_surface)); + v14->shown_iova = iova; + v14->shown_width = width; + v14->shown_height = height; + v14->shown_dst_y = dst_y; + } + return old; +} + /* * Shows the top-left @width x @height of @fb (or only the black background), * after the colour matrix of @ctm_state if that is set, * and keeps @fb until the next swap. */ -static bool dcp_v14_present(struct apple_dcp_v14 *v14, struct drm_framebuffer *fb, - u32 width, u32 height, u32 dst_y, - const struct drm_crtc_state *ctm_state) +static bool dcp_v14_present_locked(struct apple_dcp_v14 *v14, struct drm_framebuffer *fb, + u32 width, u32 height, u32 dst_y, + const struct drm_crtc_state *ctm_state, + struct drm_framebuffer **retired) { u8 surface[DCP_V14_SURFACE_SIZE]; struct drm_framebuffer *old; u64 iova = 0, start, elapsed, swaps; int ret; + lockdep_assert_held(&v14->lock); if (READ_ONCE(v14->failed)) return false; if (fb) { @@ -2651,13 +2905,12 @@ static bool dcp_v14_present(struct apple_dcp_v14 *v14, struct drm_framebuffer *f iova = drm_fb_dma_get_gem_obj(fb, 0)->dma_addr; } start = ktime_get_ns(); - ret = dcp_v14_swap(v14, fb ? surface : NULL, iova, fb ? width : 0, - fb ? height : 0, dst_y, ctm_state); + ret = dcp_v14_swap_locked(v14, fb ? surface : NULL, iova, fb ? width : 0, + fb ? height : 0, dst_y, ctm_state); elapsed = ktime_get_ns() - start; if (ret && v14->external && !READ_ONCE(v14->failed)) { /* Refused before it was taken: the old framebuffer stays on screen. */ - if (fb) - drm_framebuffer_put(fb); + *retired = fb; WRITE_ONCE(v14->mode_swaps, 0); dev_warn_ratelimited(v14->dev, "display refused a swap: %d\n", ret); return false; @@ -2667,22 +2920,91 @@ static bool dcp_v14_present(struct apple_dcp_v14 *v14, struct drm_framebuffer *f dev_err(v14->dev, "native DCP flip failed %d; buffers pinned until reboot\n", ret); return false; } - mutex_lock(&v14->lock); - old = v14->active_fb; - v14->active_fb = fb; + old = dcp_v14_publish_surface(v14, fb, surface, iova, width, height, dst_y); swaps = ++v14->swaps; if (fb) v14->mode_swaps++; v14->swap_ns_max = max(v14->swap_ns_max, elapsed); - mutex_unlock(&v14->lock); - if (old) - drm_framebuffer_put(old); + *retired = old; if (swaps <= 3) dev_info(v14->dev, "swap %llu complete: surface %d, imported %d, %llu us\n", swaps, !!fb, fb && fb->obj[0]->import_attach, elapsed / NSEC_PER_USEC); return true; } +static bool dcp_v14_present(struct apple_dcp_v14 *v14, struct drm_framebuffer *fb, + u32 width, u32 height, u32 dst_y, + const struct drm_crtc_state *ctm_state) +{ + struct drm_framebuffer *retired = NULL; + bool shown; + + mutex_lock(&v14->lock); + shown = dcp_v14_present_locked(v14, fb, width, height, dst_y, ctm_state, &retired); + mutex_unlock(&v14->lock); + if (retired) + drm_framebuffer_put(retired); + return shown; +} + +static void dcp_v14_backlight_work(struct work_struct *work) +{ + struct apple_dcp_v14 *v14 = container_of(to_delayed_work(work), struct apple_dcp_v14, + backlight_work); + struct dcp_v14_bl_facts facts; + struct drm_framebuffer *fb; + struct apple_dcp *dcp; + int ret; + + guard(mutex)(&v14->lock); + dcp = READ_ONCE(v14->dcp); + if (!dcp) + return; + fb = v14->active_fb; + facts = (struct dcp_v14_bl_facts) { + .gate = dcp_v14_bl_gate_get(v14), + .panel_nits = dcp_v14_board_nits(v14), + .external = v14->external, + .typec = dcp_is_typec_output(dcp), + .started = v14->started, + .failed = READ_ONCE(v14->failed), + .crashed = READ_ONCE(dcp->crashed), + .quiescing = READ_ONCE(dcp->quiescing), + .pending = dcp_v14_board_nits(v14) && dcp_backlight_active(dcp) && + dcp_backlight_pending(dcp), + .frame = !!fb, + .mode_valid = READ_ONCE(dcp->mode_state.valid), + }; + if (dcp_v14_bl_plan(&facts) != DCP_V14_BL_REPRESENT) + return; + ret = dcp_v14_swap_locked(v14, v14->shown_surface, v14->shown_iova, + v14->shown_width, v14->shown_height, v14->shown_dst_y, NULL); + if (!ret) + v14->brightness_presents++; + else if (ret != -EAGAIN) + dev_err(v14->dev, "brightness present failed: %d\n", ret); +} + +static bool dcp_v14_bl_dpms_applies(struct apple_dcp_v14 *v14, struct apple_dcp *dcp) +{ + if (!dcp_v14_board_nits(v14) || !dcp_backlight_active(dcp)) + return false; + return dcp_v14_bl_follow_dpms(true, true, dcp_backlight_level_known(dcp)); +} + +static void dcp_v14_blank(struct apple_dcp_v14 *v14, struct apple_dcp *dcp) +{ + struct drm_framebuffer *retired = NULL; + + mutex_lock(&v14->lock); + if (dcp_v14_bl_dpms_applies(v14, dcp)) + dcp_backlight_dpms(dcp, false); + dcp_v14_present_locked(v14, NULL, 0, 0, 0, NULL, &retired); + mutex_unlock(&v14->lock); + if (retired) + drm_framebuffer_put(retired); +} + /* Releases helper waiters without reporting a completed flip. */ static void dcp_v14_cancel_event(struct apple_dcp *dcp) { @@ -2947,6 +3269,17 @@ static int dcp_v14_external_modeset(struct apple_dcp *dcp, struct drm_crtc_state ret = dcp_v14_set_mode(v14, mode.color_mode_id, mode.timing_mode_id); if (!ret) ret = dcp_v14_set_power(v14, true); + /* + * The mode was set with the pipe off. A mode that needs DSC is then + * reset again ("pipe not enabled"), and the power-on only configures + * DSC: the firmware starts the timings in a mode set of its own, after + * the hotplug notification it sends itself, and drops a swap that + * reaches it before then without completing it, so A407 times out. + * Setting the mode once more with the pipe on starts the timings + * before the first swap. + */ + if (!ret) + ret = dcp_v14_set_mode(v14, mode.color_mode_id, mode.timing_mode_id); if (!ret) { v14->panel_width = mode.mode.hdisplay; v14->panel_height = mode.mode.vdisplay; @@ -3015,6 +3348,8 @@ void iomfb_v14_7_flush(struct apple_dcp *dcp, struct drm_crtc *crtc, dcp_drm_crtc_vblank(dcp->crtc); return; } + if (!dcp->external) + dcp_v14_configure_backlight(dcp); dcp->swap_start = ktime_get(); if (dcp->v14 && dcp_v14_present(dcp->v14, fb, fb ? (dcp->external ? p->src_w >> 16 : fb->width) : 0, @@ -3027,7 +3362,9 @@ void iomfb_v14_7_flush(struct apple_dcp *dcp, struct drm_crtc *crtc, void iomfb_v14_7_poweron(struct apple_dcp *dcp) { - /* The panel stays on; the next swap shows the plane again. */ + /* The next present restores a level only after Linux has set one. */ + if (dcp->v14 && dcp_v14_bl_dpms_applies(dcp->v14, dcp)) + dcp_backlight_dpms(dcp, true); } void iomfb_v14_7_poweroff(struct apple_dcp *dcp) @@ -3050,9 +3387,12 @@ void iomfb_v14_7_poweroff(struct apple_dcp *dcp) } /* Blank to black; the panel and the DCP stay powered. */ if (v14 && v14->started) - dcp_v14_present(v14, NULL, 0, 0, 0, NULL); + dcp_v14_blank(v14, dcp); } #if IS_ENABLED(CONFIG_DRM_APPLE_DISPLAY_KUNIT_TEST) #include "iomfb_v14_7_external_audio_test.c" +#include "iomfb_v14_7_external_modeset_test.c" +#include "iomfb_v14_7_backlight_test.c" +#include "iomfb_v14_7_backlight_lifetime_test.c" #endif diff --git a/drivers/gpu/drm/apple/iomfb_v14_7.h b/drivers/gpu/drm/apple/iomfb_v14_7.h index f9bef6ed0f109..8aa4eeda4be7a 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7.h +++ b/drivers/gpu/drm/apple/iomfb_v14_7.h @@ -31,6 +31,17 @@ const char *iomfb_v14_7_board_name(const struct dcp_v14_board *board); /* Platform probe: firmware identity, boot loader handoff, PMP acknowledgment. */ int iomfb_v14_7_probe(struct apple_dcp *dcp); +/* + * Whether @dcp is a 14.x internal panel whose swaps carry its level (the + * T6030 board record): its backlight is the nits policy in dcp_backlight.c. + */ +bool iomfb_v14_7_panel_nits(struct apple_dcp *dcp); +/* + * System sleep: no brightness-only present may be queued or run from + * _suspend() until _resume(). + */ +void iomfb_v14_7_backlight_suspend(struct apple_dcp *dcp); +void iomfb_v14_7_backlight_resume(struct apple_dcp *dcp); /* Component bind: the running DCP's RTKit session. */ int iomfb_v14_7_bind(struct apple_dcp *dcp); int iomfb_v14_7_external_start(struct apple_dcp *dcp); diff --git a/drivers/gpu/drm/apple/iomfb_v14_7_backlight.h b/drivers/gpu/drm/apple/iomfb_v14_7_backlight.h new file mode 100644 index 0000000000000..f2b71653664eb --- /dev/null +++ b/drivers/gpu/drm/apple/iomfb_v14_7_backlight.h @@ -0,0 +1,98 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ + +#ifndef __APPLE_IOMFB_V14_7_BACKLIGHT_H__ +#define __APPLE_IOMFB_V14_7_BACKLIGHT_H__ + +#include + +/* + * Decisions of the 14.7 panel's level path (iomfb_v14_7.c), kept free of + * driver state so that KUnit runs the same code. + */ + +/* + * Whether brightness-only presents may be queued and run. Set under the + * gate lock before the work is cancelled: system sleep suspends it until + * resume, unbind and remove close it for good. + */ +enum dcp_v14_bl_gate { + DCP_V14_BL_OPEN, + DCP_V14_BL_SUSPENDED, + DCP_V14_BL_CLOSED, +}; + +/* Whether a brightness-only present may be queued, or run once queued. */ +static inline bool dcp_v14_bl_may_queue(enum dcp_v14_bl_gate gate) +{ + return gate == DCP_V14_BL_OPEN; +} + +static inline enum dcp_v14_bl_gate dcp_v14_bl_gate_suspend(enum dcp_v14_bl_gate gate) +{ + return gate == DCP_V14_BL_OPEN ? DCP_V14_BL_SUSPENDED : gate; +} + +static inline enum dcp_v14_bl_gate dcp_v14_bl_gate_resume(enum dcp_v14_bl_gate gate) +{ + return gate == DCP_V14_BL_SUSPENDED ? DCP_V14_BL_OPEN : gate; +} + +/* + * Whether a swap carries the level the backlight policy reserves: only the + * internal panel of a board whose level block was validated (T6030), only + * while it shows its own panel (not a J613 internal pipe lent to a Type-C + * display), and only with an active policy. + */ +static inline bool dcp_v14_swap_takes_nits(bool panel_nits, bool external, bool typec, + bool policy_active) +{ + return panel_nits && !external && !typec && policy_active; +} + +/* + * Blanking follows DPMS only once Linux has set a level: before that the + * panel shows the boot loader's level, and a DPMS cycle must not replace it + * with the reported default. + */ +static inline bool dcp_v14_bl_follow_dpms(bool panel_nits, bool policy_active, + bool level_known) +{ + return panel_nits && policy_active && level_known; +} + +struct dcp_v14_bl_facts { + enum dcp_v14_bl_gate gate; + bool panel_nits; + bool external; + bool typec; + bool started; + bool failed; + bool crashed; + bool quiescing; + /* A level is reserved-able: dirty and not in flight. */ + bool pending; + /* The last present showed a frame (active_fb), not the background. */ + bool frame; + bool mode_valid; +}; + +enum dcp_v14_bl_action { + /* Nothing to do, or not allowed to: no RPC. */ + DCP_V14_BL_SKIP, + /* Blanked or no frame yet: the next atomic present carries the level. */ + DCP_V14_BL_WAIT, + /* Present the frame shown last again, with the level. */ + DCP_V14_BL_REPRESENT, +}; + +static inline enum dcp_v14_bl_action dcp_v14_bl_plan(const struct dcp_v14_bl_facts *f) +{ + if (!dcp_v14_bl_may_queue(f->gate) || !f->panel_nits || f->external || f->typec || + !f->started || f->failed || f->crashed || f->quiescing || !f->pending) + return DCP_V14_BL_SKIP; + if (!f->frame || !f->mode_valid) + return DCP_V14_BL_WAIT; + return DCP_V14_BL_REPRESENT; +} + +#endif /* __APPLE_IOMFB_V14_7_BACKLIGHT_H__ */ diff --git a/drivers/gpu/drm/apple/iomfb_v14_7_backlight_lifetime_test.c b/drivers/gpu/drm/apple/iomfb_v14_7_backlight_lifetime_test.c new file mode 100644 index 0000000000000..29acb9d5fe9e9 --- /dev/null +++ b/drivers/gpu/drm/apple/iomfb_v14_7_backlight_lifetime_test.c @@ -0,0 +1,421 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* + * Port-specific controls: run the real swap/present, worker, DPMS dispatch + * and class teardown. Only the RPC transport is replaced. Threads and a + * workqueue context bridge retain KUnit's per-task static-stub context. + */ +#include +#include +#include +#include + +struct v14_bl_rpc; +struct v14_bl_actor { + struct v14_bl_rpc *ctx; + struct task_struct *task; + struct completion entered, done; + unsigned int op; + bool shown; +}; + +struct v14_bl_rpc { + struct kunit *test; + struct v14_bl_fixture f; + struct drm_device *drm; + struct drm_framebuffer fb[2]; + struct drm_gem_dma_object gem[2]; + unsigned int initialized, gems_initialized; + struct v14_bl_actor actors[2]; + struct completion rpc_entered, rpc_release; + atomic_t submits; + u64 iova[5], nits[5]; + bool nits_present[5]; + struct drm_framebuffer *owner[5]; + bool block_first, refuse; +}; + +static struct v14_bl_rpc *v14_bl_rpc_current; + +static int v14_bl_rpc_call(struct apple_dcp_v14 *v14, u32 tag, const void *in, u32 in_size, + void *out, u32 out_size, u32 completion, + dcp_v14_admit_fn admit, void *admit_cookie) +{ + struct v14_bl_rpc *c = v14_bl_rpc_current; + unsigned int n; + + KUNIT_EXPECT_TRUE(c->test, mutex_is_locked(&v14->lock)); + memset(out, 0, out_size); + if (tag == A(406)) { + put_unaligned_le32(1, out); + return 0; + } + KUNIT_EXPECT_EQ(c->test, tag, A(407)); + KUNIT_EXPECT_EQ(c->test, in_size, (u32)DCP_V14_SWAP_SIZE); + n = atomic_inc_return(&c->submits) - 1; + if (n < ARRAY_SIZE(c->iova)) { + c->iova[n] = get_unaligned_le64((const u8 *)in + DCP_V14_SWAP_IOVA(0)); + c->nits_present[n] = ((const u8 *)in)[DCP_V14_SWAP_NITS_PRESENT]; + c->nits[n] = get_unaligned_le64((const u8 *)in + DCP_V14_SWAP_NITS); + c->owner[n] = v14->active_fb; + } + if (!n && c->block_first) { + complete(&c->rpc_entered); + wait_for_completion(&c->rpc_release); + } + if (c->refuse) + put_unaligned_le32(1, (u8 *)out + 5); + return 0; +} + +static void v14_bl_rpc_work(struct work_struct *work) +{ + struct kunit *previous = current->kunit_test; + + current->kunit_test = v14_bl_rpc_current->test; + dcp_v14_backlight_work(work); + current->kunit_test = previous; +} + +static int v14_bl_actor_run(void *data) +{ + struct v14_bl_actor *a = data; + struct v14_bl_rpc *c = a->ctx; + + current->kunit_test = c->test; + complete(&a->entered); + if (a->op < 2) + a->shown = dcp_v14_present(c->f.v14, &c->fb[a->op], 64, 64, 0, NULL); + else if (a->op == 2) + iomfb_v14_7_backlight_suspend(c->f.dcp); + else + iomfb_v14_7_unbind(c->f.dcp); + complete(&a->done); + current->kunit_test = NULL; + return 0; +} + +static void v14_bl_rpc_cleanup(void *data) +{ + struct v14_bl_rpc *c = data; + unsigned int i; + + complete_all(&c->rpc_release); + for (i = 0; i < ARRAY_SIZE(c->actors); i++) + if (!IS_ERR_OR_NULL(c->actors[i].task)) + kthread_stop_put(c->actors[i].task); + dcp_v14_stop_backlight(c->f.v14); + if (c->f.v14->active_fb) { + drm_framebuffer_put(c->f.v14->active_fb); + c->f.v14->active_fb = NULL; + } + for (i = 0; i < c->initialized; i++) + drm_framebuffer_put(&c->fb[i]); + for (i = 0; i < c->gems_initialized; i++) + drm_gem_object_put(&c->gem[i].base); + v14_bl_rpc_current = NULL; +} + +static void v14_bl_fb_destroy(struct drm_framebuffer *fb) +{ + drm_framebuffer_cleanup(fb); +} + +static void v14_bl_gem_free(struct drm_gem_object *obj) +{ + drm_gem_object_release(obj); +} + +static const struct drm_gem_object_funcs v14_bl_gem_funcs = { + .free = v14_bl_gem_free, +}; + +static const struct drm_framebuffer_funcs v14_bl_fb_funcs = { + .destroy = v14_bl_fb_destroy, +}; + +static struct v14_bl_rpc *v14_bl_rpc_setup(struct kunit *test) +{ + struct v14_bl_rpc *c = kunit_kzalloc(test, sizeof(*c), GFP_KERNEL); + struct device *dev; + unsigned int i; + + KUNIT_ASSERT_NOT_NULL(test, c); + c->test = test; + c->f = v14_bl_fixture(test, &dcp_v14_board_t6030, true); + dev = drm_kunit_helper_alloc_device(test); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dev); + c->drm = __drm_kunit_helper_alloc_drm_device(test, dev, sizeof(*c->drm), 0, + DRIVER_MODESET | DRIVER_GEM); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, c->drm); + init_completion(&c->rpc_entered); + init_completion(&c->rpc_release); + atomic_set(&c->submits, 0); + v14_bl_rpc_current = c; + kunit_activate_static_stub(test, dcp_v14_call_guarded, v14_bl_rpc_call); + KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, v14_bl_rpc_cleanup, c), 0); + for (i = 0; i < ARRAY_SIZE(c->fb); i++) { + c->fb[i].dev = c->drm; + c->fb[i].format = drm_format_info(DRM_FORMAT_XRGB8888); + c->fb[i].width = 64; + c->fb[i].height = 64; + c->fb[i].pitches[0] = 256; + drm_gem_private_object_init(c->drm, &c->gem[i].base, PAGE_SIZE); + c->gem[i].base.funcs = &v14_bl_gem_funcs; + c->gems_initialized++; + c->gem[i].dma_addr = 0x100000 + i * 0x10000; + c->fb[i].obj[0] = &c->gem[i].base; + KUNIT_ASSERT_EQ(test, drm_framebuffer_init(c->drm, &c->fb[i], &v14_bl_fb_funcs), 0); + c->initialized++; + } + c->f.v14->panel_width = 64; + c->f.v14->panel_height = 64; + c->f.dcp->fixed_connector_type = DRM_MODE_CONNECTOR_eDP; + c->f.dcp->connector_type = DRM_MODE_CONNECTOR_eDP; + c->f.dcp->panel.width_mm = 200; + c->f.dcp->mode_state.valid = true; + c->f.dcp->crtc = devm_kzalloc(c->f.dcp->dev, sizeof(*c->f.dcp->crtc), GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, c->f.dcp->crtc); + c->f.dcp->crtc->base.dev = c->drm; + INIT_DELAYED_WORK(&c->f.v14->backlight_work, v14_bl_rpc_work); + return c; +} + +static struct v14_bl_actor *v14_bl_actor_start(struct kunit *test, + struct v14_bl_rpc *c, unsigned int slot, + unsigned int op) +{ + struct v14_bl_actor *a = &c->actors[slot]; + + a->ctx = c; + a->op = op; + init_completion(&a->entered); + init_completion(&a->done); + a->task = kthread_create(v14_bl_actor_run, a, "v14-backlight-test"); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, a->task); + get_task_struct(a->task); + wake_up_process(a->task); + KUNIT_ASSERT_NE(test, wait_for_completion_timeout(&a->entered, HZ), 0UL); + return a; +} + +static struct drm_framebuffer *v14_bl_pause_publication(struct apple_dcp_v14 *v14, + struct drm_framebuffer *fb, + const u8 *surface, u64 iova, + u32 width, u32 height, u32 dst_y) +{ + struct v14_bl_rpc *c = v14_bl_rpc_current; + + /* Only the first publication pauses; its eventual call runs the real body. */ + kunit_deactivate_static_stub(c->test, dcp_v14_publish_surface); + complete(&c->rpc_entered); + wait_for_completion(&c->rpc_release); + return dcp_v14_publish_surface(v14, fb, surface, iova, width, height, dst_y); +} + +/* Swap and framebuffer publication cannot be reordered by another present. */ +static void v14_bl_present_publication_serialized(struct kunit *test) +{ + struct v14_bl_rpc *c = v14_bl_rpc_setup(test); + struct v14_bl_actor *first, *second; + + kunit_activate_static_stub(test, dcp_v14_publish_surface, v14_bl_pause_publication); + first = v14_bl_actor_start(test, c, 0, 0); + KUNIT_ASSERT_NE(test, wait_for_completion_timeout(&c->rpc_entered, HZ), 0UL); + second = v14_bl_actor_start(test, c, 1, 1); + KUNIT_EXPECT_EQ(test, + wait_for_completion_timeout(&second->done, msecs_to_jiffies(20)), 0UL); + KUNIT_EXPECT_EQ(test, atomic_read(&c->submits), 1); + complete_all(&c->rpc_release); + KUNIT_ASSERT_NE(test, wait_for_completion_timeout(&first->done, HZ), 0UL); + KUNIT_ASSERT_NE(test, wait_for_completion_timeout(&second->done, HZ), 0UL); + KUNIT_EXPECT_TRUE(test, first->shown); + KUNIT_EXPECT_TRUE(test, second->shown); + KUNIT_EXPECT_PTR_EQ(test, c->owner[1], &c->fb[0]); + KUNIT_EXPECT_PTR_EQ(test, c->f.v14->active_fb, &c->fb[1]); + KUNIT_EXPECT_EQ(test, c->f.v14->shown_iova, (u64)c->gem[1].dma_addr); + KUNIT_EXPECT_EQ(test, kref_read(&c->fb[0].base.refcount), 1U); + KUNIT_EXPECT_EQ(test, kref_read(&c->fb[1].base.refcount), 2U); +} + +/* A running brightness RPC pins its copied surface until suspend drains it. */ +static void v14_bl_running_work_suspend(struct kunit *test) +{ + struct v14_bl_rpc *c = v14_bl_rpc_setup(test); + struct v14_bl_actor *stop; + + KUNIT_ASSERT_TRUE(test, dcp_v14_present(c->f.v14, &c->fb[0], 64, 64, 0, NULL)); + atomic_set(&c->submits, 0); + c->block_first = true; + v14_bl_request(test, c->f.dcp, 200); + dcp_v14_backlight_kick(c->f.dcp); + KUNIT_ASSERT_NE(test, wait_for_completion_timeout(&c->rpc_entered, HZ), 0UL); + stop = v14_bl_actor_start(test, c, 0, 2); + KUNIT_EXPECT_EQ(test, wait_for_completion_timeout(&stop->done, msecs_to_jiffies(20)), 0UL); + KUNIT_EXPECT_PTR_EQ(test, c->owner[0], &c->fb[0]); + KUNIT_EXPECT_EQ(test, kref_read(&c->fb[0].base.refcount), 2U); + complete_all(&c->rpc_release); + KUNIT_ASSERT_NE(test, wait_for_completion_timeout(&stop->done, HZ), 0UL); + KUNIT_EXPECT_EQ(test, c->iova[0], (u64)c->gem[0].dma_addr); + KUNIT_EXPECT_EQ(test, c->f.v14->bl_gate, DCP_V14_BL_SUSPENDED); + v14_bl_request(test, c->f.dcp, 250); + dcp_v14_backlight_kick(c->f.dcp); + KUNIT_EXPECT_FALSE(test, work_busy(&c->f.v14->backlight_work.work)); + /* A callback queued before the gate shut must also refuse the pending level. */ + dcp_v14_backlight_work(&c->f.v14->backlight_work.work); + KUNIT_EXPECT_EQ(test, atomic_read(&c->submits), 1); + iomfb_v14_7_backlight_resume(c->f.dcp); + KUNIT_ASSERT_TRUE(test, dcp_v14_present(c->f.v14, &c->fb[0], 64, 64, 0, NULL)); + KUNIT_EXPECT_EQ(test, atomic_read(&c->submits), 2); + KUNIT_EXPECT_TRUE(test, c->nits_present[1]); + KUNIT_EXPECT_EQ(test, c->nits[1], dcp_v14_nits_binary64(250)); +} + +/* Actual dcp_poweroff/on must reach the 14.7 blank/restore hooks. */ +static void v14_bl_dpms_dispatch(struct kunit *test) +{ + struct v14_bl_rpc *c = v14_bl_rpc_setup(test); + struct platform_device *pdev = kunit_kzalloc(test, sizeof(*pdev), GFP_KERNEL); + + KUNIT_ASSERT_NOT_NULL(test, pdev); + platform_set_drvdata(pdev, c->f.dcp); + KUNIT_ASSERT_TRUE(test, dcp_v14_present(c->f.v14, &c->fb[0], 64, 64, 0, NULL)); + KUNIT_EXPECT_FALSE(test, c->nits_present[0]); + KUNIT_EXPECT_FALSE(test, dcp_backlight_level_known(c->f.dcp)); + dcp_poweroff(pdev); + KUNIT_EXPECT_EQ(test, atomic_read(&c->submits), 2); + KUNIT_EXPECT_FALSE(test, c->nits_present[1]); + KUNIT_EXPECT_PTR_EQ(test, c->f.v14->active_fb, NULL); + /* Even the advertised default must be an explicit first request. */ + dcp_poweron(pdev); + v14_bl_request(test, c->f.dcp, DCP_V14_NITS_DEFAULT); + KUNIT_ASSERT_TRUE(test, dcp_v14_present(c->f.v14, &c->fb[0], 64, 64, 0, NULL)); + KUNIT_EXPECT_TRUE(test, c->nits_present[2]); + KUNIT_EXPECT_TRUE(test, dcp_backlight_level_known(c->f.dcp)); + dcp_poweroff(pdev); + KUNIT_EXPECT_EQ(test, atomic_read(&c->submits), 4); + KUNIT_EXPECT_TRUE(test, c->nits_present[3]); + KUNIT_EXPECT_EQ(test, c->nits[3], 0ULL); + KUNIT_EXPECT_PTR_EQ(test, c->f.v14->active_fb, NULL); + dcp_poweron(pdev); + KUNIT_EXPECT_TRUE(test, dcp_backlight_pending(c->f.dcp)); + KUNIT_EXPECT_FALSE(test, c->f.dcp->backlight.state.dpms_off); + KUNIT_ASSERT_TRUE(test, dcp_v14_present(c->f.v14, &c->fb[0], 64, 64, 0, NULL)); + KUNIT_EXPECT_EQ(test, atomic_read(&c->submits), 5); + KUNIT_EXPECT_TRUE(test, c->nits_present[4]); + KUNIT_EXPECT_EQ(test, c->nits[4], dcp_v14_nits_binary64(DCP_V14_NITS_DEFAULT)); + KUNIT_EXPECT_FALSE(test, dcp_backlight_pending(c->f.dcp)); +} + +/* An actual failed submit must leave the loader's level unknown. */ +static void v14_bl_failed_submit_unknown(struct kunit *test) +{ + struct v14_bl_rpc *c = v14_bl_rpc_setup(test); + + c->refuse = true; + v14_bl_request(test, c->f.dcp, 240); + mutex_lock(&c->f.v14->lock); + KUNIT_EXPECT_EQ(test, dcp_v14_swap_locked(c->f.v14, NULL, 0, 0, 0, 0, NULL), -EIO); + mutex_unlock(&c->f.v14->lock); + KUNIT_EXPECT_FALSE(test, dcp_backlight_level_known(c->f.dcp)); + KUNIT_EXPECT_EQ(test, c->f.v14->nits_presents, 0ULL); + KUNIT_EXPECT_TRUE(test, c->f.v14->failed); + KUNIT_EXPECT_FALSE(test, work_busy(&c->f.v14->backlight_work.work)); + /* Neither a later kick nor a work callback may retry the failed panel. */ + dcp_v14_backlight_kick(c->f.dcp); + flush_delayed_work(&c->f.v14->backlight_work); + dcp_v14_backlight_work(&c->f.v14->backlight_work.work); + KUNIT_EXPECT_EQ(test, atomic_read(&c->submits), 1); + KUNIT_EXPECT_FALSE(test, work_busy(&c->f.v14->backlight_work.work)); +} + +static void v14_bl_mutex_unlock(void *data) +{ + mutex_unlock(data); +} + +/* Unbind closes class admission before draining a blocked real registrar. */ +static void v14_bl_registration_unbind_race(struct kunit *test) +{ + struct v14_bl_rpc *c = v14_bl_rpc_setup(test); + struct v14_bl_actor *stop; + unsigned long deadline; + bool running = false; + + mutex_lock(&c->f.dcp->bl_register_mutex); + KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, v14_bl_mutex_unlock, + &c->f.dcp->bl_register_mutex), 0); + KUNIT_EXPECT_TRUE(test, schedule_work(&c->f.dcp->bl_register_wq)); + deadline = jiffies + HZ; + do { + running = work_busy(&c->f.dcp->bl_register_wq) & WORK_BUSY_RUNNING; + if (running) + break; + usleep_range(50, 100); + } while (time_before(jiffies, deadline)); + KUNIT_EXPECT_TRUE(test, running); + stop = v14_bl_actor_start(test, c, 0, 3); + KUNIT_EXPECT_EQ(test, wait_for_completion_timeout(&stop->done, msecs_to_jiffies(20)), 0UL); + KUNIT_EXPECT_TRUE(test, READ_ONCE(c->f.dcp->quiescing)); + kunit_release_action(test, v14_bl_mutex_unlock, &c->f.dcp->bl_register_mutex); + KUNIT_ASSERT_NE(test, wait_for_completion_timeout(&stop->done, HZ), 0UL); + KUNIT_EXPECT_PTR_EQ(test, c->f.dcp->brightness.bl_dev, NULL); + KUNIT_EXPECT_PTR_EQ(test, c->f.v14->dcp, NULL); + KUNIT_EXPECT_EQ(test, c->f.v14->bl_gate, DCP_V14_BL_CLOSED); + /* A late shared producer is refused as well. */ + schedule_work(&c->f.dcp->bl_register_wq); + flush_work(&c->f.dcp->bl_register_wq); + KUNIT_EXPECT_PTR_EQ(test, c->f.dcp->brightness.bl_dev, NULL); +} + +static void v14_bl_class_unregistered(struct kunit *test) +{ + struct v14_bl_rpc *c = v14_bl_rpc_setup(test); + + schedule_work(&c->f.dcp->bl_register_wq); + flush_work(&c->f.dcp->bl_register_wq); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, c->f.dcp->brightness.bl_dev); + iomfb_v14_7_unbind(c->f.dcp); + KUNIT_EXPECT_PTR_EQ(test, c->f.dcp->brightness.bl_dev, NULL); + KUNIT_EXPECT_TRUE(test, c->f.dcp->quiescing); + iomfb_v14_7_backlight_resume(c->f.dcp); + KUNIT_EXPECT_EQ(test, c->f.v14->bl_gate, DCP_V14_BL_CLOSED); +} + +/* fbdev cannot turn the advertised default into the first brightness request. */ +static void v14_bl_fbdev_unknown_level(struct kunit *test) +{ + struct v14_bl_rpc *c = v14_bl_rpc_setup(test); + struct device unrelated = {}; + struct backlight_device *bd; + + schedule_work(&c->f.dcp->bl_register_wq); + flush_work(&c->f.dcp->bl_register_wq); + bd = c->f.dcp->brightness.bl_dev; + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, bd); + KUNIT_EXPECT_FALSE(test, bd->ops->controls_device(bd, NULL)); + KUNIT_EXPECT_FALSE(test, dcp_backlight_pending(c->f.dcp)); + KUNIT_EXPECT_EQ(test, atomic_read(&c->submits), 0); + v14_bl_request(test, c->f.dcp, 250); + KUNIT_ASSERT_TRUE(test, dcp_v14_present(c->f.v14, &c->fb[0], 64, 64, 0, NULL)); + KUNIT_EXPECT_TRUE(test, bd->ops->controls_device(bd, NULL)); + KUNIT_EXPECT_TRUE(test, bd->ops->controls_device(bd, c->f.dcp->dev)); + KUNIT_EXPECT_FALSE(test, bd->ops->controls_device(bd, &unrelated)); +} + +static struct kunit_case v14_bl_lifetime_cases[] = { + KUNIT_CASE(v14_bl_present_publication_serialized), + KUNIT_CASE(v14_bl_running_work_suspend), + KUNIT_CASE(v14_bl_dpms_dispatch), + KUNIT_CASE(v14_bl_failed_submit_unknown), + KUNIT_CASE(v14_bl_registration_unbind_race), + KUNIT_CASE(v14_bl_class_unregistered), + KUNIT_CASE(v14_bl_fbdev_unknown_level), + {} +}; + +static struct kunit_suite v14_bl_lifetime_suite = { + .name = "dcp_v14_backlight_lifetime", + .test_cases = v14_bl_lifetime_cases, +}; + +kunit_test_suite(v14_bl_lifetime_suite); diff --git a/drivers/gpu/drm/apple/iomfb_v14_7_backlight_test.c b/drivers/gpu/drm/apple/iomfb_v14_7_backlight_test.c new file mode 100644 index 0000000000000..e2704e5f1ba5f --- /dev/null +++ b/drivers/gpu/drm/apple/iomfb_v14_7_backlight_test.c @@ -0,0 +1,576 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* + * The 14.7 panel level path, included at the end of iomfb_v14_7.c so that + * the production functions run: the swap's level block, which boards carry + * it, the gate that keeps brightness-only presents out of system sleep and + * after unbind, and the policy as this panel uses it (no level presented + * until one is requested, no actual level reported until one was presented, + * a refused level never becomes the actual one). + * + * The objects are built here, with no DCP: nothing in a case may reach an + * RPC. A J615 or J514C panel never initialises the backlight policy; + * its state is set to answer "yes" so that a case fails if anything on + * those boards looks at it. + */ +#include +#include + +static void v14_bl_unused_work(struct work_struct *work) +{ +} + +static void v14_bl_binary64(struct kunit *test) +{ + static const struct { + u32 nits; + u64 bits; + } known[] = { + { 0, 0 }, + { 1, 0x3ff0000000000000ULL }, + { 2, 0x4000000000000000ULL }, + { 3, 0x4008000000000000ULL }, + { 140, 0x4061800000000000ULL }, + { 255, 0x406fe00000000000ULL }, + { 256, 0x4070000000000000ULL }, + { 500, 0x407f400000000000ULL }, + }; + unsigned int i; + u32 nits; + + for (i = 0; i < ARRAY_SIZE(known); i++) + KUNIT_EXPECT_EQ_MSG(test, dcp_v14_nits_binary64(known[i].nits), known[i].bits, + "%u nits", known[i].nits); + /* Every level up to the bound is an exact integral binary64. */ + for (nits = 1; nits <= DCP_V14_NITS_MAX; nits++) { + u64 bits = dcp_v14_nits_binary64(nits); + unsigned int e = (bits >> 52) - 1023; + u64 mantissa = bits & GENMASK_ULL(51, 0); + + KUNIT_ASSERT_LT(test, e, 32U); + KUNIT_EXPECT_EQ(test, mantissa & GENMASK_ULL(51 - e, 0), 0ULL); + KUNIT_EXPECT_EQ(test, (1ULL << e) + (mantissa >> (52 - e)), (u64)nits); + KUNIT_EXPECT_FALSE(test, bits >> 63); + } +} + +static void v14_bl_level_block(struct kunit *test) +{ + u8 *swap = kunit_kzalloc(test, DCP_V14_SWAP_SIZE, GFP_KERNEL); + u8 *before = kunit_kzalloc(test, DCP_V14_SWAP_SIZE, GFP_KERNEL); + unsigned int i; + + KUNIT_ASSERT_NOT_NULL(test, swap); + KUNIT_ASSERT_NOT_NULL(test, before); + memset(swap, 0xa5, DCP_V14_SWAP_SIZE); + memcpy(before, swap, DCP_V14_SWAP_SIZE); + + /* Above the wire bound: refused, the record untouched. */ + KUNIT_EXPECT_EQ(test, dcp_v14_encode_nits(swap, true, DCP_V14_NITS_MAX + 1), -EINVAL); + KUNIT_EXPECT_EQ(test, memcmp(swap, before, DCP_V14_SWAP_SIZE), 0); + + /* A level touches only the present byte and the binary64. */ + KUNIT_EXPECT_EQ(test, dcp_v14_encode_nits(swap, true, 140), 0); + for (i = 0; i < DCP_V14_SWAP_SIZE; i++) { + if (i == DCP_V14_SWAP_NITS_PRESENT || + (i >= DCP_V14_SWAP_NITS && i < DCP_V14_SWAP_NITS + 8)) + continue; + KUNIT_EXPECT_EQ_MSG(test, swap[i], before[i], "byte %#x", i); + } + KUNIT_EXPECT_EQ(test, swap[DCP_V14_SWAP_NITS_PRESENT], 1); + KUNIT_EXPECT_EQ(test, get_unaligned_le64(swap + DCP_V14_SWAP_NITS), + 0x4061800000000000ULL); + + /* No level: zero, as the record the session sent before. */ + KUNIT_EXPECT_EQ(test, dcp_v14_encode_nits(swap, false, 0), 0); + KUNIT_EXPECT_EQ(test, swap[DCP_V14_SWAP_NITS_PRESENT], 0); + KUNIT_EXPECT_EQ(test, get_unaligned_le64(swap + DCP_V14_SWAP_NITS), 0ULL); +} + +/* A swap without a level is byte for byte the one encoded before this. */ +static void v14_bl_swap_unchanged_without_level(struct kunit *test) +{ + u8 *swap = kunit_kzalloc(test, DCP_V14_SWAP_SIZE, GFP_KERNEL); + u8 *plain = kunit_kzalloc(test, DCP_V14_SWAP_SIZE, GFP_KERNEL); + u8 surface[DCP_V14_SURFACE_SIZE]; + + KUNIT_ASSERT_NOT_NULL(test, swap); + KUNIT_ASSERT_NOT_NULL(test, plain); + dcp_v14_encode_surface(surface, 3456 * 4, 3456, 2160, true); + dcp_v14_encode_swap(plain, 7, DCP_V14_BLACK, surface, 0x100000000ULL, 3456, 2160, 74, + DCP_V14_LAYER); + dcp_v14_encode_swap(swap, 7, DCP_V14_BLACK, surface, 0x100000000ULL, 3456, 2160, 74, + DCP_V14_LAYER); + dcp_v14_encode_nits(swap, false, 0); + KUNIT_EXPECT_EQ(test, memcmp(swap, plain, DCP_V14_SWAP_SIZE), 0); + /* The level block lies in the zeroed prefix, clear of every field. */ + KUNIT_EXPECT_EQ(test, plain[DCP_V14_SWAP_NITS_PRESENT], 0); + KUNIT_EXPECT_EQ(test, get_unaligned_le64(plain + DCP_V14_SWAP_NITS), 0ULL); + + dcp_v14_encode_swap(plain, 8, DCP_V14_BLACK, NULL, 0, 0, 0, 0, DCP_V14_LAYER); + dcp_v14_encode_swap(swap, 8, DCP_V14_BLACK, NULL, 0, 0, 0, 0, DCP_V14_LAYER); + dcp_v14_encode_nits(swap, false, 0); + KUNIT_EXPECT_EQ(test, memcmp(swap, plain, DCP_V14_SWAP_SIZE), 0); +} + +static bool v14_bl_ctx_dcp_is(const void *ctx, const char *compat) +{ + return !strcmp(((const char *const *)ctx)[0], compat); +} + +static bool v14_bl_ctx_machine_is(const void *ctx, const char *compat) +{ + return !strcmp(((const char *const *)ctx)[1], compat); +} + +/* Only admitted T6030, J516C and J613 internal panels carry levels. */ +static void v14_bl_boards(struct kunit *test) +{ + static const char *const t6030_j516s[] = { "apple,t6030-dcp", "apple,j516s" }; + static const char *const t6030_j514s[] = { "apple,t6030-dcp", "apple,j514s" }; + static const char *const j613[] = { "apple,t8122-dcp", "apple,j613" }; + static const char *const j615[] = { "apple,t8122-dcp", "apple,j615" }; + static const char *const j516c[] = { "apple,t6031-dcp", "apple,j516c" }; + static const char *const other_t6030[] = { "apple,t6030-dcp", "apple,other" }; + const struct dcp_v14_board *board; + bool known; + + board = dcp_v14_board_select(v14_bl_ctx_dcp_is, v14_bl_ctx_machine_is, t6030_j516s, &known); + KUNIT_ASSERT_NOT_NULL(test, board); + KUNIT_EXPECT_TRUE(test, board->panel_nits); + KUNIT_EXPECT_TRUE(test, dcp_v14_panel_nits_admit(board, v14_bl_ctx_machine_is, + t6030_j516s)); + board = dcp_v14_board_select(v14_bl_ctx_dcp_is, v14_bl_ctx_machine_is, t6030_j514s, &known); + KUNIT_ASSERT_NOT_NULL(test, board); + KUNIT_EXPECT_TRUE(test, board->panel_nits); + KUNIT_EXPECT_TRUE(test, dcp_v14_panel_nits_admit(board, v14_bl_ctx_machine_is, + t6030_j514s)); + board = dcp_v14_board_select(v14_bl_ctx_dcp_is, v14_bl_ctx_machine_is, other_t6030, &known); + KUNIT_ASSERT_NOT_NULL(test, board); + KUNIT_EXPECT_FALSE(test, dcp_v14_panel_nits_admit(board, v14_bl_ctx_machine_is, + other_t6030)); + board = dcp_v14_board_select(v14_bl_ctx_dcp_is, v14_bl_ctx_machine_is, j613, &known); + KUNIT_ASSERT_NOT_NULL(test, board); + KUNIT_EXPECT_TRUE(test, board->panel_nits); + KUNIT_EXPECT_TRUE(test, dcp_v14_panel_nits_admit(board, v14_bl_ctx_machine_is, j613)); + KUNIT_EXPECT_FALSE(test, dcp_v14_panel_nits_admit(board, v14_bl_ctx_machine_is, j615)); + board = dcp_v14_board_select(v14_bl_ctx_dcp_is, v14_bl_ctx_machine_is, j615, &known); + KUNIT_ASSERT_NOT_NULL(test, board); + KUNIT_EXPECT_FALSE(test, board->panel_nits); + board = dcp_v14_board_select(v14_bl_ctx_dcp_is, v14_bl_ctx_machine_is, j516c, &known); + KUNIT_ASSERT_NOT_NULL(test, board); + KUNIT_EXPECT_TRUE(test, board->panel_nits); + KUNIT_EXPECT_TRUE(test, dcp_v14_panel_nits_admit(board, v14_bl_ctx_machine_is, j516c)); +} + +static void v14_bl_gate_transitions(struct kunit *test) +{ + KUNIT_EXPECT_TRUE(test, dcp_v14_bl_may_queue(DCP_V14_BL_OPEN)); + KUNIT_EXPECT_FALSE(test, dcp_v14_bl_may_queue(DCP_V14_BL_SUSPENDED)); + KUNIT_EXPECT_FALSE(test, dcp_v14_bl_may_queue(DCP_V14_BL_CLOSED)); + KUNIT_EXPECT_EQ(test, dcp_v14_bl_gate_suspend(DCP_V14_BL_OPEN), DCP_V14_BL_SUSPENDED); + KUNIT_EXPECT_EQ(test, dcp_v14_bl_gate_suspend(DCP_V14_BL_SUSPENDED), DCP_V14_BL_SUSPENDED); + KUNIT_EXPECT_EQ(test, dcp_v14_bl_gate_suspend(DCP_V14_BL_CLOSED), DCP_V14_BL_CLOSED); + KUNIT_EXPECT_EQ(test, dcp_v14_bl_gate_resume(DCP_V14_BL_SUSPENDED), DCP_V14_BL_OPEN); + KUNIT_EXPECT_EQ(test, dcp_v14_bl_gate_resume(DCP_V14_BL_OPEN), DCP_V14_BL_OPEN); + /* Unbind is for good: no resume opens it again. */ + KUNIT_EXPECT_EQ(test, dcp_v14_bl_gate_resume(DCP_V14_BL_CLOSED), DCP_V14_BL_CLOSED); +} + +static void v14_bl_plan(struct kunit *test) +{ + const struct dcp_v14_bl_facts ready = { + .gate = DCP_V14_BL_OPEN, .panel_nits = true, .started = true, + .pending = true, .frame = true, .mode_valid = true, + }; + struct dcp_v14_bl_facts f; + + KUNIT_EXPECT_EQ(test, dcp_v14_bl_plan(&ready), DCP_V14_BL_REPRESENT); +#define V14_BL_EXPECT(field, value, action) do { \ + f = ready; \ + f.field = value; \ + KUNIT_EXPECT_EQ_MSG(test, dcp_v14_bl_plan(&f), action, \ + #field " = " #value); \ + } while (0) + V14_BL_EXPECT(gate, DCP_V14_BL_SUSPENDED, DCP_V14_BL_SKIP); + V14_BL_EXPECT(gate, DCP_V14_BL_CLOSED, DCP_V14_BL_SKIP); + V14_BL_EXPECT(panel_nits, false, DCP_V14_BL_SKIP); + V14_BL_EXPECT(external, true, DCP_V14_BL_SKIP); + V14_BL_EXPECT(typec, true, DCP_V14_BL_SKIP); + V14_BL_EXPECT(started, false, DCP_V14_BL_SKIP); + V14_BL_EXPECT(failed, true, DCP_V14_BL_SKIP); + V14_BL_EXPECT(crashed, true, DCP_V14_BL_SKIP); + V14_BL_EXPECT(quiescing, true, DCP_V14_BL_SKIP); + V14_BL_EXPECT(pending, false, DCP_V14_BL_SKIP); + V14_BL_EXPECT(frame, false, DCP_V14_BL_WAIT); + V14_BL_EXPECT(mode_valid, false, DCP_V14_BL_WAIT); +#undef V14_BL_EXPECT + KUNIT_EXPECT_TRUE(test, dcp_v14_swap_takes_nits(true, false, false, true)); + KUNIT_EXPECT_FALSE(test, dcp_v14_swap_takes_nits(false, false, false, true)); + KUNIT_EXPECT_FALSE(test, dcp_v14_swap_takes_nits(true, true, false, true)); + KUNIT_EXPECT_FALSE(test, dcp_v14_swap_takes_nits(true, false, true, true)); + KUNIT_EXPECT_FALSE(test, dcp_v14_swap_takes_nits(true, false, false, false)); +} + +/* The policy as this panel configures it: a default that is never presented. */ +static void v14_bl_policy(struct kunit *test) +{ + struct dcp_backlight_state s; + struct dcp_backlight_present p; + u64 seq; + + KUNIT_ASSERT_EQ(test, dcp_bl_init(&s, DCP_V14_NITS_MAX, false, 0, true, + DCP_V14_NITS_DEFAULT), 0); + /* Nothing to present at takeover: the panel keeps the loader's level. */ + KUNIT_EXPECT_EQ(test, dcp_bl_prepare(&s, true, &p), -EALREADY); + KUNIT_EXPECT_EQ(test, dcp_bl_reported_actual(&s, true), -ENODATA); + /* The H17P path keeps reporting its default. */ + KUNIT_EXPECT_EQ(test, dcp_bl_reported_actual(&s, false), DCP_V14_NITS_DEFAULT); + KUNIT_EXPECT_FALSE(test, dcp_v14_bl_follow_dpms(true, true, s.level_known)); + + /* A request is presented; a refusal never becomes the actual level. */ + KUNIT_ASSERT_EQ(test, dcp_bl_request(&s, 250, false, false), 0); + KUNIT_ASSERT_EQ(test, dcp_bl_prepare(&s, true, &p), 0); + KUNIT_EXPECT_EQ(test, p.nits, 250U); + KUNIT_EXPECT_TRUE(test, dcp_bl_complete(&s, p.sequence, false)); + KUNIT_EXPECT_EQ(test, dcp_bl_reported_actual(&s, true), -ENODATA); + KUNIT_EXPECT_TRUE(test, s.dirty); + KUNIT_EXPECT_EQ(test, dcp_bl_retry_delay(&s), 100U); + KUNIT_ASSERT_EQ(test, dcp_bl_prepare(&s, true, &p), 0); + seq = p.sequence; + /* A stale completion does not complete the newer reservation. */ + KUNIT_EXPECT_FALSE(test, dcp_bl_complete(&s, seq - 1, true)); + KUNIT_EXPECT_TRUE(test, dcp_bl_complete(&s, seq, true)); + KUNIT_EXPECT_EQ(test, dcp_bl_reported_actual(&s, true), 250); + KUNIT_EXPECT_TRUE(test, dcp_v14_bl_follow_dpms(true, true, s.level_known)); + + /* A later refusal keeps the last accepted level. */ + KUNIT_ASSERT_EQ(test, dcp_bl_request(&s, 300, false, false), 0); + KUNIT_ASSERT_EQ(test, dcp_bl_prepare(&s, true, &p), 0); + KUNIT_EXPECT_TRUE(test, dcp_bl_complete(&s, p.sequence, false)); + KUNIT_EXPECT_EQ(test, dcp_bl_reported_actual(&s, true), 250); + KUNIT_ASSERT_EQ(test, dcp_bl_prepare(&s, true, &p), 0); + KUNIT_EXPECT_TRUE(test, dcp_bl_complete(&s, p.sequence, true)); + + /* DPMS off blanks at 0 nits, on presents the level again. */ + KUNIT_ASSERT_EQ(test, dcp_bl_dpms(&s, false), 0); + KUNIT_ASSERT_EQ(test, dcp_bl_prepare(&s, true, &p), 0); + KUNIT_EXPECT_EQ(test, p.nits, 0U); + KUNIT_EXPECT_TRUE(test, dcp_bl_complete(&s, p.sequence, true)); + KUNIT_ASSERT_EQ(test, dcp_bl_dpms(&s, true), 0); + KUNIT_ASSERT_EQ(test, dcp_bl_prepare(&s, true, &p), 0); + KUNIT_EXPECT_EQ(test, p.nits, 300U); + KUNIT_EXPECT_TRUE(test, dcp_bl_complete(&s, p.sequence, true)); +} + +struct v14_bl_fixture { + struct apple_dcp *dcp; + struct apple_dcp_v14 *v14; +}; + +static void v14_bl_fixture_stop(void *data) +{ + struct apple_dcp_v14 *v14 = data; + + cancel_delayed_work_sync(&v14->backlight_work); +} + +/* + * An internal 14.7 panel on @board, started, with no DCP behind it. With + * @policy, the T6030 panel's policy as dcp_v14_configure_backlight() sets it + * up; without, a policy that was never initialised (other boards), whose + * state would answer "yes" if anything looked at it. + */ +static struct v14_bl_fixture v14_bl_fixture(struct kunit *test, + const struct dcp_v14_board *board, bool policy) +{ + static atomic_t serial = ATOMIC_INIT(0); + struct v14_bl_fixture f; + struct device *dev; + char name[32]; + + snprintf(name, sizeof(name), "dcp-v14-bl.%d", atomic_inc_return(&serial)); + dev = kunit_device_register(test, name); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dev); + /* The device's backlight cleanup must run before these objects are freed. */ + f.dcp = devm_kzalloc(dev, sizeof(*f.dcp), GFP_KERNEL); + f.v14 = devm_kzalloc(dev, sizeof(*f.v14), GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, f.dcp); + KUNIT_ASSERT_NOT_NULL(test, f.v14); + f.dcp->dev = dev; + spin_lock_init(&f.dcp->mode_state.lock); + mutex_init(&f.dcp->bl_register_mutex); + INIT_WORK(&f.dcp->bl_register_wq, dcp_work_register_backlight); + INIT_WORK(&f.dcp->bl_update_wq, v14_bl_unused_work); + INIT_DELAYED_WORK(&f.dcp->swap_watchdog_wq, v14_bl_unused_work); + f.dcp->fw_compat = DCP_FIRMWARE_V_14_7; + f.dcp->v14 = f.v14; + f.v14->dev = dev; + f.v14->board = board; + f.v14->panel_nits = board && board->panel_nits; + f.v14->dcp = f.dcp; + f.v14->started = true; + mutex_init(&f.v14->lock); + INIT_DELAYED_WORK(&f.v14->backlight_work, dcp_v14_backlight_work); + spin_lock_init(&f.v14->bl_gate_lock); + KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, v14_bl_fixture_stop, f.v14), 0); + if (policy) { + KUNIT_ASSERT_EQ(test, dcp_backlight_init(f.dcp, dev), 0); + KUNIT_ASSERT_EQ(test, dcp_bl_init(&f.dcp->backlight.state, DCP_V14_NITS_MAX, + false, 0, true, DCP_V14_NITS_DEFAULT), 0); + f.dcp->backlight.kick = dcp_v14_backlight_kick; + } else { + f.dcp->backlight.state.ready = true; + f.dcp->backlight.state.level_known = true; + f.dcp->backlight.state.dirty = true; + } + return f; +} + +/* Through the policy's own entry point, under its lock. */ +static void v14_bl_request(struct kunit *test, struct apple_dcp *dcp, u32 nits) +{ + unsigned long flags; + int ret; + + spin_lock_irqsave(&dcp->backlight.lock, flags); + ret = dcp_bl_request(&dcp->backlight.state, nits, false, false); + spin_unlock_irqrestore(&dcp->backlight.lock, flags); + KUNIT_ASSERT_EQ(test, ret, 0); +} + +/* Whether the work is queued or running; the caller holds lock. */ +static bool v14_bl_queued(struct apple_dcp_v14 *v14) +{ + return work_busy(&v14->backlight_work.work); +} + +/* + * The production paths on J615 and J514C: no policy is consulted, no + * level is encoded, the work does nothing, sleep leaves the gate alone. + */ +static void v14_bl_other_boards_untouched(struct kunit *test) +{ + const struct dcp_v14_board *boards[] = { + &dcp_v14_board_j615, &dcp_v14_board_j514c, + }; + struct dcp_backlight_present present; + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(boards); i++) { + struct v14_bl_fixture f = v14_bl_fixture(test, boards[i], false); + u8 *swap = kunit_kzalloc(test, DCP_V14_SWAP_SIZE, GFP_KERNEL); + + KUNIT_ASSERT_NOT_NULL(test, swap); + KUNIT_EXPECT_FALSE(test, iomfb_v14_7_panel_nits(f.dcp)); + KUNIT_EXPECT_FALSE(test, dcp_backlight_active(f.dcp)); + KUNIT_EXPECT_FALSE(test, dcp_backlight_level_known(f.dcp)); + KUNIT_EXPECT_FALSE(test, dcp_v14_bl_dpms_applies(f.v14, f.dcp)); + + mutex_lock(&f.v14->lock); + KUNIT_EXPECT_FALSE(test, dcp_v14_swap_nits(f.v14, swap, &present)); + mutex_unlock(&f.v14->lock); + KUNIT_EXPECT_EQ(test, swap[DCP_V14_SWAP_NITS_PRESENT], 0); + KUNIT_EXPECT_EQ(test, get_unaligned_le64(swap + DCP_V14_SWAP_NITS), 0ULL); + + /* Run directly: it must return without an RPC (there is no DCP). */ + dcp_v14_backlight_work(&f.v14->backlight_work.work); + KUNIT_EXPECT_EQ(test, f.v14->brightness_presents, 0ULL); + KUNIT_EXPECT_EQ(test, f.v14->nits_refused, 0ULL); + + iomfb_v14_7_backlight_suspend(f.dcp); + KUNIT_EXPECT_EQ(test, f.v14->bl_gate, DCP_V14_BL_OPEN); + dcp_v14_configure_backlight(f.dcp); + KUNIT_EXPECT_FALSE(test, f.dcp->backlight.kick); + } +} + +/* An external T6030 processor is no panel: nothing either. */ +static void v14_bl_external_untouched(struct kunit *test) +{ + struct v14_bl_fixture f = v14_bl_fixture(test, &dcp_v14_board_t6030, false); + struct dcp_backlight_present present; + u8 *swap = kunit_kzalloc(test, DCP_V14_SWAP_SIZE, GFP_KERNEL); + + KUNIT_ASSERT_NOT_NULL(test, swap); + f.dcp->external = true; + f.v14->external = true; + KUNIT_EXPECT_FALSE(test, iomfb_v14_7_panel_nits(f.dcp)); + KUNIT_EXPECT_FALSE(test, dcp_backlight_level_known(f.dcp)); + KUNIT_EXPECT_FALSE(test, dcp_v14_bl_dpms_applies(f.v14, f.dcp)); + mutex_lock(&f.v14->lock); + KUNIT_EXPECT_FALSE(test, dcp_v14_swap_nits(f.v14, swap, &present)); + mutex_unlock(&f.v14->lock); + KUNIT_EXPECT_EQ(test, swap[DCP_V14_SWAP_NITS_PRESENT], 0); + dcp_v14_backlight_work(&f.v14->backlight_work.work); + KUNIT_EXPECT_EQ(test, f.v14->brightness_presents, 0ULL); +} + +/* + * An admitted panel: no level until one is requested; the requested level is + * encoded; a refusal keeps the actual level without a retry; an accepted one + * is the actual level, and only then does blanking follow DPMS. + */ +static void v14_bl_swap_levels(struct kunit *test, const struct dcp_v14_board *board) +{ + struct v14_bl_fixture f = v14_bl_fixture(test, board, true); + struct dcp_backlight_present present; + u8 *swap = kunit_kzalloc(test, DCP_V14_SWAP_SIZE, GFP_KERNEL); + + KUNIT_ASSERT_NOT_NULL(test, swap); + KUNIT_EXPECT_TRUE(test, iomfb_v14_7_panel_nits(f.dcp)); + KUNIT_EXPECT_TRUE(test, dcp_backlight_active(f.dcp)); + KUNIT_EXPECT_FALSE(test, dcp_backlight_level_known(f.dcp)); + KUNIT_EXPECT_FALSE(test, dcp_v14_bl_dpms_applies(f.v14, f.dcp)); + + mutex_lock(&f.v14->lock); + /* Nothing requested: the swap carries no level. */ + KUNIT_EXPECT_FALSE(test, dcp_v14_swap_nits(f.v14, swap, &present)); + KUNIT_EXPECT_EQ(test, swap[DCP_V14_SWAP_NITS_PRESENT], 0); + mutex_unlock(&f.v14->lock); + + v14_bl_request(test, f.dcp, 250); + mutex_lock(&f.v14->lock); + KUNIT_EXPECT_TRUE(test, dcp_v14_swap_nits(f.v14, swap, &present)); + KUNIT_EXPECT_EQ(test, swap[DCP_V14_SWAP_NITS_PRESENT], 1); + KUNIT_EXPECT_EQ(test, get_unaligned_le64(swap + DCP_V14_SWAP_NITS), + dcp_v14_nits_binary64(250)); + /* Refused: no actual level, and the panel must not queue a retry. */ + dcp_v14_swap_nits_done(f.v14, &present, false); + mutex_unlock(&f.v14->lock); + KUNIT_EXPECT_FALSE(test, dcp_backlight_level_known(f.dcp)); + KUNIT_EXPECT_EQ(test, f.v14->nits_refused, 1ULL); + KUNIT_EXPECT_FALSE(test, delayed_work_pending(&f.v14->backlight_work)); + + /* While a Type-C display has the pipe, no level either. */ + f.dcp->fixed_connector_type = DRM_MODE_CONNECTOR_USB; + memset(swap, 0, DCP_V14_SWAP_SIZE); + mutex_lock(&f.v14->lock); + KUNIT_EXPECT_FALSE(test, dcp_v14_swap_nits(f.v14, swap, &present)); + mutex_unlock(&f.v14->lock); + KUNIT_EXPECT_EQ(test, swap[DCP_V14_SWAP_NITS_PRESENT], 0); + f.dcp->fixed_connector_type = DRM_MODE_CONNECTOR_eDP; + + mutex_lock(&f.v14->lock); + KUNIT_EXPECT_TRUE(test, dcp_v14_swap_nits(f.v14, swap, &present)); + dcp_v14_swap_nits_done(f.v14, &present, true); + mutex_unlock(&f.v14->lock); + KUNIT_EXPECT_TRUE(test, dcp_backlight_level_known(f.dcp)); + KUNIT_EXPECT_EQ(test, f.v14->nits_sent, 250U); + KUNIT_EXPECT_TRUE(test, dcp_v14_bl_dpms_applies(f.v14, f.dcp)); + + /* Unchanged: not sent again. */ + mutex_lock(&f.v14->lock); + KUNIT_EXPECT_FALSE(test, dcp_v14_swap_nits(f.v14, swap, &present)); + mutex_unlock(&f.v14->lock); +} + +static void v14_bl_t6030_swap_levels(struct kunit *test) +{ + v14_bl_swap_levels(test, &dcp_v14_board_t6030); +} + +static void v14_bl_j613_swap_levels(struct kunit *test) +{ + v14_bl_swap_levels(test, &dcp_v14_board_j613); +} + +/* + * The gate, through the production queue path: open, a kick queues the work; + * suspended, nothing queues until resume; closed, nothing ever does. The + * caller holds lock, so a queued work stays queued or running. + */ +static void v14_bl_t6030_gate(struct kunit *test) +{ + struct v14_bl_fixture f = v14_bl_fixture(test, &dcp_v14_board_t6030, true); + + mutex_lock(&f.v14->lock); + dcp_v14_backlight_kick(f.dcp); + KUNIT_EXPECT_TRUE(test, v14_bl_queued(f.v14)); + mutex_unlock(&f.v14->lock); + cancel_delayed_work_sync(&f.v14->backlight_work); + + iomfb_v14_7_backlight_suspend(f.dcp); + KUNIT_EXPECT_EQ(test, f.v14->bl_gate, DCP_V14_BL_SUSPENDED); + mutex_lock(&f.v14->lock); + dcp_v14_backlight_kick(f.dcp); + dcp_v14_bl_queue(f.v14, 0); + KUNIT_EXPECT_FALSE(test, v14_bl_queued(f.v14)); + mutex_unlock(&f.v14->lock); + + iomfb_v14_7_backlight_resume(f.dcp); + KUNIT_EXPECT_EQ(test, f.v14->bl_gate, DCP_V14_BL_OPEN); + mutex_lock(&f.v14->lock); + dcp_v14_backlight_kick(f.dcp); + KUNIT_EXPECT_TRUE(test, v14_bl_queued(f.v14)); + mutex_unlock(&f.v14->lock); + cancel_delayed_work_sync(&f.v14->backlight_work); + + /* A quiescing DCP takes no kick either. */ + WRITE_ONCE(f.dcp->quiescing, true); + mutex_lock(&f.v14->lock); + dcp_v14_backlight_kick(f.dcp); + KUNIT_EXPECT_FALSE(test, v14_bl_queued(f.v14)); + mutex_unlock(&f.v14->lock); + WRITE_ONCE(f.dcp->quiescing, false); + + dcp_v14_bl_shut(f.v14, true); + KUNIT_EXPECT_EQ(test, f.v14->bl_gate, DCP_V14_BL_CLOSED); + iomfb_v14_7_backlight_resume(f.dcp); + KUNIT_EXPECT_EQ(test, f.v14->bl_gate, DCP_V14_BL_CLOSED); + mutex_lock(&f.v14->lock); + dcp_v14_backlight_kick(f.dcp); + dcp_v14_bl_queue(f.v14, 0); + KUNIT_EXPECT_FALSE(test, v14_bl_queued(f.v14)); + mutex_unlock(&f.v14->lock); +} + +/* + * The work itself on the T6030 panel, with no DCP: blanked (no frame) it + * leaves the level to the next present; with the gate shut, or while + * suspended, it does nothing. None of these may reach an RPC. + */ +static void v14_bl_t6030_work(struct kunit *test) +{ + struct v14_bl_fixture f = v14_bl_fixture(test, &dcp_v14_board_t6030, true); + + v14_bl_request(test, f.dcp, 250); + WRITE_ONCE(f.dcp->mode_state.valid, true); + /* No frame shown: the level stays pending for the next present. */ + dcp_v14_backlight_work(&f.v14->backlight_work.work); + KUNIT_EXPECT_EQ(test, f.v14->brightness_presents, 0ULL); + KUNIT_EXPECT_TRUE(test, dcp_backlight_pending(f.dcp)); + + iomfb_v14_7_backlight_suspend(f.dcp); + dcp_v14_backlight_work(&f.v14->backlight_work.work); + KUNIT_EXPECT_TRUE(test, dcp_backlight_pending(f.dcp)); + iomfb_v14_7_backlight_resume(f.dcp); + + dcp_v14_bl_shut(f.v14, true); + dcp_v14_backlight_work(&f.v14->backlight_work.work); + KUNIT_EXPECT_EQ(test, f.v14->brightness_presents, 0ULL); + KUNIT_EXPECT_TRUE(test, dcp_backlight_pending(f.dcp)); +} + +static struct kunit_case v14_backlight_cases[] = { + KUNIT_CASE(v14_bl_binary64), + KUNIT_CASE(v14_bl_level_block), + KUNIT_CASE(v14_bl_swap_unchanged_without_level), + KUNIT_CASE(v14_bl_boards), + KUNIT_CASE(v14_bl_gate_transitions), + KUNIT_CASE(v14_bl_plan), + KUNIT_CASE(v14_bl_policy), + KUNIT_CASE(v14_bl_other_boards_untouched), + KUNIT_CASE(v14_bl_external_untouched), + KUNIT_CASE(v14_bl_t6030_swap_levels), + KUNIT_CASE(v14_bl_j613_swap_levels), + KUNIT_CASE(v14_bl_t6030_gate), + KUNIT_CASE(v14_bl_t6030_work), + {} +}; + +static struct kunit_suite v14_backlight_suite = { + .name = "dcp_v14_backlight", + .test_cases = v14_backlight_cases, +}; + +kunit_test_suite(v14_backlight_suite); diff --git a/drivers/gpu/drm/apple/iomfb_v14_7_board.h b/drivers/gpu/drm/apple/iomfb_v14_7_board.h index 368dab49c2944..6de1ae9004122 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7_board.h +++ b/drivers/gpu/drm/apple/iomfb_v14_7_board.h @@ -39,6 +39,15 @@ struct dcp_v14_board { * releases, so they are only set where tested. */ u32 ctm_set, ctm_get; + /* + * The panel's swaps carry the constant timestamp + * DCP_V14_SWAP_TIMESTAMP_VALUE in slots 0, 1 and 6. With zero + * timestamps this board's firmware completes at most one swap per + * 16.7 ms, so a 120 Hz panel presents at 60 fps. + */ + bool swap_timestamps; + /* This board has a qualified panel level block; machine admission is separate. */ + bool panel_nits; }; /* Whether the DCP node or the machine (as @ctx gives them) has @compat. */ @@ -59,4 +68,11 @@ const struct dcp_v14_board *dcp_v14_board_select(dcp_v14_compat_fn dcp_is, */ bool dcp_v14_board_admits(const struct dcp_v14_board *board, const char *uuid); +/* + * Whether a swap carries timestamps: only the internal panel of a board whose + * record asks for them. External processors (@external, no record) and every + * other board send zero timestamps. + */ +bool dcp_v14_board_stamps_swaps(const struct dcp_v14_board *board, bool external); + #endif /* __APPLE_IOMFB_V14_7_BOARD_H__ */ diff --git a/drivers/gpu/drm/apple/iomfb_v14_7_external_modeset_test.c b/drivers/gpu/drm/apple/iomfb_v14_7_external_modeset_test.c new file mode 100644 index 0000000000000..170aa59fb4234 --- /dev/null +++ b/drivers/gpu/drm/apple/iomfb_v14_7_external_modeset_test.c @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* + * The DCP calls of an external mode set, included at the end of + * iomfb_v14_7.c so that the production dcp_v14_external_modeset() runs. + * Every call is answered as accepted; the audio service is stubbed. + */ +#include +#include +#include + +#define V14_EXT_MODESET_MAX_CALLS 8 + +struct v14_ext_modeset { + struct apple_dcp *dcp; + struct apple_dcp_v14 *v14; + struct dcp_display_mode mode; + u32 tags[V14_EXT_MODESET_MAX_CALLS]; + bool power[V14_EXT_MODESET_MAX_CALLS]; + unsigned int calls; +}; + +static struct v14_ext_modeset *v14_ext_modeset_current; + +static int v14_ext_modeset_call(struct apple_dcp_v14 *v14, u32 tag, const void *in, u32 in_size, + void *out, u32 out_size, u32 completion, dcp_v14_admit_fn admit, + void *admit_cookie) +{ + struct v14_ext_modeset *s = v14_ext_modeset_current; + + if (s->calls < V14_EXT_MODESET_MAX_CALLS) { + s->tags[s->calls] = tag; + s->power[s->calls] = tag == A(472) && in_size >= 8 && get_unaligned_le64(in); + } + s->calls++; + if (out) + memset(out, 0, out_size); + return 0; +} + +static void v14_ext_modeset_audio(struct apple_dcp *dcp) +{ +} + +static void v14_ext_modeset_retry(struct work_struct *work) +{ +} + +static void v14_ext_modeset_stop(void *data) +{ + struct apple_dcp *dcp = data; + + cancel_delayed_work_sync(&dcp->external_retry_wq); +} + +/* A started, described native external session with one mode, its display @powered. */ +static struct v14_ext_modeset *v14_ext_modeset_setup(struct kunit *test, bool powered) +{ + struct v14_ext_modeset *s = kunit_kzalloc(test, sizeof(*s), GFP_KERNEL); + struct device *dev; + + KUNIT_ASSERT_NOT_NULL(test, s); + dev = kunit_device_register(test, "dcp-v14-ext-modeset"); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dev); + s->dcp = devm_kzalloc(dev, sizeof(*s->dcp), GFP_KERNEL); + s->v14 = devm_kzalloc(dev, sizeof(*s->v14), GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, s->dcp); + KUNIT_ASSERT_NOT_NULL(test, s->v14); + s->dcp->dev = dev; + s->dcp->fw_compat = DCP_FIRMWARE_V_14_7; + s->dcp->external = true; + s->dcp->external_native = true; + /* Never dereferenced: the audio service is stubbed. */ + s->dcp->avep = (struct apple_dcp_afkep *)s; + mutex_init(&s->dcp->modes_lock); + mutex_init(&s->dcp->hpd_mutex); + spin_lock_init(&s->dcp->metadata.lock); + spin_lock_init(&s->dcp->mode_state.lock); + INIT_DELAYED_WORK(&s->dcp->external_retry_wq, v14_ext_modeset_retry); + /* 3840x2160 at 100 Hz, timing 167 and color 219 as a J514S TV reports it. */ + s->mode.mode = (struct drm_display_mode){ + DRM_MODE("3840x2160", DRM_MODE_TYPE_DRIVER, 1188000, 3840, 4896, 4984, 5280, 0, + 2160, 2168, 2178, 2250, 0, DRM_MODE_FLAG_PHSYNC | DRM_MODE_FLAG_PVSYNC) + }; + s->mode.timing_mode_id = 167; + s->mode.color_mode_id = 219; + s->dcp->modes = &s->mode; + s->dcp->nr_modes = 1; + s->dcp->modes_admitted = true; + s->dcp->v14 = s->v14; + s->v14->dev = dev; + s->v14->dcp = s->dcp; + s->v14->external = true; + s->v14->started = true; + s->v14->opened = true; + s->v14->powered = powered; + s->v14->described = DCP_V14_DESC_ALL; + mutex_init(&s->v14->lock); + init_waitqueue_head(&s->v14->described_wait); + KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, v14_ext_modeset_stop, s->dcp), 0); + v14_ext_modeset_current = s; + kunit_activate_static_stub(test, dcp_v14_call_guarded, v14_ext_modeset_call); + kunit_activate_static_stub(test, dcp_v14_audio_off, v14_ext_modeset_audio); + kunit_activate_static_stub(test, dcp_v14_audio_on, v14_ext_modeset_audio); + return s; +} + +static int v14_ext_modeset_run(struct kunit *test, struct v14_ext_modeset *s) +{ + struct drm_crtc_state *state = kunit_kzalloc(test, sizeof(*state), GFP_KERNEL); + + KUNIT_ASSERT_NOT_NULL(test, state); + drm_mode_copy(&state->mode, &s->mode.mode); + return dcp_v14_external_modeset(s->dcp, state); +} + +/* + * A display that was off, as on attach: the mode is set, the display powered + * on, and the mode set once more with the pipe on, before any swap. A DSC + * mode set with the pipe off only takes effect then. + */ +static void v14_ext_modeset_sets_mode_again_after_power_on(struct kunit *test) +{ + struct v14_ext_modeset *s = v14_ext_modeset_setup(test, false); + + KUNIT_EXPECT_EQ(test, v14_ext_modeset_run(test, s), 0); + KUNIT_ASSERT_EQ(test, s->calls, 3U); + KUNIT_EXPECT_EQ(test, s->tags[0], A(411)); + KUNIT_EXPECT_EQ(test, s->tags[1], A(472)); + KUNIT_EXPECT_TRUE(test, s->power[1]); + KUNIT_EXPECT_EQ(test, s->tags[2], A(411)); + KUNIT_EXPECT_TRUE(test, s->v14->powered); + KUNIT_EXPECT_TRUE(test, s->v14->mode_live); + KUNIT_EXPECT_EQ(test, s->v14->mode_timing, 167U); + KUNIT_EXPECT_EQ(test, s->v14->mode_color, 219U); + KUNIT_EXPECT_FALSE(test, mutex_is_locked(&s->v14->lock)); +} + +/* A mode change on a display that is on: power down, set, power up, set again. */ +static void v14_ext_modeset_mode_change(struct kunit *test) +{ + struct v14_ext_modeset *s = v14_ext_modeset_setup(test, true); + + KUNIT_EXPECT_EQ(test, v14_ext_modeset_run(test, s), 0); + KUNIT_ASSERT_EQ(test, s->calls, 4U); + KUNIT_EXPECT_EQ(test, s->tags[0], A(472)); + KUNIT_EXPECT_FALSE(test, s->power[0]); + KUNIT_EXPECT_EQ(test, s->tags[1], A(411)); + KUNIT_EXPECT_EQ(test, s->tags[2], A(472)); + KUNIT_EXPECT_TRUE(test, s->power[2]); + KUNIT_EXPECT_EQ(test, s->tags[3], A(411)); + KUNIT_EXPECT_TRUE(test, s->v14->powered); +} + +static struct kunit_case v14_ext_modeset_cases[] = { + KUNIT_CASE(v14_ext_modeset_sets_mode_again_after_power_on), + KUNIT_CASE(v14_ext_modeset_mode_change), + {} +}; + +static struct kunit_suite v14_ext_modeset_suite = { + .name = "dcp_v14_external_modeset", + .test_cases = v14_ext_modeset_cases, +}; + +kunit_test_suite(v14_ext_modeset_suite); diff --git a/drivers/gpu/drm/apple/iomfb_v14_7_swap.h b/drivers/gpu/drm/apple/iomfb_v14_7_swap.h index aab3351dd9eea..c361103d06525 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7_swap.h +++ b/drivers/gpu/drm/apple/iomfb_v14_7_swap.h @@ -3,6 +3,9 @@ #ifndef __APPLE_IOMFB_V14_7_SWAP_H__ #define __APPLE_IOMFB_V14_7_SWAP_H__ +#include +#include +#include #include #include #include @@ -11,6 +14,8 @@ #define DCP_V14_SURFACE_SIZE 0x22c /* Offsets in the swap record. Rectangles are x, y, w, h (u32 each). */ +#define DCP_V14_SWAP_TIMESTAMP(n) ((n) * 8) /* u64, slots 0 to 6 */ +#define DCP_V14_SWAP_TIMESTAMPS 7 #define DCP_V14_SWAP_ID 0x98 #define DCP_V14_SWAP_SURF_ID(p) (0x9c + (p) * 4) #define DCP_V14_SWAP_SRC(p) (0xac + (p) * 16) /* in the surface */ @@ -26,6 +31,54 @@ #define DCP_V14_SWAP_LAYERS 0x7 #define DCP_V14_SWAP_SET_BACKGROUND BIT(31) +/* + * The panel level block of the swap record (the 14.7 J514S/J516S panel): a + * present byte at +0x354 and the level in nits as an unaligned IEEE 754 + * binary64 at +0x35c, both zero when the level does not change. The same + * layout as SiliconForest commit 44fe00e5eb58d54baf1cc9ad607293e774bfed44 + * and omacom/linux-aurora PR148 (02524534c6b8). + */ +#define DCP_V14_SWAP_NITS_PRESENT 0x354 +#define DCP_V14_SWAP_NITS 0x35c +/* The nits field takes integral levels up to the panel maximum. */ +#define DCP_V14_NITS_MAX 500 +/* + * The level userspace is shown before Linux has set one. It is never + * presented on its own: until a level is requested the panel keeps the boot + * loader's level, which Linux does not know. + */ +#define DCP_V14_NITS_DEFAULT 140 + +static_assert(DCP_V14_SWAP_NITS_PRESENT >= DCP_V14_SWAP_BACKGROUND + 4); +static_assert(DCP_V14_SWAP_NITS >= DCP_V14_SWAP_NITS_PRESENT + 1); +static_assert(DCP_V14_SWAP_NITS + 8 <= DCP_V14_SWAP_SURFACE(0)); + +/* Integral nits as IEEE 754 binary64, without kernel floating point. */ +static inline u64 dcp_v14_nits_binary64(u32 nits) +{ + unsigned int bit; + + if (!nits) + return 0; + bit = fls(nits) - 1; + return ((u64)(1023 + bit) << 52) | + ((u64)(nits - (1U << bit)) << (52 - bit)); +} + +/* + * The level block of an encoded swap: @nits when @change, else none. A level + * above the wire bound leaves the record untouched. + */ +static inline int dcp_v14_encode_nits(u8 *swap, bool change, u32 nits) +{ + if (change && nits > DCP_V14_NITS_MAX) + return -EINVAL; + swap[DCP_V14_SWAP_NITS_PRESENT] = change; + put_unaligned_le64(change ? dcp_v14_nits_binary64(nits) : 0, + swap + DCP_V14_SWAP_NITS); + return 0; +} + /* Offsets in a surface record. */ #define DCP_V14_SURF_OPAQUE 0x02 #define DCP_V14_SURF_FORMAT 0x0b @@ -115,4 +168,19 @@ static inline void dcp_v14_encode_swap(u8 *swap, u32 id, u32 background, const u } } +/* + * The swap timestamps of a board that asks for them: the constant 120 in + * slots 0, 1 and 6, a time that is never in the future. Counter values + * (1 GHz on T6031) make the firmware time out on the first swap. Call after + * dcp_v14_encode_swap(), which zeroes the record. + */ +#define DCP_V14_SWAP_TIMESTAMP_VALUE 120 + +static inline void dcp_v14_swap_timestamps(u8 *swap) +{ + put_unaligned_le64(DCP_V14_SWAP_TIMESTAMP_VALUE, swap + DCP_V14_SWAP_TIMESTAMP(0)); + put_unaligned_le64(DCP_V14_SWAP_TIMESTAMP_VALUE, swap + DCP_V14_SWAP_TIMESTAMP(1)); + put_unaligned_le64(DCP_V14_SWAP_TIMESTAMP_VALUE, swap + DCP_V14_SWAP_TIMESTAMP(6)); +} + #endif /* __APPLE_IOMFB_V14_7_SWAP_H__ */ diff --git a/drivers/gpu/drm/apple/v14-board-test.c b/drivers/gpu/drm/apple/v14-board-test.c index 801ed2f623a4c..f9b84ab36a7cc 100644 --- a/drivers/gpu/drm/apple/v14-board-test.c +++ b/drivers/gpu/drm/apple/v14-board-test.c @@ -3,12 +3,16 @@ * The 14.x IOMFB board records: which record a DCP gets on which machine, * and which DCP firmware image a record admits. The J516C (T6031) record * admits only the T6030 image, whose IOMFB layouts were validated, and - * refuses every other; the existing records are unchanged. + * refuses every other; the existing records are unchanged. Which DCPs stamp + * their swaps: the internal panel of a T6031 record; the T6030 and T8122 + * panels and external processors keep zero timestamps, byte for byte. */ #include #include +#include #include "iomfb_v14_7_board.h" +#include "iomfb_v14_7_swap.h" #define T6030_DCP_IMAGE "DDF38191-93B3-324A-BC8F-643006F5AC82" #define J613_DCP_IMAGE "90F849E1-B422-367E-B389-50246F8DEC47" @@ -174,12 +178,83 @@ static void v14_board_unchanged(struct kunit *test) KUNIT_EXPECT_TRUE(test, known); } +/* Only the internal panel of the J516C record stamps its swaps. */ +static void v14_board_swap_timestamps(struct kunit *test) +{ + const struct dcp_v14_board *j516c_b, *j514c_b, *t6030_b, *j613_b; + bool known; + + j516c_b = board_for("apple,t6031-dcp", j516c, &known); + j514c_b = board_for("apple,t6031-dcp", j514c, &known); + t6030_b = board_for("apple,t6030-dcp", j516s, &known); + j613_b = board_for("apple,t8122-dcp", j613, &known); + KUNIT_ASSERT_TRUE(test, j516c_b && j514c_b && t6030_b && j613_b); + + KUNIT_EXPECT_TRUE(test, j516c_b->swap_timestamps); + KUNIT_EXPECT_FALSE(test, j514c_b->swap_timestamps); + KUNIT_EXPECT_FALSE(test, t6030_b->swap_timestamps); + KUNIT_EXPECT_FALSE(test, j613_b->swap_timestamps); + + KUNIT_EXPECT_TRUE(test, dcp_v14_board_stamps_swaps(j516c_b, false)); + /* The J514C, the external role, the T6030 (J514S, J516S) and T8122 panels, no record. */ + KUNIT_EXPECT_FALSE(test, dcp_v14_board_stamps_swaps(j514c_b, false)); + KUNIT_EXPECT_FALSE(test, dcp_v14_board_stamps_swaps(j516c_b, true)); + KUNIT_EXPECT_FALSE(test, dcp_v14_board_stamps_swaps(t6030_b, false)); + KUNIT_EXPECT_FALSE(test, dcp_v14_board_stamps_swaps(j613_b, false)); + KUNIT_EXPECT_FALSE(test, dcp_v14_board_stamps_swaps(NULL, false)); + KUNIT_EXPECT_FALSE(test, dcp_v14_board_stamps_swaps(NULL, true)); +} + +/* + * A stamped swap holds 120 in timestamp slots 0, 1 and 6 and is otherwise the + * encoded record, with or without a surface; an unstamped one keeps every + * slot zero. + */ +static void v14_swap_timestamps_encoding(struct kunit *test) +{ + const size_t end = DCP_V14_SWAP_TIMESTAMP(DCP_V14_SWAP_TIMESTAMPS); + u8 surface[DCP_V14_SURFACE_SIZE]; + unsigned int i, pass; + u8 *plain, *stamped; + + plain = kunit_kzalloc(test, DCP_V14_SWAP_SIZE, GFP_KERNEL); + stamped = kunit_kzalloc(test, DCP_V14_SWAP_SIZE, GFP_KERNEL); + KUNIT_ASSERT_TRUE(test, plain && stamped); + dcp_v14_encode_surface(surface, 3456 * 4, 3456, 2160, true); + + for (pass = 0; pass < 2; pass++) { + const u8 *s = pass ? surface : NULL; + + dcp_v14_encode_swap(plain, 0x2a, 0xff000000, s, 0x10000, pass ? 3456 : 0, + pass ? 2160 : 0, 74, 0); + dcp_v14_encode_swap(stamped, 0x2a, 0xff000000, s, 0x10000, pass ? 3456 : 0, + pass ? 2160 : 0, 74, 0); + dcp_v14_swap_timestamps(stamped); + + for (i = 0; i < DCP_V14_SWAP_TIMESTAMPS; i++) { + size_t at = DCP_V14_SWAP_TIMESTAMP(i); + u64 want = i == 0 || i == 1 || i == 6 ? 120 : 0; + + KUNIT_EXPECT_EQ_MSG(test, get_unaligned_le64(plain + at), 0, + "surface %u: unstamped slot %u", pass, i); + KUNIT_EXPECT_EQ_MSG(test, get_unaligned_le64(stamped + at), want, + "surface %u: slot %u", pass, i); + } + /* The rest of the record, from the word after slot 6, is unchanged. */ + KUNIT_EXPECT_EQ(test, + memcmp(plain + end, stamped + end, DCP_V14_SWAP_SIZE - end), 0); + KUNIT_EXPECT_EQ(test, get_unaligned_le32(stamped + DCP_V14_SWAP_ID), 0x2a); + } +} + static struct kunit_case v14_board_cases[] = { KUNIT_CASE(v14_board_j516c), KUNIT_CASE(v14_board_j514c), KUNIT_CASE(v14_board_t6031_other_boards), KUNIT_CASE(v14_board_j516c_image), KUNIT_CASE(v14_board_unchanged), + KUNIT_CASE(v14_board_swap_timestamps), + KUNIT_CASE(v14_swap_timestamps_encoding), {} }; diff --git a/drivers/pmdomain/apple/pmp-report.c b/drivers/pmdomain/apple/pmp-report.c index f09b38aa310ca..77865b5af2687 100644 --- a/drivers/pmdomain/apple/pmp-report.c +++ b/drivers/pmdomain/apple/pmp-report.c @@ -1526,6 +1526,9 @@ struct apple_pmp_report_entry { u32 id; /* The PMP does not acknowledge this request (a started PMP only). */ bool no_ack; + /* Withdrawn for system sleep (apple,release-for-sleep), and now so. */ + bool sleep_release; + bool released; bool removed; }; @@ -1589,6 +1592,80 @@ static int apple_pmp_report_entry_set_state(struct generic_pm_domain *genpd, boo return ret; } +/* + * System sleep. An external display processor's request keeps the PMP's + * external display state up while nothing is shown, about 0.25 W in + * suspend-to-idle on J514S. By the noirq phase every display processor has + * suspended, its displays off and its links released or idle: withdraw such a + * request if it is acknowledged, with one replacement write as macOS releases + * its display requests, and assert it again before the processors resume. + */ +static int apple_pmp_report_entry_sleep(struct apple_pmp_report_entry *ent, bool release) +{ + struct apple_pmp_report *rep; + int ret = -ENODEV; + + rcu_read_lock(); + list_for_each_entry_rcu(rep, &apple_pmp_reports, list) { + if (rep->dev->of_node != ent->report_node) + continue; + if (release && (!(readq(rep->base + rep->offsets->status) & PMP_REPORT_READY) || + !(readq(rep->base + rep->offsets->actual) & BIT_ULL(ent->id)))) + ret = -EALREADY; + else + ret = apple_pmp_report_set_state(rep, ent->id, !release, ent->no_ack); + break; + } + rcu_read_unlock(); + return ret; +} + +static int apple_pmp_report_entry_suspend_noirq(struct device *dev) +{ + struct apple_pmp_report_entry *ent = dev_get_drvdata(dev); + int ret; + + if (!ent || !ent->sleep_release || READ_ONCE(ent->removed)) + return 0; + ret = apple_pmp_report_entry_sleep(ent, true); + if (ret == -EALREADY) + return 0; + /* A withdrawal that was not acknowledged is asserted again all the same. */ + if (ret) + dev_warn(dev, "request %u not withdrawn for sleep: %d\n", ent->id, ret); + ent->released = true; + return 0; +} + +static int apple_pmp_report_entry_resume_noirq(struct device *dev) +{ + struct apple_pmp_report_entry *ent = dev_get_drvdata(dev); + int ret, tries = 0; + + if (!ent || !ent->released) + return 0; + ent->released = false; + /* + * Without the request the external display stays dark until reboot: + * write it again, three times at most, while the PMP does not + * acknowledge it in time. + */ + do { + ret = apple_pmp_report_entry_sleep(ent, false); + } while (ret == -ETIMEDOUT && ++tries < 3); + if (ret) + dev_err(dev, "request %u not asserted after sleep: %d\n", ent->id, ret); + else if (tries) + dev_warn(dev, "request %u asserted after sleep on write %d\n", ent->id, + tries + 1); + return ret; +} + +static const struct dev_pm_ops apple_pmp_report_entry_pm_ops = { + NOIRQ_SYSTEM_SLEEP_PM_OPS(apple_pmp_report_entry_suspend_noirq, + apple_pmp_report_entry_resume_noirq) +}; + static int apple_pmp_report_entry_power_on(struct generic_pm_domain *genpd) { return apple_pmp_report_entry_set_state(genpd, true); @@ -1661,6 +1738,7 @@ static int apple_pmp_report_entry_probe(struct platform_device *pdev) } ent->no_ack = of_property_read_bool(node, "apple,no-ack"); } + ent->sleep_release = of_property_read_bool(node, "apple,release-for-sleep"); ret = of_property_read_string(node, "label", &name); if (ret < 0) { @@ -1749,6 +1827,7 @@ static struct platform_driver apple_pmp_report_entry_driver = { .driver = { .name = "apple-pmp-report-entry", .of_match_table = apple_pmp_report_entry_of_match, + .pm = pm_sleep_ptr(&apple_pmp_report_entry_pm_ops), /* Power-domain providers are not intended for manual unbinding. */ .suppress_bind_attrs = true, }, diff --git a/drivers/soc/apple/display-gate.c b/drivers/soc/apple/display-gate.c index deb84564bc2bc..55ec70ad2824f 100644 --- a/drivers/soc/apple/display-gate.c +++ b/drivers/soc/apple/display-gate.c @@ -1487,6 +1487,9 @@ static int __init gate_ext_apply(struct gate_ext *ext, bool native) /* The driver must wait for this request's acknowledgement before RUN. */ if (!ret) ret = gate_set_u32(cs, ext->dcp, "apple,pmp-report", ext->entry->phandle); + /* No external display is attached in system sleep: the PMP may drop it. */ + if (!ret) + ret = gate_add_bool(cs, ext->entry, "apple,release-for-sleep"); if (!ret) ret = of_changeset_update_prop_string(cs, ext->entry, "status", "okay"); if (!ret) diff --git a/include/linux/dma/apple-sio.h b/include/linux/dma/apple-sio.h new file mode 100644 index 0000000000000..8665b511659b6 --- /dev/null +++ b/include/linux/dma/apple-sio.h @@ -0,0 +1,44 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +#ifndef _LINUX_DMA_APPLE_SIO_H +#define _LINUX_DMA_APPLE_SIO_H + +#include + +/** + * struct apple_sio_idle - idle transfer of an Apple SIO channel + * @addr: DMA address of the buffer + * @len: buffer length; 0 removes the channel's idle transfer + * @period_len: length of each of its descriptors, not zero and at most @len + * + * Passed as &dma_slave_config.peripheral_config in a dmaengine_slave_config() + * call of its own, which sets or removes the idle transfer and nothing else: + * the channel's other settings take a separate call. + * + * While a channel has an idle transfer, it plays that buffer cyclically + * whenever no other transfer runs, and goes from one to the other in + * descriptor order: terminating a transfer hands over to the idle one instead + * of stopping the channel, and a transfer issued later follows the idle one's + * issued descriptors. The peripheral sees no gap in between. + * + * dmaengine_synchronize() returns once the descriptors of a terminated + * transfer have completed. If they do not in time, it stops the channel and + * waits for them first; the idle transfer then starts again, after a gap. + * Only an acknowledged TERMINATE counts as stopped. If the channel cannot be + * confirmed stopped, dmaengine_synchronize() warns, as it cannot fail. + * Removing the idle transfer stops the channel if nothing else runs, and + * returns 0 once its descriptors completed. If the channel cannot be + * confirmed stopped, it returns an error and the buffer is still in use. + * + * Setting an idle transfer fails with -EBUSY while the channel has one, or + * while it cannot take over the channel's issued descriptors. Setting and + * removing it, terminating and synchronizing must not run concurrently on a + * channel: its client serializes them. A channel that never had an idle + * transfer works as before. + */ +struct apple_sio_idle { + dma_addr_t addr; + size_t len; + size_t period_len; +}; + +#endif diff --git a/tools/asahi/t6030-pmp-graph-tests.c b/tools/asahi/t6030-pmp-graph-tests.c index 3d2493e57fb63..637359d7e6933 100644 --- a/tools/asahi/t6030-pmp-graph-tests.c +++ b/tools/asahi/t6030-pmp-graph-tests.c @@ -50,7 +50,7 @@ static void host_log(const char *format, ...) #define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) struct device_node { const char *name, *compatible; - bool available, always_on, no_ack, deferred, irq_safe, uuid, soc_record; + bool available, always_on, no_ack, deferred, irq_safe, uuid, soc_record, release_sleep; u32 reg; int refs; struct device_node *parent, *child, *sibling, *pmp; @@ -196,6 +196,7 @@ struct apple_pmp_report_entry { struct device_node *node, *report_node; u32 id; bool no_ack, removed; + bool sleep_release, released; }; #define genpd_to_apple_pmp_report_entry(p) \ container_of(p, struct apple_pmp_report_entry, genpd) @@ -394,11 +395,18 @@ static u64 readq(const void *a) { return *(const u64 *)a; } +/* The sleep cases: the PMP acknowledges the requests it is given, ack_late writes late. */ +static bool ack_follows; +static unsigned int ack_late; static void writeq(u64 v, void *a) { assert(a == (char *)regs + 0x108c0); *(u64 *)a = v; regs[0x1180 / 8] = v; + if (ack_follows && ack_late) + ack_late--; + else if (ack_follows) + regs[0x11c0 / 8] = v; } #define readq_poll_timeout(address, value, condition, delay, timeout) \ ({ \ @@ -463,6 +471,8 @@ static bool of_property_read_bool(const struct device_node *n, const char *p) return n->irq_safe; if (!strcmp(p, "apple,soc-device-record")) return n->soc_record; + if (!strcmp(p, "apple,release-for-sleep")) + return n->release_sleep; return false; } static bool of_property_present(const struct device_node *n, const char *p) @@ -832,6 +842,8 @@ static struct apple_pmp_report *setup(void) put32(devices + i * 0x7c + 8, 2); } ack_polls = 0; + ack_follows = false; + ack_late = 0; check_error = enable_error = 0; hw_binds = hw_ready = true; hw_ack = BIT_ULL(7); @@ -1030,6 +1042,49 @@ int main(void) cases++; cleanup(); #endif + +#ifdef HAVE_SLEEP_RELEASE + /* System sleep withdraws an acknowledged request marked for it, then asserts it again. */ + setup(); + entries[1].release_sleep = true; + assert(apple_pmp_report_probe(&report_dev) == 0); + assert(apple_pmp_report_wait_ready(&entries[0], 30000) == 0); + struct apple_pmp_report_entry *disp = entry_devs[0].dev.data; + struct apple_pmp_report_entry *ext = entry_devs[1].dev.data; + + assert(ext->sleep_release && !disp->sleep_release); + ack_follows = true; + regs[0x11c0 / 8] = regs[0x1180 / 8]; + u64 target = regs[0x1180 / 8]; + + assert(target & BIT_ULL(8)); + assert(apple_pmp_report_entry_suspend_noirq(&entry_devs[0].dev) == 0 && + !disp->released && regs[0x1180 / 8] == target); + assert(apple_pmp_report_entry_suspend_noirq(&entry_devs[1].dev) == 0); + assert(ext->released && regs[0x1180 / 8] == (target & ~BIT_ULL(8))); + assert(apple_pmp_report_entry_resume_noirq(&entry_devs[1].dev) == 0); + assert(!ext->released && regs[0x1180 / 8] == target); + cases++; + /* A request the PMP has not acknowledged is left as it is. */ + regs[0x11c0 / 8] &= ~BIT_ULL(8); + assert(apple_pmp_report_entry_suspend_noirq(&entry_devs[1].dev) == 0); + assert(!ext->released && regs[0x1180 / 8] == target); + assert(apple_pmp_report_entry_resume_noirq(&entry_devs[1].dev) == 0 && + regs[0x1180 / 8] == target); + cases++; + /* A re-assert the PMP acknowledges late is written again, three times at most. */ + regs[0x11c0 / 8] = target; + assert(apple_pmp_report_entry_suspend_noirq(&entry_devs[1].dev) == 0 && ext->released); + ack_late = 2; + assert(apple_pmp_report_entry_resume_noirq(&entry_devs[1].dev) == 0 && + !ack_late && regs[0x11c0 / 8] == target); + assert(apple_pmp_report_entry_suspend_noirq(&entry_devs[1].dev) == 0 && ext->released); + ack_late = 5; + assert(apple_pmp_report_entry_resume_noirq(&entry_devs[1].dev) == -ETIMEDOUT && + ack_late == 2 && regs[0x1180 / 8] == target); + cases++; + cleanup(); +#endif printf("PASS: %u actual overlay/probe/entry/DCP7/managed-start/supplier graph cases\n", cases); return 0; diff --git a/tools/asahi/t6030-pmp-graph-tests.py b/tools/asahi/t6030-pmp-graph-tests.py index bbe4dd99ea6c0..b4b417b4d4f01 100644 --- a/tools/asahi/t6030-pmp-graph-tests.py +++ b/tools/asahi/t6030-pmp-graph-tests.py @@ -50,6 +50,13 @@ def main(): names.insert(0, 'static bool apple_pmp_report_running(') if 'static int apple_pmp_report_wait_t6030_bound(' in source: names.insert(names.index('static struct device_node *apple_pmp_report_t6030_entry('), 'static int apple_pmp_report_wait_t6030_bound(') + # System sleep (apple,release-for-sleep), where the source has it. + sleep = 'static int apple_pmp_report_entry_sleep(' in source + if sleep: + at = names.index('static int apple_pmp_report_entry_power_on(') + names[at:at] = ['static int apple_pmp_report_entry_sleep(', + 'static int apple_pmp_report_entry_suspend_noirq(', + 'static int apple_pmp_report_entry_resume_noirq('] offsets = re.search(r'static const struct apple_pmp_report_offsets apple_pmp_offsets_t6030 = \{.*?\n\};', source, re.S)[0] functions = '\n'.join(extract.function(source, n) for n in names) validation = re.sub(r'^#include .*\n', '', (ROOT / 'drivers/pmdomain/apple/pmp-report-validation.h').read_text(), flags=re.M) @@ -66,7 +73,8 @@ def main(): dram_attrs = ('#pragma GCC diagnostic push\n' '#pragma GCC diagnostic ignored "-Wunused-parameter"\n' + dram_attrs + '#pragma GCC diagnostic pop\n') - code = offsets + '\n' + validation + '\n' + dram_attrs + '\n' + functions + code = (('#define HAVE_SLEEP_RELEASE 1\n' if sleep else '') + offsets + '\n' + validation + + '\n' + dram_attrs + '\n' + functions) # The actual overlay must still describe a halted supplier and disabled DMA/mailbox. assert overlay.count('status = \"disabled\";') >= 3 (args.output / 'production.h').write_text(code) @@ -115,6 +123,13 @@ def main(): ('irq-safe-without-property', 'of_property_read_bool(node, "apple,irq-safe")', 'true'), ('skip-supplier-wait', 'apple_pmp_report_wait_ready(entry, msecs_to_jiffies(timeout_ms))', '((void)timeout_ms, 0)'), ] + if sleep: + controls += [ + ('sleep-releases-unacked', '!(readq(rep->base + rep->offsets->actual) & BIT_ULL(ent->id))', 'false'), + ('sleep-not-reasserted', 'ret = apple_pmp_report_entry_sleep(ent, false);', 'ret = 0;'), + ('sleep-ignores-mark', '!ent->sleep_release ||', ''), + ('sleep-not-rewritten', '++tries < 3', '++tries < 1'), + ] for name, old, new in controls: assert code.count(old) == 1, (name, code.count(old)) (args.output / 'production.h').write_text(code.replace(old, new)) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py index b7f4f7dd633dc..8a23ffbeb83ff 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py @@ -206,6 +206,14 @@ def brightness_takeover(root): 'static void dcp_backlight_sync_hint(', 'int dcp_backlight_register(', 'static int dcp_set_brightness(', 'static bool dcp_backlight_controls_device(')) + # The 14.7 panel's nits policy and class operations (an M3 internal + # panel) never run on the desktop panel. + if 'static bool dcp_backlight_presents_nits(' in source: + functions = ('static bool iomfb_v14_7_panel_nits(struct apple_dcp *d) ' + '{(void)d;return false;}\n' + 'static const int dcp_backlight_ops_v14;\n' + + block(source, 'static bool dcp_backlight_presents_nits(') + + '\n' + functions) return prefix + r''' #include #include diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py index 89dd1f31f954b..95043ab9afc36 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py @@ -483,6 +483,25 @@ def function(file, name): prefix += ('#define DCP_FIRMWARE_V_26_6_DESKTOP 100\n' 'static int __attribute__((unused)) dcp_desktop_dptx_connect(struct apple_dcp *d, u32 port)\n' '{ (void)d; (void)port; assert(!"desktop 26.6 connect in an H17P scenario"); return -EINVAL; }\n') + # Let the real helper classify an occupied display on newer trees, where + # -EBUSY means a transient mux/PHY failure rather than a taken pipeline. + if re.search(r'cd321x_display_taken\(int ret\)', pm): + assert shim.count('mode_error == -EBUSY') == 1 + assert tests.count('mode_error = -EBUSY;') == 2 + shim = shim.replace('mode_error == -EBUSY', 'cd321x_display_taken(mode_error)') + tests = tests.replace('mode_error = -EBUSY;', 'mode_error = -ENOSPC;') + busy = ' CHECK(sink.connected && !notices && !port.display_route_active);\n' + assert tests.count(busy) == 1 + tests = tests.replace(busy, busy + r''' + CHECK(roles == 1 && port.pm.phase == CD321X_PM_RUNNING); + /* A transient busy mux must retry before switching the USB role. */ + init(); port.display_route_active = false; role = USB_ROLE_NONE; + port.update_status.status = TPS_STATUS_PLUG_PRESENT; + port.update_status.data_status = TPS_DATA_STATUS_DP_CONNECTION | CD321X_DATA_STATUS_HPD_LEVEL; + mode_error = -EBUSY; cd321x_update_work(&port.update_work.work); + CHECK(!roles && port.pm.phase == CD321X_PM_REVALIDATE); + CHECK(sink.connected && !notices && !port.display_route_active); +''') # How long a held mux has been retried is measured on the driver's clock. if 'jiffies_to_msecs(jiffies)' in funcs: shim += ('\nstatic unsigned long jiffies;\n' diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/m4.py b/tools/testing/selftests/drivers/gpu/apple-dcp/m4.py index cb3bac542a88c..f11078c87b087 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/m4.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/m4.py @@ -482,6 +482,12 @@ def blank(root): functions = '\n'.join(block(backlight, name) for name in ( 'bool dcp_backlight_active(', 'int dcp_backlight_dpms(', 'int dcp_backlight_prepare(', 'bool dcp_backlight_complete(')) + # The 14.7 panel's nits policy (an M3 internal panel) never runs here. + if 'static bool dcp_backlight_presents_nits(' in backlight: + functions = ('static bool iomfb_v14_7_panel_nits(struct apple_dcp *dcp) ' + '{ (void)dcp; return false; }\n' + + block(backlight, 'static bool dcp_backlight_presents_nits(') + + '\n' + functions) return production(root) + r''' #define min(x,y) ((x)<(y)?(x):(y)) static u64 div_u64(u64 x,u32 y) { return x/y; } diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py b/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py index faae722b9d8e1..37537310d333e 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py @@ -642,6 +642,19 @@ def function(text, name): } ''' + # Newer trees report an occupied pipeline as -ENOSPC on every SoC. + # Keep the old two-argument contract covered when testing an older tree. + if re.search(r'cd321x_display_taken\(int ret\)', pm): + old = ' assert(cd321x_display_taken(-ENOSPC,true)&&!cd321x_display_taken(-EBUSY,true)&&!cd321x_display_taken(0,true)&&cd321x_display_taken(-EBUSY,false)&&!cd321x_display_taken(-ENOSPC,false)&&!cd321x_display_taken(0,false));' + assert controls.count(old) == 1 + controls = controls.replace(old, r''' + /* An occupied display permits USB progress; transient/other errors retry. */ + assert(cd321x_display_taken(-ENOSPC)); + assert(!cd321x_display_taken(-EBUSY)&&!cd321x_display_taken(-EAGAIN)); + assert(!cd321x_display_taken(-ETIMEDOUT)&&!cd321x_display_taken(-EIO)); + assert(!cd321x_display_taken(0)); +''') + # A mux that holds a change up is retried outside the cable-state budget. if 'cd321x_pm_mux_held(' in pm: lost = ' puts("PASS actual lost events reopen a stale port; unacknowledged events keep their budget with capped backoff");\n' diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py index 52d7125f0c295..78e241892ca14 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py @@ -1105,6 +1105,65 @@ def publication(root): unit=re.sub(r'\blookup_mode\((&d,&[\w.\[\]]+)\)', r'lookup_mode(\1,NULL)', unit) return {**locals(), "block": block} + +def panel_dimensions(root): + n = publication(root) + unit, block = n['unit'], n['block'] + unit = unit.replace('bool modes_lock,modes_admitted,', + 'bool external_physically_absent,modes_lock,modes_admitted,', 1) + # The connector's physical size, which DRM keeps in display_info. + old = 'struct { struct { unsigned min_vfreq,max_vfreq; } monitor_range; } display_info;' + assert unit.count(old) == 1 + unit = unit.replace(old, 'struct { struct { unsigned min_vfreq,max_vfreq; } monitor_range; ' + 'int width_mm,height_mm; } display_info;') + dcp = (root / 'drivers/gpu/drm/apple/dcp.c').read_text() + unit = unit.replace(block(unit, 'static bool __attribute__((unused)) dcp_has_panel('), + block(dcp, 'bool dcp_has_panel(')) + unit = unit.replace(block(unit, 'static void dcp_update_connector_dimensions('), + block(dcp, 'void dcp_update_connector_dimensions(')) + # Ordinary panel modes do not take the 180 Hz exception's metadata lock. + unit = unit.replace('probing->dcpavserv.lock && probing->metadata.lock', + 'probing->dcpavserv.lock && ' + '(!probing->modes[0].automatic_rgb8_quirk || probing->metadata.lock)') + # Model DRM's display_info reset and the absence of an internal AV EDID. + unit = unit.replace(block(unit, 'static int drm_edid_connector_update('), r""" +static int drm_edid_connector_update(struct drm_connector *c,const struct drm_edid *e) { + assert(c->dev->mode_config.mutex); + c->display_info.width_mm=e ? 600 : 0; + c->display_info.height_mm=e ? 330 : 0; + return fail_update ? -EIO : 0; +} +""") + unit = unit.replace('if(!d->dcpavserv.enabled || !d->dcpavserv.service', + 'if(dcp_has_panel(d)) return ERR_PTR(-ENODEV);\n' + ' if(!d->dcpavserv.enabled || !d->dcpavserv.service') + checks = r""" +static void panel_dimension_probes(void) { + reset();callbacks(false);callbacks(true);timings();admit(0);probe(true); + assert(con.base.display_info.width_mm==600 && con.base.display_info.height_mm==330); + d.panel.width_mm=302;d.panel.height_mm=196;d.width_mm=0;d.height_mm=0; + d.modes[0].automatic_rgb8_quirk=false; + con.edid_invalidation_pending=true;probe(true); + assert(!con.drm_edid); + assert(con.base.display_info.width_mm==302 && con.base.display_info.height_mm==196); + con.edid_invalidation_pending=true;probe(true); + assert(con.base.display_info.width_mm==302 && con.base.display_info.height_mm==196); + d.width_mm=300;d.height_mm=190;con.edid_invalidation_pending=true;probe(true); + assert(con.base.display_info.width_mm==300 && con.base.display_info.height_mm==190); + d.width_mm=0;d.height_mm=190;con.edid_invalidation_pending=true;probe(true); + assert(con.base.display_info.width_mm==302 && con.base.display_info.height_mm==196); + cleanup(); + reset();callbacks(false);callbacks(true);timings();admit(0);probe(true); + assert(con.base.display_info.width_mm==600 && con.base.display_info.height_mm==330); + cleanup(); + puts("PASS: internal panel dimensions survive repeated EDID clears; firmware size and DT fallback; external EDID size preserved"); +} +""" + unit = unit.replace('int main(int argc,char **argv) {', + checks + 'int main(int argc,char **argv) {') + return unit.replace(' transport_edid=raw;', + ' transport_edid=raw;panel_dimension_probes();', 1) + def hotplug(root): n = publication(root) unit = n['unit'] diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py index a2a8332a30269..9ec7ee1ac56e8 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py @@ -90,6 +90,7 @@ def main(): ('edid', 'ordered-rx', edid.rx_progress, ((),), ('-pthread',)), ('rate', 'parser-policy', lambda r: rate.policy(r)['unit'], ((),), ()), ('rate', 'publication-hpd', rate.hotplug, ((),), ()), + ('rate', 'panel-dimensions', rate.panel_dimensions, ((),), ()), ('rate', 'metadata-decoder', lambda r: rate.metadata(r, output), ((),), ('-Wno-sign-compare',)), ('rate', 'bounded-retry', rate.retry, ((),), ()), ('phy', 'pm-ownership', pm.ownership, ((),), ()), @@ -142,7 +143,7 @@ def main(): elif isinstance(unit, tuple): unit, cases = unit if unit is not None and name in ('provider-admission', 'ordered-rx', - 'publication-hpd', 'stale-crtc-poweroff', 'readiness'): + 'publication-hpd', 'panel-dimensions', 'stale-crtc-poweroff', 'readiness'): # These fixtures select older external-display backends. The M4 # transport is covered by the separate m4 group, not mocked here. prefix = ('#include \n#include \n'