microsoft / microsoft/PowerToys

[FancyZones] Wrong layout applied after dock/sleep: stale applied-layouts entry wins (constant-0 hash + monitor-number fallback in DeviceId equality)

Open
#49,016 0 comments 0 reactions 0 assignees View on GitHub
Product-FancyZones
Dominant language
C
Stars
139k
Forks
8.6k
PR merge metrics
PR metrics pending

Description

### Microsoft PowerToys version

0.100.0

### Installation method

WinGet

### Area(s) with issue?

FancyZones

### Steps to reproduce

**Setup:** 3 monitors (laptop panel + 2 external ultrawides) through a docking station,
4 named Windows virtual desktops, each with its own FancyZones layout per monitor. Topology
changes between two locations and across sleep/wake and overnight screen lock.

**Trigger:** after a sleep/wake, undock/redock, or overnight lock, FancyZones applies the
**wrong** saved layout to a monitor on a given virtual desktop (one desktop's layout appears
to "bleed" onto another), without any manual change.

#### Root cause (traced in the 0.100.0 source — the point of this report)

`applied-layouts.json` accumulates **multiple entries for the same physical monitor on the
same virtual desktop**, differing only by `monitor-instance` and `monitor-number`, and
FancyZones then matches the live monitor to the **wrong** entry. Three code defects in
`FancyZonesLib` combine to cause this:

**1. The layout map's hash is a constant `0`.**
`AppliedLayouts::TAppliedLayoutsMap` is `std::unordered_map`
(`AppliedLayouts.h`), but the hash specialization in `FancyZonesDataTypes.h` is:

```cpp
template<> struct hash {
size_t operator()(const FancyZonesDataTypes::WorkAreaId&) const { return 0; }
};
```

Every entry lands in one bucket, so `GetDeviceLayout()`'s `m_layouts.find(id)` degrades to a
**linear scan that returns the first entry for which `operator==` is true** — the result is
decided by file/insertion order.

**2. `operator==(DeviceId)` falls back to the monitor *number* when the instance differs**
(`FancyZonesDataTypes.h`):

```cpp
inline bool operator==(const DeviceId& lhs, const DeviceId& rhs) {
if (lhs.id != rhs.id) return false;
if (lhs.instanceId != rhs.instanceId) return lhs.number == rhs.number; // <-- problem
return true;
}
```

`monitor-number` is reassigned by Windows on topology changes and is **not stable or unique**.
So a live monitor can compare *equal* to a **stale entry left over from a different dock/port**
merely because their `number` coincides. Combined with defect #1 (first match in file order
wins), FancyZones applies that stale entry's layout.

**3. Stale per-(monitor, desktop) entries are never pruned.** Because `monitor-instance`
encodes the dock/port (it changes when the same monitor is plugged into a different port/dock,
and is *shared* by different monitors on the same port), every reconnection can add a new entry
while old ones linger as wrong-match candidates.

There is also a latent correctness bug: `operator<(DeviceId)` and `operator<(WorkAreaId)` in the
same file are not valid strict weak orderings (e.g. `operator<(WorkAreaId)` compares
`lhs.virtualDesktopId.Data1 < rhs...Data1 || Data2 < Data2 || Data3 < Data3` of two possibly
different GUIDs). Harmless while the container is `unordered_map`, but a trap for future ordered
use or `std::sort`.

A likely-related second symptom: `FancyZonesCLI get-monitors` reported the monitors as belonging
to a *stale* virtual desktop GUID while the registry (`CurrentVirtualDesktop`) and
`get-active-layout` agreed on the real one. `AppliedLayouts::SyncVirtualDesktops()` copies a
layout to **all** desktops when the current VD transitions from `GUID_NULL`; if VD detection
returns `GUID_NULL` spuriously after sleep/dock, this can overwrite other desktops' layouts.

#### Concrete evidence (from my `applied-layouts.json`)

The physical monitor `DELA243` (serial redacted) had **two** entries on the same virtual desktop
`{5032E820-…}`:

