From c0daa2d92052164ac8e5726c89c77607024ec417 Mon Sep 17 00:00:00 2001 From: RT Date: Sun, 9 Aug 2026 17:04:22 -0500 Subject: [PATCH] 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) Co-authored-by: GitHub Copilot --- .../CoreTech/Libraries/GameOS/VideoCard.cpp | 355 +++++++++++------- RELEASE-NOTES-5.1.0b_RC3.html | 10 +- RELEASE-NOTES-5.1.0b_RC3.md | 10 +- testing-checklist-5.1.0b_RC3.txt | 27 +- 4 files changed, 263 insertions(+), 139 deletions(-) diff --git a/Gameleap/code/CoreTech/Libraries/GameOS/VideoCard.cpp b/Gameleap/code/CoreTech/Libraries/GameOS/VideoCard.cpp index a01dabf1..6cadd82e 100644 --- a/Gameleap/code/CoreTech/Libraries/GameOS/VideoCard.cpp +++ b/Gameleap/code/CoreTech/Libraries/GameOS/VideoCard.cpp @@ -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 %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; iDevRelease(); - 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; iDevEverything 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 @@ -557,9 +560,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.

11. Upgrade checklist

diff --git a/RELEASE-NOTES-5.1.0b_RC3.md b/RELEASE-NOTES-5.1.0b_RC3.md index 6618905d..3d962c7f 100644 --- a/RELEASE-NOTES-5.1.0b_RC3.md +++ b/RELEASE-NOTES-5.1.0b_RC3.md @@ -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. --- diff --git a/testing-checklist-5.1.0b_RC3.txt b/testing-checklist-5.1.0b_RC3.txt index 76e4329c..c727b05b 100644 --- a/testing-checklist-5.1.0b_RC3.txt +++ b/testing-checklist-5.1.0b_RC3.txt @@ -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.