pod displays: bind glass panels to EDID identity, not to Windows' shifting display numbers

Nick, after re-cabling + rebooting the pod: "the order changed ... sometimes
they change when one gets turned off and back on, at least how windows SEEs
them, even if the visual desktop tool looks the same."

Both existing binding forms are boot-fragile: `monitor:2` is an ENUMERATION
INDEX and `monitor:\.\DISPLAY4` is a GDI name Windows reassigns.  Neither
survives a panel power-cycle.  (His gos-displays.txt shows the same trap next
door in GameOS: -tmon takes DIRECTDRAW device indices -- not Windows monitor
numbers -- and a NULL-device merge shifts every index down by one on top.)

FIX: bind to the panel's own hardware identity.  EnumDisplayDevices on a
display's MONITOR child returns a DeviceID embedding the EDID manufacturer +
product code and the connector instance; neither moves across a reboot.

  DISCOVER  BT_GLASS_IDS=1 logs every attached panel's stable-id AND a
            ready-to-paste `cfg form = monitor🆔<fragment>`.  It prints the
            VOLATILE identifiers alongside on purpose: run it either side of a
            power-cycle and index/device move while stable-id does not.
  BIND      Heat MFD=monitor:id:AUO10ED,bare

NOTHING CHANGES BY DEFAULT -- no env and no `id:` prefix means identical
behaviour; `monitor:<name|index>` and raw x,y keep working, so playtester glass
builds are untouched.

An `id:` that matches nothing WARNS and falls back to computed placement.
Silence would put a picture on the wrong glass and look exactly like the bug
this form exists to prevent.

VERIFIED on a 1-monitor dev box (the pod is offline), end to end:
  * discovery printed
      stable-id = \?\DISPLAY#AUO10ED#4&31323a6c&1&UID265988#{e6f07b5f-...}
      cfg form  = monitor:id:AUO10ED
  * `Heat MFD=monitor:id:AUO10ED,bare` resolved and CENTRED correctly
      [glasswin] 'Heat MFD' bound to monitor 0,0 1920x1080 -> window at 640,300
  * a bogus id warned instead of misplacing.
