Skip to content

PowerDisplay: Always write the saved value when restoring monitor settings - #49577

Merged
moooyo merged 1 commit into
mainfrom
yuleng/powerdisplay/restore-gate/1
Jul 30, 2026
Merged

PowerDisplay: Always write the saved value when restoring monitor settings#49577
moooyo merged 1 commit into
mainfrom
yuleng/powerdisplay/restore-gate/1

Conversation

@moooyo

@moooyo moooyo commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary of the Pull Request

TryRestore skipped writing a saved monitor value when it already equalled the value MonitorViewModel was showing. That displayed value is only an observation when the discovery-time VCP read succeeded. When the read failed it is a placeholder:

setting value when the read failed source
brightness 50 MonitorDiscoveryHelper stamps it — "Initial placeholder; overwritten if the VCP read succeeds"
contrast 50 Monitor backing-field default
volume 50 Monitor backing-field default
color temperature 0x05 (6500K) Monitor backing-field default

A saved value that happened to equal one of those silently suppressed the restore, and the monitor kept whatever it powered on with. 50 is the mid-slider value and 0x05 is the most common preset, so the coincidence is not rare.

This drops the comparison: a restore now always writes.

PR Checklist

  • Closes: #xxx — no issue; found while splitting up PowerDisplay: Fall back to persisted VCP values when a monitor read fails #49445
  • Communication: I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected
  • Tests: Added/updated and all pass — none added; TryRestore is a private helper in the PowerDisplay app project, which has no test project
  • Localization: All end-user-facing strings can be localized — this PR adds none
  • Dev docs: Added/updated
  • New binaries: Added on the required places — none added; no new project, so no signing JSON, installer WXS or CI YML change is required
  • Documentation updated

Detailed Description of the Pull Request / Additional comments

Why remove the check rather than refine it

The skip-if-equal check dates from PowerDisplay's first commit (#42642, where it read // Restore brightness if different from current); #47051 only refactored it into the shared TryRestore helper. It is day-one "obviously we shouldn't write twice" code, not a response to a reported problem.

Removing it is correct by construction: with no skip branch there is no state in which a restore silently does nothing. Any narrower fix has to decide when the displayed value can be trusted, and gets that decision wrong in exactly the cases that are hardest to reproduce.

Cost

Two, both bounded:

  • A redundant VCP write when the monitor already sits at the saved value. Some panels surface a write on their OSD. Both paths that reach here are user-initiated: startup restore only runs when RestoreSettingsOnStartup is enabled, and a profile apply happens because the user invoked that profile.
  • Time. At most four writes per monitor, serialised on that monitor's I2C bus (~100 ms each). Monitors still run in parallel through the existing Task.WhenAll.

The isVisible guard is untouched, so a monitor still never receives a write for a feature it does not expose — an unsupported VCP 0x14 is not written just because a profile carries a color temperature. Input source and power state are not restored here at all.

If the redundant write turns out to matter

The narrower fix is to keep the comparison and add one clause: also write when (monitor.ReadValues & flag) != flag, i.e. when the compared value was never read off the hardware. MonitorReadFlags already carries exactly that information, and Monitor.ReadValues is already maintained by the discovery-time Initialize* methods, so it is a small change on top of this one. I went with the simpler version first — happy to switch if a maintainer would rather keep the optimisation.

Validation Steps Performed

  • built PowerDisplay and PowerDisplay.Lib.UnitTests for x64 Debug with VS MSBuild — 0 errors, 0 warnings
  • ran PowerDisplay.Lib.UnitTests.dll with vstest.console.exe: 186 passed, 0 failed — unchanged from main; this PR touches only the app project and adds no tests
  • no hardware validation performed: the placeholder path this PR fixes is reachable only on a monitor whose VCP read fails during discovery

@moooyo
moooyo force-pushed the yuleng/powerdisplay/restore-gate/1 branch from 59bea3b to 9bed3fa Compare July 30, 2026 05:56
@moooyo moooyo changed the title PowerDisplay: Don't suppress a monitor restore on an unread placeholder match PowerDisplay: Always write the saved value when restoring monitor settings Jul 30, 2026
@moooyo
moooyo marked this pull request as ready for review July 30, 2026 05:58
moooyo pushed a commit that referenced this pull request Jul 30, 2026
…ts own PR

MonitorRestorePlanner, its tests and MonitorViewModel.ShouldRestoreValue are removed,
and TryRestore is aligned with the shape #49577 lands on main: an unconditional write,
no comparison at all.

#49577 fixes the same bug — a saved value silently dropped because it happened to match
a discovery placeholder the hardware never answered for — by deleting the skip-if-equal
check outright rather than by teaching it which values are observations. Keeping this
branch's version would re-introduce the more elaborate one on top of it.

