Skip to content

PowerDisplay: Improve maximum compatibility mode reliability - #49445

Draft
moooyo wants to merge 29 commits into
mainfrom
yuleng/worktree/pd-ddc-probe-cache
Draft

PowerDisplay: Improve maximum compatibility mode reliability#49445
moooyo wants to merge 29 commits into
mainfrom
yuleng/worktree/pd-ddc-probe-cache

Conversation

@moooyo

@moooyo moooyo commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary of the Pull Request

On a monitor whose DDC/CI engine answers intermittently, Maximum compatibility mode loses controls it had a moment earlier — or drops the display from the flyout entirely. Three causes: a VCP code is re-read immediately after being probed, the first transient I2C failure is treated as final, and nothing is remembered between discoveries.

This makes the discovery path tolerate an unreliable panel: reuse the probe's own value, retry transient failures, and persist positive observations so a later failing pass can still show the control.

PR Checklist

Two boxes are deliberately left unchecked: the approach has not been agreed with a core contributor (there is an open design question below that needs a maintainer decision), and no dev docs were written.

Detailed Description of the Pull Request / Additional comments

What changed

  • reuse a successful probe value instead of immediately reading the same VCP code again
  • retry transient DDC/CI framing and I2C failures, bounded and paced
  • persist positive, range-valid VCP observations by canonical DevicePath, and reuse them only in Maximum compatibility mode
  • refresh a cached observation after a successful write, so a slider move cannot leave the cache holding the pre-write value and republish it on the next discovery
  • keep cached values non-live: MonitorReadFlags stays clear for anything the hardware did not answer this pass
  • release physical-monitor handles on every discovery path that abandons a monitor
  • name the DDC/CI error codes after winerror.h, and pin in tests both which of them are transient and what each numeric value is

Discovery is restructured around a VcpDiscoveryEvidence value that reconciles three sources — the parsed capabilities string, this pass's live probe, and the persisted known-good cache — so "which VCP codes does this monitor support" is decided in one place. DdcCiController's per-feature initialization moves out into ContinuousVcpInitializer and DiscreteVcpInitializer, and every native VCP read now goes through a single injectable IVcpFeatureReader seam.

Behaviour changes outside Maximum compatibility mode

Both land on the shared discovery/restore path and affect users who never enable the feature.

  • A handle-class error during continuous VCP initialization now discards the monitor. main logged the failure, left the read flag unset and kept the monitor, so its handle reached PhysicalMonitorHandleManager. ContinuousVcpInitializer now returns PhysicalMonitorUnavailable and BuildMonitorFromPhysical drops the display. This is deliberate: Monitor.Handle is captured once per discovery pass and never refreshed, so keeping the monitor would send every later read and write to a handle already known to be dead, and DisplayChangeWatcher schedules the rediscovery that repairs it. The cost is that a handle dying without a device-arrival/removal or console-display-state notification leaves the monitor out of the flyout until the next such event.
  • The restore gate now writes a value that was never read. TryRestore compared only saved != displayed; it now routes through MonitorRestorePlanner.ShouldWrite, which also writes when (monitor.ReadValues & flag) != flag. An unread value is a discovery placeholder or a backing-field default rather than an observation, so suppressing the restore on a coincidental match silently dropped it — this is a fix. The visible cost is VCP 0x14: on a panel that NAKs 0x14 reads but accepts writes, every profile apply and every startup restore now re-applies the color preset, which some panels surface on their OSD.

Open design question — cache invalidation

This needs a maintainer decision; it is deliberately not resolved here.

Cached observations have no invalidation path: a definitive DDCCI_VCP_NOT_SUPPORTED reply does not outrank a cached positive. That is intentional and test-pinned (Reconcile_VcpNotSupportedStillUsesCachedPositiveEvidence), with the reasoning recorded at VcpDiscoveryEvidence.Reconcile.

A 30-day freshness bound was implemented and reverted in ebe397f. It assumed every successful read restamps LastSuccessfulUtc, but on the one path where the cache is load-bearing no read happens at all: once the probe has touched a code, PreferLiveRead is false and ContinuousVcpInitializer applies the cached value without reading or upserting. Thirty days of transient probe failures expired the entry, the reconciled capabilities went null, and the monitor was dropped entirely rather than losing one slider — an absorbing failure, since only a successful probe could rebuild what had been failing.

That reasoning is no longer complete. RefreshKnownGoodAfterWrite, added later in 61d75fc, restamps LastSuccessfulUtc on a successful SetVCPFeature — including on exactly that path, because ContinuousVcpInitializer sets BrightnessVcpMax from the cached entry, so the maximum == existing.Maximum guard passes. Moving a slider therefore advances the clock with no read having succeeded. The bound is still not worth reinstating — a monitor nobody touches for 30 days still expires — but a successful write is a discriminator that was not available when ebe397f was written: a genuine phantom's SetVCPFeature is rejected by the panel and never restamps.

