PowerDisplay: Pace and retry the maximum-compatibility VCP probe (#49579)

## 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) <yuleng@microsoft.com>
This commit is contained in:
moooyo
2026-07-30 17:55:00 +08:00
committed by GitHub
parent efc0258cda
commit bc2d09abe8
10 changed files with 789 additions and 64 deletions

View File

@@ -356,6 +356,7 @@ DCBA
DCOM
DCR
ddc
DDCCI
DDEIf
Deact
debouncer