From e853c9eef448eed71b18f27df689ee0acae6c5f0 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Mon, 31 Aug 2026 01:15:06 -0400 Subject: [PATCH] drm: apple: support ProMotion variable refresh rate DCP exposes an adaptive-sync parameter taking a minimum refresh rate, a media target rate and a fractional rate. Program it during modesets and report the panel's range through the standard vrr_capable connector property, so userspace drives VRR through the atomic VRR_ENABLED CRTC property rather than a driver-specific knob. The swap path previously wrote a constant 120 into the three timestamp fields to hold the internal panel at its maximum rate. Replace that with architectural counter timestamps taken at swap submit and at present, which is what the firmware needs in order to vary the rate at all. An internal ProMotion panel carries no EDID or DisplayID, so DCP reports no adaptive-sync range for it. Assume the ProMotion floor of 24 Hz up to the rate the mode itself advertises. Key that off the connector type from the device tree rather than the main-display flag, which is filled in by an asynchronous response and is not reliably set by the time the timing elements are parsed. External displays keep the range DCP reports, and a reported range is only believed when its maximum exceeds its minimum. Fill in display_info.monitor_range as well. Nothing else populates it for a panel with no EDID, so without it the range is missing from debugfs and from anything else that reads it. Signed-off-by: DjDeveloperr --- drivers/gpu/drm/apple/dcp.c | 1 + drivers/gpu/drm/apple/iomfb.c | 26 ++++++++ drivers/gpu/drm/apple/iomfb_template.c | 84 +++++++++++++++++++++----- drivers/gpu/drm/apple/iomfb_template.h | 10 +-- drivers/gpu/drm/apple/parser.c | 29 +++++---- drivers/gpu/drm/apple/parser.h | 3 +- drivers/gpu/drm/apple/trace.h | 21 +++++++ 7 files changed, 138 insertions(+), 36 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 4fc018f4132392..08156394b233f0 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -656,6 +656,7 @@ static int dcp_register_typec_routes(struct apple_dcp *dcp) return 0; } + /* copied and simplified from drm_vblank.c */ static void send_vblank_event(struct drm_device *dev, struct drm_pending_vblank_event *e, diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index 452a35731f7cfd..3ec33353d3abe0 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -406,6 +406,8 @@ int dcp_get_modes(struct drm_connector *connector) struct drm_device *dev = connector->dev; struct drm_display_mode *mode; + u16 min_vfreq = 0, max_vfreq = 0; + bool vrr_capable = false; int i; /* A Type-C port has no pipeline while the fabric is moving it. */ @@ -414,6 +416,16 @@ int dcp_get_modes(struct drm_connector *connector) dcp = platform_get_drvdata(pdev); for (i = 0; i < dcp->nr_modes; ++i) { + if (dcp->modes[i].vrr) { + u16 lo = dcp->modes[i].min_vrr >> 16; + u16 hi = dcp->modes[i].max_vrr >> 16; + + if (!min_vfreq || lo < min_vfreq) + min_vfreq = lo; + if (hi > max_vfreq) + max_vfreq = hi; + } + vrr_capable |= dcp->modes[i].vrr; mode = drm_mode_duplicate(dev, &dcp->modes[i].mode); if (!mode) { @@ -423,6 +435,8 @@ int dcp_get_modes(struct drm_connector *connector) drm_mode_probed_add(connector, mode); } + drm_connector_set_vrr_capable_property(connector, vrr_capable); + if (dcp->nr_modes && dcp->dcpavserv.enabled && !apple_connector->drm_edid) { const struct drm_edid *edid; @@ -437,6 +451,18 @@ int dcp_get_modes(struct drm_connector *connector) if (dcp->nr_modes && apple_connector->drm_edid) drm_edid_connector_update(connector, apple_connector->drm_edid); + /* + * 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 + * reported for the mode. This has to follow the EDID update, which + * resets display_info. + */ + if (vrr_capable && max_vfreq && + !connector->display_info.monitor_range.max_vfreq) { + connector->display_info.monitor_range.min_vfreq = min_vfreq; + connector->display_info.monitor_range.max_vfreq = max_vfreq; + } + return dcp->nr_modes; } diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index 44902912c8a344..c0b533c4f8a3c7 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -4,6 +4,7 @@ * Copyright The Asahi Linux Contributors */ +#include #include #include #include @@ -34,6 +35,9 @@ /* Register defines used in bandwidth setup structure */ #define REG_DOORBELL_BIT(idx) (2 + (idx)) +static_assert(offsetof(struct DCP_FW_NAME(dcp_swap), timestamp[6]) == 0x30); +static_assert(offsetof(struct DCP_FW_NAME(dcp_swap), flags1) == 0x40); + struct dcp_wait_cookie { struct kref refcount; struct completion done; @@ -567,7 +571,9 @@ static bool dcpep_process_chunks(struct apple_dcp *dcp, if (!strcmp(req->key, "TimingElements")) { dcp->modes = enumerate_modes(&ctx, &dcp->nr_modes, dcp->width_mm, dcp->height_mm, - dcp->notch_height); + dcp->notch_height, + dcp->fixed_connector_type == + DRM_MODE_CONNECTOR_eDP); if (IS_ERR(dcp->modes)) { dev_warn(dcp->dev, "failed to parse modes\n"); @@ -1152,6 +1158,7 @@ static void dcp_swapped(struct apple_dcp *dcp, void *data, void *cookie) return; } dcp->swap_start = ktime_get(); + dcp->swap_submit_timestamp = arch_timer_read_counter(); while (!list_empty(&dcp->swapped_out_fbs)) { struct dcp_fb_reference *entry; @@ -1198,6 +1205,35 @@ static void complete_set_digital_out_mode(struct apple_dcp *dcp, void *data, } } +/* DCP applies Adaptive Sync changes when the display mode is reselected. */ +static void dcp_on_set_adaptive_sync(struct apple_dcp *dcp, void *out, + void *cookie) +{ + dcp_set_digital_out_mode(dcp, false, &dcp->mode, + complete_set_digital_out_mode, cookie); +} + +static void dcp_set_adaptive_sync(struct apple_dcp *dcp, u32 min_vrr, + void *cookie) +{ + struct dcp_set_parameter_dcp param = { + .param = IOMFBPARAM_ADAPTIVE_SYNC, + .value = { + min_vrr, /* minRR, 16.16 fixed-point Hz */ + 0, /* mediaTargetRate */ + 0, /* fractional rate */ + }, +#if DCP_FW_VER >= DCP_FW_VERSION(13, 2, 0) + .count = 3, +#else + .count = 1, +#endif + }; + + dcp_set_parameter_dcp(dcp, false, ¶m, dcp_on_set_adaptive_sync, + cookie); +} + int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, struct drm_crtc_state *crtc_state) { @@ -1237,8 +1273,8 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, .timing_mode_id = mode->timing_mode_id }; - /* Keep track of suspected vrr modes */ - dcp->use_timestamps = mode->vrr; + /* Built-in ProMotion panels require timestamps even in fixed-120 mode. */ + dcp->use_timestamps = mode->vrr && dcp->main_display; cookie = kzalloc(sizeof(*cookie), GFP_KERNEL); if (!cookie) { @@ -1251,9 +1287,15 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, kref_get(&cookie->refcount); dcp->during_modeset = true; + dcp->swap_submit_timestamp = 0; - dcp_set_digital_out_mode(dcp, false, &dcp->mode, - complete_set_digital_out_mode, cookie); + if (mode->vrr) + dcp_set_adaptive_sync(dcp, + crtc_state->vrr_enabled ? mode->min_vrr : 0, + cookie); + else + dcp_set_digital_out_mode(dcp, false, &dcp->mode, + complete_set_digital_out_mode, cookie); /* * The DCP firmware has an internal timeout of ~8 seconds for @@ -1263,7 +1305,6 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, ret = wait_for_completion_timeout(&cookie->done, msecs_to_jiffies(8500)); - kref_put(&cookie->refcount, release_wait_cookie); dcp->during_modeset = false; if (dcp->pending_hotplug) { @@ -1287,18 +1328,21 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, if (ret == 0) { dev_info(dcp->dev, "set_digital_out_mode timed out\n"); + kref_put(&cookie->refcount, release_wait_cookie); return -EIO; } else if (ret < 0) { dev_info(dcp->dev, "waiting on set_digital_out_mode failed:%d\n", ret); + kref_put(&cookie->refcount, release_wait_cookie); return -EIO; - - } else if (ret > 0) { + } else { dev_dbg(dcp->dev, "set_digital_out_mode finished with %d to spare\n", jiffies_to_msecs(ret)); } + kref_put(&cookie->refcount, release_wait_cookie); dcp->valid_mode = true; + dcp->vrr_enabled = mode->vrr && crtc_state->vrr_enabled; return 0; } @@ -1405,14 +1449,26 @@ void DCP_FW_NAME(iomfb_flush)(struct apple_dcp *dcp, struct drm_crtc *crtc, stru req->clear = 1; } - if (has_surface && dcp->use_timestamps) { + if (has_surface && (dcp->use_timestamps || dcp->vrr_enabled)) { + u64 submit_timestamp = dcp->swap_submit_timestamp; + u64 timestamp = arch_timer_read_counter(); + /* - * Fake timstamps to get 120hz refresh rate. It looks - * like the actual value does not matter, as long as it is non zero. + * IOMobileFramebuffer uses Mach continuous-time values here. On + * Apple Silicon that is the ARM architectural counter. Empirical + * testing shows that using the current submission time together + * with the previous accepted swap makes DCP follow swap pacing. */ - req->swap.ts1 = 120; - req->swap.ts2 = 120; - req->swap.ts3 = 120; + if (!submit_timestamp) + submit_timestamp = timestamp; + + /* Firmware 12.x/13.x requires timestamp types 1, 2, and 7. */ + req->swap.timestamp[0] = timestamp; + req->swap.timestamp[1] = submit_timestamp; + req->swap.timestamp[6] = timestamp; + + trace_iomfb_vrr_timestamps(dcp, dcp->vrr_enabled, timestamp, + submit_timestamp); } /* These fields should be set together */ diff --git a/drivers/gpu/drm/apple/iomfb_template.h b/drivers/gpu/drm/apple/iomfb_template.h index 8efab49cc53d08..ce9fb92513cd1b 100644 --- a/drivers/gpu/drm/apple/iomfb_template.h +++ b/drivers/gpu/drm/apple/iomfb_template.h @@ -18,14 +18,8 @@ #include "version_utils.h" struct DCP_FW_NAME(dcp_swap) { - u64 ts1; - u64 ts2; - - u64 unk_10; - u64 unk_18; - u64 ts64_unk; - u64 unk_28; - u64 ts3; + /* IOMobileFramebuffer timestamp types 1 through 7, in wire order. */ + u64 timestamp[7]; u64 unk_38; u64 flags1; diff --git a/drivers/gpu/drm/apple/parser.c b/drivers/gpu/drm/apple/parser.c index 56960fa1fda265..3d520938a2900f 100644 --- a/drivers/gpu/drm/apple/parser.c +++ b/drivers/gpu/drm/apple/parser.c @@ -443,7 +443,7 @@ static u32 calculate_clock(struct dimension *horiz, struct dimension *vert) static int parse_mode(struct dcp_parse_ctx *handle, struct dcp_display_mode *out, s64 *score, int width_mm, - int height_mm, unsigned notch_height) + int height_mm, unsigned notch_height, bool internal) { int ret = 0; struct iterator it; @@ -515,20 +515,21 @@ static int parse_mode(struct dcp_parse_ctx *handle, return -EINVAL; /* - * HACK: - * Mark the 120 Hz mode on j314/j316 (identified by resolution) as vrr. - * We still do not know how to drive VRR but at least seetinng timestamps - * in the the swap_surface message to non-zero values drives the display - * at 120 fps. - */ - if (vert.precise_sync_rate >> 16 == 120 && - ((horiz.active == 3024 && vert.active == 1964) || - (horiz.active == 3456 && vert.active == 2234))) + * An internal ProMotion panel carries no EDID or DisplayID, so DCP + * reports no adaptive-sync range for it. Assume the ProMotion floor of + * 24 Hz up to whatever rate the mode itself advertises. + */ + if (internal && vert.precise_sync_rate >> 16 == 120) { + out->min_vrr = 24 << 16; + out->max_vrr = vert.precise_sync_rate; out->vrr = true; + } - if (min_vrr && max_vrr) { + /* Refresh rates are reported by DCP as 16.16 fixed-point Hz. */ + if (min_vrr && max_vrr > min_vrr) { out->min_vrr = min_vrr; out->max_vrr = max_vrr; + out->vrr = true; } vert.active -= notch_height; @@ -568,7 +569,8 @@ static int parse_mode(struct dcp_parse_ctx *handle, struct dcp_display_mode *enumerate_modes(struct dcp_parse_ctx *handle, unsigned int *count, int width_mm, - int height_mm, unsigned notch_height) + int height_mm, unsigned notch_height, + bool internal) { struct iterator it; int ret; @@ -590,7 +592,8 @@ struct dcp_display_mode *enumerate_modes(struct dcp_parse_ctx *handle, for (; it.idx < it.len; ++it.idx) { mode = &modes[*count]; - ret = parse_mode(it.handle, mode, &score, width_mm, height_mm, notch_height); + ret = parse_mode(it.handle, mode, &score, width_mm, height_mm, + notch_height, internal); /* Errors for a single mode are recoverable -- just skip it. */ if (ret) diff --git a/drivers/gpu/drm/apple/parser.h b/drivers/gpu/drm/apple/parser.h index 52c4de46d2f79f..35f4e81e762125 100644 --- a/drivers/gpu/drm/apple/parser.h +++ b/drivers/gpu/drm/apple/parser.h @@ -104,7 +104,8 @@ struct dimension { int parse(const void *blob, size_t size, struct dcp_parse_ctx *ctx); struct dcp_display_mode *enumerate_modes(struct dcp_parse_ctx *handle, unsigned int *count, int width_mm, - int height_mm, unsigned notch_height); + int height_mm, unsigned notch_height, + bool internal); int parse_display_attributes(struct dcp_parse_ctx *handle, int *width_mm, int *height_mm); int parse_epic_service_init(struct dcp_parse_ctx *handle, const char **name, diff --git a/drivers/gpu/drm/apple/trace.h b/drivers/gpu/drm/apple/trace.h index a13dd34fb7aab1..c0acac6ee690ab 100644 --- a/drivers/gpu/drm/apple/trace.h +++ b/drivers/gpu/drm/apple/trace.h @@ -263,6 +263,27 @@ TRACE_EVENT(iomfb_swap_submit, __entry->swap_id) ); +TRACE_EVENT(iomfb_vrr_timestamps, + TP_PROTO(struct apple_dcp *dcp, bool enabled, u64 timestamp, + u64 submit_timestamp), + TP_ARGS(dcp, enabled, timestamp, submit_timestamp), + TP_STRUCT__entry( + __field(u64, dcp) + __field(bool, enabled) + __field(u64, timestamp) + __field(u64, submit_timestamp) + ), + TP_fast_assign( + __entry->dcp = (u64)dcp; + __entry->enabled = enabled; + __entry->timestamp = timestamp; + __entry->submit_timestamp = submit_timestamp; + ), + TP_printk("dcp=%llx, enabled=%u, timestamp=%llu, previous=%llu", + __entry->dcp, __entry->enabled, __entry->timestamp, + __entry->submit_timestamp) +); + TRACE_EVENT(iomfb_swap_complete, TP_PROTO(struct apple_dcp *dcp, u32 swap_id), TP_ARGS(dcp, swap_id),