```jsonc
// stale leftover from a previous dock connection
{ "device": { "monitor": "DELA243", "monitor-instance": "4&…&UID20550", "monitor-number": 1,
"serial-number": "…", "virtual-desktop": "{5032E820-…}" },
"applied-layout": { "type": "custom", "uuid": "{609F1F3E-…}" } }
// current connection — the one that should win
{ "device": { "monitor": "DELA243", "monitor-instance": "4&…&UID8261", "monitor-number": 3,
"serial-number": "…", "virtual-desktop": "{5032E820-…}" },
"applied-layout": { "type": "grid", "uuid": "{821FE1CE-…}" } }
```

The live `DELA243` was instance `…UID8261` — yet `get-active-layout` reported the **custom**
`{609F1F3E}` layout from the stale `…UID20550` entry. Across the whole file the same monitor
appears under three instances (`UID8261`, `UID12613`, `UID20550`) with conflicting layouts.

#### Reliable manual workaround

Remove every entry for a monitor on a desktop and keep a single one keyed to the current device
record, then restart PowerToys so FancyZones re-reads the file. Collapsing to one entry per
(monitor, desktop) makes the correct layout stick.

#### How this differs from existing issues

Closely related reports are all **closed** and are user *symptom* reports without the code-level
cause: #40638 (same "by port vs display id" intuition; closed, never triaged), #16370 (the
reset-to-*default* variant; Priority-1, closed as duplicate), #13733 (partial fix committed, yet
recurrence), #28371 and #36897 (closed as duplicate), and the now-closed tracker #12985. None
pins the specific defects above (constant-`0` hash → first-match-by-file-order; the `number`
fallback in `operator==(DeviceId)`; un-pruned stale entries) with file evidence and a repro.

#### Proposed fix (direction)

1. Match monitors by a stable identity — prefer `id + serialNumber` (already in the data) over
the volatile `monitor-instance`/`number`, guarding empty/non-unique serials (some panels
report `"0"`).
2. Drop the `number` fallback in `operator==(DeviceId)` so a differing `instanceId` can't be made
"equal" by a coincidental monitor number; resolve identity via serial instead.
3. Prune stale entries for the same `(monitor id + serial, virtualDesktop)` on load/save.
4. Give `WorkAreaId` a real hash (the current one returns `0`) and fix
`operator<(DeviceId)`/`operator<(WorkAreaId)` to be valid strict weak orderings.
5. Re-check `SyncVirtualDesktops()`' copy-to-all-desktops path against a spurious `GUID_NULL`
current desktop after sleep/dock.

Relevant files: `src/modules/fancyzones/FancyZonesLib/FancyZonesData/AppliedLayouts.cpp`,
`src/modules/fancyzones/FancyZonesLib/FancyZonesDataTypes.h`.

### ✔️ Expected Behavior

Each physical monitor keeps its assigned layout per virtual desktop across sleep/wake,
undock/redock, and lock. FancyZones should match a monitor to its layout by a **stable**
identity, and stale entries from previous topologies should not be able to win the match.

### ❌ Actual Behavior

After a topology change, FancyZones applies a layout from a stale `applied-layouts.json` entry
belonging to a previous dock/port for that monitor, because (a) the map hash is constant so the
first `operator==` match in file order wins, and (b) `operator==(DeviceId)` treats a differing
instance as equal when the (unstable) monitor `number` coincides.

### Other Software

Connected through a docking station; the external monitors' `monitor-instance` changes by
dock/port.

Contributor guide

Open the contributing guide

Research direction

Start with src/modules/fancyzones/FancyZonesLib/FancyZonesData/AppliedLayouts.cpp and src/modules/fancyzones/FancyZonesLib/FancyZonesDataTypes.h, then trace GetDeviceLayout(), DeviceId equality, hashing, and SyncVirtualDesktops(). Reproduce the topology-change case if possible and inspect applied-layouts.json. Done means stable monitor matching and no stale entry can override the current layout across sleep, docking, and virtual desktops.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
desktop
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.