mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-05 01:50:26 +02:00
Compare commits
16 Commits
user/muyua
...
yuleng/pd/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4ec92bb8f | ||
|
|
450dd5dd59 | ||
|
|
53fc27b552 | ||
|
|
0d5eb84d4f | ||
|
|
8e8e4408ae | ||
|
|
9c762c720c | ||
|
|
f8bf1be06b | ||
|
|
b0059e2c1d | ||
|
|
a81effce10 | ||
|
|
5933cb9ece | ||
|
|
bf81179b35 | ||
|
|
6e7232862f | ||
|
|
0f434b5344 | ||
|
|
d7916e0457 | ||
|
|
649a1dda15 | ||
|
|
5487807d6f |
299
doc/specs/2026-06-30-powerdisplay-vcp-code-resolution-design.md
Normal file
299
doc/specs/2026-06-30-powerdisplay-vcp-code-resolution-design.md
Normal file
@@ -0,0 +1,299 @@
|
||||
# PowerDisplay — Per-Feature VCP Code Resolution & Persistence
|
||||
|
||||
Design spec. Date: 2026-06-30. Branch: `yuleng/powerdisplay/vcp-code-resolution/1`.
|
||||
|
||||
## 1. Problem
|
||||
|
||||
Each PowerDisplay feature (brightness, contrast, volume, color temperature, input
|
||||
source, power state) is currently bound to **exactly one hardcoded VCP code**
|
||||
(`NativeConstants.cs`: brightness `0x10`, contrast `0x12`, volume `0x62`, color
|
||||
preset `0x14`, input source `0x60`, power mode `0xD6`). `DdcCiController` uses these
|
||||
constants verbatim in its `Get*/Set*/Initialize*` methods.
|
||||
|
||||
In reality a single logical feature can map to **different VCP codes on different
|
||||
monitors**. The canonical case is brightness: most monitors expose it on `0x10`
|
||||
(Luminance), but some expose only `0x13` (Backlight Control) or `0x6B`
|
||||
(Backlight Level: White). With a single hardcoded code these monitors appear to not
|
||||
support the feature.
|
||||
|
||||
We need a mechanism where, for each feature, we define an **ordered list of candidate
|
||||
VCP codes by priority**, then **resolve** which code a given monitor actually uses,
|
||||
**persist** that decision per monitor (so we never re-probe needlessly — including a
|
||||
"checked, none supported" sentinel), and **surface** the resolved code in the
|
||||
diagnostics output.
|
||||
|
||||
## 2. Goals / Non-Goals
|
||||
|
||||
### Goals
|
||||
- A general, data-driven registry mapping each feature to an ordered candidate-code list.
|
||||
- A pure, unit-testable resolver implementing **cap-string-first, probe-as-fallback**.
|
||||
- Per-monitor persistence of the resolved map (including a "not supported" sentinel),
|
||||
reused across sessions and invalidated **only** by a user-initiated Refresh.
|
||||
- `DdcCiController` reads/writes each feature through its resolved code.
|
||||
- Resolved codes appear in the per-monitor diagnostics ("Copy Diagnostics").
|
||||
|
||||
### Non-Goals
|
||||
- Changing native P/Invoke signatures (`GetVCPFeatureAndVCPFeatureReply`, `SetVCPFeature`).
|
||||
- Reworking discrete features' value enumeration (input-source / power-state value
|
||||
lists still come from the capabilities string).
|
||||
- A user-facing per-monitor code-override UI (out of scope; the registry is the source
|
||||
of candidate lists).
|
||||
- Seeding alternate codes for features other than brightness. The mechanism is general;
|
||||
only brightness is seeded with multiple candidates now (decision: "general framework,
|
||||
brightness-only multi-candidate"). All other features keep a single-candidate list
|
||||
equal to their current constant.
|
||||
|
||||
## 3. Key Decisions (confirmed)
|
||||
|
||||
1. **Resolution model = cap-string-first, probe-as-fallback.**
|
||||
- Normal mode: resolve purely from the parsed capabilities string, in priority order.
|
||||
- Max-compatibility mode: same Phase 1; **only if** the cap string lists none of a
|
||||
feature's candidates do we actively probe candidates (read-only `GetVCP`) in
|
||||
priority order and take the first that responds with a usable value.
|
||||
2. **Brightness candidate priority = `0x10` → `0x13` → `0x6B`.** Other features:
|
||||
single-candidate (their existing constant).
|
||||
3. **Invalidation = user Refresh only.** A persisted map (code or sentinel) is reused
|
||||
indefinitely; the user-initiated Refresh clears it and forces re-resolution.
|
||||
Startup and the display-change watcher reuse persisted maps without clearing.
|
||||
- Documented consequence: a feature resolved to "not supported" via cap-string-only
|
||||
(max-compat off) will **not** be auto-re-probed when max-compat is later turned on;
|
||||
the user must press Refresh. This is the intended trade-off.
|
||||
4. **Shape = general framework, brightness-only multi-candidate** (see Non-Goals).
|
||||
|
||||
## 4. Architecture
|
||||
|
||||
```
|
||||
PowerDisplay (app) PowerDisplay.Lib
|
||||
MainViewModel ── reads/writes persisted maps ──► MonitorStateManager (state file)
|
||||
│ push persisted maps + maxCompat ▲ persist resolved maps
|
||||
▼ (before discovery) │
|
||||
MonitorManager ──────────────────────────────► DdcCiController
|
||||
│ BuildMonitorFromPhysical
|
||||
│ caps + handle
|
||||
▼
|
||||
VcpFeatureResolver.Resolve(
|
||||
caps, maxCompat, persistedMap, probe)
|
||||
│ uses VcpFeatureRegistry
|
||||
▼
|
||||
Monitor.ResolvedVcpCodes (VcpFeatureCodeMap)
|
||||
│
|
||||
CreateMonitorInfo ──► MonitorInfo.ResolvedVcpCodes ─┘ (settings.json → Settings UI)
|
||||
│
|
||||
▼
|
||||
MonitorInfo.GetDiagnosticsAsText() (hex-only section)
|
||||
```
|
||||
|
||||
Resolution runs **inside the discovery pipeline** so each feature's current value is
|
||||
initialized through the correct code in a single pass. The persisted map and the
|
||||
max-compat flag are pushed into the controller before discovery (mirroring the existing
|
||||
`SetMaxCompatibilityMode`). The resolver itself is a pure function (no I/O, no
|
||||
persistence) and is fully unit-testable with a fake `probe` delegate.
|
||||
|
||||
## 5. New types (`PowerDisplay.Lib`)
|
||||
|
||||
### 5.1 `VcpFeature` (enum)
|
||||
`Brightness, Contrast, Volume, ColorTemperature, InputSource, PowerState`.
|
||||
Each has a stable persistence/diagnostic key:
|
||||
`brightness, contrast, volume, colorTemperature, inputSource, powerState`.
|
||||
|
||||
### 5.2 `VcpFeatureRegistry` (static)
|
||||
Ordered candidate codes per feature (composed from `NativeConstants` literals, which
|
||||
remain the single source of truth for the values):
|
||||
|
||||
| Feature | Candidates (priority order) |
|
||||
|------------------|-------------------------------------|
|
||||
| Brightness | `0x10` → `0x13` → `0x6B` |
|
||||
| Contrast | `0x12` |
|
||||
| Volume | `0x62` |
|
||||
| ColorTemperature | `0x14` |
|
||||
| InputSource | `0x60` |
|
||||
| PowerState | `0xD6` |
|
||||
|
||||
API: `IReadOnlyList<byte> Candidates(VcpFeature)`, `byte Primary(VcpFeature)` (= first
|
||||
candidate, used as a safe default), `IReadOnlyList<VcpFeature> AllFeatures`, plus
|
||||
`string Key(VcpFeature)` / `bool TryParseKey(string, out VcpFeature)`.
|
||||
|
||||
### 5.3 `VcpFeatureCodeMap`
|
||||
Per-monitor resolved result. Internally `Dictionary<VcpFeature, int>`:
|
||||
- value `0x00`–`0xFF` → resolved code,
|
||||
- value `NotSupportedSentinel` (`= -1`, named constant) → checked, no candidate works,
|
||||
- key absent → that feature not resolved yet.
|
||||
|
||||
The `-1` sentinel is required (not laziness): the monitor-state file serializes with
|
||||
`DefaultIgnoreCondition = WhenWritingNull`, which would silently drop a `null`-means-
|
||||
unsupported encoding and erase the distinction between "checked-unsupported" and
|
||||
"never-resolved". `-1` cannot collide with a real byte code.
|
||||
|
||||
API:
|
||||
- `int GetCode(VcpFeature)` → resolved code if supported, else `VcpFeatureRegistry.Primary`
|
||||
(callers gate on `IsSupported`; primary is a safe fallback so a byte is always returned).
|
||||
- `bool IsSupported(VcpFeature)` → has entry and entry != sentinel.
|
||||
- `bool IsResolved(VcpFeature)` → has any entry (code or sentinel).
|
||||
- `Dictionary<string,int> ToPersisted()` / `static VcpFeatureCodeMap FromPersisted(Dictionary<string,int>?)`
|
||||
(string keys via `VcpFeatureRegistry.Key`; unknown keys ignored for forward-compat).
|
||||
|
||||
### 5.4 `VcpFeatureResolver` (static, pure)
|
||||
```csharp
|
||||
static VcpFeatureCodeMap Resolve(
|
||||
VcpCapabilities caps,
|
||||
bool maxCompatibilityMode,
|
||||
VcpFeatureCodeMap? persisted, // per-feature reuse source
|
||||
Func<byte, bool> probe); // read-only GetVCP, returns true iff usable
|
||||
```
|
||||
Algorithm, per feature in `VcpFeatureRegistry.AllFeatures`:
|
||||
1. **Per-feature reuse.** If `persisted` has an entry (code or sentinel) for this
|
||||
feature → copy it verbatim (no cap-string lookup, no probe). This makes reuse
|
||||
forward-compatible: a persisted map missing a newly-added feature resolves only the
|
||||
missing one.
|
||||
2. Otherwise resolve fresh:
|
||||
1. **Phase 1 (both modes).** First candidate with `caps.SupportsVcpCode(candidate)` → code.
|
||||
2. **Phase 2 (max-compat only, fresh discovery, if Phase 1 found nothing).** Runs only when
|
||||
`persisted == null` — i.e. a first-time discovery or a post-refresh re-resolution (the user
|
||||
Refresh clears persisted maps to null). When a persisted map is supplied (a normal
|
||||
discovery that reuses prior results) probing is skipped entirely, since probing is the
|
||||
expensive path and the persisted decision already covers every feature. First candidate
|
||||
where `probe(candidate)` returns true → code.
|
||||
3. Else → `NotSupportedSentinel`.
|
||||
|
||||
> In practice a persisted map always covers all features (resolution writes every feature), so
|
||||
> the per-feature reuse at the top and the `persisted == null` guard on Phase 2 together yield:
|
||||
> known monitor → reuse, no probe; new monitor or post-refresh → full resolve incl. probe.
|
||||
|
||||
`probe` is supplied by the controller and encodes **usability**, not mere call success:
|
||||
`code => TryGetVcpFeature(handle, code, out cur, out max) && max > 0` — so brightness
|
||||
never resolves to an advertised-but-dead code (aligns with the existing
|
||||
`InitializeBrightness` range guard). Normal mode never invokes `probe`.
|
||||
|
||||
## 6. Controller changes (`DdcCiController`)
|
||||
|
||||
- Add `IReadOnlyDictionary<string, VcpFeatureCodeMap> PersistedVcpCodeMaps { get; set; }`
|
||||
(default empty), pushed before discovery like `MaxCompatibilityMode`.
|
||||
- In `BuildMonitorFromPhysical`, **immediately after `monitor.VcpCapabilitiesInfo = caps;`
|
||||
and before `UpdateMonitorCapabilitiesFromVcp` / the `Initialize*` calls** (ordering is
|
||||
load-bearing — both downstream steps now read the resolved map):
|
||||
```csharp
|
||||
PersistedVcpCodeMaps.TryGetValue(monitor.Id, out var persisted);
|
||||
monitor.ResolvedVcpCodes = VcpFeatureResolver.Resolve(
|
||||
caps, MaxCompatibilityMode, persisted,
|
||||
code => TryGetVcpFeature(physical.HPhysicalMonitor, code, monitor.Id, out var c, out var m) && m > 0);
|
||||
```
|
||||
- `UpdateMonitorCapabilitiesFromVcp`: **brightness support only** now derives from the
|
||||
resolved map — `if (monitor.ResolvedVcpCodes.IsSupported(VcpFeature.Brightness)) monitor.Capabilities |= Brightness;`.
|
||||
Contrast/volume/color-temperature support detection is **unchanged** (still cap-string
|
||||
based); those are single-candidate, so behavior is identical and risk is contained.
|
||||
- `Initialize*` and `Get*/Set*` use `monitor.ResolvedVcpCodes.GetCode(feature)` instead
|
||||
of the `NativeConstants` literal. `InitializeBrightness` reads its max via the resolved
|
||||
brightness code; percent↔raw scaling (`BrightnessVcpMax`) is unchanged. For single-
|
||||
candidate features the resolved code equals the old constant, so reads/writes are
|
||||
byte-for-byte identical to today.
|
||||
|
||||
`Monitor` (model) gains `public VcpFeatureCodeMap ResolvedVcpCodes { get; set; } = new();`.
|
||||
|
||||
## 7. Persistence (`MonitorStateEntry` + `MonitorStateManager`)
|
||||
|
||||
- `MonitorStateEntry` gains
|
||||
`[JsonPropertyName("vcpFeatureCodes")] Dictionary<string,int>? VcpFeatureCodes`
|
||||
(null when never resolved; values include the `-1` sentinel). Register
|
||||
`Dictionary<string,int>` in `MonitorStateSerializationContext`.
|
||||
- `MonitorStateManager` internal `MonitorState` mirrors the field; `LoadStateFromDisk`,
|
||||
`BuildStateJson`, `CloneState` carry it through. New API:
|
||||
- `VcpFeatureCodeMap? GetVcpCodeMap(string monitorId)`
|
||||
- `void UpdateVcpCodeMap(string monitorId, VcpFeatureCodeMap map)` — dirty-flag +
|
||||
debounced save; **skips write if the persisted dict is unchanged** (idempotent).
|
||||
- `void ClearAllVcpCodeMaps()` — nulls only `VcpFeatureCodes` on every entry (brightness/
|
||||
contrast/volume/color values are preserved), marks dirty, schedules save.
|
||||
|
||||
Example `monitor_state.json` entry:
|
||||
```json
|
||||
"\\\\?\\DISPLAY#DELD1A8#5&abc&0&UID1": {
|
||||
"brightness": 75,
|
||||
"vcpFeatureCodes": { "brightness": 107, "contrast": 18, "volume": -1,
|
||||
"colorTemperature": 20, "inputSource": 96, "powerState": 214 },
|
||||
"lastUpdated": "2026-06-30T10:30:45.1"
|
||||
}
|
||||
```
|
||||
(`107`=`0x6B`, `18`=`0x12`, `-1`=not supported, `20`=`0x14`, `96`=`0x60`, `214`=`0xD6`.)
|
||||
|
||||
## 8. App orchestration (`MainViewModel`)
|
||||
|
||||
- **Before discovery** (`InitializeAsync` and `RefreshMonitorsAsync`): build
|
||||
`Dictionary<string, VcpFeatureCodeMap>` from `_stateManager` for all known monitors and
|
||||
push via `_monitorManager.SetPersistedVcpCodeMaps(maps)` (new pass-through to the DDC
|
||||
controller), alongside the existing `SetMaxCompatibilityMode`.
|
||||
- **After discovery** (in `UpdateMonitorList`, where `SaveMonitorsToSettings` already
|
||||
runs): for each discovered monitor call
|
||||
`_stateManager.UpdateVcpCodeMap(monitor.Id, monitor.ResolvedVcpCodes)` (idempotent).
|
||||
- **Refresh clears**: `RefreshMonitorsAsync` calls `_stateManager.ClearAllVcpCodeMaps()`
|
||||
and pushes empty maps **before** re-discovery, forcing full re-resolution. Startup and
|
||||
the watcher path do **not** clear.
|
||||
- The "Refresh" the user means is the existing user-initiated refresh command that calls
|
||||
`RefreshMonitorsAsync` (confirm the exact command/button binding during implementation;
|
||||
the display-change-watcher entry must remain on the reuse path).
|
||||
|
||||
## 9. Diagnostics (`MonitorInfo` + `CreateMonitorInfo`)
|
||||
|
||||
- `MonitorInfo` (Settings.UI.Library) gains
|
||||
`[JsonPropertyName("resolvedVcpCodes")] Dictionary<string,int> ResolvedVcpCodes`
|
||||
and copies it in `UpdateFrom`. Register the dict type in the settings source-gen context.
|
||||
- App `CreateMonitorInfo` populates it from `monitor.ResolvedVcpCodes.ToPersisted()`.
|
||||
- `GetDiagnosticsAsText()` inserts a section between "Detected support" and
|
||||
"Raw capabilities":
|
||||
```
|
||||
Resolved feature -> VCP code
|
||||
--------------------------------------------------
|
||||
Brightness: 0x6B
|
||||
Contrast: 0x12
|
||||
Volume: not supported
|
||||
ColorTemperature: 0x14
|
||||
InputSource: 0x60
|
||||
PowerState: 0xD6
|
||||
```
|
||||
**Hex-only, no friendly names.** `GetDiagnosticsAsText` lives in
|
||||
`Settings.UI.Library`, which must not take a binary dependency on `PowerDisplay.Lib`
|
||||
(where `VcpNames` lives) — that pattern has crashed root apps before. Code names are
|
||||
still available to the reader: the existing `GetVcpCodesAsText()` block below already
|
||||
lists every detected code with its name. Sentinel renders `not supported`; an absent
|
||||
entry renders `not resolved`.
|
||||
|
||||
## 10. Tests (`PowerDisplay.Lib.UnitTests`, MSTest + Moq)
|
||||
|
||||
- `VcpFeatureResolverTests`:
|
||||
- Phase-1 priority: caps with `0x10` → brightness `0x10`; caps with only `0x6B` →
|
||||
brightness `0x6B`; caps with `0x13`+`0x6B` → `0x13` (priority).
|
||||
- Normal mode never probes: assert the `probe` delegate is invoked zero times.
|
||||
- Max-compat probe fallback: caps lists no brightness candidate, `probe(0x6B)` true →
|
||||
`0x6B`; assert probe call order honors priority and stops at first success.
|
||||
- Probe usability: `probe` returns false when `max == 0` → not chosen.
|
||||
- All-miss → sentinel.
|
||||
- Per-feature reuse: a persisted entry is copied verbatim and `probe`/caps are not
|
||||
consulted for that feature; a partially-populated persisted map resolves only the
|
||||
missing features.
|
||||
- `VcpFeatureCodeMapTests`: `ToPersisted`/`FromPersisted` round-trip, sentinel semantics,
|
||||
unknown-key tolerance, `GetCode` fallback to `Primary`.
|
||||
- `MonitorStateManager` round-trip test extended to assert `vcpFeatureCodes` survives
|
||||
save/load and `ClearAllVcpCodeMaps` preserves the value fields.
|
||||
|
||||
## 11. Edge cases & notes
|
||||
|
||||
- **Cap-string totally unreadable + max-compat**: existing `ProbeSupportedVcpFeatures`
|
||||
reconstructs a synthetic caps object from `{0x10,0x12,0x62}` only. Our resolver then
|
||||
runs on that synthetic caps; brightness alternates (`0x13`,`0x6B`) are found via our
|
||||
Phase-2 probe. This causes at most 1–2 redundant `GetVCP` reads on `0x10` in that rare
|
||||
path — accepted, not optimized.
|
||||
- **Discrete features**: `GetCode` returns the standard code; value lists
|
||||
(`SupportedInputSources`, color presets) still come from `caps`. The resolved map only
|
||||
governs which code we read/write.
|
||||
- **Forward/backward compat**: new JSON fields are additive. Old state/settings files
|
||||
lack them → treated as "never resolved" → resolved on next discovery. New files read by
|
||||
an older build → unknown fields ignored.
|
||||
- **Concurrency**: discovery resolves per-monitor concurrently; the pushed persisted map
|
||||
and static registry are read-only during discovery; write-back is single-threaded in the
|
||||
app. No shared mutable state.
|
||||
- **No new external dependencies**; no ABI/native-signature changes; settings/IPC schema
|
||||
only gains additive fields.
|
||||
|
||||
## 12. Out-of-scope follow-ups (not in this change)
|
||||
- Friendly code names in the diagnostics "Resolved" section (would need a shared
|
||||
code-name table in `PowerDisplay.Models`, referenced by both Lib and Settings.UI.Library).
|
||||
- Seeding multi-candidate lists for non-brightness features (extend `VcpFeatureRegistry`).
|
||||
- A per-monitor manual code-override UI.
|
||||
1518
doc/specs/2026-06-30-powerdisplay-vcp-code-resolution-plan.md
Normal file
1518
doc/specs/2026-06-30-powerdisplay-vcp-code-resolution-plan.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using PowerDisplay.Common.Models;
|
||||
using PowerDisplay.Common.Serialization;
|
||||
|
||||
namespace PowerDisplay.UnitTests;
|
||||
|
||||
/// <summary>Serialization tests for the MonitorStateEntry VCP-code persistence field.</summary>
|
||||
[TestClass]
|
||||
public class MonitorStateEntrySerializationTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void VcpFeatureCodes_RoundTripsThroughStateContext()
|
||||
{
|
||||
var entry = new MonitorStateEntry
|
||||
{
|
||||
Brightness = 75,
|
||||
VcpFeatureCodes = new Dictionary<string, int> { ["brightness"] = 0x6B, ["volume"] = -1 },
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(entry, MonitorStateSerializationContext.Default.MonitorStateEntry);
|
||||
var roundTripped = JsonSerializer.Deserialize(json, MonitorStateSerializationContext.Default.MonitorStateEntry);
|
||||
|
||||
Assert.IsNotNull(roundTripped);
|
||||
Assert.IsNotNull(roundTripped!.VcpFeatureCodes);
|
||||
Assert.AreEqual(0x6B, roundTripped.VcpFeatureCodes!["brightness"]);
|
||||
Assert.AreEqual(-1, roundTripped.VcpFeatureCodes["volume"]);
|
||||
StringAssert.Contains(json, "vcpFeatureCodes");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NullVcpFeatureCodes_OmittedFromJson()
|
||||
{
|
||||
var entry = new MonitorStateEntry { Brightness = 50 };
|
||||
var json = JsonSerializer.Serialize(entry, MonitorStateSerializationContext.Default.MonitorStateEntry);
|
||||
Assert.IsFalse(json.Contains("vcpFeatureCodes"), "Null map must be omitted (WhenWritingNull).");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using PowerDisplay.Common.Models;
|
||||
using PowerDisplay.Common.Utils;
|
||||
|
||||
namespace PowerDisplay.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class VcpFeatureCodeMapTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void SetCode_MarksSupported_AndReturnsCode()
|
||||
{
|
||||
var map = new VcpFeatureCodeMap();
|
||||
map.SetCode(VcpFeature.Brightness, 0x6B);
|
||||
|
||||
Assert.IsTrue(map.IsResolved(VcpFeature.Brightness));
|
||||
Assert.IsTrue(map.IsSupported(VcpFeature.Brightness));
|
||||
Assert.AreEqual((byte)0x6B, map.GetCode(VcpFeature.Brightness));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SetNotSupported_IsResolvedButNotSupported()
|
||||
{
|
||||
var map = new VcpFeatureCodeMap();
|
||||
map.SetNotSupported(VcpFeature.Volume);
|
||||
|
||||
Assert.IsTrue(map.IsResolved(VcpFeature.Volume));
|
||||
Assert.IsFalse(map.IsSupported(VcpFeature.Volume));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetCode_WhenUnresolved_FallsBackToRegistryPrimary()
|
||||
{
|
||||
var map = new VcpFeatureCodeMap();
|
||||
Assert.AreEqual(VcpFeatureRegistry.Primary(VcpFeature.Brightness), map.GetCode(VcpFeature.Brightness));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetCode_WhenNotSupported_FallsBackToRegistryPrimary()
|
||||
{
|
||||
var map = new VcpFeatureCodeMap();
|
||||
map.SetNotSupported(VcpFeature.Brightness);
|
||||
Assert.AreEqual(VcpFeatureRegistry.Primary(VcpFeature.Brightness), map.GetCode(VcpFeature.Brightness));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ToPersisted_UsesStringKeysAndSentinel()
|
||||
{
|
||||
var map = new VcpFeatureCodeMap();
|
||||
map.SetCode(VcpFeature.Brightness, 0x6B);
|
||||
map.SetNotSupported(VcpFeature.Volume);
|
||||
|
||||
var persisted = map.ToPersisted();
|
||||
|
||||
Assert.AreEqual(0x6B, persisted["brightness"]);
|
||||
Assert.AreEqual(-1, persisted["volume"]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FromPersisted_RoundTripsValuesAndSentinel()
|
||||
{
|
||||
var source = new Dictionary<string, int> { ["brightness"] = 0x13, ["contrast"] = -1 };
|
||||
var map = VcpFeatureCodeMap.FromPersisted(source);
|
||||
|
||||
Assert.IsTrue(map.IsSupported(VcpFeature.Brightness));
|
||||
Assert.AreEqual((byte)0x13, map.GetCode(VcpFeature.Brightness));
|
||||
Assert.IsTrue(map.IsResolved(VcpFeature.Contrast));
|
||||
Assert.IsFalse(map.IsSupported(VcpFeature.Contrast));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FromPersisted_Null_ReturnsEmptyMap()
|
||||
{
|
||||
var map = VcpFeatureCodeMap.FromPersisted(null);
|
||||
Assert.IsFalse(map.IsResolved(VcpFeature.Brightness));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FromPersisted_IgnoresUnknownKeys()
|
||||
{
|
||||
var map = VcpFeatureCodeMap.FromPersisted(new Dictionary<string, int> { ["future"] = 5 });
|
||||
Assert.IsFalse(map.IsResolved(VcpFeature.Brightness));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FromPersisted_OutOfRangeValues_AreIgnored()
|
||||
{
|
||||
var map = VcpFeatureCodeMap.FromPersisted(new Dictionary<string, int>
|
||||
{
|
||||
["brightness"] = 300, // > 255
|
||||
["contrast"] = -2, // negative, not the sentinel
|
||||
["volume"] = -1, // valid sentinel
|
||||
["colorTemperature"] = 0x14, // valid code
|
||||
});
|
||||
|
||||
Assert.IsFalse(map.IsResolved(VcpFeature.Brightness));
|
||||
Assert.IsFalse(map.IsResolved(VcpFeature.Contrast));
|
||||
Assert.IsTrue(map.IsResolved(VcpFeature.Volume));
|
||||
Assert.IsFalse(map.IsSupported(VcpFeature.Volume));
|
||||
Assert.AreEqual((byte)0x14, map.GetCode(VcpFeature.ColorTemperature));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using PowerDisplay.Common.Models;
|
||||
using PowerDisplay.Common.Utils;
|
||||
|
||||
namespace PowerDisplay.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class VcpFeatureRegistryTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void Brightness_HasThreeCandidatesInPriorityOrder()
|
||||
{
|
||||
CollectionAssert.AreEqual(
|
||||
new byte[] { 0x10, 0x13, 0x6B },
|
||||
new List<byte>(VcpFeatureRegistry.Candidates(VcpFeature.Brightness)));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SingleCandidateFeatures_MatchTheirStandardCode()
|
||||
{
|
||||
Assert.AreEqual((byte)0x12, VcpFeatureRegistry.Primary(VcpFeature.Contrast));
|
||||
Assert.AreEqual((byte)0x62, VcpFeatureRegistry.Primary(VcpFeature.Volume));
|
||||
Assert.AreEqual((byte)0x14, VcpFeatureRegistry.Primary(VcpFeature.ColorTemperature));
|
||||
Assert.AreEqual((byte)0x60, VcpFeatureRegistry.Primary(VcpFeature.InputSource));
|
||||
Assert.AreEqual((byte)0xD6, VcpFeatureRegistry.Primary(VcpFeature.PowerState));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Primary_IsFirstCandidate()
|
||||
{
|
||||
Assert.AreEqual((byte)0x10, VcpFeatureRegistry.Primary(VcpFeature.Brightness));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AllFeatures_ContainsEverySixFeatures()
|
||||
{
|
||||
Assert.AreEqual(6, VcpFeatureRegistry.AllFeatures.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Key_RoundTripsThroughTryParseKey()
|
||||
{
|
||||
foreach (var feature in VcpFeatureRegistry.AllFeatures)
|
||||
{
|
||||
Assert.IsTrue(VcpFeatureRegistry.TryParseKey(VcpFeatureRegistry.Key(feature), out var parsed));
|
||||
Assert.AreEqual(feature, parsed);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TryParseKey_UnknownKey_ReturnsFalse()
|
||||
{
|
||||
Assert.IsFalse(VcpFeatureRegistry.TryParseKey("nonsense", out _));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using PowerDisplay.Common.Models;
|
||||
using PowerDisplay.Common.Utils;
|
||||
|
||||
namespace PowerDisplay.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class VcpFeatureResolverTests
|
||||
{
|
||||
private static VcpCapabilities CapsWith(params byte[] codes)
|
||||
{
|
||||
var caps = new VcpCapabilities();
|
||||
foreach (var code in codes)
|
||||
{
|
||||
caps.SupportedVcpCodes[code] = new VcpCodeInfo(code, "test");
|
||||
}
|
||||
|
||||
return caps;
|
||||
}
|
||||
|
||||
private static Func<byte, bool> NeverProbe(List<byte> log) => code =>
|
||||
{
|
||||
log.Add(code);
|
||||
return false;
|
||||
};
|
||||
|
||||
[TestMethod]
|
||||
public void Phase1_PicksFirstCandidatePresentInCaps()
|
||||
{
|
||||
var caps = CapsWith(0x10, 0x12, 0x62);
|
||||
var probeLog = new List<byte>();
|
||||
|
||||
var map = VcpFeatureResolver.Resolve(caps, maxCompatibilityMode: false, persisted: null, probe: NeverProbe(probeLog));
|
||||
|
||||
Assert.AreEqual((byte)0x10, map.GetCode(VcpFeature.Brightness));
|
||||
Assert.AreEqual(0, probeLog.Count, "Normal mode must never probe.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Phase1_FallsToLowerPriorityCandidate()
|
||||
{
|
||||
// 0x10 absent, 0x6B present -> brightness resolves to 0x6B.
|
||||
var caps = CapsWith(0x6B);
|
||||
|
||||
var map = VcpFeatureResolver.Resolve(caps, maxCompatibilityMode: false, persisted: null, probe: _ => false);
|
||||
|
||||
Assert.AreEqual((byte)0x6B, map.GetCode(VcpFeature.Brightness));
|
||||
Assert.IsTrue(map.IsSupported(VcpFeature.Brightness));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Phase1_HonorsPriorityWhenMultiplePresent()
|
||||
{
|
||||
// VcpFeatureRegistry.Candidates(VcpFeature.Brightness) == [0x10, 0x13, 0x6B] (priority order).
|
||||
var caps = CapsWith(0x13, 0x6B); // both alternates present, no 0x10
|
||||
var map = VcpFeatureResolver.Resolve(caps, maxCompatibilityMode: false, persisted: null, probe: _ => false);
|
||||
Assert.AreEqual((byte)0x13, map.GetCode(VcpFeature.Brightness));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NormalMode_NoCandidate_ResolvesNotSupported_WithoutProbing()
|
||||
{
|
||||
var caps = CapsWith(0x12); // contrast only; brightness candidates absent
|
||||
var probeLog = new List<byte>();
|
||||
|
||||
var map = VcpFeatureResolver.Resolve(caps, maxCompatibilityMode: false, persisted: null, probe: NeverProbe(probeLog));
|
||||
|
||||
Assert.IsFalse(map.IsSupported(VcpFeature.Brightness));
|
||||
Assert.IsTrue(map.IsResolved(VcpFeature.Brightness));
|
||||
Assert.AreEqual(0, probeLog.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MaxCompat_ProbesInPriorityOrder_AndStopsAtFirstSuccess()
|
||||
{
|
||||
// VcpFeatureRegistry.Candidates(VcpFeature.Brightness) == [0x10, 0x13, 0x6B] (priority order).
|
||||
var caps = CapsWith(); // empty: no candidate for any feature
|
||||
var probeLog = new List<byte>();
|
||||
Func<byte, bool> probe = code =>
|
||||
{
|
||||
probeLog.Add(code);
|
||||
return code == 0x6B; // only the third brightness candidate responds
|
||||
};
|
||||
|
||||
var map = VcpFeatureResolver.Resolve(caps, maxCompatibilityMode: true, persisted: null, probe: probe);
|
||||
|
||||
Assert.AreEqual((byte)0x6B, map.GetCode(VcpFeature.Brightness));
|
||||
|
||||
// Assert the first 3 brightness probes happened in order 0x10, 0x13, 0x6B (stops at 0x6B for brightness).
|
||||
CollectionAssert.AreEqual(new byte[] { 0x10, 0x13, 0x6B }, probeLog.GetRange(0, 3));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MaxCompat_NoCandidateResponds_ResolvesNotSupported()
|
||||
{
|
||||
var caps = CapsWith();
|
||||
var map = VcpFeatureResolver.Resolve(caps, maxCompatibilityMode: true, persisted: null, probe: _ => false);
|
||||
Assert.IsFalse(map.IsSupported(VcpFeature.Brightness));
|
||||
Assert.IsTrue(map.IsResolved(VcpFeature.Brightness));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Persisted_IsReusedVerbatim_WithoutCapsOrProbe()
|
||||
{
|
||||
var persisted = new VcpFeatureCodeMap();
|
||||
persisted.SetCode(VcpFeature.Brightness, 0x6B);
|
||||
persisted.SetNotSupported(VcpFeature.Volume);
|
||||
|
||||
var probeLog = new List<byte>();
|
||||
|
||||
// caps says brightness is on 0x10, but persisted (0x6B) must win.
|
||||
var map = VcpFeatureResolver.Resolve(CapsWith(0x10, 0x62), maxCompatibilityMode: true, persisted: persisted, probe: NeverProbe(probeLog));
|
||||
|
||||
Assert.AreEqual((byte)0x6B, map.GetCode(VcpFeature.Brightness));
|
||||
Assert.IsFalse(map.IsSupported(VcpFeature.Volume));
|
||||
Assert.IsTrue(map.IsResolved(VcpFeature.Volume));
|
||||
Assert.AreEqual(0, probeLog.Count, "Reused features must not be probed.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Persisted_PartialMap_ResolvesOnlyMissingFeatures()
|
||||
{
|
||||
var persisted = new VcpFeatureCodeMap();
|
||||
persisted.SetCode(VcpFeature.Brightness, 0x6B); // only brightness persisted
|
||||
|
||||
// contrast resolved fresh from caps; brightness reused.
|
||||
var map = VcpFeatureResolver.Resolve(CapsWith(0x12), maxCompatibilityMode: false, persisted: persisted, probe: _ => false);
|
||||
|
||||
Assert.AreEqual((byte)0x6B, map.GetCode(VcpFeature.Brightness));
|
||||
Assert.IsTrue(map.IsSupported(VcpFeature.Contrast));
|
||||
Assert.AreEqual((byte)0x12, map.GetCode(VcpFeature.Contrast));
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,14 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
/// </summary>
|
||||
public bool MaxCompatibilityMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the per-monitor persisted resolved-code maps (keyed by <c>Monitor.Id</c>),
|
||||
/// pushed by <see cref="MonitorManager"/> before each discovery. A monitor present here
|
||||
/// reuses its persisted decision instead of re-resolving/probing. Default: empty.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, VcpFeatureCodeMap> PersistedVcpCodeMaps { get; set; }
|
||||
= new Dictionary<string, VcpFeatureCodeMap>(MonitorIdComparer.Instance);
|
||||
|
||||
public DdcCiController()
|
||||
{
|
||||
_discoveryHelper = new MonitorDiscoveryHelper();
|
||||
@@ -55,7 +63,7 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
public async Task<VcpFeatureValue> GetBrightnessAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(monitor);
|
||||
return await GetVcpFeatureAsync(monitor, VcpCodeBrightness, cancellationToken);
|
||||
return await GetVcpFeatureAsync(monitor, monitor.ResolvedVcpCodes.GetCode(VcpFeature.Brightness), cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -63,14 +71,14 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(monitor);
|
||||
var raw = VcpFeatureValue.FromPercentage(brightness, monitor.BrightnessVcpMax);
|
||||
return SetVcpFeatureAsync(monitor, NativeConstants.VcpCodeBrightness, raw, cancellationToken);
|
||||
return SetVcpFeatureAsync(monitor, monitor.ResolvedVcpCodes.GetCode(VcpFeature.Brightness), raw, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<VcpFeatureValue> GetContrastAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(monitor);
|
||||
return await GetVcpFeatureAsync(monitor, VcpCodeContrast, cancellationToken);
|
||||
return await GetVcpFeatureAsync(monitor, monitor.ResolvedVcpCodes.GetCode(VcpFeature.Contrast), cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -78,14 +86,14 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(monitor);
|
||||
var raw = VcpFeatureValue.FromPercentage(contrast, monitor.ContrastVcpMax);
|
||||
return SetVcpFeatureAsync(monitor, NativeConstants.VcpCodeContrast, raw, cancellationToken);
|
||||
return SetVcpFeatureAsync(monitor, monitor.ResolvedVcpCodes.GetCode(VcpFeature.Contrast), raw, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<VcpFeatureValue> GetVolumeAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(monitor);
|
||||
return await GetVcpFeatureAsync(monitor, VcpCodeVolume, cancellationToken);
|
||||
return await GetVcpFeatureAsync(monitor, monitor.ResolvedVcpCodes.GetCode(VcpFeature.Volume), cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -93,57 +101,57 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(monitor);
|
||||
var raw = VcpFeatureValue.FromPercentage(volume, monitor.VolumeVcpMax);
|
||||
return SetVcpFeatureAsync(monitor, NativeConstants.VcpCodeVolume, raw, cancellationToken);
|
||||
return SetVcpFeatureAsync(monitor, monitor.ResolvedVcpCodes.GetCode(VcpFeature.Volume), raw, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get monitor color temperature using VCP code 0x14 (Select Color Preset)
|
||||
/// Returns the raw VCP preset value (e.g., 0x05 for 6500K), not Kelvin temperature
|
||||
/// Get monitor color temperature using the resolved color-temperature VCP code.
|
||||
/// Returns the raw VCP preset value (e.g., 0x05 for 6500K), not Kelvin temperature.
|
||||
/// </summary>
|
||||
public async Task<VcpFeatureValue> GetColorTemperatureAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(monitor);
|
||||
return await GetVcpFeatureAsync(monitor, VcpCodeSelectColorPreset, cancellationToken);
|
||||
return await GetVcpFeatureAsync(monitor, monitor.ResolvedVcpCodes.GetCode(VcpFeature.ColorTemperature), cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set monitor color temperature using VCP code 0x14 (Select Color Preset)
|
||||
/// Set monitor color temperature using the resolved color-temperature VCP code.
|
||||
/// </summary>
|
||||
public Task<MonitorOperationResult> SetColorTemperatureAsync(Monitor monitor, int colorTemperature, CancellationToken cancellationToken = default)
|
||||
=> SetVcpFeatureAsync(monitor, VcpCodeSelectColorPreset, colorTemperature, cancellationToken);
|
||||
=> SetVcpFeatureAsync(monitor, monitor.ResolvedVcpCodes.GetCode(VcpFeature.ColorTemperature), colorTemperature, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Get current input source using VCP code 0x60
|
||||
/// Returns the raw VCP value (e.g., 0x11 for HDMI-1)
|
||||
/// Get current input source using the resolved input-source VCP code.
|
||||
/// Returns the raw VCP value (e.g., 0x11 for HDMI-1).
|
||||
/// </summary>
|
||||
public async Task<VcpFeatureValue> GetInputSourceAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(monitor);
|
||||
return await GetVcpFeatureAsync(monitor, VcpCodeInputSource, cancellationToken);
|
||||
return await GetVcpFeatureAsync(monitor, monitor.ResolvedVcpCodes.GetCode(VcpFeature.InputSource), cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set input source using VCP code 0x60
|
||||
/// Set input source using the resolved input-source VCP code.
|
||||
/// </summary>
|
||||
public Task<MonitorOperationResult> SetInputSourceAsync(Monitor monitor, int inputSource, CancellationToken cancellationToken = default)
|
||||
=> SetVcpFeatureAsync(monitor, VcpCodeInputSource, inputSource, cancellationToken);
|
||||
=> SetVcpFeatureAsync(monitor, monitor.ResolvedVcpCodes.GetCode(VcpFeature.InputSource), inputSource, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Set power state using VCP code 0xD6 (Power Mode).
|
||||
/// Set power state for a monitor using the resolved power-state VCP code.
|
||||
/// Values: 0x01=On, 0x02=Standby, 0x03=Suspend, 0x04=Off(DPM), 0x05=Off(Hard).
|
||||
/// Note: Setting any value other than 0x01 (On) will turn off the display.
|
||||
/// </summary>
|
||||
public Task<MonitorOperationResult> SetPowerStateAsync(Monitor monitor, int powerState, CancellationToken cancellationToken = default)
|
||||
=> SetVcpFeatureAsync(monitor, VcpCodePowerMode, powerState, cancellationToken);
|
||||
=> SetVcpFeatureAsync(monitor, monitor.ResolvedVcpCodes.GetCode(VcpFeature.PowerState), powerState, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Get current power state using VCP code 0xD6 (Power Mode).
|
||||
/// Get current power state for a monitor using the resolved power-state VCP code.
|
||||
/// Returns the raw VCP value (0x01=On, 0x02=Standby, etc.)
|
||||
/// </summary>
|
||||
public async Task<VcpFeatureValue> GetPowerStateAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(monitor);
|
||||
return await GetVcpFeatureAsync(monitor, VcpCodePowerMode, cancellationToken);
|
||||
return await GetVcpFeatureAsync(monitor, monitor.ResolvedVcpCodes.GetCode(VcpFeature.PowerState), cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -305,6 +313,16 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
}
|
||||
|
||||
monitor.VcpCapabilitiesInfo = caps;
|
||||
|
||||
// Resolve each feature to a concrete VCP code BEFORE capability flags and
|
||||
// value initialization, both of which now read monitor.ResolvedVcpCodes.
|
||||
PersistedVcpCodeMaps.TryGetValue(monitor.Id, out var persistedCodeMap);
|
||||
monitor.ResolvedVcpCodes = VcpFeatureResolver.Resolve(
|
||||
caps,
|
||||
MaxCompatibilityMode,
|
||||
persistedCodeMap,
|
||||
code => TryGetVcpFeature(physical.HPhysicalMonitor, code, monitor.Id, out _, out var max) && max > 0);
|
||||
|
||||
UpdateMonitorCapabilitiesFromVcp(monitor, caps);
|
||||
|
||||
// Initialize current values for every VCP feature the device reports
|
||||
@@ -437,22 +455,24 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize input source value for a monitor using VCP 0x60.
|
||||
/// Initialize input source value for a monitor using the resolved input-source VCP code.
|
||||
/// </summary>
|
||||
private static void InitializeInputSource(Monitor monitor, IntPtr handle)
|
||||
{
|
||||
if (TryGetVcpFeature(handle, VcpCodeInputSource, monitor.Id, out uint current, out uint _))
|
||||
var code = monitor.ResolvedVcpCodes.GetCode(VcpFeature.InputSource);
|
||||
if (TryGetVcpFeature(handle, code, monitor.Id, out uint current, out uint _))
|
||||
{
|
||||
monitor.CurrentInputSource = (int)current;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize color temperature value for a monitor using VCP 0x14.
|
||||
/// Initialize color temperature value for a monitor using the resolved color-temperature VCP code.
|
||||
/// </summary>
|
||||
private static void InitializeColorTemperature(Monitor monitor, IntPtr handle)
|
||||
{
|
||||
if (TryGetVcpFeature(handle, VcpCodeSelectColorPreset, monitor.Id, out uint current, out uint _))
|
||||
var code = monitor.ResolvedVcpCodes.GetCode(VcpFeature.ColorTemperature);
|
||||
if (TryGetVcpFeature(handle, code, monitor.Id, out uint current, out uint _))
|
||||
{
|
||||
monitor.CurrentColorTemperature = (int)current;
|
||||
}
|
||||
@@ -463,19 +483,21 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
/// </summary>
|
||||
private static void InitializePowerState(Monitor monitor, IntPtr handle)
|
||||
{
|
||||
if (TryGetVcpFeature(handle, VcpCodePowerMode, monitor.Id, out uint current, out uint _))
|
||||
var code = monitor.ResolvedVcpCodes.GetCode(VcpFeature.PowerState);
|
||||
if (TryGetVcpFeature(handle, code, monitor.Id, out uint current, out uint _))
|
||||
{
|
||||
monitor.CurrentPowerState = (int)current;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize brightness value for a monitor using VCP 0x10.
|
||||
/// Initialize brightness value for a monitor using the resolved brightness VCP code.
|
||||
/// Persists the device-reported raw maximum so subsequent writes can scale percent → raw.
|
||||
/// </summary>
|
||||
private static void InitializeBrightness(Monitor monitor, IntPtr handle)
|
||||
{
|
||||
if (TryGetVcpFeature(handle, VcpCodeBrightness, monitor.Id, out uint current, out uint max))
|
||||
var code = monitor.ResolvedVcpCodes.GetCode(VcpFeature.Brightness);
|
||||
if (TryGetVcpFeature(handle, code, monitor.Id, out uint current, out uint max))
|
||||
{
|
||||
var brightnessInfo = new VcpFeatureValue((int)current, 0, (int)max);
|
||||
if (!brightnessInfo.IsValid)
|
||||
@@ -491,12 +513,13 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize contrast value for a monitor using VCP 0x12.
|
||||
/// Initialize contrast value for a monitor using the resolved contrast VCP code.
|
||||
/// Persists the device-reported raw maximum so subsequent writes can scale percent → raw.
|
||||
/// </summary>
|
||||
private static void InitializeContrast(Monitor monitor, IntPtr handle)
|
||||
{
|
||||
if (TryGetVcpFeature(handle, VcpCodeContrast, monitor.Id, out uint current, out uint max))
|
||||
var code = monitor.ResolvedVcpCodes.GetCode(VcpFeature.Contrast);
|
||||
if (TryGetVcpFeature(handle, code, monitor.Id, out uint current, out uint max))
|
||||
{
|
||||
monitor.ContrastVcpMax = (int)max;
|
||||
var contrastInfo = new VcpFeatureValue((int)current, 0, (int)max);
|
||||
@@ -505,12 +528,13 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize volume value for a monitor using VCP 0x62.
|
||||
/// Initialize volume value for a monitor using the resolved volume VCP code.
|
||||
/// Persists the device-reported raw maximum so subsequent writes can scale percent → raw.
|
||||
/// </summary>
|
||||
private static void InitializeVolume(Monitor monitor, IntPtr handle)
|
||||
{
|
||||
if (TryGetVcpFeature(handle, VcpCodeVolume, monitor.Id, out uint current, out uint max))
|
||||
var code = monitor.ResolvedVcpCodes.GetCode(VcpFeature.Volume);
|
||||
if (TryGetVcpFeature(handle, code, monitor.Id, out uint current, out uint max))
|
||||
{
|
||||
monitor.VolumeVcpMax = (int)max;
|
||||
var volumeInfo = new VcpFeatureValue((int)current, 0, (int)max);
|
||||
@@ -545,8 +569,9 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
/// </summary>
|
||||
private static void UpdateMonitorCapabilitiesFromVcp(Monitor monitor, VcpCapabilities vcpCaps)
|
||||
{
|
||||
// Check for Brightness support (VCP 0x10)
|
||||
if (vcpCaps.SupportsVcpCode(VcpCodeBrightness))
|
||||
// Brightness support derives from the resolved map so a monitor that exposes
|
||||
// brightness only via an alternate code (0x13/0x6B) is still recognized.
|
||||
if (monitor.ResolvedVcpCodes.IsSupported(VcpFeature.Brightness))
|
||||
{
|
||||
monitor.Capabilities |= MonitorCapabilities.Brightness;
|
||||
}
|
||||
|
||||
@@ -12,10 +12,22 @@ namespace PowerDisplay.Common.Drivers
|
||||
/// <summary>
|
||||
/// VCP code: Brightness (0x10)
|
||||
/// Standard VESA MCCS brightness control.
|
||||
/// This is the ONLY brightness code used by PowerDisplay.
|
||||
/// Primary brightness candidate; see also VcpCodeBacklightControl (0x13) and VcpCodeBacklightLevelWhite (0x6B).
|
||||
/// </summary>
|
||||
public const byte VcpCodeBrightness = 0x10;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Backlight Control (0x13).
|
||||
/// Alternate brightness control used by some panels that do not implement 0x10.
|
||||
/// </summary>
|
||||
public const byte VcpCodeBacklightControl = 0x13;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Backlight Level: White (0x6B).
|
||||
/// Alternate brightness control used by some panels that expose the backlight directly.
|
||||
/// </summary>
|
||||
public const byte VcpCodeBacklightLevelWhite = 0x6B;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Contrast (0x12)
|
||||
/// Standard VESA MCCS contrast control.
|
||||
|
||||
@@ -264,6 +264,13 @@ namespace PowerDisplay.Common.Models
|
||||
/// </summary>
|
||||
public VcpCapabilities? VcpCapabilitiesInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the per-feature resolved VCP code map for this monitor, produced by
|
||||
/// <see cref="PowerDisplay.Common.Utils.VcpFeatureResolver"/> during discovery and
|
||||
/// persisted via <c>MonitorStateManager</c>.
|
||||
/// </summary>
|
||||
public VcpFeatureCodeMap ResolvedVcpCodes { get; set; } = new();
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace PowerDisplay.Common.Models
|
||||
@@ -43,6 +44,14 @@ namespace PowerDisplay.Common.Models
|
||||
[JsonPropertyName("capabilitiesRaw")]
|
||||
public string? CapabilitiesRaw { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the resolved feature-to-VCP-code map (JSON key = feature name,
|
||||
/// value = VCP code, or <c>-1</c> = checked-but-unsupported). <c>null</c> when the
|
||||
/// monitor's features have never been resolved.
|
||||
/// </summary>
|
||||
[JsonPropertyName("vcpFeatureCodes")]
|
||||
public Dictionary<string, int>? VcpFeatureCodes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets when this entry was last updated.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace PowerDisplay.Common.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Logical monitor features that may map to one of several candidate VCP codes.
|
||||
/// </summary>
|
||||
public enum VcpFeature
|
||||
{
|
||||
Brightness,
|
||||
Contrast,
|
||||
Volume,
|
||||
ColorTemperature,
|
||||
InputSource,
|
||||
PowerState,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using PowerDisplay.Common.Utils;
|
||||
|
||||
namespace PowerDisplay.Common.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Per-monitor resolved VCP code for each <see cref="VcpFeature"/>:
|
||||
/// a real code (0x00-0xFF), the <see cref="NotSupportedSentinel"/> (checked, no
|
||||
/// candidate worked), or absent (not yet resolved). Persisted as
|
||||
/// <c>Dictionary<string,int></c> keyed by <see cref="VcpFeatureRegistry.Key"/>.
|
||||
/// </summary>
|
||||
public sealed class VcpFeatureCodeMap
|
||||
{
|
||||
/// <summary>
|
||||
/// Value stored for a feature that was checked but has no usable candidate code.
|
||||
/// A non-null sentinel is required because the monitor-state file serializes with
|
||||
/// <c>WhenWritingNull</c>, which would drop a null and lose the "checked" fact.
|
||||
/// </summary>
|
||||
public const int NotSupportedSentinel = -1;
|
||||
|
||||
private readonly Dictionary<VcpFeature, int> _codes = new();
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see langword="true"/> if <paramref name="feature"/> has been resolved
|
||||
/// (supported or explicitly marked not supported); <see langword="false"/> if still
|
||||
/// pending resolution.
|
||||
/// </summary>
|
||||
/// <param name="feature">The feature to check.</param>
|
||||
/// <returns><see langword="true"/> when a resolution result has been stored.</returns>
|
||||
public bool IsResolved(VcpFeature feature) => _codes.ContainsKey(feature);
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see langword="true"/> if <paramref name="feature"/> was resolved to a
|
||||
/// usable VCP code (i.e., not <see cref="NotSupportedSentinel"/>).
|
||||
/// </summary>
|
||||
/// <param name="feature">The feature to check.</param>
|
||||
/// <returns><see langword="true"/> when a real code is stored for the feature.</returns>
|
||||
public bool IsSupported(VcpFeature feature) =>
|
||||
_codes.TryGetValue(feature, out var code) && code != NotSupportedSentinel;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the resolved code when supported; otherwise the registry's primary
|
||||
/// candidate as a safe default (callers gate writes on <see cref="IsSupported"/>).
|
||||
/// </summary>
|
||||
/// <param name="feature">The feature whose VCP code is requested.</param>
|
||||
/// <returns>
|
||||
/// The stored VCP code when <paramref name="feature"/> is supported; otherwise the
|
||||
/// first candidate from <see cref="VcpFeatureRegistry.Primary"/>.
|
||||
/// </returns>
|
||||
public byte GetCode(VcpFeature feature) =>
|
||||
_codes.TryGetValue(feature, out var code) && code != NotSupportedSentinel
|
||||
? (byte)code
|
||||
: VcpFeatureRegistry.Primary(feature);
|
||||
|
||||
/// <summary>
|
||||
/// Records a resolved VCP code for <paramref name="feature"/>.
|
||||
/// </summary>
|
||||
/// <param name="feature">The feature being resolved.</param>
|
||||
/// <param name="code">The VCP code that responded on this monitor.</param>
|
||||
public void SetCode(VcpFeature feature, byte code) => _codes[feature] = code;
|
||||
|
||||
/// <summary>
|
||||
/// Marks <paramref name="feature"/> as resolved but not supported on this monitor
|
||||
/// (no candidate code elicited a valid response).
|
||||
/// </summary>
|
||||
/// <param name="feature">The feature to mark as not supported.</param>
|
||||
public void SetNotSupported(VcpFeature feature) => _codes[feature] = NotSupportedSentinel;
|
||||
|
||||
/// <summary>
|
||||
/// Serialises the map to a <c>Dictionary<string,int></c> suitable for JSON
|
||||
/// persistence. Keys are the stable strings from <see cref="VcpFeatureRegistry.Key"/>;
|
||||
/// values are byte codes or <see cref="NotSupportedSentinel"/>.
|
||||
/// </summary>
|
||||
/// <returns>Serialisable dictionary of resolved codes.</returns>
|
||||
public Dictionary<string, int> ToPersisted()
|
||||
{
|
||||
var result = new Dictionary<string, int>(_codes.Count);
|
||||
foreach (var kvp in _codes)
|
||||
{
|
||||
result[VcpFeatureRegistry.Key(kvp.Key)] = kvp.Value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialises a <see cref="VcpFeatureCodeMap"/> from a previously persisted
|
||||
/// dictionary. Unknown keys are silently ignored; <see langword="null"/> input
|
||||
/// returns an empty (unresolved) map. Values outside the valid byte range [0, 255]
|
||||
/// that are not the <see cref="NotSupportedSentinel"/> are also ignored (treated as
|
||||
/// unresolved) to guard against corrupt or hand-edited state files.
|
||||
/// </summary>
|
||||
/// <param name="persisted">
|
||||
/// Dictionary produced by <see cref="ToPersisted"/>, or <see langword="null"/>.
|
||||
/// </param>
|
||||
/// <returns>A new <see cref="VcpFeatureCodeMap"/> populated from <paramref name="persisted"/>.</returns>
|
||||
public static VcpFeatureCodeMap FromPersisted(Dictionary<string, int>? persisted)
|
||||
{
|
||||
var map = new VcpFeatureCodeMap();
|
||||
if (persisted == null)
|
||||
{
|
||||
return map;
|
||||
}
|
||||
|
||||
foreach (var kvp in persisted)
|
||||
{
|
||||
if (VcpFeatureRegistry.TryParseKey(kvp.Key, out var feature) &&
|
||||
(kvp.Value == NotSupportedSentinel || (kvp.Value >= 0 && kvp.Value <= 255)))
|
||||
{
|
||||
map._codes[feature] = kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ namespace PowerDisplay.Common.Serialization
|
||||
[JsonSerializable(typeof(MonitorStateFile))]
|
||||
[JsonSerializable(typeof(MonitorStateEntry))]
|
||||
[JsonSerializable(typeof(Dictionary<string, MonitorStateEntry>))]
|
||||
[JsonSerializable(typeof(Dictionary<string, int>))]
|
||||
public partial class MonitorStateSerializationContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@ namespace PowerDisplay.Common.Services
|
||||
public int? Volume { get; set; }
|
||||
|
||||
public string? CapabilitiesRaw { get; set; }
|
||||
|
||||
public Dictionary<string, int>? VcpFeatureCodes { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -146,6 +148,97 @@ namespace PowerDisplay.Common.Services
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the persisted resolved feature-to-VCP-code map for a monitor, or <c>null</c>
|
||||
/// if it has never been resolved.
|
||||
/// </summary>
|
||||
public VcpFeatureCodeMap? GetVcpCodeMap(string monitorId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(monitorId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_states.TryGetValue(monitorId, out var state) && state.VcpFeatureCodes != null)
|
||||
{
|
||||
return VcpFeatureCodeMap.FromPersisted(state.VcpFeatureCodes);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Persists the resolved feature-to-VCP-code map for a monitor. Idempotent: skips the
|
||||
/// write when the serialized map is unchanged, so re-running discovery does not churn disk.
|
||||
/// </summary>
|
||||
public void UpdateVcpCodeMap(string monitorId, VcpFeatureCodeMap map)
|
||||
{
|
||||
if (string.IsNullOrEmpty(monitorId) || map == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var persisted = map.ToPersisted();
|
||||
var state = _states.GetOrAdd(monitorId, _ => new MonitorState());
|
||||
if (DictionariesEqual(state.VcpFeatureCodes, persisted))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
state.VcpFeatureCodes = persisted;
|
||||
_isDirty = true;
|
||||
_saveDebouncer.Debounce(SaveStateToDiskAsync);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to update VCP code map for monitorId '{monitorId}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears every monitor's resolved feature-to-VCP-code map (leaving brightness/contrast/
|
||||
/// volume/color values intact), forcing a fresh resolution on the next discovery. Called
|
||||
/// by the user-initiated Refresh path.
|
||||
/// </summary>
|
||||
public void ClearAllVcpCodeMaps()
|
||||
{
|
||||
bool changed = false;
|
||||
foreach (var state in _states.Values)
|
||||
{
|
||||
if (state.VcpFeatureCodes != null)
|
||||
{
|
||||
state.VcpFeatureCodes = null;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed)
|
||||
{
|
||||
_isDirty = true;
|
||||
_saveDebouncer.Debounce(SaveStateToDiskAsync);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool DictionariesEqual(Dictionary<string, int>? a, Dictionary<string, int> b)
|
||||
{
|
||||
if (a == null || a.Count != b.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var kvp in b)
|
||||
{
|
||||
if (!a.TryGetValue(kvp.Key, out var v) || v != kvp.Value)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One-shot upgrade migration: rewrite legacy <c>"{Source}_{EdidId}_{N}"</c> keys
|
||||
/// (pre-PR #47712) onto the matching DevicePath-based monitor Ids by joining on
|
||||
@@ -221,6 +314,7 @@ namespace PowerDisplay.Common.Services
|
||||
Contrast = s.Contrast,
|
||||
Volume = s.Volume,
|
||||
CapabilitiesRaw = s.CapabilitiesRaw,
|
||||
VcpFeatureCodes = s.VcpFeatureCodes,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
@@ -252,6 +346,7 @@ namespace PowerDisplay.Common.Services
|
||||
Contrast = entry.Contrast,
|
||||
Volume = entry.Volume,
|
||||
CapabilitiesRaw = entry.CapabilitiesRaw,
|
||||
VcpFeatureCodes = entry.VcpFeatureCodes,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -333,6 +428,7 @@ namespace PowerDisplay.Common.Services
|
||||
Contrast = state.Contrast,
|
||||
Volume = state.Volume,
|
||||
CapabilitiesRaw = state.CapabilitiesRaw,
|
||||
VcpFeatureCodes = state.VcpFeatureCodes,
|
||||
LastUpdated = now,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using PowerDisplay.Common.Models;
|
||||
using static PowerDisplay.Common.Drivers.NativeConstants;
|
||||
|
||||
namespace PowerDisplay.Common.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps each <see cref="VcpFeature"/> to its ordered list of candidate VCP codes
|
||||
/// (highest priority first) and a stable persistence/diagnostic key. Candidate
|
||||
/// values come from <see cref="PowerDisplay.Common.Drivers.NativeConstants"/>.
|
||||
/// Only brightness has multiple candidates today; the rest are single-candidate.
|
||||
/// </summary>
|
||||
public static class VcpFeatureRegistry
|
||||
{
|
||||
private static readonly VcpFeature[] AllFeaturesArray =
|
||||
{
|
||||
VcpFeature.Brightness,
|
||||
VcpFeature.Contrast,
|
||||
VcpFeature.Volume,
|
||||
VcpFeature.ColorTemperature,
|
||||
VcpFeature.InputSource,
|
||||
VcpFeature.PowerState,
|
||||
};
|
||||
|
||||
private static readonly Dictionary<VcpFeature, byte[]> CandidatesByFeature = new()
|
||||
{
|
||||
[VcpFeature.Brightness] = new[] { VcpCodeBrightness, VcpCodeBacklightControl, VcpCodeBacklightLevelWhite },
|
||||
[VcpFeature.Contrast] = new[] { VcpCodeContrast },
|
||||
[VcpFeature.Volume] = new[] { VcpCodeVolume },
|
||||
[VcpFeature.ColorTemperature] = new[] { VcpCodeSelectColorPreset },
|
||||
[VcpFeature.InputSource] = new[] { VcpCodeInputSource },
|
||||
[VcpFeature.PowerState] = new[] { VcpCodePowerMode },
|
||||
};
|
||||
|
||||
private static readonly Dictionary<VcpFeature, string> KeysByFeature = new()
|
||||
{
|
||||
[VcpFeature.Brightness] = "brightness",
|
||||
[VcpFeature.Contrast] = "contrast",
|
||||
[VcpFeature.Volume] = "volume",
|
||||
[VcpFeature.ColorTemperature] = "colorTemperature",
|
||||
[VcpFeature.InputSource] = "inputSource",
|
||||
[VcpFeature.PowerState] = "powerState",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// All supported <see cref="VcpFeature"/> values in display order.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<VcpFeature> AllFeatures => AllFeaturesArray;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the ordered list of candidate VCP codes for <paramref name="feature"/>,
|
||||
/// highest priority first.
|
||||
/// </summary>
|
||||
/// <param name="feature">The feature whose candidates are requested.</param>
|
||||
/// <returns>Ordered candidate byte values (highest priority first).</returns>
|
||||
public static IReadOnlyList<byte> Candidates(VcpFeature feature) => CandidatesByFeature[feature];
|
||||
|
||||
/// <summary>
|
||||
/// Returns the highest-priority VCP code for <paramref name="feature"/>.
|
||||
/// </summary>
|
||||
/// <param name="feature">The feature whose primary code is requested.</param>
|
||||
/// <returns>The first (highest-priority) candidate VCP code.</returns>
|
||||
public static byte Primary(VcpFeature feature) => CandidatesByFeature[feature][0];
|
||||
|
||||
/// <summary>
|
||||
/// Returns the stable persistence/diagnostic key string for <paramref name="feature"/>
|
||||
/// (e.g., <c>"brightness"</c>).
|
||||
/// </summary>
|
||||
/// <param name="feature">The feature whose key is requested.</param>
|
||||
/// <returns>Lowercase camelCase key string.</returns>
|
||||
public static string Key(VcpFeature feature) => KeysByFeature[feature];
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to look up the <see cref="VcpFeature"/> corresponding to a persistence key.
|
||||
/// </summary>
|
||||
/// <param name="key">Key string previously returned by <see cref="Key"/>.</param>
|
||||
/// <param name="feature">
|
||||
/// When this method returns <see langword="true"/>, contains the matching feature;
|
||||
/// otherwise the default value.
|
||||
/// </param>
|
||||
/// <returns><see langword="true"/> if the key was recognised; otherwise <see langword="false"/>.</returns>
|
||||
public static bool TryParseKey(string key, out VcpFeature feature)
|
||||
{
|
||||
foreach (var kvp in KeysByFeature)
|
||||
{
|
||||
if (string.Equals(kvp.Value, key, StringComparison.Ordinal))
|
||||
{
|
||||
feature = kvp.Key;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
feature = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using PowerDisplay.Common.Models;
|
||||
|
||||
namespace PowerDisplay.Common.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves each <see cref="VcpFeature"/> to a concrete VCP code for one monitor:
|
||||
/// cap-string-first, probe-as-fallback. Pure and side-effect free — the only I/O is
|
||||
/// the caller-supplied <paramref name="probe"/> delegate, which must be a read-only
|
||||
/// VCP GET that returns true only when the code yields a usable value.
|
||||
/// </summary>
|
||||
public static class VcpFeatureResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves every <see cref="VcpFeature"/> to a VCP code (or not-supported sentinel)
|
||||
/// for a single monitor, using cap-string-first then probe-as-fallback strategy.
|
||||
/// <para>
|
||||
/// Algorithm per feature:
|
||||
/// <list type="number">
|
||||
/// <item>If <paramref name="persisted"/> already has a resolved decision (code or
|
||||
/// sentinel), reuse it verbatim without touching <paramref name="caps"/> or
|
||||
/// <paramref name="probe"/>.</item>
|
||||
/// <item>Phase 1 (both modes): walk the priority-ordered candidates from
|
||||
/// <see cref="VcpFeatureRegistry.Candidates"/>; pick the first code the cap
|
||||
/// string reports as supported.</item>
|
||||
/// <item>Phase 2 (<paramref name="maxCompatibilityMode"/> only, fresh discovery
|
||||
/// when <paramref name="persisted"/> is <see langword="null"/>, and Phase 1 found
|
||||
/// nothing): call <paramref name="probe"/> on each candidate in priority order;
|
||||
/// stop at the first that returns <see langword="true"/>. Skipped when
|
||||
/// <paramref name="persisted"/> is non-<see langword="null"/> (cap-string refresh
|
||||
/// mode — probing is expensive and not appropriate for refreshes).</item>
|
||||
/// <item>If nothing resolves, store <see cref="VcpFeatureCodeMap.NotSupportedSentinel"/>.</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="caps">Cap string data for the monitor.</param>
|
||||
/// <param name="maxCompatibilityMode">
|
||||
/// When <see langword="true"/>, Phase 2 probe fallback is enabled for features not
|
||||
/// found in the cap string.
|
||||
/// </param>
|
||||
/// <param name="persisted">
|
||||
/// Previously resolved map to reuse, or <see langword="null"/> for a fresh resolution.
|
||||
/// Only features already resolved in this map are reused; missing features are resolved
|
||||
/// fresh.
|
||||
/// </param>
|
||||
/// <param name="probe">
|
||||
/// Caller-supplied read-only VCP GET delegate. Invoked only in Phase 2. Must return
|
||||
/// <see langword="true"/> when the given code elicits a usable value from the monitor.
|
||||
/// </param>
|
||||
/// <returns>A fully resolved <see cref="VcpFeatureCodeMap"/> covering all features.</returns>
|
||||
public static VcpFeatureCodeMap Resolve(
|
||||
VcpCapabilities caps,
|
||||
bool maxCompatibilityMode,
|
||||
VcpFeatureCodeMap? persisted,
|
||||
Func<byte, bool> probe)
|
||||
{
|
||||
var map = new VcpFeatureCodeMap();
|
||||
|
||||
foreach (var feature in VcpFeatureRegistry.AllFeatures)
|
||||
{
|
||||
// Per-feature reuse: a persisted decision (code or sentinel) wins verbatim,
|
||||
// and is neither re-derived from caps nor re-probed.
|
||||
if (persisted != null && persisted.IsResolved(feature))
|
||||
{
|
||||
if (persisted.IsSupported(feature))
|
||||
{
|
||||
map.SetCode(feature, persisted.GetCode(feature));
|
||||
}
|
||||
else
|
||||
{
|
||||
map.SetNotSupported(feature);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var candidates = VcpFeatureRegistry.Candidates(feature);
|
||||
byte? resolved = null;
|
||||
|
||||
// Phase 1 (both modes): first candidate the cap string reports as supported.
|
||||
foreach (var code in candidates)
|
||||
{
|
||||
if (caps.SupportsVcpCode(code))
|
||||
{
|
||||
resolved = code;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2 (max-compat only, fresh discovery, when Phase 1 found nothing): probe in priority order.
|
||||
// When a persisted map is supplied the caller is doing a cap-string refresh, not a
|
||||
// first-time discovery; probing is expensive/disruptive and is therefore skipped.
|
||||
if (resolved == null && maxCompatibilityMode && persisted == null)
|
||||
{
|
||||
foreach (var code in candidates)
|
||||
{
|
||||
if (probe(code))
|
||||
{
|
||||
resolved = code;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (resolved.HasValue)
|
||||
{
|
||||
map.SetCode(feature, resolved.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
map.SetNotSupported(feature);
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,6 +87,19 @@ namespace PowerDisplay.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pushes the per-monitor persisted resolved-code maps onto the DDC/CI controller before
|
||||
/// discovery so already-resolved monitors are reused without re-probing. No-op if the DDC
|
||||
/// controller failed to initialize.
|
||||
/// </summary>
|
||||
public void SetPersistedVcpCodeMaps(IReadOnlyDictionary<string, VcpFeatureCodeMap> maps)
|
||||
{
|
||||
if (_ddcController != null)
|
||||
{
|
||||
_ddcController.PersistedVcpCodeMaps = maps;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discover all monitors from all controllers.
|
||||
/// Each controller is responsible for fully initializing its monitors
|
||||
|
||||
@@ -34,6 +34,7 @@ namespace PowerDisplay.Serialization
|
||||
[JsonSerializable(typeof(List<VcpValueInfo>))]
|
||||
[JsonSerializable(typeof(List<PowerDisplayProfile>))]
|
||||
[JsonSerializable(typeof(List<ProfileMonitorSetting>))]
|
||||
[JsonSerializable(typeof(Dictionary<string, int>))]
|
||||
|
||||
[JsonSourceGenerationOptions(
|
||||
WriteIndented = true,
|
||||
|
||||
@@ -10,6 +10,7 @@ using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using PowerDisplay.Common.Models;
|
||||
using PowerDisplay.Common.Utils;
|
||||
using PowerDisplay.Helpers;
|
||||
using PowerDisplay.Models;
|
||||
using Monitor = PowerDisplay.Common.Models.Monitor;
|
||||
@@ -31,6 +32,7 @@ public partial class MainViewModel
|
||||
// DDC/CI controller picks up toggle changes without a process restart.
|
||||
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>(PowerDisplaySettings.ModuleName);
|
||||
_monitorManager.SetMaxCompatibilityMode(settings.Properties.MaxCompatibilityMode);
|
||||
PushPersistedVcpCodeMaps();
|
||||
|
||||
// Discover monitors
|
||||
var monitors = await _monitorManager.DiscoverMonitorsAsync(cancellationToken);
|
||||
@@ -115,6 +117,15 @@ public partial class MainViewModel
|
||||
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>(PowerDisplaySettings.ModuleName);
|
||||
_monitorManager.SetMaxCompatibilityMode(settings.Properties.MaxCompatibilityMode);
|
||||
|
||||
// User-initiated Refresh only (not the display-change watcher): drop persisted
|
||||
// resolved-code maps so every feature is re-resolved from scratch.
|
||||
if (!skipScanningCheck)
|
||||
{
|
||||
_stateManager.ClearAllVcpCodeMaps();
|
||||
}
|
||||
|
||||
PushPersistedVcpCodeMaps();
|
||||
|
||||
var monitors = await _monitorManager.DiscoverMonitorsAsync(_cancellationTokenSource.Token);
|
||||
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
@@ -167,6 +178,15 @@ public partial class MainViewModel
|
||||
OnPropertyChanged(nameof(ShowLinkLevelsToggle));
|
||||
RecomputeLinkedBrightnessAvailability();
|
||||
|
||||
// Persist the freshly resolved feature->code maps (idempotent; debounced).
|
||||
foreach (var monitor in monitors)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(monitor.Id))
|
||||
{
|
||||
_stateManager.UpdateVcpCodeMap(monitor.Id, monitor.ResolvedVcpCodes);
|
||||
}
|
||||
}
|
||||
|
||||
// Save monitor information to settings
|
||||
SaveMonitorsToSettings();
|
||||
|
||||
@@ -191,4 +211,30 @@ public partial class MainViewModel
|
||||
.Where(m => m.IsHidden)
|
||||
.Select(m => m.Id),
|
||||
MonitorIdComparer.Instance);
|
||||
|
||||
/// <summary>
|
||||
/// Builds the per-monitor persisted resolved-code maps from the state manager and pushes
|
||||
/// them onto the controller stack ahead of discovery. Empty maps force fresh resolution.
|
||||
/// </summary>
|
||||
private void PushPersistedVcpCodeMaps()
|
||||
{
|
||||
var maps = new Dictionary<string, VcpFeatureCodeMap>(MonitorIdComparer.Instance);
|
||||
|
||||
// Enumerate the settings monitor list (kept in sync with monitor_state.json by UpdateMonitorList) and look up each monitor's persisted resolved-code map; monitors without a persisted map simply resolve fresh.
|
||||
foreach (var existing in _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>(PowerDisplaySettings.ModuleName).Properties.Monitors)
|
||||
{
|
||||
if (string.IsNullOrEmpty(existing.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var map = _stateManager.GetVcpCodeMap(existing.Id);
|
||||
if (map != null)
|
||||
{
|
||||
maps[existing.Id] = map;
|
||||
}
|
||||
}
|
||||
|
||||
_monitorManager.SetPersistedVcpCodeMaps(maps);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -529,6 +529,7 @@ public partial class MainViewModel
|
||||
// Monitor number for display name formatting
|
||||
MonitorNumber = vm.MonitorNumber,
|
||||
LastSeenUtc = _clock.UtcNow,
|
||||
ResolvedVcpCodes = vm.ResolvedVcpCodes.ToPersisted(),
|
||||
};
|
||||
|
||||
return monitorInfo;
|
||||
|
||||
@@ -288,6 +288,9 @@ public partial class MonitorViewModel : ObservableObject, IDisposable
|
||||
|
||||
public string? CapabilitiesRaw => _monitor.CapabilitiesRaw;
|
||||
|
||||
/// <summary>Gets the resolved per-feature VCP code map for this monitor.</summary>
|
||||
public VcpFeatureCodeMap ResolvedVcpCodes => _monitor.ResolvedVcpCodes;
|
||||
|
||||
public VcpCapabilities? VcpCapabilitiesInfo => _monitor.VcpCapabilitiesInfo;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -33,6 +33,20 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
private System.DateTime? _lastSeenUtc;
|
||||
private string _capabilitiesRaw = string.Empty;
|
||||
private List<VcpCodeDisplayInfo> _vcpCodesFormatted = new List<VcpCodeDisplayInfo>();
|
||||
private Dictionary<string, int> _resolvedVcpCodes = new Dictionary<string, int>();
|
||||
|
||||
// Display order + labels for the diagnostics "Resolved feature" section. Keys must match
|
||||
// PowerDisplay.Lib VcpFeatureRegistry.Key(...) (Settings.UI.Library must not depend on .Lib).
|
||||
private static readonly (string Key, string Label)[] DiagnosticFeatureLabels =
|
||||
{
|
||||
("brightness", "Brightness"),
|
||||
("contrast", "Contrast"),
|
||||
("volume", "Volume"),
|
||||
("colorTemperature", "Color temperature"),
|
||||
("inputSource", "Input source"),
|
||||
("powerState", "Power state"),
|
||||
};
|
||||
|
||||
private int _monitorNumber;
|
||||
private int _totalMonitorCount;
|
||||
|
||||
@@ -387,6 +401,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the resolved feature-to-VCP-code map (feature name -> code, or -1 =
|
||||
/// checked-but-unsupported). Written by the PowerDisplay app; shown in diagnostics.
|
||||
/// </summary>
|
||||
[JsonPropertyName("resolvedVcpCodes")]
|
||||
public Dictionary<string, int> ResolvedVcpCodes
|
||||
{
|
||||
get => _resolvedVcpCodes;
|
||||
set
|
||||
{
|
||||
_resolvedVcpCodes = value ?? new Dictionary<string, int>();
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compare two VcpCodesFormatted lists for equality by content.
|
||||
/// Returns true if both lists have the same VCP codes (by code value).
|
||||
@@ -710,6 +739,22 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
lines.Add($"Supports power state: {SupportsPowerState}");
|
||||
lines.Add(string.Empty);
|
||||
|
||||
lines.Add("Resolved feature -> VCP code");
|
||||
lines.Add(new string('-', 50));
|
||||
foreach (var (key, label) in DiagnosticFeatureLabels)
|
||||
{
|
||||
if (_resolvedVcpCodes.TryGetValue(key, out var code))
|
||||
{
|
||||
lines.Add(code < 0 ? $"{label}: not supported" : $"{label}: 0x{code:X2}");
|
||||
}
|
||||
else
|
||||
{
|
||||
lines.Add($"{label}: not resolved");
|
||||
}
|
||||
}
|
||||
|
||||
lines.Add(string.Empty);
|
||||
|
||||
lines.Add("Raw capabilities");
|
||||
lines.Add(new string('-', 50));
|
||||
lines.Add(string.IsNullOrWhiteSpace(CapabilitiesRaw) ? "No raw capabilities detected" : CapabilitiesRaw);
|
||||
@@ -749,6 +794,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
EnablePowerState = other.EnablePowerState;
|
||||
CapabilitiesRaw = other.CapabilitiesRaw;
|
||||
VcpCodesFormatted = other.VcpCodesFormatted;
|
||||
ResolvedVcpCodes = other.ResolvedVcpCodes;
|
||||
SupportsBrightness = other.SupportsBrightness;
|
||||
SupportsContrast = other.SupportsContrast;
|
||||
SupportsColorTemperature = other.SupportsColorTemperature;
|
||||
|
||||
@@ -144,6 +144,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
[JsonSerializable(typeof(ImageResizerCustomSizeProperty))]
|
||||
[JsonSerializable(typeof(KeyboardKeysProperty))]
|
||||
[JsonSerializable(typeof(MonitorInfo))]
|
||||
[JsonSerializable(typeof(Dictionary<string, int>))]
|
||||
[JsonSerializable(typeof(PowerDisplayActionMessage))]
|
||||
[JsonSerializable(typeof(PowerDisplayActionMessage.ActionData))]
|
||||
[JsonSerializable(typeof(PowerDisplayActionMessage.PowerDisplayAction))]
|
||||
|
||||
@@ -36,6 +36,7 @@ namespace Microsoft.PowerToys.Settings.UI.SerializationContext;
|
||||
[JsonSerializable(typeof(PowerOcrSettings))]
|
||||
[JsonSerializable(typeof(PowerOcrSettings))]
|
||||
[JsonSerializable(typeof(PowerDisplaySettings))]
|
||||
[JsonSerializable(typeof(Dictionary<string, int>))]
|
||||
[JsonSerializable(typeof(RegistryPreviewSettings))]
|
||||
[JsonSerializable(typeof(ShortcutConflictProperties))]
|
||||
[JsonSerializable(typeof(ShortcutGuideSettings))]
|
||||
|
||||
Reference in New Issue
Block a user