The remaining option is to let a definitive DDCCI_VCP_NOT_SUPPORTED outrank the cache. The trade-off is asymmetric:

cost
Keep the cache permanent (current) A panel that answers an unimplemented code with a plausible non-zero range leaves a control that writes into the void. The window is narrow: the common current=0 / max=0 garbage reply fails VcpFeatureValue.IsValid and is never cached.
Evict on NOT_SUPPORTED One unretried I2C reply can delete the persisted evidence that keeps a monitor visible. NOT_SUPPORTED is deliberately outside the transient set, so it gets a single attempt — while the capabilities string gets three.

If eviction is wanted, it has to run after Reconcile, not before: dropping the entry inline leaves Reconcile with null capabilities, and BuildMonitorFromPhysical then discards the display outright, taking its rotation and discrete VCPs with it.

Known limitations

  • A monitor that slips a false positive into the cache keeps the phantom control until settings retention drops its entry, and that requires 30 days both undiscovered and unhidden — MonitorSettingsRebuilder re-emits every currently-discovered monitor with a fresh LastSeenUtc and keeps IsHidden entries unconditionally, so for a monitor in daily use the phantom is permanent in practice. There is no user-facing reset; a dedicated "reset detection cache" action was considered and deferred as out of scope.
  • ReleaseAbandonedPhysical ships without test coverage. Reaching its call sites means faking the whole native enumeration surface (EnumDisplayMonitors, GetMonitorInfo, GetPhysicalMonitorsFromHMONITOR), a larger seam than this PR should introduce; the abandon paths were verified by reading instead.
  • Several pre-existing physical-monitor handle leaks remain outside this PR's scope: the retry loop in GetPhysicalMonitorsWithRetryAsync discards a whole array of live handles, and cancellation unwinds before UpdateHandleMap runs. Both predate this branch and are better addressed separately.

Validation Steps Performed

  • built PowerDisplay.Lib.UnitTests and PowerDisplay for x64 Debug with VS MSBuild — 0 errors, 0 warnings
  • ran PowerDisplay.Lib.UnitTests.dll with vstest.console.exe: 275 passed, 0 failed
  • the new locking in MonitorStateManager was verified by deleting the lock (state) statements and confirming ConcurrentUpsertAndRead_OnSameMonitorDoNotTearTheFeatureMap then fails with InvalidOperationException: Collection was modified
  • the empty-Id guard in RefreshKnownGoodAfterWrite was verified the same way — removing it fails RefreshKnownGoodAfterWrite_EmptyMonitorIdIsIgnored
  • DdcErrorClassifierTests pins both the membership of the two error sets and the numeric value of every constant against winerror.h, so a typo cannot move production and tests together

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.

Yu Leng and others added 9 commits July 21, 2026 17:15
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6ea38c04-6f68-4c42-91d9-8a03b49bdd81
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6ea38c04-6f68-4c42-91d9-8a03b49bdd81
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6ea38c04-6f68-4c42-91d9-8a03b49bdd81
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6ea38c04-6f68-4c42-91d9-8a03b49bdd81
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6ea38c04-6f68-4c42-91d9-8a03b49bdd81
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6ea38c04-6f68-4c42-91d9-8a03b49bdd81
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6ea38c04-6f68-4c42-91d9-8a03b49bdd81
Offload direct probe reads, aggregate attempt diagnostics, and prefer live initialization before same-code cache fallback.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6ea38c04-6f68-4c42-91d9-8a03b49bdd81
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6ea38c04-6f68-4c42-91d9-8a03b49bdd81
@moooyo
moooyo marked this pull request as draft July 22, 2026 08:21
Yu Leng and others added 19 commits July 22, 2026 17:34
Preserve legacy monitor state during migration, require known live values before skipping restore writes, and reject invalid physical monitor handles across probe and initialization paths.
Delay state pruning until an ID is absent from consecutive settings snapshots, and retry the HRESULT form of invalid continuous VCP ranges.
Review of the maximum-compatibility discovery rework surfaced several defects
introduced by this branch. Each is fixed at its cause rather than at the symptom.

VcpDiscoveryEvidence.Reconcile used "the parsed caps advertise this code" as a
proxy for "a live read was already attempted for this code". Those coincide only
when the caps string is unusable, which is the only case where the probe runs. On
the caps-parsed path no probe runs, so a cached code the caps string omits was
applied to the monitor, advertised to the UI, and never read from hardware — and
its cache entry could never be refreshed. PreferLiveRead now derives from whether
the probe actually touched the code.

A probe reply carrying an unusable range no longer hides the feature: the device
answered, which proves support (unimplemented codes fail with
DDCCI_VCP_NOT_SUPPORTED), so the code stays reachable as it did before the
rework. The reply is recorded on the observation rather than inferred.

Cache evidence could overwrite VcpCodeInfo entries parsed from the capabilities
string, discarding discrete value lists and custom names. Marking support is now
add-if-absent through a single helper, so evidence can only widen capabilities.

Known-good observations had no invalidation: a monitor that loses DDC/CI support
for a code while keeping its DevicePath would keep advertising it indefinitely.
LastSuccessfulUtc was persisted but never read; it now bounds how long an
observation may stand in for a live read. Successful reads restamp it, so a
feature that still works never expires.

Monitor-state pruning deleted entries by absence from the rebuilt settings list.
A missing or corrupt settings.json makes GetSettingsOrDefault persist and return
an empty monitor list that is indistinguishable from a real one, so every
disconnected monitor's saved brightness, contrast, volume and known-good cache
was deleted. Pruning is now driven by the entries the rebuild observably dropped,
so an unreadable snapshot drops nothing.

The discrete VCP stage no longer discards a monitor. Handle liveness is already
decided by the probe and the continuous stage; repeating the decision after the
continuous values were applied meant one bad 0xD6 read removed a working monitor
from the flyout, in normal mode as well as maximum compatibility mode.

The restore gate compared only the monitor snapshot, so a profile apply issued
while a debounced slider commit was pending was skipped and then overwritten by
that commit. It now also requires the optimistic UI value to agree.

A throwing native read is contained to its own VCP code instead of unwinding into
the pipeline-wide catch that drops every monitor sharing the hMonitor.

Also fixes a mis-indented XML doc block in MonitorStateRetentionPlanner.

Validation: built PowerDisplay.Lib.UnitTests and PowerDisplay for x64 Debug with
VS MSBuild; ran PowerDisplay.Lib.UnitTests.dll with vstest.console.exe — 268
passed, 0 failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… the cache

Second-pass review of the previous fix commit found that the 30-day freshness
bound on known-good VCP observations was worse than the problem it addressed.

The bound assumed every successful read restamps LastSuccessfulUtc, but on the
one path where the cache is load-bearing no read happens at all: once the probe
has touched a code, PreferLiveRead is false and ContinuousVcpInitializer applies
the cached value and returns without reading or upserting. So the clock only
advances on a successful probe -- the very event whose absence makes the cache
necessary. After thirty days of transient probe failures the entry expired, the
reconciled capabilities went null, and DdcCiController dropped the monitor
entirely rather than merely hiding one slider. The failure was absorbing: only a
successful probe could rebuild the entry, and that was what had been failing.
Restamping when the cached value is applied would fix the regression but make the
bound unreachable, since an entry can then only expire while its monitor is
undiscovered, which settings retention already handles. The bound is removed.

That leaves cached observations without invalidation, which was the original
objection. Fixing it properly means letting a definitive DDCCI_VCP_NOT_SUPPORTED
reply outrank the cache, and that contradicts a deliberate, test-pinned decision
in this branch that positive cache evidence survives such a reply in maximum
compatibility mode. It is a design question for review, not a unilateral change,
and is called out in the PR description instead.

Monitor state collection is also narrowed. Removing a whole state entry deleted
the user's saved brightness, contrast, volume, color temperature and capabilities
for any monitor that aged out of settings -- values that survived indefinitely
before this branch introduced the cache. The known-good cache is discovery state
this feature owns, so only that sub-object is collected now; saved user values
are never removed. RemoveMonitorStates becomes RemoveKnownGoodFeatures.

Validation: built PowerDisplay.Lib.UnitTests and PowerDisplay for x64 Debug with
VS MSBuild; ran PowerDisplay.Lib.UnitTests.dll with vstest.console.exe -- 264
passed, 0 failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The branch introduces three tokens check-spelling does not know. DDCCI appears
as an all-caps run in the DDCCI_VCP_NOT_SUPPORTED comments; main only ever had
the PascalCase DdcCi, which tokenizes to ddc + ci, and ddc is already expected.
AOCB and XMN come from the AOC Q27G3XMN device paths and friendly name used by
the new test fixtures.

spelling2.yml does not set only_check_changed_files, so the Check Spelling job
fails on all three until they are expected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
IsTransient carried eight bare 0xC02625xx literals plus a bare 1460 with no
direct test, so neither its membership nor the reasoning behind the gaps in the
sequence was reviewable. Move it next to IsPhysicalMonitorUnavailable in
DdcErrorClassifier, give every code its winerror.h name, and record why each
neighbouring code stays out: VCP_NOT_SUPPORTED is the device's final answer,
I2C_NOT_SUPPORTED and I2C_DEVICE_DOES_NOT_EXIST are permanent bus-level facts,
MCA_INVALID_CAPABILITIES_STRING belongs to the capabilities path rather than to a
VCP read, the handle-class codes are owned by IsPhysicalMonitorUnavailable and
must abort rather than retry, and MONITOR_RETURNED_INVALID_TIMING_STATUS_BYTE is
raised only by the get-timing-report command, never by
GetVCPFeatureAndVCPFeatureReply.