Only the known-good retention block remains in MainViewModel.Settings.cs.
@moooyo
moooyo merged commit af5665e into main Jul 30, 2026
9 checks passed
@moooyo
moooyo deleted the yuleng/powerdisplay/restore-gate/1 branch July 30, 2026 07:09
moooyo added a commit that referenced this pull request Aug 3, 2026
…ails (#49445)

## Summary of the Pull Request

On a monitor whose DDC/CI engine answers intermittently, every discovery
pass starts from nothing. A panel that reported its brightness a minute
ago can lose that control — or drop out of the flyout entirely — because
one pass happened to fail.

This persists every range-valid VCP value read off a monitor, keyed by
its canonical DevicePath. In Maximum compatibility mode a later
discovery falls back to that value when the hardware will not answer.

Scope is intermittent failure, not permanent failure: the cache can only
replay a value the hardware answered at least once, so a panel that
never reads a code successfully sees no change. This partially addresses
#49342.

## PR Checklist

- [x] Closes: #49342
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Tests:** Added/updated and all pass
- [x] **Localization:** All end-user-facing strings can be localized —
this PR adds none
- [ ] **Dev docs:** Added/updated
- [x] **New binaries:** Added on the required places — none added, so no
signing JSON, installer WXS or CI YML change is required
- [ ] **Documentation updated**

## Detailed Description of the Pull Request / Additional comments

### What is stored

`MonitorStateManager` implements `IKnownGoodVcpStore`, so the cache
rides in the existing `monitor_state.json` next to the user's saved
brightness rather than in a new file. Each entry is a
`KnownGoodVcpFeature`: code, current, maximum, and when it was last
read.

Only range-valid observations are stored, so the common `current=0 /
max=0` garbage reply never enters — it fails `VcpFeatureValue.IsValid`.

Writes are not gated on Maximum compatibility mode, only reads are. A
monitor that reads cleanly today can start failing after a cable or dock
change, and a lazily populated cache would be empty on exactly the first
pass that needs it.

### How a cached value is used

`VcpDiscoveryEvidence.Reconcile` gains the cache as a third source
alongside the parsed capabilities string and this pass's probe:

| this pass | cache | result |
| --- | --- | --- |
| read succeeded | — | live value wins, cache refreshed |
| replied, range unusable | hit | cached value applied,
`MonitorReadFlags` left clear |
| no reply | hit | cached value applied, `MonitorReadFlags` left clear |
| code never probed (caps parsed) | hit | value applied only after one
live read is attempted |

The last row matters: on the caps-parsed path nothing has confirmed the
cached value this pass, so the hardware is asked first. On the probe
path it has already been asked, and re-reading would be pure I2C noise.

`MonitorReadFlags` stays clear for anything the hardware did not answer,
so a cached value never masquerades as an observation — which #49577
depends on, since it made the restore path write whenever the flag is
unset. One consequence is worth naming: the flyout draws a slider at the
cached position while `powerdisplay get` reports that setting as
unknown, because `MonitorDtoProjector` gates on `supported && read`.

### Keeping the cache current

`RefreshKnownGoodAfterWrite` restamps an entry after a successful
`SetVCPFeature`, so a slider move cannot leave the cache holding the
pre-write value. It refreshes only an entry a real read established, and
only when the value was scaled against the maximum that entry holds — a
monitor whose discovery read failed still carries a placeholder max, and
writing that back would mis-scale every later write.

`RemoveKnownGoodFeatures` clears the cache for monitors a settings
reconciliation observably dropped, leaving the user's saved values
alone. Cleanup is driven by an observed drop, never by absence from the
rebuilt list: a missing or corrupt `settings.json` yields a defaults
object indistinguishable from a real one, and pruning by absence would
wipe every monitor not connected at that instant.

A re-observation that changes nothing refreshes the in-memory timestamp
but does not mark the file dirty, so a discovery pass no longer rewrites
`monitor_state.json` for a moved timestamp alone.

## Validation Steps Performed

- `PowerDisplay.Lib`, `PowerDisplay.Lib.UnitTests` and `PowerDisplay`
built for x64 Debug with VS MSBuild — 0 errors, 0 warnings;
`PowerDisplay.Lib.UnitTests.dll` under `vstest.console.exe`: **301
passed, 0 failed**
- **Affected-hardware validation on the AOC Q27G3XMN is still pending.**
That monitor, or an equivalent controllable DDC/CI setup, was not
available locally. The paths this PR changes are reachable only on
hardware whose capabilities string is unusable or whose VCP reads fail
intermittently, so this is the main outstanding risk.

---------

Co-authored-by: Yu Leng <yuleng@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot-Session: 6ea38c04-6f68-4c42-91d9-8a03b49bdd81
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants