mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-08-29 10:09:43 +02:00
Spec: Power Display ambient-light (ALS) adaptive brightness for external monitors (#49199)
## Summary Design spec for **ambient-light (ALS) adaptive brightness for external monitors** in Power Display, as discussed and agreed in #49038. The feature continuously maps the device''s ambient light sensor reading (the same ALS **Light Switch** already reads) to each external monitor''s brightness over **DDC/CI** (the path **Power Display** already uses) — i.e. auto-brightness for external monitors, the continuous/sensor-driven counterpart to the schedule-based #47480. This PR adds only the spec doc (`doc/specs/power-display-adaptive-brightness.md`), per the repo''s spec-first process (`doc/specs/readme.md`). No code. ## What the spec covers - **Core model** — a single per-monitor formula: `target = clamp(curve(lux) + offset, min, max)`. - **Per-monitor calibration curve** — the mechanism that keeps differently-behaving panels visually matched across the whole ambient range. - **Per-monitor offset** — live, phone-style personalization; manual/CLI nudges become an offset (default) with an optional `pause` behaviour. - **Live slider UX** — when adaptive is on, the slider reflects the computed value and dragging it sets the offset. - **Sensor trust & lifecycle** — lid-closed / clamshell detection (`GUID_LIDSWITCH_STATE_CHANGE` + `QueryDisplayConfig`), hold-last-good instead of dimming to black, and honest fallbacks. - Smoothing/hysteresis/rate-limiting for flicker- and wear-safe DDC/CI writes, settings persistence, telemetry, a11y/localization, risks, and phased delivery. ## Discussion / sign-off Behaviour was reviewed with @moooyo on #49038 (configurability, CLI/manual +/- while adaptive, slider behaviour) — agreed to proceed. Closes nothing yet; tracks #49038. Related: #47480, #42566, #35564. --------- Co-authored-by: Rishabh Jain <14334305+MrRishabhJain@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
212
doc/specs/power-display-adaptive-brightness.md
Normal file
212
doc/specs/power-display-adaptive-brightness.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# Power Display – Ambient-Light (ALS) Adaptive Brightness for External Monitors
|
||||
|
||||
Tracking issue: microsoft/PowerToys#49038
|
||||
Behaviour discussion: #49038 (with @moooyo)
|
||||
Related: #47480 (schedule-based auto-brightness), #42566 (Light Switch ALS → theme), #35564 (manual multi-monitor brightness), #1052 (Quick Display adjustment umbrella, completed)
|
||||
|
||||
## 1. Overview
|
||||
|
||||
Windows can auto-adjust the **built-in laptop panel** from the device's ambient light sensor (ALS), but
|
||||
**external monitors have no such capability** — they almost never carry an ALS, and Windows will not pipe the
|
||||
laptop's ALS reading to an external display. Today's PowerToys covers the two halves separately:
|
||||
|
||||
* **Light Switch** already reads the device ALS (`Windows.Devices.Sensors.LightSensor` → `IlluminanceInLux`) to drive theme.
|
||||
* **Power Display** already writes external-monitor brightness over **DDC/CI** (`SetMonitorBrightness`, VCP `0x10`).
|
||||
|
||||
This feature wires them together: a per-monitor **Adaptive (ambient light)** mode in Power Display that continuously
|
||||
maps the device's ambient lux to each external monitor's brightness. It is the **continuous, sensor-driven** counterpart
|
||||
to the **time/schedule-based** request tracked in #47480 — and it is what most users actually mean by "auto-brightness".
|
||||
|
||||
## 2. Goals / Non-goals
|
||||
|
||||
### Goals
|
||||
* Per-external-monitor opt-in adaptive brightness driven by the device ALS.
|
||||
* A per-monitor **calibration curve** (lux → %), so differently-behaving panels can be matched.
|
||||
* A per-monitor **offset** for manual/CLI nudges that rides on top of the curve (iOS-style personalization).
|
||||
* Correct, safe behaviour when the sensor can't be trusted (e.g. laptop lid closed).
|
||||
* Smooth, flicker-free, DDC/CI-friendly updates (smoothing + hysteresis + rate limiting).
|
||||
* Coexist cleanly with manual brightness changes and the existing schedule/profile features.
|
||||
|
||||
### Non-goals
|
||||
* Driving the built-in laptop panel (Windows already does this).
|
||||
* Bundling external/USB ambient sensors (possible future extension).
|
||||
* Content-adaptive / gamma-overlay dimming (a different, software-only approach).
|
||||
* Colour-temperature / Night-Light behaviour.
|
||||
|
||||
## 3. Scenarios
|
||||
|
||||
1. A laptop is docked to one or two external monitors. As the room darkens through the evening, the externals dim
|
||||
automatically to track the room — today only the built-in panel does this.
|
||||
2. A light-sensitive user wants externals to follow ambient light continuously, without manual sliders or rigid schedules.
|
||||
3. Two different external panels are calibrated so they *look* equally bright across the whole ambient range.
|
||||
4. Clamshell (lid closed): the sensor is blocked, so brightness holds steady instead of dimming to black.
|
||||
5. A non-DDC/CI monitor: adaptive is greyed out for that monitor with a clear "not supported" reason.
|
||||
|
||||
## 4. Fit with existing modules
|
||||
|
||||
```
|
||||
[ Device ALS ]
|
||||
LightSensor.ReadingChanged (lux) <-- already consumed by Light Switch (#42566)
|
||||
│
|
||||
▼
|
||||
[ Adaptive Brightness engine ] (new glue, lives in Power Display)
|
||||
smoothing (EMA) → hysteresis/deadband → curve(lux) + offset (per monitor) → rate limit
|
||||
│
|
||||
▼
|
||||
[ Power Display DDC/CI writer ] <-- already exists (SetMonitorBrightness, VCP 0x10)
|
||||
per external monitor
|
||||
```
|
||||
|
||||
No new module. The ALS reader can be refactored into a small shared helper so both Light Switch and Power Display
|
||||
consume one sensor source (avoids two sensor subscriptions). On #47480 a maintainer noted Light Switch can already
|
||||
link to a Power Display *profile* on a scheduler; this design adds an *ambient-light* trigger alongside the existing
|
||||
*time/sun* trigger and addresses the "apply a Power Display profile without forcing a theme change" limitation raised there.
|
||||
|
||||
## 5. Core model — adaptive curve + per-monitor offset
|
||||
|
||||
The whole feature reduces to a single formula, applied **independently per external monitor**:
|
||||
|
||||
```
|
||||
target%(lux) = clamp( curve(lux) + offset, minPercent, maxPercent )
|
||||
```
|
||||
|
||||
### 5.1 The loop
|
||||
Continuously: read device ALS (lux) → smooth → map through the monitor's **curve** → add the monitor's **offset** →
|
||||
clamp → write via DDC/CI. One shared sensor subscription drives an independent engine per external monitor.
|
||||
|
||||
### 5.2 Per-monitor calibration curve — the multi-monitor sync mechanism
|
||||
Each monitor has its own editable `lux → %` curve. This is what keeps multiple monitors *looking* equally bright:
|
||||
panels differ in nits and have **non-linear** backlight response, so the same ambient lux should map to a *different*
|
||||
percentage on different panels. A per-monitor curve corrects this across the **entire** ambient range — something a
|
||||
single scalar value cannot do (a uniform shift would keep them matched at one light level but drift apart at others).
|
||||
|
||||
### 5.3 Per-monitor offset — live personalization
|
||||
A signed per-monitor `offset%` added on top of the curve. Manual/CLI nudges adjust it, so the system learns the
|
||||
user's preference (iOS-style) instead of overriding them. The **curve does calibration**; the **offset does live,
|
||||
personal fine-tuning** (and is the quick way to nudge one screen to match the others without editing its curve).
|
||||
|
||||
### 5.4 Manual / CLI interaction while adaptive is ON
|
||||
When the user changes brightness (slider or CLI increase/decrease) with adaptive on, per-monitor `manualBehavior`:
|
||||
* **`offset` (default):** the delta becomes a persistent offset; adaptation keeps tracking ambient, just shifted.
|
||||
A CLI "brightness +10" makes the monitor 10% brighter *than the curve would pick*, and tracking continues.
|
||||
* **`pause`:** adaptation suspends for that monitor (hard manual override) until the user re-enables it.
|
||||
|
||||
## 6. UX / Settings design
|
||||
|
||||
Per-monitor (WinUI3 `SettingsCard`/`SettingsExpander`):
|
||||
|
||||
* **Brightness source:** `Manual` (default) | `Adaptive (ambient light)`. Adaptive is disabled with a reason string
|
||||
when (a) no ALS is present, or (b) the monitor has no DDC/CI brightness control.
|
||||
* **Default (zero-config):** on/off + a single **Responsiveness** slider (Calm ↔ Snappy). A sensible default curve
|
||||
ships out of the box so most users touch nothing else.
|
||||
* **Advanced (collapsible):** editable **calibration curve** (anchor points, e.g. `0 lx→20%, 50→40%, 300→70%, 1000→100%`
|
||||
with a "Reset"), **min/max** clamps, and the `manualBehavior` choice.
|
||||
* **Live slider:** when adaptive is on, the brightness slider becomes **live** — it reflects the current computed
|
||||
target and moves on its own as ambient changes, with an **"Auto"** badge and a readout (`Ambient 240 lx → 65%`).
|
||||
Dragging it applies the **offset** (§5.3); it does not silently disable adaptive.
|
||||
|
||||
Global: a module-level toggle and an enable/disable hotkey (consistent with other Power Display shortcuts, e.g. #48784).
|
||||
|
||||
## 7. Sensor trust & lifecycle
|
||||
|
||||
The ALS reading is used **only when it can be trusted**.
|
||||
|
||||
### 7.1 ALS / DDC-CI availability
|
||||
`LightSensor.GetDefault()` null → adaptive unavailable everywhere (e.g. a desktop). Per monitor, if DDC/CI brightness
|
||||
is unsupported → adaptive is disabled for that monitor with a reason.
|
||||
|
||||
### 7.2 Lid closed / clamshell (sensor blocked)
|
||||
When the lid is closed the ALS faces the keyboard deck and reads ~darkness, so we must **not** drive brightness from it.
|
||||
* **Detect:** lid state via `RegisterPowerSettingNotification(GUID_LIDSWITCH_STATE_CHANGE)` (0 = closed, 1 = open),
|
||||
corroborated by `QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS)` for an active internal panel
|
||||
(`DISPLAYCONFIG_OUTPUT_TECHNOLOGY_INTERNAL`). Lid *position* is the true determinant — a user may have the lid open
|
||||
but the internal display disabled, and there the sensor still sees the room.
|
||||
* **Behaviour:** on lid-close → pause adaptation and **hold the last known-good brightness** (never dim-to-black),
|
||||
surface a status ("Adaptive paused — lid closed / ambient sensor blocked"), and **resume live on reopen**.
|
||||
|
||||
### 7.3 Honest boundary + fallbacks
|
||||
Clamshell-docked is a *primary* scenario for this feature, yet it's exactly where the laptop ALS can't see the room.
|
||||
Therefore:
|
||||
* **v1:** full ALS adaptive when the lid is open; hold/pause when closed.
|
||||
* **Optional fallback:** when the ALS is untrusted, fall back to **time/schedule-based** brightness (the #47480
|
||||
mechanism) so clamshell users still get some automation.
|
||||
* **Future:** an **external ambient sensor** (monitor-integrated or USB) for true clamshell tracking (deferred).
|
||||
|
||||
## 8. Technical design
|
||||
|
||||
### 8.1 ALS acquisition
|
||||
* `LightSensor.GetDefault()`; subscribe to `ReadingChanged`; set `ReportInterval = max(minimumReportInterval, ~500ms)`
|
||||
to limit churn/battery. Value: `LightSensorReading.IlluminanceInLux` (double).
|
||||
|
||||
### 8.2 Smoothing + hysteresis (anti-flicker, anti-wear)
|
||||
* **EMA** on lux: `s_t = α·lux + (1−α)·s_{t−1}` (α from the Responsiveness slider).
|
||||
* **Deadband:** ignore changes whose mapped target moves < ~2–3% from the last applied value.
|
||||
* **Min update interval** per monitor (~1–2 s) and optional **ramping** (step toward target).
|
||||
* Rationale: DDC/CI writes are slow (tens–hundreds of ms) and excessive writes can flicker or wear some panels.
|
||||
|
||||
### 8.3 Mapping
|
||||
* Piecewise-linear interpolation over the anchor points; add `offset`; clamp to per-monitor min/max.
|
||||
* Curve + offset stored per monitor (keyed by stable monitor id, as Power Display already does for profiles).
|
||||
|
||||
### 8.4 Brightness write
|
||||
* Reuse Power Display's DDC/CI path (`GetPhysicalMonitorsFromHMONITOR` → `SetMonitorBrightness`, VCP `0x10`).
|
||||
* Capability probe per monitor (`GetMonitorBrightness` / VCP support); if unsupported, disable adaptive for it.
|
||||
|
||||
### 8.5 Concurrency
|
||||
* One engine per external monitor, one shared ALS subscription. Serialize DDC/CI writes per monitor on a worker;
|
||||
**never block the sensor callback**. Handle monitor hotplug / dock connect-disconnect (Power Display already tracks add/remove).
|
||||
|
||||
## 9. Settings persistence
|
||||
|
||||
Extend Power Display's `settings.json` per-monitor entry, e.g.:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"monitors": [{
|
||||
"id": "<stable-monitor-id>",
|
||||
"brightnessSource": "adaptive", // "manual" | "adaptive"
|
||||
"adaptive": {
|
||||
"curve": [[0,20],[50,40],[300,70],[1000,100]], // [lux, percent] calibration, per monitor
|
||||
"offsetPercent": 0, // signed live nudge added on top of the curve
|
||||
"minPercent": 15,
|
||||
"maxPercent": 100,
|
||||
"responsiveness": 0.4, // 0..1 → EMA α / update cadence
|
||||
"manualBehavior": "offset", // "offset" (default) | "pause"
|
||||
"deadbandPercent": 3,
|
||||
"minUpdateMs": 1500
|
||||
}
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
`target = clamp( interpolate(curve, lux) + offsetPercent, minPercent, maxPercent )`.
|
||||
|
||||
## 10. Telemetry
|
||||
|
||||
Per `doc/devdocs/development/logging.md`, emit (privacy-respecting, counts only):
|
||||
* Adaptive mode enabled/disabled per monitor; ALS-present; DDC/CI-supported; `manualBehavior` chosen.
|
||||
* Apply-rate (writes/min), deadband/clamp suppression counts, and lid-close pause events (to validate anti-wear tuning).
|
||||
* No raw lux series or PII.
|
||||
|
||||
## 11. Performance & reliability
|
||||
* Sensor at ~0.5–2 s cadence; writes throttled per §8.2 → negligible CPU and few DDC/CI writes.
|
||||
* Worker isolates slow DDC/CI calls from the UI/sensor thread.
|
||||
* Graceful degradation: no ALS → mode hidden/disabled; no DDC/CI → mode disabled per monitor with reason; lid closed → hold last-good.
|
||||
|
||||
## 12. Accessibility / Localization
|
||||
* All new strings via `.resw` + `x:Uid` (and discoverable by the settings search index, settings-search.md).
|
||||
* Curve editor keyboard-navigable; the live slider value, "Auto" badge, and ambient readout exposed to narrator.
|
||||
|
||||
## 13. Risks & open questions
|
||||
* **Manual-vs-adaptive arbitration** — resolved: default **`offset`** (iOS-style), with **`pause`** opt-in (direction agreed in #49038).
|
||||
* **ALS placement** — chassis lux ≠ panel-facing lux; mitigated by the per-monitor curve; documented as a limitation.
|
||||
* **Clamshell** — laptop ALS is blind when the lid is closed; handled via hold/pause + optional time fallback; external sensor is future work.
|
||||
* **DDC/CI variance** across docks/USB-C/adapters (cf. #47577) — robust capability probing + clear unsupported messaging.
|
||||
* **Shared ALS source** — refactor Light Switch's reader into a shared helper (preferred) vs. two subscriptions.
|
||||
* **Open (tune during implementation):** exact default curve values and the Responsiveness → (α, min-update-interval) mapping.
|
||||
|
||||
## 14. Phased delivery
|
||||
1. Engine + per-monitor `adaptive` (curve + offset) + default curve + DDC/CI write reuse + sensor-trust gating (JSON-config; minimal UI).
|
||||
2. Settings UX: source dropdown, live slider + "Auto" badge/readout, curve editor, responsiveness, clamps, `manualBehavior`.
|
||||
3. Shared ALS helper refactor (Light Switch + Power Display); precedence with schedule/profile; optional time-based fallback.
|
||||
4. Telemetry + docs (`doc/devdocs/modules/powerdisplay/…`) + tests.
|
||||
Reference in New Issue
Block a user