CURRENT_CURRENT_VALUE_GREATER_THAN_MAXIMUM_VALUE stays in on purpose and now says
why: a device that genuinely reports current > maximum simply exhausts the
budget, but the same code also results from a corrupted reply, which a retry does
fix.

A data-driven test pins both sets and asserts they stay disjoint, since an
overlap would keep retrying against a handle already known to be gone.

No behaviour change: the membership is identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…l write

The cache was only ever written from a successful read, so a slider move left it
holding the pre-write value. Maximum compatibility mode then republished that
stale value on a later discovery whose probe touched the code but could not read
it: PreferLiveRead is false once the probe has touched a code, so
ContinuousVcpInitializer applies the cached value without re-reading. The flyout
does not gate on ReadValues, so the user saw the stale value presented as live,
and a one-notch slider move snapped the panel to it. This needed no restart --
UpdateMonitorList rebuilds every MonitorViewModel on any display-change refresh.

SetVcpFeatureAsync is the single funnel for all six codes and already holds the
device-native value, monitor.Id, the store and the clock, so the refresh goes
there. It only refreshes an entry a real read already established -- a successful
SetVCPFeature is not evidence that the device implements the code -- and only
when the write was scaled against the very maximum that entry holds. That guard
is load-bearing: BrightnessVcpMax and its siblings default to the placeholder
100 and Monitor is rebuilt on every discovery, so a monitor whose read failed
would otherwise overwrite a read-proven 0-50 range with the placeholder and
mis-scale every later write.

The internal constructor also gains the delayAsync seam VcpFeatureProbeService
already had, and FetchCapabilitiesWithFallbackAsync becomes internal, so the
compatibility-mode gate and the probe-observation persist loop are covered for
the first time -- InternalsVisibleTo and the injecting constructor existed but no
test used them. IntPtr.Zero is a safe handle to drive that with:
TryGetCapabilitiesString short-circuits on it without issuing a native call, so
the capabilities string is deterministically unusable and every remaining
decision comes from the injected reader, clock and store.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ocks

MarkDirtyAndScheduleSave had no _disposed guard, unlike SaveStateToDiskAsync and
Dispose. Dispose snapshots _isDirty, sets _disposed, disposes the debouncer and
flushes, so an upsert arriving after that point mutated the state and set
_isDirty again with no path left to clear it. The reachable window is the
synchronous BuildMonitorFromPhysical block, which performs no cancellation checks
and upserts after each successful blocking VCP read. Late observations are now
dropped on purpose: the next discovery pass re-derives them. MigrateLegacyKeys
stops duplicating the schedule inline and routes through the same helper.

The locks this branch added were exercised by nothing. The one concurrency test
upserts two different monitor Ids, which GetOrAdd maps to two distinct
MonitorState instances and therefore two distinct lock objects, so the suite
passed with every lock statement deleted. The contention they actually guard is
one monitor Id: the discovery thread upserting several VCP codes while the
debounced save enumerates that same KnownGoodVcpFeatures dictionary. The new test
drives exactly that from two threads and fails with InvalidOperationException out
of the ToDictionary inside GetKnownGoodFeatures once the locks are removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three statements did not match the code they describe.

Reconcile claimed the probe "spends its full paced retry budget on every code it
touches". ProbeCodeAsync breaks after a single attempt on a non-transient error
or a throwing read, which this branch's own ProbeAsync_NonTransientFailureDoes
NotRetry asserts. The guarantee that actually holds -- at least one transaction
per code the probe touched -- is what justifies not re-reading it.

DiscreteVcpInitializer claimed handle liveness is already settled before it runs.
A capabilities string that parses but advertises none of 0x10/0x12/0x62 leaves
nothing for the continuous stage to read and suppresses the probe, so the
discrete reads are the first on that handle. The stage is still safe, for a
different reason: failing here only leaves the read flag unset.

MonitorRestorePlanner described an unflagged value as merely cached. It is just
as often the never-read backing-field default, and naming those defaults -- 0
brightness, 50 contrast, 50 volume, 0x05 color temperature -- explains why a
saved value that happens to match one still has to be written.

Two decisions are also recorded where they are made, rather than left for the
next reviewer to re-litigate. Why a handle-class error discards the monitor
outright: Monitor.Handle is captured once per discovery pass and never
refreshed, so a monitor kept here would answer every later read and write
against a handle already known to be dead. And why positive cache evidence is
deliberately permanent while the monitor stays connected: retracting it on a
DDCCI_VCP_NOT_SUPPORTED reply trades a stale control that writes into the void
for a display that disappears entirely, and that reply is not dependable
negative evidence on the hardware this mode exists for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…known-good store

