Rewrite -tident: it painted one screen and left the rest blank

Reported from a four-monitor pod: -tident filled the main screen green and did
nothing to the other three.

The first version opened each DirectDraw device with DDSCL_NORMAL and drew on
that device's primary surface. Under DDSCL_NORMAL a secondary device's primary
surface is not that monitor's framebuffer - it either fails to create or
resolves to the desktop primary - so every device painted the SAME screen, each
over the last. Green is acrFill[1], so device 1 painted last and devices 2 and 3
failed outright, which matches the report exactly.

Now does what the Windows Identify button does: one borderless topmost GDI
window per monitor, positioned from the rectangle that device's HMONITOR
reports, with a real message pump so the windows actually receive WM_PAINT.
Nothing takes exclusive mode and no display mode is changed, which was the point
of the original design and still holds.

Two things fall out of the rewrite:

* Devices resolving to the same rectangle are now grouped behind one window
  labelled with both numbers ("1 & 2") instead of overpainting each other. That
  was a parked item in CLAUDE.md - it happens when a driver presents one monitor
  as two devices, which is common on a single-screen test PC.
* A device reporting no HMONITOR is the primary display driver alias and now
  falls back to the primary monitor rectangle rather than being lost.

gos-displays.txt gains a painted size/position line per monitor, a
"shares a monitor with device N" line, and a summary count, so a failure to show
anything is visible in the log instead of silent.

Docs: this matters because RC2 told pod owners to run -tident and it did not
work. Added to the RC3 change list, corrected the now-obsolete known issue about
single-monitor overpainting, and rewrote checklist section 12 to lead with the
regression - all four monitors must be covered.