The EDID-code extractor is deliberately STRUCTURAL (3 letters + 4 hex digits,
tokenising on \ # ?) rather than positional: the first cut walked separators by
position and returned EMPTY for the `\?\DISPLAY#...` interface-name form, which
is exactly the form this machine produces.  Which form you get depends on
whether EDD_GET_DEVICE_INTERFACE_NAME succeeds, so both must parse.

STILL UNPROVEN [T3] -- the pod is offline: multi-panel disambiguation when
several MFDs share one model (EDID codes collide).  The documented answer is a
longer fragment from stable-id, whose UID/instance tail differs per connector,
but that needs the cab to confirm.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
This commit is contained in:
Joe DiPrima
2026-08-08 14:20:38 -05:00
co-authored by Claude Opus 5
parent 9657fbb11e
commit ec080cd61d
3 changed files with 229 additions and 3 deletions
+189 -3
View File
@@ -744,7 +744,70 @@ void
// Heat MFD=monitor:2,bare (index into the enumeration order)
// The surface is centred on that monitor; an exact-size panel fills it.
//---------------------------------------------------------------------------
struct MonScan { int index; const char *want; int wantIndex; RECT rect; int found; };
struct MonScan { int index; const char *want; const char *wantId; int wantIndex; RECT rect; int found; };
// ---------------------------------------------------------------------------
// STABLE PANEL IDENTITY (2026-08-08) -- the fix for "the monitors came back in a
// different order after I moved cables and rebooted".
//
// Windows renumbers \\.\DISPLAYn, and reorders the enumeration, when a panel is
// power-cycled or re-cabled -- even when the visible desktop arrangement is
// unchanged. So BOTH existing binding forms are boot-fragile: `monitor:2` is an
// enumeration index and `monitor:\.\DISPLAY4` is a GDI name Windows reassigns.
// (The same trap bites GameOS next door: its -tmon takes DirectDraw device
// indices, which are neither Windows monitor numbers nor stable -- and its
// NULL-device merge shifts every index down by one on top of that.)
//
// What IS stable is the panel's own hardware identity. EnumDisplayDevices on a
// display's MONITOR child returns a DeviceID like
// MONITOR\DEL4231\{4d36e96e-e325-11ce-bfc1-08002be10318}\0002
// or, with EDD_GET_DEVICE_INTERFACE_NAME,
// \\?\DISPLAY#DEL4231#5&1a2b3c&0&UID4353#{e6f07b5f-...}
// The `DEL4231` field is the EDID manufacturer + product code, and the UID/
// instance identifies the physical connector. Neither moves on a reboot.
//
// Fill `out` with that string for a \\.\DISPLAYn adapter name. Empty on failure
// (no monitor child / remote session) -- callers must treat "" as "no identity"
// and fall back, never as a match.
static void
MonitorStableId(const char *displayName, char *out, size_t outLen)
{
if (outLen == 0) return;
out[0] = '\0';
if (displayName == NULL || displayName[0] == '\0') return;
DISPLAY_DEVICEA mon;
memset(&mon, 0, sizeof(mon));
mon.cb = sizeof(mon);
// index 0 = the attached monitor child. The interface-name flag gives the
// richer path (includes the connector UID); it is Vista+, and if the call
// fails we retry without it for the plain MONITOR\... form.
if (!EnumDisplayDevicesA(displayName, 0, &mon, EDD_GET_DEVICE_INTERFACE_NAME))
{
memset(&mon, 0, sizeof(mon));
mon.cb = sizeof(mon);
if (!EnumDisplayDevicesA(displayName, 0, &mon, 0))
return;
}
strncpy(out, mon.DeviceID, outLen - 1);
out[outLen - 1] = '\0';
}
// Case-insensitive substring test -- the cfg quotes a FRAGMENT of the identity
// (usually just the EDID code, e.g. `id:DEL4231`) rather than the whole path,
// because the full string is long and contains characters a config file and a
// shell each mangle differently.
static int
IdContains(const char *haystack, const char *needle)
{
if (haystack == NULL || needle == NULL || *needle == '\0') return 0;
size_t hl = strlen(haystack), nl = strlen(needle);
if (nl > hl) return 0;
for (size_t i = 0; i + nl <= hl; ++i)
if (_strnicmp(haystack + i, needle, nl) == 0)
return 1;
return 0;
}
static BOOL CALLBACK
MonScanProc(HMONITOR mon, HDC, LPRECT, LPARAM param)
@@ -756,7 +819,17 @@ static BOOL CALLBACK
if (GetMonitorInfoA(mon, (MONITORINFO *)&mi))
{
int hit = 0;
if (sc->want != NULL)
// `id:<fragment>` -- match the panel's HARDWARE identity, not its
// current \\.\DISPLAYn or enumeration slot. Boot-stable; this is the
// form the pod should use.
if (sc->wantId != NULL)
{
char sid[256];
MonitorStableId(mi.szDevice, sid, sizeof(sid));
if (sid[0] != '\0' && IdContains(sid, sc->wantId))
hit = 1;
}
if (!hit && sc->want != NULL)
{
// Match the full device name OR just its tail, so a hand-written
// cfg can say `monitor:DISPLAY4` and skip the \\.\ prefix entirely
@@ -781,6 +854,96 @@ static BOOL CALLBACK
return TRUE;
}
// BT_GLASS_IDS=1 -- dump every attached panel with its BOOT-STABLE identity, and
// a ready-to-paste `monitor:id:` fragment for glass_layout.cfg. This is the
// discovery half of the fix: run it once on the pod, copy the fragments into the
// cfg, and the assignment survives reboots and re-cabling.
//
// Prints the volatile identifiers too, deliberately side by side -- seeing
// \\.\DISPLAYn and the enumeration index MOVE between two runs while the id
// stays put is the proof that the id form is the right one.
struct MonDump { int index; };
static BOOL CALLBACK
MonDumpProc(HMONITOR mon, HDC, LPRECT, LPARAM param)
{
MonDump *d = (MonDump *)param;
MONITORINFOEXA mi;
memset(&mi, 0, sizeof(mi));
mi.cbSize = sizeof(mi);
if (GetMonitorInfoA(mon, (MONITORINFO *)&mi))
{
char sid[256];
MonitorStableId(mi.szDevice, sid, sizeof(sid));
// The EDID make+product sits between the first two separators of the
// DeviceID (MONITOR\DEL4231\... or \\?\DISPLAY#DEL4231#...). Offer it
// as the suggested fragment -- short, and unique when the panels are
// different models. Identical models need more (see the note below).
// Find it STRUCTURALLY rather than by position: an EDID PnP code is
// exactly 3 letters + 4 hex digits (AUO10ED, DEL4231). Walking the
// separators positionally broke on the interface-name form, whose
// `\\?\DISPLAY#...` prefix has a different number of leading segments
// than the plain `MONITOR\...` form -- and which form you get depends on
// whether EDD_GET_DEVICE_INTERFACE_NAME succeeded. Tokenising on all of
// \ # ? handles every variant the same way.
char frag[64];
frag[0] = '\0';
for (const char *t = sid; *t != '\0'; )
{
while (*t == '\\' || *t == '#' || *t == '?') ++t;
const char *e = t;
while (*e != '\0' && *e != '\\' && *e != '#' && *e != '?') ++e;
size_t n = (size_t)(e - t);
if (n == 7)
{
int ok = 1;
for (int i = 0; i < 3 && ok; ++i)
if (!isalpha((unsigned char)t[i])) ok = 0;
for (int i = 3; i < 7 && ok; ++i)
if (!isxdigit((unsigned char)t[i])) ok = 0;
if (ok)
{
memcpy(frag, t, 7);
frag[7] = '\0';
break;
}
}
t = e;
}
DEBUG_STREAM << "[glassid] index=" << d->index
<< " device=" << mi.szDevice
<< (((mi.dwFlags & MONITORINFOF_PRIMARY) != 0) ? " PRIMARY" : "")
<< " rect=" << (int)mi.rcMonitor.left << "," << (int)mi.rcMonitor.top
<< " " << (int)(mi.rcMonitor.right - mi.rcMonitor.left)
<< "x" << (int)(mi.rcMonitor.bottom - mi.rcMonitor.top)
<< "\n stable-id = " << (sid[0] ? sid : "(unavailable)")
<< "\n cfg form = monitor:id:" << (frag[0] ? frag : "<see stable-id>")
<< "\n" << std::flush;
}
++d->index;
return TRUE;
}
void
BTGlassDumpMonitorIds()
{
if (getenv("BT_GLASS_IDS") == NULL)
return;
DEBUG_STREAM << "[glassid] ---- attached panels, boot-STABLE identities ----\n"
<< "[glassid] index and device= move across reboots / power-cycles;\n"
<< "[glassid] stable-id does not. Bind the pod with monitor:id:<fragment>.\n"
<< "[glassid] If two panels are the SAME MODEL their EDID codes match --\n"
<< "[glassid] use a longer fragment from stable-id (the UID/instance tail\n"
<< "[glassid] differs per connector) so each line matches exactly one.\n"
<< std::flush;
MonDump d;
d.index = 0;
EnumDisplayMonitors(NULL, NULL, MonDumpProc, (LPARAM)&d);
DEBUG_STREAM << "[glassid] ---- " << d.index << " panel(s) ----\n" << std::flush;
}
// Resolve "monitor:<spec>" to a rect. Returns 1 on success.
static int
ResolveMonitorSpec(const char *spec, RECT *out)
@@ -789,13 +952,31 @@ static int
MonScan sc;
memset(&sc, 0, sizeof(sc));
sc.wantIndex = -1;
if (spec[0] >= '0' && spec[0] <= '9')
// `monitor:id:<fragment>` -- BOOT-STABLE hardware identity (preferred on the
// pod). Anything else keeps its historic meaning exactly: a leading digit is
// the enumeration index, otherwise a \\.\DISPLAYn device name (or its tail).
if (_strnicmp(spec, "id:", 3) == 0)
{
sc.wantId = spec + 3;
while (*sc.wantId == ' ' || *sc.wantId == '\t') ++sc.wantId;
}
else if (spec[0] >= '0' && spec[0] <= '9')
sc.wantIndex = atoi(spec);
else
sc.want = spec;
EnumDisplayMonitors(NULL, NULL, MonScanProc, (LPARAM)&sc);
if (sc.found)
*out = sc.rect;
else if (sc.wantId != NULL)
{
// An id: binding that matched nothing is worth shouting about: the panel
// is unplugged, asleep, or the cfg fragment is wrong. Silently falling
// back to computed placement would put a picture on the wrong glass and
// look like the very bug this form exists to prevent.
DEBUG_STREAM << "[glasswin] monitor id '" << sc.wantId
<< "' matched NO attached panel -- check BT_GLASS_IDS=1 output"
<< "\n" << std::flush;
}
return sc.found;
}
@@ -1616,6 +1797,11 @@ void
if (gWinCount != 0)
return; // already up
// BT_GLASS_IDS=1: dump every panel's boot-stable identity before any window
// is placed, so the log shows what the cfg COULD bind to right next to what
// it actually did. No-op without the env -- playtesters see nothing new.
BTGlassDumpMonitorIds();
// BT_GAUGE_SEC_ROT: how far to turn the secondary/radar surface.
// 0 = none, 1 = 90 CCW, 2 = 180, 3 = 90 CW (default -- the pod's
// portrait CRT, user-verified upright).