Physical-monitor handles only reach PhysicalMonitorHandleManager through monitors
that were successfully built: the map is rebuilt from the returned monitor list,
and its cleanup pass only destroys handles that were already in the previous map.
A handle dropped inside the discovery loop therefore never gets destroyed. This
branch added a new way to drop one -- a handle-class error during continuous VCP
initialization now returns PhysicalMonitorUnavailable and discards the monitor,
where main logged it, left the read flag unset and kept the monitor so its handle
entered the map. A panel that intermittently answers that way leaks one kernel
handle per discovery, and a discovery runs on every display-topology change.
ReleaseAbandonedPhysical now covers all three abandon paths, including the two
that already leaked on main.

FetchCapabilitiesWithFallbackAsync also snapshots MaxCompatibilityMode once. The
property is settable from the UI thread on every settings reload and the method
awaits several times; main read it at a single decision point, this branch grew
that to three, so a toggle arriving mid-pass could have one monitor's evidence
gathered under one mode and reconciled under the other.

NullKnownGoodVcpStore and the two `?? NullKnownGoodVcpStore.Instance` fallbacks
are removed. Nothing ever reached them -- MainViewModel always supplies the real
MonitorStateManager and every test uses the injecting constructor -- so they were
dead code whose only effect would have been to silently disable the discovery
cache if a caller ever forgot the argument. The parameter is now required, which
turns that into a compile error.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ial-value source

LoadStateFromDisk guards stateFile.Monitors but not the KnownGoodVcpFeatures list
this branch added, even though System.Text.Json writes an explicit JSON null
straight over the member initializer and the array can carry null elements. The
resulting NullReferenceException escapes to the method-level catch, so every
monitor entry after the offending one is dropped -- and the next whole-file
rewrite in BuildStateJson makes that loss permanent. The property is now typed
`List<KnownGoodVcpFeature>?`, matching how the same file already declares
CapabilitiesRaw and reflecting what deserialization can actually produce, and the
loop null-checks both the collection and its elements.

VcpInitialValue.Source is deleted. It was populated at both construction sites in
Reconcile and read by nothing: the record's only consumers -- InitializeFeature
and the probe-outcome log line -- read Value, IsLive and PreferLiveRead only.
KnownGoodVcpFeature.Source is kept; it is read when a successful write refreshes
a cache entry, and it is a useful field diagnostic in monitor_state.json.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three tests added in the previous review round overclaimed.

RefreshKnownGoodAfterWrite_EmptyMonitorIdIsIgnored did not pin the guard it is
named for: the fake store did its own monitor-Id comparison, so deleting
`string.IsNullOrEmpty(monitor.Id)` from the production path left the test green.
The fake now answers any Id and counts lookups, and the test asserts the store
was never queried -- removing the guard now fails it.

HandleClassFailuresAreNeverTransient asserted, verbatim, two rows that
IsTransient_RejectsEverythingElse already covers, and its comment claimed to test
set disjointness while hardcoding the two handle codes rather than deriving them,
so an overlap introduced by widening IsPhysicalMonitorUnavailable would have
sailed past it. Deleted; the rationale moves into the DataRow comment that owns
those two inputs.

The comment on ConcurrentUpsertAndRead_OnSameMonitorDoNotTearTheFeatureMap spent
most of its lines critiquing a sibling test instead of describing its own, and
would have gone stale the moment that sibling changed. Rewritten to state what
this test drives and why two monitor Ids cannot reproduce it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ce a control rests on

ReleaseAbandonedPhysical missed one exit. When GetPhysicalMonitorsFromHMONITOR
returns more physicals than QueryDisplayConfig has entries for, the loop breaks
without releasing physicals[i..] -- the one abandon path in that loop still
leaking the handles the helper's own remark describes. The break now releases the
remainder before leaving.

Two observability gaps in exactly the mechanism most likely to produce a
confusing bug report -- a control the cache created rather than the hardware:

The probe outcome line reports attempts, status and lastError but not Replied,
even though Replied is the single input that flips a feature to Supported when
the device answers with a range that cannot scale a percentage. In a shipped
build a replied-but-unusable outcome was indistinguishable from three plain
failures. It is now in the message.

Nothing at all was logged when cached evidence widened a monitor's capabilities
past what the capabilities string advertised. The existing probe-outcome block is
gated on `live.Count > 0`, and live is only populated when the caps string is
unusable -- so on the caps-parsed path, which is where the cache silently adds a
code, there was no line to find. Reconcile now reports which codes it supported on
cache evidence alone, and discovery logs them with the observation's age, which
also gives the persisted LastSuccessfulUtc its first reader.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This branch introduced IVcpFeatureReader as the seam for native VCP reads, but left
DdcCiController.TryGetVcpFeature in place. It deleted six of that helper's seven call
sites, so the controller ended up carrying two parallel paths to the same syscall:
TryGetVcpFeature and DdcCiNative.ReadVcpFeature both wrap
GetVCPFeatureAndVCPFeatureReply and both report Marshal.GetLastWin32Error().

