PowerDisplay: Fall back to persisted VCP values when a monitor read fails - #49445
Draft
moooyo wants to merge 43 commits into
Draft
PowerDisplay: Fall back to persisted VCP values when a monitor read fails#49445moooyo wants to merge 43 commits into
moooyo wants to merge 43 commits into
Conversation
Co-authored-by: Copilot <[email protected]> Copilot-Session: 6ea38c04-6f68-4c42-91d9-8a03b49bdd81
Co-authored-by: Copilot <[email protected]> Copilot-Session: 6ea38c04-6f68-4c42-91d9-8a03b49bdd81
Co-authored-by: Copilot <[email protected]> Copilot-Session: 6ea38c04-6f68-4c42-91d9-8a03b49bdd81
Co-authored-by: Copilot <[email protected]> Copilot-Session: 6ea38c04-6f68-4c42-91d9-8a03b49bdd81
Co-authored-by: Copilot <[email protected]> Copilot-Session: 6ea38c04-6f68-4c42-91d9-8a03b49bdd81
Co-authored-by: Copilot <[email protected]> Copilot-Session: 6ea38c04-6f68-4c42-91d9-8a03b49bdd81
Co-authored-by: Copilot <[email protected]> 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 <[email protected]> Copilot-Session: 6ea38c04-6f68-4c42-91d9-8a03b49bdd81
Co-authored-by: Copilot <[email protected]> Copilot-Session: 6ea38c04-6f68-4c42-91d9-8a03b49bdd81
moooyo
marked this pull request as draft
July 22, 2026 08:21
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) <[email protected]>
… 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) <[email protected]>
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) <[email protected]>
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) <[email protected]>
…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) <[email protected]>
…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) <[email protected]>
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) <[email protected]>
…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) <[email protected]>
…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) <[email protected]>
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) <[email protected]>
…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) <[email protected]>
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) <[email protected]>
…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) <[email protected]>
…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) <[email protected]>
…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) <[email protected]>
…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) <[email protected]>
…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) <[email protected]>
…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) <[email protected]>
This was referenced Jul 30, 2026
…d observation source Collapses ten duplicated test-double definitions to four: FixedClock existed three times verbatim, the queue-backed IVcpFeatureReader three times, the IKnownGoodVcpStore fake three times, and the Cached(...) fixture three times. They now live in DdcFakes, pulled in with 'using static'. Also removes the double indirection between DdcCiNative.ReadVcpFeature and NativeVcpFeatureReader (the former had exactly one caller), drops the SA1649 suppression in VcpDiscoveryEvidence.cs by ordering the file after its namesake type, and documents why ErrorGraphicsDdcCiVcpNotSupported is deliberately a member of neither classification set. VcpObservationSource is deleted outright: no branch anywhere read it and nothing logged it, so it was write-only state carried through the persisted schema. The field never shipped, and System.Text.Json skips unmapped members, so an on-disk file written by an earlier build of this branch still loads.
7 tasks
…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
added a commit
that referenced
this pull request
Jul 30, 2026
#49578) ## Summary of the Pull Request `DdcCiController.DiscoverFromHandleAsync` abandons a physical monitor on three paths without destroying its handle. 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 in the *previous* map. A handle dropped on an abandon path therefore never gets destroyed. A discovery runs on every display-topology change, so a monitor that keeps failing leaks one more handle per discovery for the process lifetime — a docking-station user accumulates them. Extracted from #49445, where the same fix is bundled with maximum-compatibility-mode work it does not depend on. ## PR Checklist - [ ] Closes: #xxx — no issue; extracted from #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; rationale below - [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; 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 ### The three leaking paths | path | before this PR | | --- | --- | | more physical monitors than `QueryDisplayConfig` entries for the GDI name | `break` leaves `physicals[i..]` unreleased — the whole tail, not just the current one | | capabilities unavailable | `continue` | | `BuildMonitorFromPhysical` returned null (construction failed, or it threw and was caught) | no `else` branch at all | `ReleaseAbandonedPhysical` is null-handle safe and swallows a failing `DestroyPhysicalMonitor` at warn level: one handle that cannot be destroyed must not take down the rest of the discovery pass. ### Why there are no tests Reaching these call sites means faking the whole native enumeration surface — `EnumDisplayMonitors`, `GetMonitorInfo`, `GetPhysicalMonitorsFromHMONITOR` — which is a larger seam than a one-file leak fix should introduce. The paths were verified by reading instead. Happy to add the seam if a maintainer would rather have it covered. ### Known remaining leaks, deliberately out of scope - `GetPhysicalMonitorsWithRetryAsync`'s retry loop discards a whole array of live handles when it retries after seeing NULL handles. - Cancellation unwinds `DiscoverMonitorsAsync` before `UpdateHandleMap` runs, so that pass's handles never enter the map and are never destroyed. Both predate this change and are better addressed separately. ## Validation Steps Performed - built `PowerDisplay.Lib` 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** — no new tests; this only confirms nothing regressed - no hardware validation performed: reaching an abandon path needs a monitor whose capabilities fetch fails or whose construction throws Co-authored-by: Yu Leng (from Dev Box) <[email protected]>
moooyo
added a commit
that referenced
this pull request
Jul 30, 2026
…tings (#49577) ## 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 #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 - [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; 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 Co-authored-by: Yu Leng (from Dev Box) <[email protected]>
added 2 commits
July 30, 2026 15:15
# Conflicts: # src/modules/powerdisplay/PowerDisplay.Lib/Drivers/DDC/DdcCiController.cs
…rop two suppressions VcpFeatureProbeService.cs held six top-level types and suppressed both SA1649 and SA1402 to do it. Moving NativeVcpFeatureReader out leaves a single class, so SA1402 no longer applies, and ordering the file after its namesake type satisfies SA1649. The move stands on its own merit: NativeVcpFeatureReader is the production P/Invoke seam that DdcCiController and both VCP initializers depend on, not a detail of the probe service it happened to live inside.
moooyo
pushed a commit
that referenced
this pull request
Jul 30, 2026
…mments The retry budget was being spent where it could not change the outcome. Once GetVCPFeatureAndVCPFeatureReply returns TRUE, support is proven and Replied is latched -- BuildCapabilitiesFromProbe reads nothing else -- so the remaining attempts against a degenerate range only cost I2C transactions and 100 ms of pacing each. A monitor with no speakers that still ACKs 0x62 with max=0 is the common shape here: it burned three transactions to produce the same capability set the first one already settled. The probe now returns on the reply. Every reply therefore returns from inside the attempt loop, which leaves the `replied` local provably false at the bottom -- the loop can only fall through when the device never answered. It is gone, and the terminal Indeterminate takes the default. Returning early also keeps the earlier attempt's error on the observation instead of leaving it to be overwritten by a later failure, so a transient fault followed by a degenerate reply now reports both facts. Three doc comments described collaborators that do not exist in this PR: DdcErrorClassifier claimed the initializers agree with the probe on what to retry, but TryGetVcpFeature is still a single unclassified read; ContinuousVcpCodes named an evidence reconciler that arrives with #49445; and RecordingVcpReader promised a hard failure on an extra read that the probe's catch-all actually swallows. Each now says what the code does. DdcErrorClassifier also moves to a block-scoped namespace, matching csharp_style_namespace_declarations in src/.editorconfig and the rest of the Drivers/DDC folder.
moooyo
added a commit
that referenced
this pull request
Jul 30, 2026
) ## Summary of the Pull Request In Maximum compatibility mode, when a monitor's capabilities string is missing or unparsable, discovery falls back to probing each continuous VCP code directly. That probe issues **one** `GetVCPFeatureAndVCPFeatureReply` per code, back to back, and treats any failure as final. On a panel whose DDC/CI engine answers intermittently, a single transient I2C fault permanently drops that control for the whole discovery pass — and if every code happens to fault, the monitor disappears from the flyout entirely. This replaces the probe with `VcpFeatureProbeService`: - **paced** — 100 ms between transactions, instead of hammering the I2C bus back to back - **retried** — up to 3 attempts, but only for failures another attempt can plausibly get past - **classified** — `DdcErrorClassifier` decides what "transient" means, so the retry budget is not burned on a definitive `DDCCI_VCP_NOT_SUPPORTED` or on a dead physical-monitor handle - **aborted early** — a handle-class error stops the remaining codes rather than issuing more requests against a handle already known to be invalid Extracted from #49445, which bundles this with a persisted discovery cache and a discovery restructure it does not depend on. This piece stands alone and addresses one of the root causes in #49342 by itself. ## PR Checklist - [ ] Closes: #xxx — partially addresses #49342; the remaining causes are in #49445 - [ ] **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; 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 ### What is and is not retried `DdcErrorClassifier` names the DDC/CI error codes after `winerror.h` and splits them into two sets. `DdcErrorClassifierTests` pins both the membership of each set **and** the numeric value of every constant against `winerror.h`, so a typo cannot move production and tests together and leave the suite green. Retried — framing, arbitration and timing faults on the I2C bus: `I2C_ERROR_TRANSMITTING_DATA`, `I2C_ERROR_RECEIVING_DATA`, `DDCCI_INVALID_DATA`, `MCA_INTERNAL_ERROR`, `DDCCI_INVALID_MESSAGE_COMMAND`, `DDCCI_INVALID_MESSAGE_LENGTH`, `DDCCI_INVALID_MESSAGE_CHECKSUM`, `DDCCI_CURRENT_CURRENT_VALUE_GREATER_THAN_MAXIMUM_VALUE`, `ERROR_TIMEOUT`. Not retried, each for a stated reason recorded on the predicate: `DDCCI_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, not to a VCP read; and the two handle-class codes must abort rather than retry. ### Behaviour preserved `FetchCapabilitiesWithFallbackAsync` keeps its signature and still returns `(string, VcpCapabilities?)`, so nothing outside the probe changes. `BuildCapabilitiesFromProbe` synthesizes the same shape `DdcCiNative.ProbeSupportedVcpFeatures` used to, and decides membership the same way: a code counts as supported when the device *replied*, not when the value was usable. A reply proves the opcode is implemented even if the reported range cannot scale a percentage — an unimplemented code fails with `DDCCI_VCP_NOT_SUPPORTED` instead. The set of probed codes moves from a private array in `DdcCiNative` to `NativeConstants.ContinuousVcpCodes`, where the follow-up work in #49445 also needs it. ### Cost The probe only runs in Maximum compatibility mode, and only when the capabilities string is already unusable — so this adds no I2C traffic to a monitor that parses normally. For a monitor that does reach it, the worst case grows from 3 transactions to 9 plus 900 ms of pacing, and it is bounded: a definitive refusal stops after one attempt, and a handle-class error stops the whole probe. ### What is deliberately left out The probe's values are still discarded — `BuildMonitorFromPhysical` re-reads each code immediately afterwards. Reusing them needs a carrier for the observed value, which is `VcpDiscoveryEvidence` in #49445. `VcpFeatureProbeService` already returns everything that needs (`VcpProbeObservation` carries the value, the attempt count and the last error); this PR simply does not consume it yet. ## Validation Steps Performed - built `PowerDisplay.Lib.UnitTests` for x64 Debug with VS MSBuild — 0 errors, 0 warnings - ran `PowerDisplay.Lib.UnitTests.dll` with `vstest.console.exe`: **223 passed, 0 failed** (186 on `main` + 37 added here) - `VcpFeatureProbeServiceTests` drives the pacing, the retry budget, the transient/definitive split, cancellation before and during the inter-transaction delay, a throwing native read, and that reads run off the caller's thread — all through an injected reader and an injected delay, so no hardware is needed - no hardware validation performed: reaching this path needs a panel whose capabilities string is unusable **and** whose VCP reads fail intermittently --------- Co-authored-by: Yu Leng (from Dev Box) <[email protected]>
added 4 commits
July 30, 2026 18:11
#49579 landed with three post-review revisions that this branch has to absorb: - VcpProbeDisposition was replaced by VcpProbeObservation.IsPhysicalMonitorUnavailable; VcpDiscoveryEvidence.Reconcile now uses the property. - ProbeCodeAsync returns on the first reply instead of retrying a degenerate range. - RecordingVcpReader was folded into VcpFeatureProbeServiceTests as a private class because it had a single consumer there. This branch gives it three, so it goes back into DdcFakes, carrying the corrected remark from ca3cd3c. Also drops a duplicate '_probeService = new VcpFeatureProbeService(new NativeVcpFeatureReader())' that auto-merged into the injecting constructor and would have silently overridden the injected reader and delay.
… decision VcpInitializationResult was a two-value enum in its own file whose only branch asked one question -- the same shape as VcpProbeDisposition, dropped in b20712a. Eight of its ten references were 'return Completed', meaning nothing to report. Initialize now returns bool and documents the polarity. MonitorStateRetentionPlanner was 51 lines plus a 50-line test file wrapping a set difference with one call site. The rationale comment moves to that call site. No coverage is lost: MonitorIdComparer has its own tests, and the case-insensitive matching of the consumer is already pinned by MonitorStateManagerTests.RemoveKnownGoodFeatures_MatchesIdsCaseInsensitively. UpsertKnownGoodFeature threw ArgumentOutOfRangeException on an invalid range that no caller can produce: all three pre-validate, and nothing tested the throw. The guard that matters is in LoadStateFromDisk, where untrusted values actually enter. BuildMonitorFromPhysical tested evidence.IsPhysicalMonitorUnavailable alongside the null capabilities check. Reconcile only ever sets that flag together with null capabilities, and the single caller already skips both cases with their own log lines, so the disjunct could never decide anything.
…aximum compatibility mode DdcCiController gates only the read side on MaxCompatibilityMode, so the unconditional upsert here reads like an oversight. It is not: a monitor that reads cleanly today can start failing after a cable or dock change, and a cache populated lazily would be empty on exactly the first pass that needs it.
7 tasks
#49596 takes the discovery restructure and the probe-value reuse, leaving this branch to be the persisted known-good cache alone. Conflicts resolved in favour of this branch throughout: its versions of VcpDiscoveryEvidence, the two initializers and DdcCiController are supersets of #49596's, adding the cached source, the store and the clock. The three DDC files also move to a block-scoped namespace, matching #49596 and the rest of the Drivers/DDC folder, so the delta between the two branches is the cache and nothing else.
moooyo
changed the base branch from
main
to
yuleng/powerdisplay/probe-value-reuse/1
July 31, 2026 01:00
… I2C The known-good cache keys off the canonical DevicePath, so DiscoverFromHandleAsync now derives that Id up front. An empty one — a target with no DevicePath — would reach UpsertKnownGoodFeature, whose empty-key rejection unwinds into the per-hMonitor catch and abandons every remaining physical on that handle along with its Monitor. Deciding it at the top of the loop releases the handle and skips one entry instead, which is what the old CreateMonitorFromPhysical guard did before the cache moved Id derivation earlier. DisplayConfigInventory already filters empty DevicePaths, so this guards a path discovery should not reach. The CreateMonitorFromPhysical guard stays as a contract check on an internal helper; its comment now says which of the two is load-bearing. Also corrects the CacheSupplementedCodes log comment: it claimed the caps-parsed path is the only one where the cache silently adds a code, but a probe that ran and got no reply proving the code reaches it too, so the two log lines can overlap.
moooyo
added a commit
that referenced
this pull request
Jul 31, 2026
…ad (#49596) ## Summary of the Pull Request In Maximum compatibility mode, when a monitor's capabilities string is unusable, discovery probes each continuous VCP code directly to find out which ones the panel implements — and then **throws the values away**. `BuildMonitorFromPhysical` immediately re-reads every one of those codes. That doubles the I2C traffic on exactly the hardware that cannot take it, and the re-read is the one whose result the user actually sees: a panel that answered the probe a moment ago but fails the re-read shows its brightness slider parked at the never-read default instead of where the panel really is. This makes the probe's values survive into the build stage. Extracted from #49445, which bundles it with a persisted discovery cache it does not depend on. ## PR Checklist - [ ] Closes: #xxx — partially addresses #49342; the remaining cause is in #49445 - [ ] **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; 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 ### What the re-read costs Worth being precise about, because it is not the slider's *existence*: | decided by | set from | affected by a failed re-read | | --- | --- | --- | | slider visible (`MonitorViewModel.ShowBrightness`) | `Monitor.Capabilities`, via `UpdateMonitorCapabilitiesFromVcp` before the initializer runs | no | | slider position (`Monitor.CurrentBrightness`) | the read | yes — stays at the never-read default | | `powerdisplay get` reporting a live reading (`Monitor.ReadValues`) | the read | yes — reported as unknown | | relative `powerdisplay adjust` (`AdjustCommandExecutor`) | `Monitor.ReadValues` | yes — no before-value to adjust from | So the flyout keeps the control either way; what the second read decides is whether it is pointed anywhere real, and whether the CLI will admit to a value. Halving the transactions on a bus that is both slow and, on this hardware, unreliable is the other half of the win. ### The seam `FetchCapabilitiesWithFallbackAsync` used to return `(string capsString, VcpCapabilities? caps)` — capabilities only, no values. It now returns a `VcpDiscoveryEvidence`, which carries the same two things plus the values the probe already read and a flag for a handle that died mid-probe. `VcpDiscoveryEvidence.Reconcile` folds the probe observations into the parsed capabilities in one place: | observation | capabilities | value carried | | --- | --- | --- | | read succeeded | code marked supported | yes | | device replied, range unusable (e.g. `max=0`) | code marked supported | no — the initializer still owes it a read | | no reply | unchanged | no | | handle-class failure | everything discarded | — | The second row is why membership keys off `Replied` rather than the value being usable: an unimplemented code fails with `DDCCI_VCP_NOT_SUPPORTED` and never sets the flag, so a reply proves the opcode exists even when the reported range cannot scale a percentage. That is the same rule `BuildCapabilitiesFromProbe` used before this PR; it just moves next to the value handling. Like that method, `Reconcile` iterates the observations rather than `NativeConstants.ContinuousVcpCodes`, which `VcpFeatureProbeService` only takes as the default for its constructor-injected sweep list. That keeps a widened sweep from silently dropping a code that answered, but it is not sufficient on its own: the carried value is consumed only for codes `ContinuousVcpInitializer` walks, so widening the sweep still needs a matching edit there. The comment and `Reconcile_ProbedCodeOutsideTheDefaultSweepIsStillHonoured` both say so rather than claiming the seam alone covers it. On the normal path nothing changes: the probe only runs when the caps string is unusable, so `live` is empty and `Reconcile` is a pass-through. `Reconcile_ParsedCapabilitiesSurviveWhenNoProbeRan` pins that. ### Continuous-VCP initialization moves out of the controller `DdcCiController` carried six near-identical `Initialize*` methods. The three percent-scaled ones become **`ContinuousVcpInitializer`** — brightness, contrast, volume. It skips any code the evidence already has a value for, and returns `false` when a read fails with a handle-class error, because `Monitor.Handle` is captured once per discovery pass and never refreshed: a monitor kept alive on a dead handle would send every later read and write into the void. It reads through the `IVcpFeatureReader` seam introduced in #49579, so it is testable without hardware. The three discrete-enum ones — color preset, input source, power mode — stay in `DdcCiController`, unchanged. The probe sweeps only `NativeConstants.ContinuousVcpCodes`, so no discrete value is ever carried across the seam and extracting them would be a refactor this change does not need; see *What is deliberately left out*. Only the continuous stage discards the monitor. That is a policy choice, not a property of the stage: losing a whole display because `0xD6` answered badly is worse than showing it without a power control. `DdcCiController.TryGetVcpFeature` therefore still has three discovery callers plus `GetVcpFeatureAsync`, the runtime refresh path. ### Behaviour change outside Maximum compatibility mode **A handle-class error during continuous VCP initialization now discards the monitor.** Before, the failure was logged, the read flag left unset, and the monitor kept — so its handle reached `PhysicalMonitorHandleManager` and every later operation went to a handle already known to be dead. The cost is that the monitor stays out of the flyout until a rediscovery: `DisplayChangeWatcher` schedules one for device-arrival/removal and console-display-state notifications, and the flyout's Refresh button forces one on demand. Note this check is not on every path. A caps string that parses but advertises none of `0x10`/`0x12`/`0x62` leaves `ContinuousVcpInitializer` nothing to read and suppresses the probe, so such a monitor is still published with a handle no VCP read has exercised — and its first VCP read then happens in the discrete stage, which never discards. ### What is deliberately left out **Extracting the discrete-VCP initialization.** The probe sweeps only the continuous codes, so no discrete value is ever reused and moving `0x14`/`0x60`/`0xD6` out of the controller would be a drive-by refactor with no bearing on this change. It is worth doing on its own, where the added test coverage can be reviewed for what it is. **Remembering a probe value across discoveries.** A probe value is only useful for the pass that produced it. Carrying one forward — so a later failing pass can still show the control — is the persisted known-good cache in #49445, a much larger change with an open design question attached. This PR is complete without it. ## 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`: **240 passed, 0 failed** — 16 of those cases are added here (8 in `ContinuousVcpInitializerTests`, 7 in `VcpDiscoveryEvidenceTests`, 1 in `DdcErrorClassifierTests`) - `VcpDiscoveryEvidenceTests` pins each row of the table above, plus that a probed code outside the default sweep is still honoured - `ContinuousVcpInitializerTests` pins that a probed code is never re-read (the reader is primed with a failure it must not reach), that a handle-class error stops the remaining codes, and that a feature-level refusal does not. `Initialize_EveryContinuousCodeIsReadAndApplied` walks the whole `ContinuousVcpCodes` array with a distinct range and percentage per feature, so a code added to that array without an arm in both `IsSupported` and `ApplyValue` fails rather than being silently skipped or silently discarded — checked by mutation: removing either volume switch arm fails that test and `Initialize_ProbedVolumeIsAppliedWithoutReadingAgain` - not covered by tests: the `DdcCiController` side of the contract — that `evidence.IsPhysicalMonitorUnavailable` skips the monitor and releases the physical, and that a `false` from `ContinuousVcpInitializer` does the same. That layer takes no injectable dependencies today - no hardware validation performed: this path is reachable only on a panel whose capabilities string is unusable, which needs an incomplete or unreliable DDC/CI implementation --------- Co-authored-by: Yu Leng (from Dev Box) <[email protected]>
added 3 commits
July 31, 2026 16:49
#49596 landed on main as a squash commit, so this branch's copy of that work is now a duplicate of an older revision of it. Resolved by taking main's final revision wherever the two disagree and replaying only the known-good VCP cache on top: - VcpDiscoveryEvidence.Reconcile keeps main's probe-driven iteration instead of sweeping NativeConstants.ContinuousVcpCodes, so a widened probe list is still honoured, and walks the cached codes the probe did not touch as well. Main's class and member documentation is restored. - DdcCiController keeps main's inline discrete-VCP initialization; DiscreteVcpInitializer and its test are dropped, since #49596 folded that work back into the controller and nothing references them now. - ContinuousVcpInitializerTests keeps main's per-code coverage invariant and the volume path, adapted to the initializer's store and clock.
…he' into pr-49445-head
…ous codes Review follow-ups on the known-good VCP cache. MonitorStateManager.UpsertKnownGoodFeature no longer marks the state file dirty when neither Current nor Maximum moved. Every discovery pass re-reads all three continuous codes, so re-observing an unchanged value was the common case, and each one rewrote the whole file and reset the save debouncer — cancelling the pending Task.Delay with an OperationCanceledException — for a timestamp only the support log reads. The entry is still replaced in memory so LastSuccessfulUtc stays accurate; the comment records that this holds only while the timestamp is diagnostic, and has to go with any reintroduced freshness bound. The probe-success upsert loop in FetchCapabilitiesWithFallbackAsync is bounded to ContinuousVcpCodes. VcpFeatureProbeService takes its sweep list through the constructor, so widening it without a matching arm in ContinuousVcpInitializer would have persisted a code nothing ever applies while Reconcile kept marking it supported. DdcCiController's internal constructor now forwards that sweep list so the bound is testable. The same loop gained a comment explaining why a reading taken before the handle died is still persisted even though Reconcile discards the pass: the probe stops at the first handle-class failure, so the value is proven even when the pass is not. Reconcile and MarkSupported now document that a non-null parsedCapabilities is extended in place and returned as the same instance — reference identity is already asserted by Reconcile_ParsedCapabilitiesSurviveWhenNoProbeRan, and a deep copy would silently lose metadata a later field addition forgot to carry. RemoveKnownGoodFeatures records the retention blind spot: a monitor whose handle dies mid-discovery seeds an entry but never reaches settings.json, so it is never named for collection. Restored the ContinuousVcpInitializer class summary dropped in the merge with PR #49596, undid two signature reflows that changed no parameters, and dropped a fully-qualified System.Array.Empty in favour of a using. Tests: three pin the save short-circuit (unchanged value keeps the in-memory timestamp, does not recreate a deleted state file, and the changed-value control does); two cover the upsert loop (a reading that preceded a dead handle is persisted, a widened sweep does not reach the cache). Restored the return-value assertion on Initialize_InvalidRangeDoesNotApplyOrPersist, took the concurrency test from 100 rounds to 20, and made the temp-directory cleanup tolerant of a save still in flight.
Per maintainer decision, the persist loop in FetchCapabilitiesWithFallbackAsync no longer filters probe successes by ContinuousVcpCodes, and DdcCiController's internal constructor no longer forwards a probe sweep list. The bound only mattered if someone widened VcpFeatureProbeService's sweep without adding a matching arm to ContinuousVcpInitializer, and the injection point existed solely to make that bound testable; both are removed rather than carried. VcpFeatureProbeService keeps its own codes parameter, which is unchanged. MaxCompatibility_OnlyContinuousCodesReachTheCache goes with them. The comment explaining why a reading taken before the handle died is still persisted stays, along with MaxCompatibility_ReadingsTakenBeforeTheHandleDiedAreStillPersisted. Also reworded "interleavings" in the concurrency test comment, which is not in the spell-check dictionary or expect.txt and would fail CI once this PR leaves draft.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
Detailed Description of the Pull Request / Additional comments
What is stored
MonitorStateManagerimplementsIKnownGoodVcpStore, so the cache rides in the existingmonitor_state.jsonnext to the user's saved brightness rather than in a new file. Each entry is aKnownGoodVcpFeature: code, current, maximum, and when it was last read.Only range-valid observations are stored, so the common
current=0 / max=0garbage reply never enters — it failsVcpFeatureValue.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.Reconcilegains the cache as a third source alongside the parsed capabilities string and this pass's probe:MonitorReadFlagsleft clearMonitorReadFlagsleft clearThe 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.
MonitorReadFlagsstays 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 whilepowerdisplay getreports that setting as unknown, becauseMonitorDtoProjectorgates onsupported && read.Keeping the cache current
RefreshKnownGoodAfterWriterestamps an entry after a successfulSetVCPFeature, 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.RemoveKnownGoodFeaturesclears 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 corruptsettings.jsonyields 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.jsonfor a moved timestamp alone.Validation Steps Performed
PowerDisplay.Lib,PowerDisplay.Lib.UnitTestsandPowerDisplaybuilt for x64 Debug with VS MSBuild — 0 errors, 0 warnings;PowerDisplay.Lib.UnitTests.dllundervstest.console.exe: 301 passed, 0 failed