Compare commits

...

16 Commits

Author SHA1 Message Date
Yu Leng (from Dev Box)
c4ec92bb8f [PowerDisplay] Polish: harden VcpFeatureCodeMap, refresh DDC docs, clarify orchestration 2026-06-30 12:50:20 +08:00
Yu Leng (from Dev Box)
450dd5dd59 [PowerDisplay] Show resolved feature->VCP code in diagnostics 2026-06-30 12:37:12 +08:00
Yu Leng (from Dev Box)
53fc27b552 [PowerDisplay] Document MonitorViewModel.ResolvedVcpCodes 2026-06-30 12:25:30 +08:00
Yu Leng (from Dev Box)
0d5eb84d4f [PowerDisplay] Wire persisted VCP code maps through discovery/refresh 2026-06-30 12:21:32 +08:00
Yu Leng (from Dev Box)
8e8e4408ae [PowerDisplay] Resolve and use per-feature VCP codes in DDC controller 2026-06-30 12:15:46 +08:00
Yu Leng (from Dev Box)
9c762c720c [PowerDisplay] Add get/update/clear APIs for resolved VCP code maps 2026-06-30 12:09:06 +08:00
Yu Leng (from Dev Box)
f8bf1be06b [PowerDisplay] Persist resolved VCP codes in monitor state 2026-06-30 12:04:58 +08:00
Yu Leng (from Dev Box)
b0059e2c1d [PowerDisplay] Tighten VcpFeatureResolver caps contract and resolver tests 2026-06-30 12:01:35 +08:00
Yu Leng (from Dev Box)
a81effce10 [PowerDisplay] Spec: clarify Phase 2 probe runs only on fresh discovery (persisted==null) 2026-06-30 11:58:09 +08:00
Yu Leng (from Dev Box)
5933cb9ece [PowerDisplay] Add VcpFeatureResolver (cap-string-first, probe fallback) 2026-06-30 11:55:21 +08:00
Yu Leng (from Dev Box)
bf81179b35 [PowerDisplay] Add VcpFeatureCodeMap with persistence round-trip 2026-06-30 11:47:21 +08:00
Yu Leng (from Dev Box)
6e7232862f [PowerDisplay] Document VcpFeatureRegistry API; fix stale brightness comment 2026-06-30 11:43:11 +08:00
Yu Leng (from Dev Box)
0f434b5344 [PowerDisplay] Add VcpFeature enum and candidate-code registry 2026-06-30 11:38:32 +08:00
Yu Leng (from Dev Box)
d7916e0457 [PowerDisplay] Plan: use MSBuild/vstest toolchain (not dotnet CLI) 2026-06-30 11:35:10 +08:00
Yu Leng (from Dev Box)
649a1dda15 [PowerDisplay] Add implementation plan for VCP code resolution
TDD, bite-sized task plan covering the registry/resolver/map, persistence,
DDC controller wiring, app orchestration (push/persist/clear-on-refresh),
and the diagnostics surface.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 11:17:42 +08:00
Yu Leng (from Dev Box)
5487807d6f [PowerDisplay] Add design spec for per-feature VCP code resolution
Design for resolving each PowerDisplay feature to one of an ordered list
of candidate VCP codes per monitor (cap-string-first, probe-as-fallback),
persisting the decision (including a not-supported sentinel) until the
user refreshes, and surfacing the resolved code in diagnostics.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 11:02:46 +08:00
24 changed files with 2826 additions and 34 deletions

View 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 12 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.

File diff suppressed because it is too large Load Diff

View File

@@ -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).");
}
}

View File

@@ -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));
}
}

View File

@@ -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 _));
}
}

View File

@@ -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));
}
}

View File

@@ -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;
}

View File

@@ -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.

View File

@@ -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)

View File

@@ -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>

View File

@@ -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,
}
}

View File

@@ -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&lt;string,int&gt;</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&lt;string,int&gt;</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;
}
}
}

View File

@@ -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
{
}

View File

@@ -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,
};
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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

View File

@@ -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,

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;

View File

@@ -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))]

View File

@@ -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))]