Route the one remaining caller, GetVcpFeatureAsync, through the injected reader and
delete the helper. The error value and the log line are unchanged. Alongside removing
the duplication this puts GetBrightnessAsync, GetContrastAsync, GetVolumeAsync,
GetColorTemperatureAsync, GetInputSourceAsync and GetPowerStateAsync behind the same
injectable seam the initializers already use; they had no test coverage before because
there was no way to reach them without hardware.

VcpFeatureProbeService.TransactionInterval also drops from internal to private. It has
no reader outside its own class, in production or in the tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hree claims

The commentary this branch added had grown into design essays sitting inside method
bodies, and three statements did not survive being checked against the code.

Corrections:

VcpDiscoveryEvidence said a slipped-through false positive lasts "until its settings
entry ages out", which reads as a 30-day bound. RemoveKnownGoodFeatures is driven by
MonitorStateRetentionPlanner.BuildDroppedIds, i.e. the Rebuild input minus its output,
and MonitorSettingsRebuilder re-emits every currently-discovered monitor with a fresh
LastSeenUtc and keeps IsHidden entries unconditionally. Reclamation therefore needs
30 days both undiscovered and unhidden, which a monitor in daily use never reaches.
The comment now says so.

MonitorRestorePlanner enumerated the never-read values as "0 brightness, 50 contrast,
50 volume, 0x05 color temperature". Those are the Monitor backing-field defaults, but
on the DDC path the value it actually sees is MonitorDiscoveryHelper's placeholder of
50 brightness -- the mid-slider value users routinely save -- so the stated cost of a
redundant write pointed at the wrong number.

MonitorStateManager.MarkDirtyAndScheduleSave claimed Dispose has already flushed by the
time a late caller arrives, and that discovery is the only such caller. Dispose sets
_disposed before it flushes, and UpdateMonitorParameter reaches the same path; the
remark now states what actually holds and why it is still harmless.

Trimming: the 24-line block in Reconcile drops to 17 and keeps the traps rather than
the argument -- the full asymmetry is argued in the PR description, not at the call
site. DiscreteVcpInitializer's remark loses the restatement of when its stage runs but
gains what keeping the monitor costs. MonitorViewModel.ShouldRestoreValue and the
retention call site in MainViewModel.Settings stop repeating the callee's own remarks
verbatim and link to them instead.

Also drops the SA1402 suppression from VcpDiscoveryEvidence.cs. StyleCop.json does not
override maintainabilityRules.topLevelTypes, so SA1402 uses its default of ["class"],
and the file declares one class -- VcpInitialValue is a record struct. Verified by
building without it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…or values

Four tests added by this branch hand their subject an input it cannot distinguish from
a test that already exists, and each has a twin asserting a strict superset.

ContinuousVcpInitializer never dereferences evidence.Capabilities or
CacheSupplementedCodes -- InitializeFeature reads only InitialValues, and feature gating
comes from the Monitor -- so Initialize_OmittedCodeFallsBackToCacheWhenLiveReadFails
drives the same ApplyCachedFallback branch as its Advertised twin, which additionally
asserts BrightnessVcpMax. Its VcpNotSupported reader script remains covered by
Initialize_VcpNotSupportedUsesCacheAndContinuesRemainingReads. The sibling
Initialize_OmittedCodeCachedValueUsesFreshLiveValueAndPersists is deliberately kept: it
is the only test that feeds caps-omitted, cache-supplemented evidence through the
initializer and asserts the Monitor is populated, which is the regression e2852c0
fixed.

ProbeCodeAsync consults the error code exactly once, through IsTransient, so
ProbeAsync_CurrentValueGreaterThanMaximumThenSuccessRetriesWithPacing walks the same
path as the InvalidCommand test; its extra Disposition assertion moves to the survivor.
0xC02625D8's membership in the transient set stays pinned by DdcErrorClassifierTests.

Reconcile_MaximumCompatibilityMarksAdvertisedCachedCodeAsLiveFallback differs from the
union test only in whether the caps string advertises the cached code, and the sole
observable effect of that -- CacheSupplementedCodes -- is asserted by
Reconcile_ReportsOnlyTheCodesTheCacheAloneProved, which now also pins PreferLiveRead for
a caps-advertised cached code. Reconcile_NormalModeReportsNoCacheSupplementedCodes built
input byte-identical to Reconcile_NormalModeIgnoresCache; its assertion moves there.

Adding DdcErrorClassifierTests.Constants_MatchWinerrorValues. Every other assertion in
the suite addresses these codes by name, so the numeric values were pinned only
incidentally, by the raw-hex constants the other test files declare as inputs. A typo in
a constant would have moved production and tests together and left the suite green. With
the values now pinned in one place, VcpFeatureProbeServiceTests' local
CurrentValueGreaterThanMaximum constant goes with the test that used it.

GetKnownGoodFeatures_UsesExactDevicePathComparer uses the file's existing Feature()
helper; its inline initializer differed only in a Source it never asserts on. The
round-trip test keeps its explicit initializer, since the fields it spells out are the
ones it verifies.

