Backlight brightness is a property of a display, and thus of a DRM connector, yet it has historically only been controllable through the separate backlight sysfs interface. Add a generic, backend-agnostic per-connector LUMINANCE range property so brightness can be driven through the atomic modeset path like any other connector state.
A struct drm_backlight is embedded in every connector and initialized by the core; drivers do not allocate it. A driver links a backend (today a backlight_device, in the future DDC/CI, MIPI-DCS, ...) with drm_backlight_link(), which creates the connector's LUMINANCE property with the backend's range. The property value is staged into the atomic connector state and only pushed to the hardware from the commit/enable path, via a workqueue so that slow backends never stall a commit. DPMS off drives the backlight to 0 and DPMS on restores the committed value. The property range is per-connector (created from the backend's max_brightness), so multiple panels no longer share and corrupt a single device-wide range. drm_backlight_link() also carries the legacy-sysfs takeover accounting used by the client capability added in a later patch. The whole feature is guarded by CONFIG_DRM_BACKLIGHT (which depends on, rather than selects, BACKLIGHT_CLASS_DEVICE) so DRM does not pull the backlight subsystem into the kernel when it is not wanted. Co-developed-by: David Herrmann <[email protected]> Signed-off-by: David Herrmann <[email protected]> Signed-off-by: Mario Limonciello (AMD) <[email protected]> --- drivers/gpu/drm/Kconfig | 18 + drivers/gpu/drm/Makefile | 2 + drivers/gpu/drm/drm_atomic_helper.c | 35 ++ drivers/gpu/drm/drm_atomic_uapi.c | 49 ++- drivers/gpu/drm/drm_backlight.c | 550 ++++++++++++++++++++++++++++ drivers/gpu/drm/drm_connector.c | 56 +++ drivers/gpu/drm/drm_drv.c | 8 + drivers/gpu/drm/drm_mode_config.c | 1 + drivers/gpu/drm/drm_sysfs.c | 32 +- include/drm/drm_atomic_helper.h | 2 + include/drm/drm_backlight.h | 166 +++++++++ include/drm/drm_connector.h | 20 + include/drm/drm_mode_config.h | 11 + 13 files changed, 945 insertions(+), 5 deletions(-) create mode 100644 drivers/gpu/drm/drm_backlight.c create mode 100644 include/drm/drm_backlight.h diff --git a/drivers/gpu/drm/Kconfig b/drivers/gpu/drm/Kconfig index 323422861e8f6..ddb6827d613f8 100644 --- a/drivers/gpu/drm/Kconfig +++ b/drivers/gpu/drm/Kconfig @@ -33,6 +33,24 @@ endmenu if DRM +config DRM_BACKLIGHT + bool "DRM connector backlight (luminance) support" + depends on DRM + depends on BACKLIGHT_CLASS_DEVICE + default DRM + help + Expose per-connector backlight brightness control through the DRM + connector LUMINANCE property, backed by the backlight subsystem (and, + in the future, other backends such as DDC/CI). This lets luminance + changes go through the same atomic modeset path as the rest of the + display state. + + This depends on BACKLIGHT_CLASS_DEVICE. When that is disabled, DRM is + built without this support and does not pull the backlight subsystem + into the kernel. + + If in doubt, say Y. + config DRM_MIPI_DBI tristate depends on DRM diff --git a/drivers/gpu/drm/Makefile b/drivers/gpu/drm/Makefile index e635fcffd3790..bcb06dbb71343 100644 --- a/drivers/gpu/drm/Makefile +++ b/drivers/gpu/drm/Makefile @@ -78,6 +78,8 @@ drm-$(CONFIG_DRM_CLIENT) += \ drm_client_event.o \ drm_client_modeset.o \ drm_client_sysrq.o +drm-$(CONFIG_DRM_BACKLIGHT) += drm_backlight.o +drm-$(CONFIG_DRM_LIB_RANDOM) += lib/drm_random.o drm-$(CONFIG_COMPAT) += drm_ioc32.o drm-$(CONFIG_DRM_PANEL) += drm_panel.o drm-$(CONFIG_OF) += drm_of.o diff --git a/drivers/gpu/drm/drm_atomic_helper.c b/drivers/gpu/drm/drm_atomic_helper.c index 285aac3554dfd..110cd81e5d6db 100644 --- a/drivers/gpu/drm/drm_atomic_helper.c +++ b/drivers/gpu/drm/drm_atomic_helper.c @@ -33,6 +33,7 @@ #include <drm/drm_atomic.h> #include <drm/drm_atomic_helper.h> #include <drm/drm_atomic_uapi.h> +#include <drm/drm_backlight.h> #include <drm/drm_blend.h> #include <drm/drm_bridge.h> #include <drm/drm_colorop.h> @@ -1230,6 +1231,10 @@ drm_atomic_helper_commit_encoder_bridge_disable(struct drm_device *dev, * it away), so we won't call disable hooks twice. */ bridge = drm_bridge_chain_get_first_bridge(encoder); + + /* Turn the backlight off before disabling the pipeline. */ + drm_backlight_set_luminance(connector, 0); + drm_atomic_bridge_chain_disable(bridge, state); drm_bridge_put(bridge); @@ -1775,6 +1780,24 @@ void drm_atomic_helper_commit_modeset_enables(struct drm_device *dev, } EXPORT_SYMBOL(drm_atomic_helper_commit_modeset_enables); +/** + * drm_atomic_helper_connector_apply_luminance - apply connector luminance from atomic state + * @conn_state: atomic connector state to apply luminance for + * + * Updates the backlight luminance from the atomic connector state. If the + * connector has a linked backlight and is associated with an active CRTC, + * push the luminance value to hardware. + */ +void drm_atomic_helper_connector_apply_luminance(const struct drm_connector_state *conn_state) +{ + struct drm_connector *connector = conn_state->connector; + + if (conn_state->crtc && conn_state->crtc->state && + conn_state->crtc->state->active) + drm_backlight_set_luminance(connector, conn_state->luminance); +} +EXPORT_SYMBOL(drm_atomic_helper_connector_apply_luminance); + /* * For atomic updates which touch just a single CRTC, calculate the time of the * next vblank, and inform all the fences of the deadline. @@ -1989,6 +2012,9 @@ EXPORT_SYMBOL(drm_atomic_helper_wait_for_flip_done); void drm_atomic_helper_commit_tail(struct drm_atomic_commit *state) { struct drm_device *dev = state->dev; + struct drm_connector *connector; + struct drm_connector_state *new_conn_state; + int i; drm_atomic_helper_commit_modeset_disables(dev, state); @@ -1996,6 +2022,9 @@ void drm_atomic_helper_commit_tail(struct drm_atomic_commit *state) drm_atomic_helper_commit_modeset_enables(dev, state); + for_each_new_connector_in_state(state, connector, new_conn_state, i) + drm_atomic_helper_connector_apply_luminance(new_conn_state); + drm_atomic_helper_fake_vblank(state); drm_atomic_helper_commit_hw_done(state); @@ -2019,11 +2048,17 @@ EXPORT_SYMBOL(drm_atomic_helper_commit_tail); void drm_atomic_helper_commit_tail_rpm(struct drm_atomic_commit *state) { struct drm_device *dev = state->dev; + struct drm_connector *connector; + struct drm_connector_state *new_conn_state; + int i; drm_atomic_helper_commit_modeset_disables(dev, state); drm_atomic_helper_commit_modeset_enables(dev, state); + for_each_new_connector_in_state(state, connector, new_conn_state, i) + drm_atomic_helper_connector_apply_luminance(new_conn_state); + drm_atomic_helper_commit_planes(dev, state, DRM_PLANE_COMMIT_ACTIVE_ONLY); diff --git a/drivers/gpu/drm/drm_atomic_uapi.c b/drivers/gpu/drm/drm_atomic_uapi.c index 5ea593b3a98ec..07d4bcb16cd23 100644 --- a/drivers/gpu/drm/drm_atomic_uapi.c +++ b/drivers/gpu/drm/drm_atomic_uapi.c @@ -30,6 +30,8 @@ #include <drm/drm_atomic.h> #include <drm/drm_atomic_helper.h> #include <drm/drm_atomic_uapi.h> +#include <drm/drm_backlight.h> +#include <drm/drm_connector.h> #include <drm/drm_framebuffer.h> #include <drm/drm_print.h> #include <drm/drm_drv.h> @@ -962,6 +964,13 @@ static int drm_atomic_connector_set_property(struct drm_connector *connector, state->hdmi.broadcast_rgb = val; } else if (property == connector->color_format_property) { state->color_format = val; + } else if (property == connector->luminance_property) { + /* + * Only stage the value into the atomic state; the hardware is + * updated from the commit path (see + * drm_atomic_helper_connector_apply_luminance()). + */ + state->luminance = val; } else if (connector->funcs->atomic_set_property) { return connector->funcs->atomic_set_property(connector, state, property, val); @@ -1049,6 +1058,8 @@ drm_atomic_connector_get_property(struct drm_connector *connector, *val = state->hdmi.broadcast_rgb; } else if (property == connector->color_format_property) { *val = state->color_format; + } else if (property == connector->luminance_property) { + *val = state->luminance; } else if (connector->funcs->atomic_get_property) { return connector->funcs->atomic_get_property(connector, state, property, val); @@ -1133,6 +1144,22 @@ static struct drm_pending_vblank_event *create_vblank_event( return e; } +static void drm_atomic_crtc_set_backlight(struct drm_crtc *crtc, bool active) +{ + struct drm_connector_list_iter conn_iter; + struct drm_connector *connector; + + drm_connector_list_iter_begin(crtc->dev, &conn_iter); + drm_for_each_connector_iter(connector, &conn_iter) { + if (!connector->state || connector->state->crtc != crtc) + continue; + + drm_backlight_set_luminance(connector, + active ? connector->state->luminance : 0); + } + drm_connector_list_iter_end(&conn_iter); +} + int drm_atomic_connector_commit_dpms(struct drm_atomic_commit *state, struct drm_connector *connector, int mode) @@ -1155,9 +1182,29 @@ int drm_atomic_connector_commit_dpms(struct drm_atomic_commit *state, if (connector->dpms == mode) goto out; + crtc = connector->state ? connector->state->crtc : NULL; + + /* Handle backlight brightness coordination with DPMS state changes */ + if (old_mode != DRM_MODE_DPMS_OFF && mode == DRM_MODE_DPMS_OFF) { + /* DPMS ON -> OFF: dim all connectors driven by this CRTC. */ + if (crtc) + drm_atomic_crtc_set_backlight(crtc, false); + else + drm_backlight_set_luminance(connector, 0); + } + connector->dpms = mode; - crtc = connector->state->crtc; + /* DPMS OFF -> ON: restore brightness to property value */ + if (old_mode == DRM_MODE_DPMS_OFF && mode == DRM_MODE_DPMS_ON && + connector->state) { + if (crtc) + drm_atomic_crtc_set_backlight(crtc, true); + else + drm_backlight_set_luminance(connector, + connector->state->luminance); + } + if (!crtc) goto out; ret = drm_atomic_add_affected_connectors(state, crtc); diff --git a/drivers/gpu/drm/drm_backlight.c b/drivers/gpu/drm/drm_backlight.c new file mode 100644 index 0000000000000..dd02d1878727e --- /dev/null +++ b/drivers/gpu/drm/drm_backlight.c @@ -0,0 +1,550 @@ +// SPDX-License-Identifier: MIT +/* + * DRM Backlight Helpers + * Copyright (c) 2014 David Herrmann + * Copyright (c) 2026 Advanced Micro Devices, Inc. + */ + +#include <linux/backlight.h> +#include <linux/list.h> +#include <linux/notifier.h> +#include <linux/spinlock.h> +#include <linux/workqueue.h> + +#include <drm/drm_backlight.h> +#include <drm/drm_connector.h> +#include <drm/drm_device.h> +#include <drm/drm_property.h> + +/** + * DOC: Backlight Devices + * + * Backlight devices have always been managed as a separate subsystem, + * independent of DRM. They are usually controlled via separate hardware + * interfaces than the display controller, so the split works out fine. + * However, backlight brightness is a property of a display, and thus a + * property of a DRM connector. We already manage DPMS states via connector + * properties, so it is natural to keep brightness control at the same place. + * + * This DRM backlight interface implements a generic per-connector LUMINANCE + * property. The core is backend-agnostic: it does not talk to any hardware + * itself, it only forwards luminance requests to a backend that a driver has + * linked. The backend is described by a &struct drm_backlight_funcs. Today the + * only backend is the backlight subsystem (&struct backlight_device), linked + * with drm_backlight_link(), but other backends (DDC/CI, MIPI-DCS, ...) can be + * added by providing a different set of operations without changing the core. + * + * A &struct drm_backlight is embedded in every &struct drm_connector and + * initialized by the DRM core (drm_backlight_connector_init()); drivers do not + * allocate it. Drivers link a backend once it is available by calling + * drm_backlight_link(); this creates the connector's LUMINANCE property with + * the backend's range. Passing NULL unlinks the backend. Hardware is only ever + * touched from a workqueue, so slow backends never stall an atomic commit. + */ + +static LIST_HEAD(drm_backlight_list); +static DEFINE_SPINLOCK(drm_backlight_lock); + +/* caller must hold @drm_backlight_lock */ +static bool __drm_backlight_is_linked(struct drm_backlight *b) +{ + lockdep_assert_held(&drm_backlight_lock); + /* a backlight is live while it is on @drm_backlight_list */ + return !list_empty(&b->list); +} + +/* + * Return the linked &backlight_device if the current backend is the backlight + * subsystem, or NULL otherwise. The legacy-sysfs takeover accounting only + * applies to that backend. + */ +static const struct drm_backlight_funcs drm_backlight_bd_funcs; + +static struct backlight_device *drm_backlight_bd(struct drm_backlight *b) +{ + if (b->funcs != &drm_backlight_bd_funcs) + return NULL; + return b->backend; +} + +/* caller must hold @drm_backlight_lock */ +static void __drm_backlight_schedule(struct drm_backlight *b) +{ + lockdep_assert_held(&drm_backlight_lock); + if (__drm_backlight_is_linked(b)) + schedule_work(&b->work); +} + +static void __drm_backlight_worker(struct work_struct *w) +{ + struct drm_backlight *b = container_of(w, struct drm_backlight, work); + static char *ep[] = { "BACKLIGHT=1", NULL }; + const struct drm_backlight_funcs *funcs; + bool send_uevent; + unsigned int v; + + scoped_guard(spinlock, &drm_backlight_lock) { + send_uevent = b->changed; + b->changed = false; + v = b->set_value; + funcs = b->funcs; + } + + /* + * The backend stays valid here: drm_backlight_do_unlink() clears + * @backend and drops its reference only after cancel_work_sync(), so an + * in-flight worker always sees a live backend. + */ + if (funcs && funcs->set_luminance) + WARN_ON(funcs->set_luminance(b, v)); + + if (send_uevent && b->connector->kdev) + kobject_uevent_env(&b->connector->kdev->kobj, KOBJ_CHANGE, ep); +} + +/* caller must hold @drm_backlight_lock */ +static void __drm_backlight_set(struct drm_backlight *b, unsigned int v) +{ + unsigned int max = 0; + bool can_disable = false; + + lockdep_assert_held(&drm_backlight_lock); + + if (!b->funcs || !b->funcs->get_range) + return; + + b->funcs->get_range(b, &max, &can_disable); + if (!max) + return; + + /* clamp to the backend maximum */ + b->set_value = min(v, max); + __drm_backlight_schedule(b); +} + +/* caller must hold @drm_backlight_lock */ +static void __drm_backlight_readback(struct drm_backlight *b, unsigned int v) +{ + struct drm_connector *connector = b->connector; + unsigned int max = 0; + bool can_disable = false; + + lockdep_assert_held(&drm_backlight_lock); + + if (!b->funcs || !b->funcs->get_range) + return; + + b->funcs->get_range(b, &max, &can_disable); + if (!max) + return; + + /* + * Reflect a hardware-side brightness change (firmware hotkeys, or a + * legacy sysfs write while not inhibited) back into the connector's + * committed luminance so a read-back returns the real value. + */ + if (connector->state) + connector->state->luminance = min(v, max); +} + +/** + * drm_backlight_create_property - create and attach a connector's LUMINANCE property + * @connector: connector to modify + * @max: maximum luminance value + * @can_disable: true if luminance 0 disables the backlight + * + * Create and attach the per-connector LUMINANCE property. Must be called during + * connector initialization before the DRM device is registered. + * + * Returns: 0 on success, or a negative error code. + */ +int drm_backlight_create_property(struct drm_connector *connector, + unsigned int max, bool can_disable) +{ + struct drm_device *dev = connector->dev; + struct drm_property *prop; + unsigned int min = can_disable ? 0 : 1; + + if (!dev || !connector->base.properties || connector->luminance_property) + return 0; + + prop = drm_property_create_range(dev, DRM_MODE_PROP_ATOMIC, + "LUMINANCE", min, max); + if (!prop) + return -ENOMEM; + + prop->is_luminance = true; + connector->luminance_property = prop; + drm_object_attach_property(&connector->base, prop, min); + if (connector->state) + connector->state->luminance = min; + + return 0; +} +EXPORT_SYMBOL(drm_backlight_create_property); + +static void drm_backlight_do_unlink(struct drm_backlight *b) +{ + struct backlight_device *bd; + unsigned int clients; + + /* + * Stop new work first, but leave @backend in place so an in-flight + * worker keeps a valid backend to operate on. Capture the linked + * backlight_device (if any) while @funcs is still set. + */ + scoped_guard(spinlock, &drm_backlight_lock) { + if (!b->funcs) + return; + bd = drm_backlight_bd(b); + clients = b->luminance_clients; + b->funcs = NULL; + list_del_init(&b->list); + } + + cancel_work_sync(&b->work); + + scoped_guard(spinlock, &drm_backlight_lock) { + if (clients && bd) + atomic_sub(clients, &bd->drm_takeover); + /* + * Only clear @backend if no concurrent drm_backlight_link() + * re-linked a new backend after we dropped the lock for + * cancel_work_sync(); otherwise we would clobber it. + */ + if (!b->funcs) + b->backend = NULL; + } + + backlight_device_unref(bd); +} + +/** + * drm_backlight_connector_init - initialize a connector's embedded backlight + * @connector: connector to initialize + * + * Called by the DRM core from drm_connector_init(). Drivers never call this. + */ +void drm_backlight_connector_init(struct drm_connector *connector) +{ + struct drm_backlight *b = &connector->backlight; + + b->connector = connector; + INIT_LIST_HEAD(&b->list); + INIT_WORK(&b->work, __drm_backlight_worker); +} + +/** + * drm_backlight_connector_cleanup - tear down a connector's embedded backlight + * @connector: connector being cleaned up + * + * Called by the DRM core from drm_connector_cleanup(). Any still-linked backend + * is unlinked here as a safety net for connectors that are torn down on a probe + * error path before drm_connector_unregister() ever runs, so no stale entry is + * left on the global list. The LUMINANCE property itself is freed by + * drm_mode_config_cleanup(). + */ +void drm_backlight_connector_cleanup(struct drm_connector *connector) +{ + drm_backlight_do_unlink(&connector->backlight); +} + +/** + * drm_backlight_unregister - unlink a connector's backlight on unregister + * @connector: connector being unregistered + * + * Called by the DRM core from drm_connector_unregister() as a safety net in + * case a driver did not unlink its backend itself. + */ +void drm_backlight_unregister(struct drm_connector *connector) +{ + drm_backlight_do_unlink(&connector->backlight); +} + +/** + * drm_backlight_link - link a backlight device to a connector + * @connector: connector to modify + * @bd: backlight device to link, or NULL to unlink + * + * Establish the link between a connector's LUMINANCE property and a registered + * backlight_device. On the first link the connector's LUMINANCE property is + * created with the backend's range. Passing NULL unlinks any linked device. + * + * User-space cannot create or modify this link. + * + * Returns: 0 on success, or a negative error code if the property could not be + * created (in which case no backend is linked). + */ +int drm_backlight_link(struct drm_connector *connector, + struct backlight_device *bd) +{ + struct drm_backlight *b = &connector->backlight; + unsigned int max; + int ret; + + if (!bd) { + drm_backlight_do_unlink(b); + return 0; + } + + /* Retarget: drop any previously linked backend first. */ + if (b->funcs) + drm_backlight_do_unlink(b); + + max = bd->props.max_brightness; + if (max && !connector->luminance_property) { + ret = drm_backlight_create_property(connector, max, false); + if (ret) + return ret; + } + + scoped_guard(spinlock, &drm_backlight_lock) { + b->funcs = &drm_backlight_bd_funcs; + b->backend = bd; + backlight_device_ref(bd); + list_add(&b->list, &drm_backlight_list); + /* + * Inherit the device-wide inhibit level so a connector linked + * (or hotplugged) while luminance-aware clients are active also + * has its legacy sysfs control inhibited. + */ + b->luminance_clients = connector->dev->mode_config.luminance_clients; + if (b->luminance_clients) + atomic_add(b->luminance_clients, &bd->drm_takeover); + __drm_backlight_readback(b, bd->props.brightness); + b->changed = true; + __drm_backlight_set(b, bd->props.brightness); + } + + return 0; +} +EXPORT_SYMBOL(drm_backlight_link); + +/** + * drm_backlight_get_device - get the backlight_device linked to a connector + * @connector: connector to query + * + * Returns the &backlight_device linked to @connector with an additional + * reference taken, or NULL if no backlight subsystem device is linked. The + * caller must drop the reference with backlight_device_unref() when done. + */ +struct backlight_device *drm_backlight_get_device(struct drm_connector *connector) +{ + struct backlight_device *bd; + + guard(spinlock)(&drm_backlight_lock); + bd = drm_backlight_bd(&connector->backlight); + backlight_device_ref(bd); + + return bd; +} +EXPORT_SYMBOL(drm_backlight_get_device); + +/** + * drm_backlight_inhibit_legacy - disable legacy sysfs control of the backend + * @connector: connector whose backlight should be inhibited + * + * Record that one more luminance-aware DRM client has taken over this + * connector's backlight. While any clients are recorded, writes to the linked + * backlight_device's legacy ``brightness`` sysfs attribute return ``-EBUSY``. + * The takeover follows the linked device if the link changes. + * + * Calls must be balanced with drm_backlight_uninhibit_legacy(). + */ +void drm_backlight_inhibit_legacy(struct drm_connector *connector) +{ + struct drm_backlight *b = &connector->backlight; + struct backlight_device *bd; + + guard(spinlock)(&drm_backlight_lock); + b->luminance_clients++; + bd = drm_backlight_bd(b); + if (bd) + atomic_inc(&bd->drm_takeover); +} +EXPORT_SYMBOL(drm_backlight_inhibit_legacy); + +/** + * drm_backlight_uninhibit_legacy - re-enable legacy sysfs control + * @connector: connector to uninhibit + * + * Balances a previous drm_backlight_inhibit_legacy() call. + */ +void drm_backlight_uninhibit_legacy(struct drm_connector *connector) +{ + struct drm_backlight *b = &connector->backlight; + struct backlight_device *bd; + + guard(spinlock)(&drm_backlight_lock); + if (WARN_ON(b->luminance_clients == 0)) + return; + b->luminance_clients--; + bd = drm_backlight_bd(b); + if (bd) + atomic_dec(&bd->drm_takeover); +} +EXPORT_SYMBOL(drm_backlight_uninhibit_legacy); + +/** + * drm_backlight_inhibit_legacy_all - inhibit legacy sysfs on every connector + * @dev: DRM device whose connectors should be inhibited + * + * Used when a client declares it is luminance-aware via + * DRM_CLIENT_CAP_LUMINANCE. The device-wide count is bumped so connectors + * hotplugged later inherit the inhibit in drm_backlight_link(). + */ +void drm_backlight_inhibit_legacy_all(struct drm_device *dev) +{ + struct drm_connector_list_iter iter; + struct drm_connector *connector; + + scoped_guard(spinlock, &drm_backlight_lock) + dev->mode_config.luminance_clients++; + + drm_connector_list_iter_begin(dev, &iter); + drm_for_each_connector_iter(connector, &iter) + drm_backlight_inhibit_legacy(connector); + drm_connector_list_iter_end(&iter); +} +EXPORT_SYMBOL(drm_backlight_inhibit_legacy_all); + +/** + * drm_backlight_uninhibit_legacy_all - reverse drm_backlight_inhibit_legacy_all() + * @dev: DRM device whose connectors should be uninhibited + */ +void drm_backlight_uninhibit_legacy_all(struct drm_device *dev) +{ + struct drm_connector_list_iter iter; + struct drm_connector *connector; + + scoped_guard(spinlock, &drm_backlight_lock) { + if (WARN_ON(dev->mode_config.luminance_clients == 0)) + return; + dev->mode_config.luminance_clients--; + } + + drm_connector_list_iter_begin(dev, &iter); + drm_for_each_connector_iter(connector, &iter) + drm_backlight_uninhibit_legacy(connector); + drm_connector_list_iter_end(&iter); +} +EXPORT_SYMBOL(drm_backlight_uninhibit_legacy_all); + +/** + * drm_backlight_set_luminance - request a luminance change on a connector + * @connector: connector to update + * @value: luminance value to apply + * + * Clamp @value to the backend range and schedule the hardware update. Safe to + * call from an atomic commit tail: the actual hardware access happens later + * from a workqueue. + */ +void drm_backlight_set_luminance(struct drm_connector *connector, + unsigned int value) +{ + guard(spinlock)(&drm_backlight_lock); + __drm_backlight_set(&connector->backlight, value); +} +EXPORT_SYMBOL(drm_backlight_set_luminance); + +/* backlight_device backend ------------------------------------------------- */ + +static int drm_backlight_bd_set_luminance(struct drm_backlight *b, + unsigned int value) +{ + struct backlight_device *bd = b->backend; + int rc; + + rc = backlight_set_brightness(bd, value, BACKLIGHT_UPDATE_DRM); + if (rc) + backlight_set_brightness(bd, U16_MAX, BACKLIGHT_UPDATE_DRM); + + return rc; +} + +static int drm_backlight_bd_get_luminance(struct drm_backlight *b, + unsigned int *value) +{ + struct backlight_device *bd = b->backend; + + if (!bd) + return -ENODEV; + *value = bd->props.brightness; + + return 0; +} + +static void drm_backlight_bd_get_range(struct drm_backlight *b, + unsigned int *max, bool *can_disable) +{ + struct backlight_device *bd = b->backend; + + *max = bd ? bd->props.max_brightness : 0; + /* + * A generic backlight_device gives no guarantee that a value of 0 turns + * the panel fully off, so keep 0 reserved for the DPMS-off sentinel and + * expose a 1..max range. + */ + *can_disable = false; +} + +static const struct drm_backlight_funcs drm_backlight_bd_funcs = { + .set_luminance = drm_backlight_bd_set_luminance, + .get_luminance = drm_backlight_bd_get_luminance, + .get_range = drm_backlight_bd_get_range, +}; + +static int drm_backlight_notify(struct notifier_block *self, + unsigned long event, void *data) +{ + struct backlight_device *bd = data; + struct drm_backlight *b; + + switch (event) { + case BACKLIGHT_UNREGISTERED: + /* + * Unlink every connector using @bd. drm_backlight_do_unlink() + * sleeps (cancel_work_sync()), so it cannot run under the list + * spinlock; re-scan for the next match after each unlink. + */ + for (;;) { + struct drm_backlight *found = NULL; + + scoped_guard(spinlock, &drm_backlight_lock) { + list_for_each_entry(b, &drm_backlight_list, list) { + if (drm_backlight_bd(b) == bd) { + found = b; + break; + } + } + } + if (!found) + break; + drm_backlight_do_unlink(found); + } + break; + case BACKLIGHT_BRIGHTNESS_CHANGED: + scoped_guard(spinlock, &drm_backlight_lock) { + list_for_each_entry(b, &drm_backlight_list, list) + if (drm_backlight_bd(b) == bd) + __drm_backlight_readback(b, bd->props.brightness); + } + break; + } + + return 0; +} + +static struct notifier_block drm_backlight_notifier = { + .notifier_call = drm_backlight_notify, +}; + +int drm_backlight_init(void) +{ + return backlight_register_notifier(&drm_backlight_notifier); +} + +void drm_backlight_exit(void) +{ + backlight_unregister_notifier(&drm_backlight_notifier); +} diff --git a/drivers/gpu/drm/drm_connector.c b/drivers/gpu/drm/drm_connector.c index 8b4baed060f3a..92ed0c1879d53 100644 --- a/drivers/gpu/drm/drm_connector.c +++ b/drivers/gpu/drm/drm_connector.c @@ -21,6 +21,7 @@ */ #include <drm/drm_auth.h> +#include <drm/drm_backlight.h> #include <drm/drm_connector.h> #include <drm/drm_drv.h> #include <drm/drm_edid.h> @@ -316,6 +317,8 @@ static int drm_connector_init_only(struct drm_device *dev, drm_object_attach_property(&connector->base, config->prop_crtc_id, 0); } + drm_backlight_connector_init(connector); + connector->debugfs_entry = NULL; out_put_type_id: if (ret) @@ -773,6 +776,7 @@ void drm_connector_cleanup(struct drm_connector *connector) struct drm_device *dev = connector->dev; struct drm_display_mode *mode, *t; + drm_backlight_connector_cleanup(connector); /* The connector should have been removed from userspace long before * it is finally destroyed. */ @@ -944,6 +948,8 @@ EXPORT_SYMBOL(drm_connector_dynamic_register); void drm_connector_unregister(struct drm_connector *connector) { mutex_lock(&connector->mutex); + drm_backlight_unregister(connector); + if (connector->registration_state != DRM_CONNECTOR_REGISTERED) { mutex_unlock(&connector->mutex); return; @@ -1532,6 +1538,56 @@ EXPORT_SYMBOL(drm_hdmi_connector_get_output_format_name); * Summarizing: Only set "DPMS" when the connector is known to be enabled, * assume that a successful SETCONFIG call also sets "DPMS" to on, and * never read back the value of "DPMS" because it can be incorrect. + * LUMINANCE: + * Atomic, per-connector range property for controlling the backlight + * brightness level of the connector's display. It provides unified access + * to the display backlight through the atomic modeset path, replacing the + * legacy sysfs interface for brightness control. + * + * The property value is an unsigned integer. Its valid range is baked in + * when a backlight backend is linked to the connector and reflects the + * backend's capabilities: + * + * - Range 1-N: Normal operation for a backend that cannot guarantee a full + * off state. 1 is the minimum *visible* brightness and N is the backend + * maximum. Drivers are expected never to program a duty cycle of 0 for a + * value of 1. + * - Range 0-N: Used when the backend can also fully turn the panel off, so + * 0 is a normal in-range value. + * + * Value 0 is always accepted, even when the advertised range starts at 1: + * it is the sentinel used to turn the backlight off when the connector is + * powered down (DPMS off). Turning the backlight off this way may power the + * panel down entirely; unlike programming a legacy sysfs duty cycle of 0, + * this can stop vblank/pageflip events until the connector is enabled + * again, so luminance-aware clients must not rely on such events while the + * connector is off. + * + * Connectors without a linked backend do not expose this property at all. + * + * For atomic drivers the value is stored in &drm_connector_state.luminance + * and applied to the hardware from the atomic commit path once the + * connector is enabled. When DPMS transitions to OFF the backlight is set + * to 0; when it transitions back to ON the committed luminance is restored. + * Reading the property returns the last committed value (or the hardware's + * current state for backends that support reading brightness back). + * + * The property is created by the DRM core when a driver links a backlight + * backend with drm_backlight_link(); drivers do not create it directly. + * + * Client Capability: + * User-space must set the DRM_CLIENT_CAP_LUMINANCE client capability + * to 1 before using this property. When this capability is enabled, + * the legacy sysfs backlight interface is inhibited to prevent + * conflicts between multiple clients trying to control the same + * backlight. This ensures that only luminance-aware clients control + * the backlight through the DRM atomic interface. + * + * Legacy clients that do not set this capability should continue + * using the sysfs interface (if available). + * + * Note: This property can be set through the MODE_ATOMIC ioctl as part of + * the atomic state. * panel_type: * Immutable enum property to indicate the type of connected panel. * Possible values are "unknown" (default), "OLED", and "LCD". diff --git a/drivers/gpu/drm/drm_drv.c b/drivers/gpu/drm/drm_drv.c index c808958a2188e..c8aa6834c1f4e 100644 --- a/drivers/gpu/drm/drm_drv.c +++ b/drivers/gpu/drm/drm_drv.c @@ -42,6 +42,7 @@ #include <linux/xarray.h> #include <drm/drm_accel.h> +#include <drm/drm_backlight.h> #include <drm/drm_bridge.h> #include <drm/drm_cache.h> #include <drm/drm_client_event.h> @@ -1250,6 +1251,7 @@ static void drm_core_exit(void) drm_privacy_screen_lookup_exit(); drm_panic_exit(); accel_core_exit(); + drm_backlight_exit(); unregister_chrdev(DRM_MAJOR, "drm"); drm_debugfs_remove_root(); drm_sysfs_destroy(); @@ -1273,6 +1275,12 @@ static int __init drm_core_init(void) drm_debugfs_init_root(); drm_debugfs_bridge_params(); + ret = drm_backlight_init(); + if (ret < 0) { + DRM_ERROR("Cannot initialize backlight interface\n"); + goto error; + } + ret = register_chrdev(DRM_MAJOR, "drm", &drm_stub_fops); if (ret < 0) goto error; diff --git a/drivers/gpu/drm/drm_mode_config.c b/drivers/gpu/drm/drm_mode_config.c index 366f6d8212425..36579e8a7c0e5 100644 --- a/drivers/gpu/drm/drm_mode_config.c +++ b/drivers/gpu/drm/drm_mode_config.c @@ -33,6 +33,7 @@ #include <drm/drm_print.h> #include <drm/drm_colorop.h> #include <linux/dma-resv.h> +#include <drm/drm_backlight.h> #include "drm_crtc_internal.h" #include "drm_internal.h" diff --git a/drivers/gpu/drm/drm_sysfs.c b/drivers/gpu/drm/drm_sysfs.c index ef4e923a87284..2f4ce7d040f9d 100644 --- a/drivers/gpu/drm/drm_sysfs.c +++ b/drivers/gpu/drm/drm_sysfs.c @@ -11,6 +11,7 @@ */ #include <linux/acpi.h> +#include <linux/backlight.h> #include <linux/component.h> #include <linux/device.h> #include <linux/err.h> @@ -27,6 +28,7 @@ #include <drm/drm_device.h> #include <drm/drm_file.h> #include <drm/drm_modes.h> +#include <drm/drm_backlight.h> #include <drm/drm_print.h> #include <drm/drm_property.h> #include <drm/drm_sysfs.h> @@ -391,15 +393,37 @@ int drm_sysfs_connector_add(struct drm_connector *connector) int drm_sysfs_connector_add_late(struct drm_connector *connector) { - if (connector->ddc) - return sysfs_create_link(&connector->kdev->kobj, - &connector->ddc->dev.kobj, "ddc"); + struct backlight_device *bd = drm_backlight_get_device(connector); + int ret = 0; + + if (connector->ddc) { + ret = sysfs_create_link(&connector->kdev->kobj, + &connector->ddc->dev.kobj, "ddc"); + if (ret) + goto out; + } - return 0; + if (bd) { + ret = sysfs_create_link(&connector->kdev->kobj, + &bd->dev.kobj, "backlight"); + if (ret && connector->ddc) + sysfs_remove_link(&connector->kdev->kobj, "ddc"); + } + +out: + backlight_device_unref(bd); + return ret; } void drm_sysfs_connector_remove_early(struct drm_connector *connector) { + struct backlight_device *bd = drm_backlight_get_device(connector); + + if (bd) { + sysfs_remove_link(&connector->kdev->kobj, "backlight"); + backlight_device_unref(bd); + } + if (connector->ddc) sysfs_remove_link(&connector->kdev->kobj, "ddc"); } diff --git a/include/drm/drm_atomic_helper.h b/include/drm/drm_atomic_helper.h index 4cfeec70d648a..8a166ead13c55 100644 --- a/include/drm/drm_atomic_helper.h +++ b/include/drm/drm_atomic_helper.h @@ -116,6 +116,8 @@ void drm_atomic_helper_commit_encoder_bridge_enable(struct drm_device *dev, void drm_atomic_helper_commit_modeset_enables(struct drm_device *dev, struct drm_atomic_commit *old_state); +void drm_atomic_helper_connector_apply_luminance(const struct drm_connector_state *conn_state); + int drm_atomic_helper_prepare_planes(struct drm_device *dev, struct drm_atomic_commit *state); void drm_atomic_helper_unprepare_planes(struct drm_device *dev, diff --git a/include/drm/drm_backlight.h b/include/drm/drm_backlight.h new file mode 100644 index 0000000000000..417ca4ecbf15b --- /dev/null +++ b/include/drm/drm_backlight.h @@ -0,0 +1,166 @@ +/* SPDX-License-Identifier: MIT */ +#ifndef __DRM_BACKLIGHT_H__ +#define __DRM_BACKLIGHT_H__ + +/* + * Copyright (c) 2014 David Herrmann <dh.herrmann at gmail.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER(S) OR AUTHOR(S) BE LIABLE FOR ANY CLAIM, DAMAGES OR + * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +#include <linux/list.h> +#include <linux/types.h> +#include <linux/workqueue.h> + +struct backlight_device; +struct drm_backlight; +struct drm_connector; +struct drm_device; + +/** + * struct drm_backlight_funcs - backend operations for a DRM backlight + * + * A DRM backlight is backend-agnostic: the core forwards luminance requests to + * whatever backend a driver has linked. Today the only backend is the backlight + * subsystem (&struct backlight_device), but the same core can drive other + * backends (DDC/CI, MIPI-DCS, ...) by providing a different set of these + * operations without any change to the core. + * + * All callbacks are invoked from process context (a workqueue), never from an + * atomic commit tail or while holding a spinlock, so they are allowed to sleep. + */ +struct drm_backlight_funcs { + /** + * @set_luminance: + * + * Push @value (already clamped to the backend range) to the hardware. + * Returns 0 on success or a negative error code. + */ + int (*set_luminance)(struct drm_backlight *b, unsigned int value); + + /** + * @get_luminance: + * + * Read the current hardware luminance into @value. Returns 0 on success + * or a negative error code. May be NULL if the backend cannot be read + * back. + */ + int (*get_luminance)(struct drm_backlight *b, unsigned int *value); + + /** + * @get_range: + * + * Report the backend's maximum luminance in @max and whether the + * backend can turn the panel off at luminance 0 in @can_disable. + */ + void (*get_range)(struct drm_backlight *b, unsigned int *max, + bool *can_disable); +}; + +/** + * struct drm_backlight - per-connector backlight state + * + * This structure is embedded in &struct drm_connector and initialized by the + * DRM core; drivers never allocate it. It becomes active once a driver links a + * backend with drm_backlight_link(). + */ +struct drm_backlight { + /** @connector: connector this backlight belongs to */ + struct drm_connector *connector; + /** @funcs: backend operations, or NULL while no backend is linked */ + const struct drm_backlight_funcs *funcs; + /** @backend: backend private pointer (e.g. the &backlight_device) */ + void *backend; + /** @list: entry on the global list of linked DRM backlights */ + struct list_head list; + /** @work: deferred hardware update and uevent */ + struct work_struct work; + /** @set_value: luminance value pending application by @work */ + unsigned int set_value; + /** + * @luminance_clients: number of luminance-aware DRM clients that have + * taken this backlight over. While > 0, legacy sysfs writes to the + * linked backend return -EBUSY. + */ + unsigned int luminance_clients; + /** @changed: a uevent is pending for @work to emit */ + bool changed : 1; +}; + +#if IS_ENABLED(CONFIG_DRM_BACKLIGHT) + +int drm_backlight_init(void); +void drm_backlight_exit(void); + +void drm_backlight_connector_init(struct drm_connector *connector); +void drm_backlight_connector_cleanup(struct drm_connector *connector); +void drm_backlight_unregister(struct drm_connector *connector); + +int drm_backlight_create_property(struct drm_connector *connector, + unsigned int max, bool can_disable); +int drm_backlight_link(struct drm_connector *connector, + struct backlight_device *bd); +struct backlight_device *drm_backlight_get_device(struct drm_connector *connector); + +void drm_backlight_inhibit_legacy(struct drm_connector *connector); +void drm_backlight_uninhibit_legacy(struct drm_connector *connector); +void drm_backlight_inhibit_legacy_all(struct drm_device *dev); +void drm_backlight_uninhibit_legacy_all(struct drm_device *dev); + +void drm_backlight_set_luminance(struct drm_connector *connector, + unsigned int value); + +#else /* CONFIG_DRM_BACKLIGHT */ + +static inline int drm_backlight_init(void) { return 0; } +static inline void drm_backlight_exit(void) {} + +static inline void drm_backlight_connector_init(struct drm_connector *connector) {} +static inline void drm_backlight_connector_cleanup(struct drm_connector *connector) {} +static inline void drm_backlight_unregister(struct drm_connector *connector) {} + +static inline int drm_backlight_create_property(struct drm_connector *connector, + unsigned int max, bool can_disable) +{ + return 0; +} + +static inline int drm_backlight_link(struct drm_connector *connector, + struct backlight_device *bd) +{ + return 0; +} + +static inline struct backlight_device * +drm_backlight_get_device(struct drm_connector *connector) +{ + return NULL; +} + +static inline void drm_backlight_inhibit_legacy(struct drm_connector *connector) {} +static inline void drm_backlight_uninhibit_legacy(struct drm_connector *connector) {} +static inline void drm_backlight_inhibit_legacy_all(struct drm_device *dev) {} +static inline void drm_backlight_uninhibit_legacy_all(struct drm_device *dev) {} + +static inline void drm_backlight_set_luminance(struct drm_connector *connector, + unsigned int value) {} + +#endif /* CONFIG_DRM_BACKLIGHT */ + +#endif /* __DRM_BACKLIGHT_H__ */ diff --git a/include/drm/drm_connector.h b/include/drm/drm_connector.h index a0cf0268de483..0535f20e7dba5 100644 --- a/include/drm/drm_connector.h +++ b/include/drm/drm_connector.h @@ -28,6 +28,7 @@ #include <linux/ctype.h> #include <linux/hdmi.h> #include <linux/notifier.h> +#include <drm/drm_backlight.h> #include <drm/drm_mode_object.h> #include <drm/drm_util.h> #include <drm/drm_property.h> @@ -1290,6 +1291,11 @@ struct drm_connector_state { * @drm_atomic_helper_connector_hdmi_check(). */ struct drm_connector_hdmi_state hdmi; + + /** + * @luminance: Luminance for the connector + */ + unsigned int luminance; }; struct drm_connector_hdmi_audio_funcs { @@ -2526,6 +2532,20 @@ struct drm_connector { * @cec: CEC-related data. */ struct drm_connector_cec cec; + + /** + * @backlight: DRM backlight state, embedded and initialized by the DRM + * core. Becomes active once a driver links a backend with + * drm_backlight_link(). + */ + struct drm_backlight backlight; + + /** + * @luminance_property: Per-connector range property controlling the + * connector's backlight luminance. Created with the backend's range + * when a backlight is linked; NULL while no backlight is linked. + */ + struct drm_property *luminance_property; }; #define obj_to_connector(x) container_of(x, struct drm_connector, base) diff --git a/include/drm/drm_mode_config.h b/include/drm/drm_mode_config.h index d8f5b7e9673ee..4ab45bd4edf7e 100644 --- a/include/drm/drm_mode_config.h +++ b/include/drm/drm_mode_config.h @@ -390,6 +390,17 @@ struct drm_mode_config { */ struct drm_modeset_acquire_ctx *acquire_ctx; + /** + * @luminance_clients: + * + * Number of open DRM clients that have enabled + * &DRM_CLIENT_CAP_LUMINANCE and thereby inhibited legacy sysfs + * backlight control device-wide. Connectors linked while this is + * non-zero inherit the inhibit so hotplugged outputs stay consistent. + * Managed and serialized by the DRM backlight helpers. + */ + unsigned int luminance_clients; + /** * @idr_mutex: * -- 2.43.0