Co-authored-by: Claude Opus 5 (Anthropic) <noreply@anthropic.com>
Co-authored-by: GitHub Copilot <copilot@github.com>
This commit is contained in:
2026-08-09 17:04:22 -05:00
co-authored by Claude Opus 5 GitHub Copilot
parent 1684c3fbd2
commit c0daa2d920
4 changed files with 263 additions and 139 deletions
@@ -669,16 +669,82 @@ static void DispLogDesktopOrder()
// This is the same answer: open every device, put its number on the glass, and let
// the operator read the mapping off the pod instead of deducing it.
//
// Deliberately uses DDSCL_NORMAL and paints the desktop through GDI. Taking
// exclusive fullscreen on several devices at once is the very thing that fails on
// modern Windows, and a diagnostic that trips over the fault it is diagnosing is
// worthless. Nothing here changes a display mode.
// Paints through ordinary GDI windows, one per monitor, positioned from the
// rectangle the device's HMONITOR reports. The first version opened each
// DirectDraw device with DDSCL_NORMAL and drew on its primary surface; that does
// not work. Under DDSCL_NORMAL a secondary device's "primary surface" is not that
// monitor's framebuffer - it either fails to create or resolves to the desktop
// primary - so on a four-monitor pod every device painted the main screen, each
// over the last, and only the final colour survived. Nothing here takes exclusive
// mode or changes a display mode, which was the point of the original design and
// still holds.
//
// [tident] Per-window paint data, indexed by the device that owns the window.
static struct
{
COLORREF cr;
char szBig[64];
char szLine1[160];
char szLine2[192];
} g_aIdentPaint[8];
static LRESULT CALLBACK IdentWndProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam )
{
PAINTSTRUCT ps;
HDC hdc;
RECT rc;
HBRUSH hBrush;
HFONT hFontBig;
HFONT hFontSmall;
HFONT hOld;
int idx;
int nHeight;
if( uMsg!=WM_PAINT )
return DefWindowProcA( hWnd, uMsg, wParam, lParam );
idx=(int)GetWindowLongA( hWnd, GWL_USERDATA );
if( idx<0 || idx>7 )
idx=0;
hdc=BeginPaint( hWnd, &ps );
GetClientRect( hWnd, &rc );
nHeight=rc.bottom-rc.top;
hBrush=CreateSolidBrush( g_aIdentPaint[idx].cr );
FillRect( hdc, &rc, hBrush );
DeleteObject( hBrush );
SetBkMode( hdc, TRANSPARENT );
SetTextColor( hdc, RGB(255,255,255) );
// The number the operator types; everything else is confirmation.
hFontBig=CreateFontA( nHeight/2, 0, 0, 0, FW_BOLD, FALSE, FALSE, FALSE,
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
ANTIALIASED_QUALITY, FF_DONTCARE, "Arial" );
hOld=(HFONT)SelectObject( hdc, hFontBig );
rc.top=nHeight/8;
DrawTextA( hdc, g_aIdentPaint[idx].szBig, -1, &rc, DT_CENTER|DT_TOP|DT_SINGLELINE );
SelectObject( hdc, hOld );
DeleteObject( hFontBig );
hFontSmall=CreateFontA( nHeight/16, 0, 0, 0, FW_BOLD, FALSE, FALSE, FALSE,
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
ANTIALIASED_QUALITY, FF_DONTCARE, "Arial" );
hOld=(HFONT)SelectObject( hdc, hFontSmall );
rc.top=nHeight*3/4;
DrawTextA( hdc, g_aIdentPaint[idx].szLine1, -1, &rc, DT_CENTER|DT_TOP|DT_SINGLELINE );
rc.top=nHeight*3/4 + nHeight/14;
DrawTextA( hdc, g_aIdentPaint[idx].szLine2, -1, &rc, DT_CENTER|DT_TOP|DT_SINGLELINE );
SelectObject( hdc, hOld );
DeleteObject( hFontSmall );
EndPaint( hWnd, &ps );
return 0;
}
static void IdentifyDisplays()
{
IDirectDraw7* apDD[8];
IDirectDrawSurface7* apSurf[8];
DWORD iDev;
// Distinct fill per device so the panels remain tellable apart from across the
// room, and readable if the font fails to create for any reason.
static const COLORREF acrFill[8] =
@@ -687,7 +753,18 @@ static void IdentifyDisplays()
RGB(120,0,140), RGB(0,120,140), RGB(90,90,90), RGB(180,60,0)
};
static const char* apszRole[6] = { "main", "radar", "span", "span2", "mfd1", "mfd2" };
int anRoleDev[6];
static const char* pszIdentClass = "MW4TIdentWindow";
int anRoleDev[6];
const char* apszDevRole[8];
RECT arcMon[8];
int anGroup[8];
HWND ahWnd[8];
WNDCLASSA wc;
DWORD iDev;
DWORD dwStart;
int iRole;
int j;
int nWindows=0;
anRoleDev[0]=Environment.FullScreenDevice;
anRoleDev[1]=g_nNonDualHead;
@@ -700,139 +777,161 @@ static void IdentifyDisplays()
DispLog( "Painting each display with its -tmon number. No display mode is changed and\r\n"
"no device takes exclusive mode. The game exits when the display period ends.\r\n\r\n" );
for( iDev=0; iDev<NumDevices && iDev<8; iDev++ )
{
apDD[iDev]=0;
apSurf[iDev]=0;
}
DispLogResolveApis();
for( iDev=0; iDev<NumDevices && iDev<8; iDev++ )
{
DDSURFACEDESC2 ddsd;
HRESULT hr;
HWND hCoop = hWindow ? hWindow : GetDesktopWindow();
char szMon[192];
DispLogMonitorInfo mi;
char szMon[192];
ahWnd[iDev]=NULL;
anGroup[iDev]=(int)iDev;
apszDevRole[iDev]="unused";
for( iRole=0; iRole<6; iRole++ )
if( anRoleDev[iRole]==(int)iDev )
{
apszDevRole[iDev]=apszRole[iRole];
break;
}
// A device reporting no HMONITOR is the primary display driver alias, so
// default to the Windows primary rather than skipping it.
arcMon[iDev].left =0;
arcMon[iDev].top =0;
arcMon[iDev].right =GetSystemMetrics( SM_CXSCREEN );
arcMon[iDev].bottom=GetSystemMetrics( SM_CYSCREEN );
if( g_ahDevMonitor[iDev] && pfnDispLogGetMonitorInfoA )
{
memset( &mi, 0, sizeof(mi) );
mi.cbSize=sizeof(mi);
if( pfnDispLogGetMonitorInfoA( g_ahDevMonitor[iDev], &mi ) )
arcMon[iDev]=mi.rcMonitor;
}
DispLog( " device %d -> %s\r\n", (int)iDev,
DispLogMonitorName( g_ahDevMonitor[iDev], szMon ) );
hr=wDirectDrawCreateEx( &DeviceArray[iDev].DeviceGUID, (VOID**)&apDD[iDev], IID_IDirectDraw7, NULL );
if( FAILED(hr) || !apDD[iDev] )
{
DispLog( " *** DirectDrawCreateEx failed (0x%08lX) - cannot identify this one ***\r\n",
(unsigned long)hr );
apDD[iDev]=0;
continue;
}
hr=apDD[iDev]->SetCooperativeLevel( hCoop, DDSCL_NORMAL );
if( FAILED(hr) )
{
DispLog( " *** SetCooperativeLevel(NORMAL) failed (0x%08lX) ***\r\n", (unsigned long)hr );
continue;
}
memset( &ddsd, 0, sizeof(ddsd) );
ddsd.dwSize=sizeof(ddsd);
ddsd.dwFlags=DDSD_CAPS;
ddsd.ddsCaps.dwCaps=DDSCAPS_PRIMARYSURFACE;
hr=apDD[iDev]->CreateSurface( &ddsd, &apSurf[iDev], NULL );
if( FAILED(hr) || !apSurf[iDev] )
{
DispLog( " *** CreateSurface(primary) failed (0x%08lX) ***\r\n", (unsigned long)hr );
apSurf[iDev]=0;
continue;
}
{
HDC hdc=NULL;
const char* pszRole="unused";
int iRole;
for( iRole=0; iRole<6; iRole++ )
if( anRoleDev[iRole]==(int)iDev )
{
pszRole=apszRole[iRole];
break;
}
hr=apSurf[iDev]->GetDC( &hdc );
if( FAILED(hr) || !hdc )
{
DispLog( " *** GetDC failed (0x%08lX) ***\r\n", (unsigned long)hr );
continue;
}
{
RECT rc;
HBRUSH hBrush;
HFONT hFontBig;
HFONT hFontSmall;
HFONT hOld;
char szBig[16];
char szLine[256];
int nWidth;
int nHeight;
nWidth =GetDeviceCaps( hdc, HORZRES );
nHeight=GetDeviceCaps( hdc, VERTRES );
rc.left=0; rc.top=0; rc.right=nWidth; rc.bottom=nHeight;
hBrush=CreateSolidBrush( acrFill[iDev] );
FillRect( hdc, &rc, hBrush );
DeleteObject( hBrush );
SetBkMode( hdc, TRANSPARENT );
SetTextColor( hdc, RGB(255,255,255) );
// The number the operator types; everything else is confirmation.
sprintf( szBig, "%d", (int)iDev+1 );
hFontBig=CreateFontA( nHeight/2, 0, 0, 0, FW_BOLD, FALSE, FALSE, FALSE,
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
ANTIALIASED_QUALITY, FF_DONTCARE, "Arial" );
hOld=(HFONT)SelectObject( hdc, hFontBig );
rc.top=nHeight/8;
DrawTextA( hdc, szBig, -1, &rc, DT_CENTER|DT_TOP|DT_SINGLELINE );
SelectObject( hdc, hOld );
DeleteObject( hFontBig );
hFontSmall=CreateFontA( nHeight/16, 0, 0, 0, FW_BOLD, FALSE, FALSE, FALSE,
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
ANTIALIASED_QUALITY, FF_DONTCARE, "Arial" );
hOld=(HFONT)SelectObject( hdc, hFontSmall );
rc.top=nHeight*3/4;
sprintf( szLine, "use %d in -tmon", (int)iDev+1 );
DrawTextA( hdc, szLine, -1, &rc, DT_CENTER|DT_TOP|DT_SINGLELINE );
rc.top=nHeight*3/4 + nHeight/14;
sprintf( szLine, "device %d currently: %s", (int)iDev, pszRole );
DrawTextA( hdc, szLine, -1, &rc, DT_CENTER|DT_TOP|DT_SINGLELINE );
SelectObject( hdc, hOld );
DeleteObject( hFontSmall );
}
apSurf[iDev]->ReleaseDC( hdc );
DispLog( " painted: -tmon value %d, currently assigned to %s\r\n",
(int)iDev+1, pszRole );
}
}
DispLog( "\r\n Write down the number on each monitor, then set -tmon in the order\r\n"
" main,radar,mfd1,mfd2 using those numbers.\r\n" );
DispLog( " Holding the display for %d seconds...\r\n", g_nIdentSeconds );
Sleep( (DWORD)g_nIdentSeconds * 1000 );
// Two devices resolving to the same rectangle are one physical monitor - the
// usual cause is a driver exposing a primary alias. One window each would just
// overpaint, so they share a window and the label names both numbers.
for( iDev=0; iDev<NumDevices && iDev<8; iDev++ )
for( j=0; j<(int)iDev; j++ )
if( arcMon[iDev].left ==arcMon[j].left && arcMon[iDev].top ==arcMon[j].top &&
arcMon[iDev].right ==arcMon[j].right && arcMon[iDev].bottom==arcMon[j].bottom )
{
anGroup[iDev]=anGroup[j];
break;
}
for( iDev=0; iDev<NumDevices && iDev<8; iDev++ )
{
if( apSurf[iDev] )
apSurf[iDev]->Release();
if( apDD[iDev] )
apDD[iDev]->Release();
char szNums[64];
char szRoles[192];
char szTmp[64];
int nMembers=0;
if( anGroup[iDev]!=(int)iDev )
continue;
szNums[0]='\0';
szRoles[0]='\0';
for( j=0; j<(int)NumDevices && j<8; j++ )
{
if( anGroup[j]!=(int)iDev )
continue;
if( nMembers )
{
strcat( szNums, " & " );
strcat( szRoles, " " );
}
sprintf( szTmp, "%d", j+1 );
strcat( szNums, szTmp );
sprintf( szTmp, "device %d: %s", j, apszDevRole[j] );
strcat( szRoles, szTmp );
nMembers++;
}
g_aIdentPaint[iDev].cr=acrFill[iDev];
strcpy( g_aIdentPaint[iDev].szBig, szNums );
if( nMembers>1 )
sprintf( g_aIdentPaint[iDev].szLine1,
"one monitor, %d device numbers - either works in -tmon", nMembers );
else
sprintf( g_aIdentPaint[iDev].szLine1, "use %s in -tmon", szNums );
strcpy( g_aIdentPaint[iDev].szLine2, szRoles );
}
memset( &wc, 0, sizeof(wc) );
wc.lpfnWndProc =IdentWndProc;
wc.hInstance =GetModuleHandleA( NULL );
wc.hCursor =LoadCursorA( NULL, IDC_ARROW );
wc.hbrBackground=NULL;
wc.lpszClassName=pszIdentClass;
RegisterClassA( &wc );
for( iDev=0; iDev<NumDevices && iDev<8; iDev++ )
{
if( anGroup[iDev]!=(int)iDev )
{
DispLog( " device %d shares a monitor with device %d - labelled together\r\n",
(int)iDev, anGroup[iDev] );
continue;
}
ahWnd[iDev]=CreateWindowExA( WS_EX_TOPMOST|WS_EX_TOOLWINDOW,
pszIdentClass, "", WS_POPUP,
arcMon[iDev].left, arcMon[iDev].top,
arcMon[iDev].right -arcMon[iDev].left,
arcMon[iDev].bottom-arcMon[iDev].top,
NULL, NULL, GetModuleHandleA( NULL ), NULL );
if( !ahWnd[iDev] )
{
DispLog( " *** CreateWindowEx failed for device %d (%lu) ***\r\n",
(int)iDev, (unsigned long)GetLastError() );
continue;
}
SetWindowLongA( ahWnd[iDev], GWL_USERDATA, (LONG)iDev );
ShowWindow( ahWnd[iDev], SW_SHOW );
UpdateWindow( ahWnd[iDev] );
nWindows++;
DispLog( " painted %ldx%ld at %ld,%ld: '%s', %s\r\n",
(long)(arcMon[iDev].right -arcMon[iDev].left),
(long)(arcMon[iDev].bottom-arcMon[iDev].top),
(long)arcMon[iDev].left, (long)arcMon[iDev].top,
g_aIdentPaint[iDev].szBig, g_aIdentPaint[iDev].szLine2 );
}
DispLog( "\r\n %d window(s) shown across %d device(s).\r\n", nWindows, (int)NumDevices );
DispLog( " Write down the number on each monitor, then set -tmon in the order\r\n"
" main,radar,mfd1,mfd2 using those numbers.\r\n" );
DispLog( " Holding the display for %d seconds...\r\n", g_nIdentSeconds );
// Real message pump: without one the windows never receive WM_PAINT and stay blank.
// Unsigned subtraction so a GetTickCount wrap cannot end the hold early.
dwStart=GetTickCount();
while( (GetTickCount()-dwStart) < (DWORD)g_nIdentSeconds*1000 )
{
MSG msg;
while( PeekMessageA( &msg, NULL, 0, 0, PM_REMOVE ) )
{
TranslateMessage( &msg );
DispatchMessageA( &msg );
}
Sleep( 50 );
}
for( iDev=0; iDev<NumDevices && iDev<8; iDev++ )
if( ahWnd[iDev] )
DestroyWindow( ahWnd[iDev] );
UnregisterClassA( pszIdentClass, GetModuleHandleA( NULL ) );
if( !nWindows )
DispLog( " *** no windows could be created - nothing was shown ***\r\n" );
DispLog( " -tident finished; exiting.\r\n" );
}
+7 -3
View File
@@ -39,6 +39,9 @@ automatically, or an option you have to switch on deliberately. Nothing new is e
<p>Everything in this list is a fix -- none of it needs configuring. The console lobby now reads
<strong><code>V5.1.0b3</code></strong>; use that to confirm a pod actually received the new build.</p>
<ul>
<li><strong><code>-tident</code> now actually works.</strong> In RC2 it painted a single coloured
rectangle on the main screen and nothing on the other monitors, so it could not be used for what
it was added for. See section 4.</li>
<li><strong>Damage paper dolls corrected on 22 'Mechs.</strong> The external MFD and Radar damage
displays were showing mirrored or misaligned zones. See section 5.</li>
<li><strong>Targeting a Zeus showed an Annihilator.</strong> Fixed, along with two other 'Mechs whose
@@ -557,9 +560,10 @@ screen (section 4).</p>
<li><strong>Sound output is two-speaker stereo.</strong> Surround and quad speaker rigs are downmixed.
This is how the game has always behaved; making use of more speakers is being investigated for a
later release.</li>
<li><strong><code>-tident</code> is only meaningful on a machine with more than one monitor.</strong> On a
single-screen test PC it may briefly paint one number over another, because Windows can present a
single monitor to the game as two devices. Harmless, and it cannot happen on a real pod.</li>
<li><strong>If two device numbers land on one monitor, <code>-tident</code> now says so</strong> rather
than painting one number over the other. The monitor shows both numbers, e.g. <code>1 &amp; 2</code>,
and either may be used in <code>-tmon</code>. This happens when a driver presents one monitor to the
game as two devices, which is common on single-screen test PCs and does not occur on a pod.</li>
</ul>
<hr>
<h2>11. Upgrade checklist</h2>
+7 -3
View File
@@ -15,6 +15,9 @@ match printer.
Everything in this list is a fix -- none of it needs configuring. The console lobby now reads
**`V5.1.0b3`**; use that to confirm a pod actually received the new build.
- **`-tident` now actually works.** In RC2 it painted a single coloured rectangle on the main
screen and nothing on the other monitors, so it could not be used for what it was added for.
See section 4.
- **Damage paper dolls corrected on 22 'Mechs.** The external MFD and Radar damage displays
were showing mirrored or misaligned zones. See section 5.
- **Targeting a Zeus showed an Annihilator.** Fixed, along with two other 'Mechs whose target
@@ -605,9 +608,10 @@ screen (section 4).
- **Sound output is two-speaker stereo.** Surround and quad speaker rigs are downmixed. This is
how the game has always behaved; making use of more speakers is being investigated for a
later release.
- **`-tident` is only meaningful on a machine with more than one monitor.** On a single-screen
test PC it may briefly paint one number over another, because Windows can present a single
monitor to the game as two devices. Harmless, and it cannot happen on a real pod.
- **If two device numbers land on one monitor, `-tident` now says so** rather than painting one
number over the other. The monitor shows both numbers, e.g. `1 & 2`, and either may be used in
`-tmon`. This happens when a driver presents one monitor to the game as two devices, which is
common on single-screen test PCs and does not occur on a pod.
---
+22 -5
View File
@@ -138,13 +138,23 @@ Added 2026-07-23 (commit 5813aeb6)
Added for RC2 (2026-07-25 through 2026-08-07)
------------------------------------------------
12. -tident DISPLAY IDENTIFY MODE (new switch)
12. -tident DISPLAY IDENTIFY MODE
MW4.exe -tident [3..120, default 20] paints every display with a colour,
its -tmon number, its DirectDraw device index and its current role, then
exits. Uses DDSCL_NORMAL + GDI: no exclusive mode, no display mode change.
exits. No exclusive mode, no display mode change.
[ ] MW4.exe -tident on a 4-monitor pod: every monitor shows a distinct
colour and a large number; no monitor is left on the desktop
REWRITTEN FOR RC3 -- in RC2 this did not work. It opened each DirectDraw
device with DDSCL_NORMAL and drew on the device's primary surface, but a
secondary device's primary surface under DDSCL_NORMAL is not that monitor's
framebuffer, so on a 4-monitor pod every device painted the MAIN screen in
turn and only the last survived. Observed symptom: one green rectangle on
the main monitor, nothing anywhere else. It now creates one borderless
topmost GDI window per monitor, positioned from that device's monitor
rectangle, and runs a real message pump so the windows actually paint.
[ ] MW4.exe -tident on a 4-monitor pod: ALL FOUR monitors are covered, each
a different colour with a large number. This is the RC2 regression --
check it first
[ ] The role shown on each monitor (main / radar / mfd1 / mfd2) matches
what that monitor actually displays during a mission
[ ] The game exits on its own when the timer expires, and does NOT start
@@ -153,8 +163,15 @@ Added for RC2 (2026-07-25 through 2026-08-07)
and monitor arrangement as before (nothing to restore by hand)
[ ] Works on a pod where the MFD modes are currently failing (that is the
case it exists for)
[ ] Everything painted on screen also appears in gos-displays.txt
[ ] Everything painted on screen also appears in gos-displays.txt, including
the new "N window(s) shown across M device(s)" line
[ ] gos-displays.txt reports a painted line per monitor with its pixel size
and desktop position, and those match the physical layout
[ ] Typing those numbers into -tmon puts each panel on the intended monitor
[ ] On a SINGLE-monitor test PC (not a pod): if the driver presents one
monitor as two devices, one window appears showing both numbers, e.g.
"1 & 2", rather than two windows overpainting. gos-displays.txt says
"shares a monitor with device N"
13. gos-displays.txt FULL START-UP TRACE
Startup-only logging; no frame-loop cost. File is truncated each launch.