Validation: built PowerDisplay.Lib.UnitTests and PowerDisplay for x64 Debug with VS
MSBuild -- 0 errors, 0 warnings; ran PowerDisplay.Lib.UnitTests.dll with
vstest.console.exe -- 296 passed, 0 failed (299 - 4 + 1).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e redundant handle

Three cleanups to code this branch introduced, all behaviour-preserving.

The continuous VCP set had been triplicated. Base had exactly one list --
DdcCiNative.ProbeableContinuousVcpCodes -- which this branch deleted and replaced with
three byte-identical private copies in VcpDiscoveryEvidence, ContinuousVcpInitializer
and VcpFeatureProbeService. All three iterate it in declaration order, so a code added
to one and not the others is probed but never applied, or applied but never proven. The
set moves to NativeConstants.ContinuousVcpCodes, next to the three codes it is built
from; hosting it on any of the three consumers would have made the other two depend on
a peer for a constant.

Both initializers took an IntPtr handle that is provably Monitor.Handle:
MonitorDiscoveryHelper.cs:133 is the only write to that property in the module and it
stores physicalMonitor.HPhysicalMonitor, which is exactly what BuildMonitorFromPhysical
passed alongside the monitor. Beyond the duplication this was a coherence problem --
ContinuousVcpInitializer justifies discarding a monitor by reasoning about
Monitor.Handle while actually reading from the parameter, and nothing stopped a caller
from passing one that disagreed. Both Initialize signatures lose the parameter and read
Monitor.Handle directly.

The three near-identical KnownGoodVcpFeature initializers fold into
KnownGoodVcpFeature.From, the inverse of the existing ToVcpFeatureValue.
RefreshKnownGoodAfterWrite now names the VcpFeatureValue it was already constructing for
validation and reuses it.

DiscreteVcpInitializerTests' two tests also collapse to one: Initialize never inspects
read.ErrorCode -- it is used once, interpolated into a log string, and the file never
references DdcErrorClassifier -- so VcpNotSupported and the two handle-class codes are
indistinguishable inputs. The DataTestMethod survives because its handle-class rows are
what would fail if someone copied ContinuousVcpInitializer's drop-the-monitor branch
into this stage; the deleted test's two value assertions move into it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ate the error rationale

Reconcile_IndeterminateLiveUsesCachedPositiveEvidence and
Reconcile_VcpNotSupportedStillUsesCachedPositiveEvidence differ only in the error code
handed to VcpProbeObservation.Indeterminate. Reconcile never reads LastError -- it
consults Disposition, IsSuccess and Replied -- and Disposition routes both 0xC0262589
and 0xC0262584 to Indeterminate, since neither is handle-class. Same cached entry, same
null parsed capabilities, same includeCache, so the two calls are provably identical
inputs. The survivor is the stronger mutation-killer, because a regression where
DDCCI_VCP_NOT_SUPPORTED retracts cached evidence fails it and not the other; it inherits
the deleted test's IsLive assertion and its retry-budget comment.

ProbeAsync_UnansweredCodeIsNotMarkedAsReplied repeated ProbeAsync_NonTransientFailure
DoesNotRetry's arrange and act verbatim; its one assertion moves into that test.

The <remarks> on DdcErrorClassifier.IsTransient and the DataRow comments on
IsTransient_RejectsEverythingElse were written by the same commit and say the same five
things. The production remarks win -- that is where someone editing the predicate looks
-- and the test keeps only the reason its handle-class rows are load-bearing here, which
is specific to the test rather than to the classifier.

Validation: built PowerDisplay.Lib.UnitTests and PowerDisplay for x64 Debug with VS
MSBuild -- 0 errors, 0 warnings; ran PowerDisplay.Lib.UnitTests.dll with
vstest.console.exe -- 293 passed, 0 failed (296 - 2 methods - 1 DataRow case).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eter

Third cleanup pass. Where the first two hunted duplicated text, this one asks whether
each shape earns its keep.

Reconcile took both a `cached` dictionary and an `includeCache` flag, but the sole
production caller derives both from the same local: DdcCiController passes
`maxCompatibility ? GetKnownGoodFeatures(monitorId) : new Dictionary<...>()` and then
restates the decision as `includeCache: maxCompatibility`. Since `!includeCache` always
implies `cached` is empty, the `includeCache &&` conjunct could never change the result
of the `cached.TryGetValue` beside it -- one decision expressed twice, in two places a
maintainer had to keep in agreement by hand. The parameter goes; the gate stays where it
has to live anyway, because in normal mode the controller must not even call
GetKnownGoodFeatures. That is what DdcCiControllerCompatibilityGateTests.NormalMode_
NeitherProbesNorTouchesTheCache pins, with `store.GetCallCount == 0` -- strictly stronger
than the unit test it replaces, which only checked that Reconcile ignored a cache it had
already been handed.

`using System;` was orphaned in both initializers when round 2 removed their IntPtr
parameter, and `using System.Threading.Tasks;` in MonitorStateManagerTests by the test
deleted below.

Four more tests hand their subject input it cannot distinguish:

- Reconcile_NormalModeIgnoresCache existed only to exercise the removed parameter.
- Reconcile_NoLiveOrCacheLeavesFeatureUnavailable: Reconcile has no branch keyed on
  emptiness, so it kills no mutant the other tests miss.
- Initialize_AdvertisedCachedCodeUsesFreshLiveValueAndPersists differs from its Omitted
  twin only in CachedEvidence's flag, which changes evidence.Capabilities --
  ContinuousVcpInitializer reads only evidence.InitialValues, and Reconcile emits the
  identical entry for both. The Omitted twin is kept, being the discriminating input; its
  one unique assertion moves across.
- ConcurrentUpserts_PreserveBothMonitorEntries raced two different monitor Ids, which the
  PR's own sibling test explains cannot contend: GetOrAdd maps them to two MonitorState
  instances and therefore two disjoint locks. It exercised ConcurrentDictionary, not this
  branch. Its non-concurrent content is already asserted by RemoveKnownGoodFeatures_
  ClearsCacheButKeepsSavedUserValues, and the real contention path by
  ConcurrentUpsertAndRead_OnSameMonitorDoNotTearTheFeatureMap.
- BuildDroppedIds_KeepsEntriesThatSurvivedTheRebuild: BuildDroppedIds has a single code
  path and two other tests already assert an empty result.

Finally the nine test-local DDC error constants and one inline literal now reference
DdcErrorClassifier by name. Round 1 blocked this because those locals were the only
value-level check on the constants; Constants_MatchWinerrorValues, added in the same
round, is now that oracle, so the duplication can go. Codes DdcErrorClassifier does not
declare stay as raw hex.

Validation: built PowerDisplay.Lib.UnitTests and PowerDisplay for x64 Debug with VS
MSBuild -- 0 errors, 0 warnings; ran PowerDisplay.Lib.UnitTests.dll with
vstest.console.exe -- 288 passed, 0 failed (293 - 5).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e-planner tests

MonitorStateManagerTests

ControllerDerivedCacheKey_MatchesMonitorIdAndSurvivesRetentionRoundTrip arranged a temp
directory, two MonitorStateManager instances and a JsonDocument parse for one property.
The only mutation it uniquely killed is DeriveMonitorId returning info.DevicePath instead
of MonitorIdentity.FromDevicePath(info.DevicePath), which its first two asserts catch on
their own. Everything after them either asserts the absence of a key no writer produces
-- MonitorIdComparer is OrdinalIgnoreCase, so a raw path with its trailing GUID can never
collide with the canonical Id -- or models a RemoveKnownGoodFeatures call production never
makes, since the retention path passes Monitor.Id, already canonical. It becomes
DeriveMonitorId_ReturnsTheCanonicalIdTheStateFileIsKeyedBy, a pure 14-line test. The
save/reload properties it also touched stay pinned by
KnownGoodFeatures_RoundTripPreservesObservation and the removal tests.

RemoveKnownGoodFeatures_EmptySetKeepsCompleteState is deleted. RemoveKnownGoodFeatures has
no branch on the collection being empty -- it is a bare foreach whose body touches only the
state it looked up -- so MonitorA's fate there is produced by the same code path as
MonitorA's fate in RemoveKnownGoodFeatures_ClearsCacheButKeepsSavedUserValues, where it is
likewise never named in the removal list. Its one piece of genuinely unique coverage was
accidental and unrelated to its name: it was the only place the Volume field made a disk
round trip. That moves to the surviving test, whose comment now also records that it stands
for the no-observed-drop case.

Both removal tests dropped their JsonDocument assertions. GetMonitorParameters and
GetKnownGoodFeatures after a reload already prove what the on-disk shape has to be, and
System.Text.Json is no longer needed in the file.

MonitorRestorePlannerTests

Four of the five DataTestMethods varied readFlag across four rows that cannot discriminate.
ShouldWrite is `(ReadValues & readFlag) != readFlag || target != current || target !=
displayed`, and in those four cases a mis-mapped switch arm yields a Monitor default that
still satisfies the clause under test, so every row reaches the same clause with the same
verdict. They collapse to one row each, spread across different flags.

ShouldWrite_MatchingValueWasRead_ReturnsFalse keeps all four rows: it is the only case
where all three clauses are false, so it is the only one whose verdict depends on the
switch reading the right field, and a comment now says so. The class also gains `sealed`,
matching every other test class in the project.

Validation: built PowerDisplay.Lib.UnitTests for x64 Debug with VS MSBuild -- 0 errors,
0 warnings; ran PowerDisplay.Lib.UnitTests.dll with vstest.console.exe -- 275 passed,
0 failed (288 - 1 test method - 12 DataRow cases). No production code changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

PowerDisplay: DDC/CI Monitor does not show brightness slider, but other sliders are available in max. compatibility mode

1 participant