diff --git a/CLAUDE.md b/CLAUDE.md index 8f8e3265..cfe27b3e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -937,7 +937,162 @@ Docs updates: - missing `MissionName` now falls back to first map for that game type, - added `NoReturn` semantics and examples. +## 📋 STEP 10: Four-monitor MFD bring-up, display diagnostics, and the native-DirectDraw +## exclusive-mode wall (2026-07-25) + +A long, dense session on the new 4-monitor bench (`MR_new`: AMD FirePro W4100, 4 outputs, +Win10). Everything below is empirical — measured on real hardware, not inferred. + +### The root problem that made all of this hard +`CHSH_Device::InitFirst` / `InitSecond` (`CoreTech\Libraries\GameOS\render.cpp`) **discarded +every single `HRESULT`** (`SetCooperativeLevel`, `SetDisplayMode`, `CreateSurface`, +`GetAttachedSurface`, `QueryInterface`, `CreateDevice`) and **returned `true` +unconditionally**. A panel that failed to open produced no error, no crash and no log entry — +the monitor simply stayed on the desktop. SPEW is compiled out of shipping builds, so there +was no way to see any of it. **Fixing that visibility was what unblocked the whole session.** + +### ✅ Diagnostics added (KEEP THESE — they are why everything below was findable) +- **`gos-displays.txt`** (written next to the exe by `VideoCard.cpp` `LogDisplayDevices()`): + `NumDevices`/`NumHWDevices`/`NumMonitors`, every DirectDraw device + `hw_rasterization`, + the role assignment (`FullScreenDevice`/`g_nNonDualHead`/`g_nDualHead`/`g_nDualHead2`/ + `g_nMFD1`/`g_nMFD2`), `-tmon` APPLIED/REJECTED per slot, and the decisive + "mode 4 requires BOTH mfd1 and mfd2" line. +- **Per-call HRESULT logging** in `InitFirst`/`InitSecond` via `HSH_LogInit()` / `HSH_CheckHR()` + / `HSH_HRName()` (27 `DDERR_*` codes decoded by name). Appends to the same file; each line is + written and flushed individually so **the log survives a crash**. +- **`gos-fps.txt`** — new **`-fps`** switch (`WinMain.cpp` `GOS_LogFrameRate`, hooked onto the + existing `frameRate` global). Per-second: frames, avg fps, **1% low**, worst frame in ms, + and a count of frames over 2x average ("hitches"). Works in **Release** — the engine's own + `AddDebugData("FrameRate")` is `#ifdef LAB_ONLY` (`MWMission.cpp`) so it only exists in + `MW4pro.exe`. Off unless `-fps` is given (a global read + branch when absent, no file + created). Handle is held open for the process lifetime so the once-per-second write does not + put an unpredictable syscall on the render thread — the tool must not perturb what it measures. + +### ✅ Crash-safety fix (independent of everything else, worth keeping) +`hsh_initialized` was set **unconditionally** after panel init. A failed panel therefore left +null surfaces and a null `IDirect3DDevice7` behind, and the per-frame path called straight +through them. Now: all four `InitSecond` overrides (`CMR`/`CRadar`/`CMFD`/`CMFDRight`) bail on +base failure, `CMFD_Device::InitFirst` reports the real result instead of always `true`, and +`hsh_initialized` is only set when the panels genuinely came up → **the game runs without MFDs +instead of dying**. +- **Crash signature to recognise:** `call [ecx+0x44]` with `ECX=0` = `IDirectDrawSurface7::GetDC` + on a never-created surface (verified against `build-env\dx7asdk\include\ddraw.h` vtable order). + Reported as `EXCEPTION : Attempt to read from address 0x00000044`. + +### ✅ AppCompat shim — the single most important operational fact +**`DWM8And16BitMitigation` is keyed on the executable's FULL PATH.** A copy of the game at a +new path silently loses it. MW4 renders at `bitdepth=16` and **modern GPUs expose ZERO 16-bit +display modes** — the shim *synthesises* them (proved directly: crash dump shows +`16 bit modes :` EMPTY without the shim, populated with it). +- **Without the shim the error message actively misleads.** GameOS raises + `GOS_DXRASTERIZER_NOFULLSCREEN` — *"Another application is preventing use of full screen + mode"* (`DXRasterizer.cpp:~1125`). That is a **catch-all** fired after every `SetDisplayMode` + attempt fails; it even scans for NetMeeting. It sends you hunting for a conflicting program + that does not exist. The real cause is the missing shim / absent 16-bit modes. +- **NEW: `build-env\set-appcompat.ps1` + `set-appcompat.bat`** — self-locating (`$PSScriptRoot`) + one-click installer. Applies the layer to `MW4.exe`/`MW4pro.exe`/`MW4Ed2.exe` sitting next to + it, wherever that install lives; HKCU always, HKLM too if elevated (note the HKLM value format + differs — it carries a leading `$` marker). Verifies by reading back; detects the + `HIGHDPIAWARE`-only entry that *suppresses* the auto-shim (the original STEP 5 bug). + `-Remove` / `-WhatIfOnly` supported. **`deploy-mw4.ps1` now ships both into every deployment.** + +### ✅ Exclusive fullscreen DOES work on Win10 without dgVoodoo2 +Confirmed on the W4100 with system `ddraw.dll` and dgVoodoo2 physically removed: `MW4.exe` +alt-enters to exclusive fullscreen correctly **once the shim is applied to that exe path**. +Windowed mode also works natively with no shim at all (windowed sets +`Environment.bitDepth = DesktopBpp` = 32, so there is no mode switch). Console/shell confirmed +windowed; a **full mission windowed is still untested**. + +### ❌ Native multi-monitor MFD is BLOCKED — and no flag combination fixes it +The panels' cooperative-level call was genuinely wrong (a latent 2002 bug): every panel asked to +be **both** the process focus window **and** its own device window, on the one shared `hWindow`, +*after* the main device had already taken exclusive mode on it. The main device +(`DXRasterizer.cpp:~1027`) already uses the correct two-call idiom +(`SETFOCUSWINDOW` alone, then `EXCLUSIVE|FULLSCREEN`) — tagged `//sanghoon`, same author. The +panels never were. + +A new **`-tcoop <0-5>`** switch was added to test every plausible form on real hardware without a +rebuild between attempts. Results (no dgVoodoo2, shim applied, `-tmfds 4`): + +| `-tcoop` | Flags | Result | +|---|---|---| +| 0 | `SETFOCUSWINDOW\|CREATEDEVICEWINDOW\|ALLOWREBOOT\|EXCLUSIVE\|FULLSCREEN` (legacy) | `DDERR_EXCLUSIVEMODEALREADYSET` | +| 1 | `CREATEDEVICEWINDOW\|EXCLUSIVE\|FULLSCREEN` (no focus claim) | `DDERR_INVALIDPARAMS` | +| 2 | `SETFOCUSWINDOW`, then `CREATEDEVICEWINDOW\|…` | `DDERR_INVALIDPARAMS` | +| 3 | `SETFOCUSWINDOW`, then `EXCLUSIVE\|FULLSCREEN` | 1st panel collides; **that collision steals exclusive from the main display**, after which panels 2+3 fully init (radar reached `CreateDevice(HAL) = DD_OK`). Side effect: desktop left at 1920x1080 **16bpp**. Not viable. | +| 4 | `EXCLUSIVE\|FULLSCREEN` only | `DDERR_EXCLUSIVEMODEALREADYSET` on **all** panels | +| 5 | `ALLOWREBOOT\|EXCLUSIVE\|FULLSCREEN` | `DDERR_EXCLUSIVEMODEALREADYSET` on **all** panels | + +**CONCLUSION: on modern Windows only ONE DirectDraw object per process may hold exclusive +fullscreen.** The main display takes it; every secondary panel is refused. XP allowed multiple; +**dgVoodoo2 allows it because it is a full reimplementation of ddraw, not bound by that rule.** +=> **dgVoodoo2 cannot be removed for `-tmfds 1/3/4` by fixing flags.** Default stays `-tcoop 0`. +(`-tcoop` is retained: it is how this was settled and will re-settle it on different hardware.) + +### ✅ Working 4-monitor config (WITH dgVoodoo2) +- **dgVoodoo2 Scaling mode MUST be "Stretched, Keep Aspect Ratio".** Plain "Stretched" fails + *silently*: main + radar go fullscreen black, both MFD monitors keep showing the desktop, and + **every DirectDraw call still returns `DD_OK`** — the devices are alive but dgVoodoo2 never + drives those outputs. Diagnosed with a temporary per-panel colour-flash test (since removed). +- Device index → physical monitor is **1:1** on this bench (colour test confirmed): main=0, + radar=1, mfd-left=2, mfd-right=3, so `-tmon 1,2,3,4` is identical to auto-detection. +- **Confirmed working end-to-end: all three secondary panels present, full mission played.** +- ⚠️ **The working `dgVoodoo.conf` is NOT versioned in the repo** (it was removed in `0ceba9c7`). + A fresh deploy will reproduce the silent MFD failure. TODO: commit it + have `deploy-mw4.ps1` + place it. + +### 📐 Engine is 4:3 ONLY (relevant to every display decision) +`ImageHlp.cpp:~464` asserts the complete supported resolution set: **640x480, 512x384, 800x600, +960x720, 1024x768, 1280x1024 (5:4), 1600x1200**. There is no 16:9 mode and **no aspect +correction anywhere** in the codebase (the only `aspect` hits are texture-dimension caps and +CameraShip). On a 16:9 monitor a 4:3 image must be adapted by the scaler: plain stretch = +distorted (circles → ovals, reticle wrong); keep-aspect = correct geometry + pillarbox bars. +Native widescreen would require re-authoring every 2D/shell layout (all fixed 640x480 pixel +coordinates) — a content project, not a code tweak. + +### 📐 Assessment: moving to borderless windowed (discussed, NOT started) +The only native path to multi-monitor, since it removes exclusive mode from the picture entirely. +Smaller than it sounds because the mechanisms already exist: +- The windowed present is already `wBlt(FrontBufferSurface, &Window, BackBufferSurface, …)` — + a blit to an arbitrary screen rect. `Blt` stretches when the dest rect differs in size, so + aspect-preserving borderless output is **rect maths, not new machinery**. +- Clipper wrappers (`wCreateClipper`/`wSetClipper`/`wSetHWnd`) already exist in `DirectDraw.cpp`, + unused by the fullscreen path. +- **None of the drawing code changes** — panels already render into `pDDSTarget` and composite; + `DrawQuad`/`DrawTexture`/fonts/`SwapRightState` don't care whether the present is Flip or Blt. +- Work is confined to `render.cpp`, `DXRasterizer.cpp`, `WinMain.cpp` (+ a little `Windows.cpp`): + panel init → `DDSCL_NORMAL` + clipper + offscreen render surface (~150 lines); ~4 panel `Flip` + sites → `Blt`; break the `HSH_EnterFullScreen2()` ↔ `EnterFullScreenMode()` coupling + (`DXRasterizer.cpp:~1132` is its ONLY call site, which is also why windowed mode currently + creates no panels at all); **new: explicit frame pacing** (windowed Blt has no vsync — this is + the same root cause as the known mechlab fast-spin bug). +- **Gains:** no dgVoodoo2, no shim, no 16-bit dependency, no exclusive-mode contention, aspect + under our control, and panels stay at their monitor's native mode so "this panel won't accept + 640x480" becomes impossible. +- **Risk that decides it:** windowed D3D7 device creation is per-GPU (the *editor* hit + `DDERR_INVALIDOBJECT` on Win11 and needed DDrawCompat; the *game* succeeded natively on the + W4100). Version lockstep means one build must serve every pod. +- **Recommended staging:** (1) `-borderless` for the MAIN display only, default off; (2) convert + **one** panel (radar) — the small, make-or-break test of `DDSCL_NORMAL` + clipper + + D3D-on-offscreen on the target GPU; (3) all three panels; (4) retire dgVoodoo2 + shim. Keep + exclusive fullscreen switch-selectable indefinitely. + +### Incidental findings +- **`-2dt` is not a recognised switch anywhere in the codebase**, despite appearing in production + `ctcl.ini` launch lines. Completely inert. (2D targets are already the default; `-3dt` is what + switches to the 3D model.) +- `NumHWDevices` (5) can exceed `NumDevices` (4) — it counts D3D device-enumeration callbacks, and + an adapter exposing both a HAL and a T&L HAL yields two. Benign; `InitSecond` hardcodes + `IID_IDirect3DHALDevice`. +- New switches are documented in `-help` (`-fps` under LOGGING AND DIAGNOSTICS, `-tcoop` under + DISPLAY AND VIDEO). + ## Next steps (proposed) +- [ ] **Commit the working `dgVoodoo.conf`** and have `deploy-mw4.ps1` place it — without it a + fresh 4-monitor deploy silently fails (see STEP 10). +- [ ] (Decision pending) Borderless-windowed migration — see the staged assessment in STEP 10. + Step 2 (one panel) is the cheap, decisive experiment. +- [ ] Test a **full mission windowed** (only the console/shell has been verified windowed). - [x] ~~Windowed 3D viewport on Win11~~ — DONE via DDrawCompat (see STEP 8 viewport section). - [ ] (Optional) Curate remaining WIP content as features are exercised (editor loads dev `Content\`). - [ ] (Optional) Build Debug/Armor configs (need `dbg.bin`/`arm.bin` output dirs). diff --git a/Gameleap/code/CoreTech/Libraries/GameOS/VideoCard.cpp b/Gameleap/code/CoreTech/Libraries/GameOS/VideoCard.cpp index 11d7f523..f7fb8fee 100644 --- a/Gameleap/code/CoreTech/Libraries/GameOS/VideoCard.cpp +++ b/Gameleap/code/CoreTech/Libraries/GameOS/VideoCard.cpp @@ -285,7 +285,9 @@ HRESULT CALLBACK EnumModesCallback2( LPDDSURFACEDESC2 lpDDSurfaceDesc, LPVOID lp // [tmon] Apply a hard-assigned display device, but only if the operator supplied one // and it names a device that actually exists. Anything else is ignored, so a stale // -tmon left on the command line of a machine with fewer monitors simply falls back -// to auto-detection instead of breaking startup. +// to auto-detection instead of breaking startup. Rejections are reported in +// gos-displays.txt - silently doing nothing is worse than useless when the operator +// is trying to work out why a monitor did not light up. // static void ApplyMonitorOverride(int slot, int* pTarget) { @@ -294,6 +296,114 @@ static void ApplyMonitorOverride(int slot, int* pTarget) *pTarget = dev; } +// +// [displaylog] Append one printf-style line to the display report. +// +static void EmitDisplayLine( HANDLE hFile, const char* fmt, ... ) +{ + char szLine[512]; + DWORD dwWritten=0; + va_list ap; + va_start( ap, fmt ); + vsprintf( szLine, fmt, ap ); + va_end( ap ); + WriteFile( hFile, szLine, (DWORD)strlen(szLine), &dwWritten, NULL ); +} + +// +// [displaylog] Write gos-displays.txt next to the executable, describing every +// DirectDraw device GameOS found and which display role each one was assigned. +// +// This exists because there is no other way to see it: the SPEW macros are compiled +// out of shipping builds, and a monitor that simply never lights up gives no clue +// whether the device was missing, lacked hardware caps, or was assigned elsewhere. +// +static void LogDisplayDevices() +{ + char szDir[MAX_PATH]; + char szPath[MAX_PATH]; + + szDir[0]='\0'; + GetModuleFileNameA( NULL, szDir, sizeof(szDir) ); + char* pSlash=strrchr( szDir, '\\' ); + if( pSlash ) + *(pSlash+1)='\0'; + else + szDir[0]='\0'; + sprintf( szPath, "%sgos-displays.txt", szDir ); + + HANDLE hFile=CreateFileA( szPath, GENERIC_WRITE, FILE_SHARE_READ, NULL, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL ); + if( hFile==INVALID_HANDLE_VALUE ) + return; + + EmitDisplayLine( hFile, "GameOS display device enumeration\r\n" ); + EmitDisplayLine( hFile, "=================================\r\n\r\n" ); + EmitDisplayLine( hFile, "NumDevices = %d (DeviceArray holds at most 8)\r\n", (int)NumDevices ); + EmitDisplayLine( hFile, "NumHWDevices = %d\r\n", (int)NumHWDevices ); + EmitDisplayLine( hFile, "NumMonitors = %d\r\n\r\n", (int)NumMonitors ); + + for( DWORD t0=0; t0= 0 ) + bAny=true; + if( bAny ) + { + EmitDisplayLine( hFile, "\r\n-tmon override:\r\n" ); + for( int s2=0; s2<4; s2++ ) + { + int dev=g_naMonitorOverride[s2]; + if( dev < 0 ) + EmitDisplayLine( hFile, " %-5s : auto\r\n", apszRole[s2] ); + else if( dev < (int)NumDevices ) + EmitDisplayLine( hFile, " %-5s : device %d APPLIED\r\n", apszRole[s2], dev ); + else + EmitDisplayLine( hFile, " %-5s : device %d *** REJECTED - only %d device(s) exist ***\r\n", + apszRole[s2], dev, (int)NumDevices ); + } + } + else + { + EmitDisplayLine( hFile, "\r\n-tmon override: not supplied (all roles auto-detected)\r\n" ); + } + } + + // The decisive line: whether the MFD subsystem will initialise at all. + EmitDisplayLine( hFile, "\r\nMFD mode (-tmfds) = %d\r\n", g_nTypeOfMFDs ); + if( g_nTypeOfMFDs == 4 ) + { + bool bOK = (g_nMFD1 != -1) && (g_nMFD2 != -1); + EmitDisplayLine( hFile, "mode 4 requires BOTH mfd1 and mfd2: %s\r\n", bOK ? "OK" : "*** FAILED ***" ); + if( !bOK ) + EmitDisplayLine( hFile, + " -> IsMultimonitorAvaliable() returns false, so HSH_EnterFullScreen2()\r\n" + " falls back to the single-secondary-monitor path and the two MFD\r\n" + " displays are never opened. Four hardware-rasterizing DirectDraw\r\n" + " devices are needed: main, radar, mfd1, mfd2.\r\n" ); + } + + CloseHandle( hFile ); +} + // // void FindVideoCards() @@ -420,10 +530,10 @@ void FindVideoCards() // [tmon] Hard-assigned MFD overrides (only meaningful with -tmfds 4). ApplyMonitorOverride(2, &g_nMFD1); ApplyMonitorOverride(3, &g_nMFD2); - // Log the final assignment so the operator can confirm which physical monitor each - // role landed on without needing a debugger. - SPEW(( GROUP_GAMEOS, "Display roles: devices=%d main=%d radar=%d mfd1=%d mfd2=%d span=%d", - (int)NumDevices, Environment.FullScreenDevice, g_nNonDualHead, g_nMFD1, g_nMFD2, g_nDualHead )); + // [displaylog] Write gos-displays.txt with the full enumeration and the role + // assignment. SPEW is compiled out of shipping builds, so a file is the only way + // the operator can see why a display did or did not get picked up. + LogDisplayDevices(); // jcem // // Check any known video cards have up to date drivers (Only on Windows9x) diff --git a/Gameleap/code/CoreTech/Libraries/GameOS/WinMain.cpp b/Gameleap/code/CoreTech/Libraries/GameOS/WinMain.cpp index b60b5435..4838079d 100644 --- a/Gameleap/code/CoreTech/Libraries/GameOS/WinMain.cpp +++ b/Gameleap/code/CoreTech/Libraries/GameOS/WinMain.cpp @@ -841,6 +841,133 @@ HRESULT App_Render( LPDIRECT3DDEVICE7 pd3dDevice,LPDIRECTDRAWSURFACE7 pddsTextur //sanghoon end +// +// [fpslog] Per-second frame pacing report, written to gos-fps.txt next to the executable. +// +// Off unless the -fps switch is given, so a normal pod run does no extra work at all. +// Set from MW4Application's command-line parser. +// +int g_nFpsLog = 0; + +// The engine's own FrameRate readout is inside #ifdef LAB_ONLY, so it exists only in +// MW4pro.exe. This works in Release builds, which is what actually runs on the pods. +// +// Average FPS on its own hides stutter: 60 fps average is indistinguishable from 60 fps +// with a dropped frame every second. So this also reports the slowest single frame and a +// 1% low (the mean of the worst 1% of frames in that second), which is where rhythmic +// hitching shows up, plus a count of frames that took more than twice the average. +// +static void GOS_FpsWriteLine( const char* pszLine ) +{ + // The handle is held open for the life of the process. Opening and closing the file + // every second would put a syscall of unpredictable latency (worse with antivirus or + // a network path) on the render thread - which is exactly what this code is trying to + // measure. Writes still survive a crash, because WriteFile hands the data to the OS + // cache rather than a CRT buffer. + static HANDLE s_hFile=INVALID_HANDLE_VALUE; + char szDir[MAX_PATH]; + char szPath[MAX_PATH]; + DWORD dwWritten=0; + + if( s_hFile==INVALID_HANDLE_VALUE ) + { + szDir[0]='\0'; + GetModuleFileNameA( NULL, szDir, sizeof(szDir) ); + char* pSlash=strrchr( szDir, '\\' ); + if( pSlash ) + *(pSlash+1)='\0'; + else + szDir[0]='\0'; + sprintf( szPath, "%sgos-fps.txt", szDir ); + + // FILE_SHARE_READ so the file can be tailed while the game is running. + s_hFile=CreateFileA( szPath, GENERIC_WRITE, FILE_SHARE_READ, NULL, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL ); + if( s_hFile==INVALID_HANDLE_VALUE ) + return; + } + + WriteFile( s_hFile, pszLine, (DWORD)strlen(pszLine), &dwWritten, NULL ); +} + +static void GOS_LogFrameRate( float fFrameRate ) +{ + enum { MAX_WORST = 64 }; // >= 1% of any plausible per-second frame count + static float s_afWorst[MAX_WORST]; // longest frames this second, descending ms + static int s_nWorst = 0; + static float s_fAccumMS= 0.0f; + static float s_fSumMS = 0.0f; + static float s_fMaxMS = 0.0f; + static int s_nFrames = 0; + static int s_nSecond = 0; + char szLine[256]; + + if( !g_nFpsLog ) // [fpslog] -fps not given: do nothing + return; + + if( fFrameRate<=0.0f ) + return; + + float fMS = 1000.0f/fFrameRate; + + s_nFrames++; + s_fSumMS += fMS; + s_fAccumMS += fMS; + if( fMS>s_fMaxMS ) + s_fMaxMS = fMS; + + // Maintain the longest frames of this second, sorted longest-first. + if( s_nWorsts_afWorst[MAX_WORST-1] ) + s_afWorst[MAX_WORST-1] = fMS; + for( int i=s_nWorst-1; i>0 && s_afWorst[i]>s_afWorst[i-1]; i-- ) { + float fTmp=s_afWorst[i]; s_afWorst[i]=s_afWorst[i-1]; s_afWorst[i-1]=fTmp; + } + + if( s_fAccumMS<1000.0f ) + return; + + float fAvgMS = s_fSumMS/(float)s_nFrames; + float fAvgFPS = 1000.0f/fAvgMS; + float fMinFPS = 1000.0f/s_fMaxMS; + + // 1% low: mean of the worst 1% of frames (at least one). + int nLow = s_nFrames/100; + if( nLow<1 ) + nLow = 1; + if( nLow>s_nWorst ) + nLow = s_nWorst; + float fLowSum=0.0f; + for( int j=0; j0.0f) ? (1000.0f/(fLowSum/(float)nLow)) : 0.0f; + + // Frames that took more than twice the average - i.e. visible hitches. + int nHitch=0; + for( int k=0; kfAvgMS*2.0f ) + nHitch++; + + if( s_nSecond==0 ) + GOS_FpsWriteLine( " sec frames avg fps 1% low worst frame hitches\r\n" + " --- ------ ------- ------ ----------- -------\r\n" ); + + sprintf( szLine, "%5d %6d %8.1f %8.1f %8.1f ms %s%d\r\n", + s_nSecond, s_nFrames, fAvgFPS, fLowFPS, s_fMaxMS, + nHitch ? "<-- " : "", nHitch ); + GOS_FpsWriteLine( szLine ); + + s_nSecond++; + s_nFrames = 0; + s_nWorst = 0; + s_fSumMS = 0.0f; + s_fMaxMS = 0.0f; + s_fAccumMS-= 1000.0f; + if( s_fAccumMS<0.0f ) + s_fAccumMS = 0.0f; +} + // // Update the display (call all the 'Update Renderers' routines // @@ -885,6 +1012,7 @@ void gos_UpdateDisplay( bool Everything ) frameRate=(float)frequency/(float)thisframe; // Calculate frame rate based on clock frequency StartOfRenderTime=GetCycles(); + GOS_LogFrameRate( frameRate ); // [fpslog] per-second pacing report // // Don't trigger watchdog timer // diff --git a/Gameleap/code/CoreTech/Libraries/GameOS/render.cpp b/Gameleap/code/CoreTech/Libraries/GameOS/render.cpp index 4138258c..62c67ff8 100644 --- a/Gameleap/code/CoreTech/Libraries/GameOS/render.cpp +++ b/Gameleap/code/CoreTech/Libraries/GameOS/render.cpp @@ -4,6 +4,7 @@ #include #include #include +#include // [displaylog] va_list/va_start for HSH_LogInit //#include "directx.hpp" #include "dxrasterizer.hpp" #define SAFE_RELEASE(p) { if(p) { (p)->Release(); (p)=NULL; } } @@ -522,19 +523,185 @@ void CHSH_Device::SetRenderTargetTexture() const DWORD dwFlags = DDSCL_SETFOCUSWINDOW | DDSCL_CREATEDEVICEWINDOW | DDSCL_ALLOWREBOOT | DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN; +// [panelcoop] Which SetCooperativeLevel form the secondary panels use; 0 = legacy. +// Selected with -tcoop <0-3>. See CHSH_Device::InitFirst for what each one does. +int g_nHshCoopMode = 0; + extern HRESULT wDirectDrawCreateEx( GUID* lpGUID, void** lplpDD, REFIID iid, IUnknown* pUnkOuter ); +// +// [displaylog] Append a printf-style line to gos-displays.txt, the report VideoCard.cpp +// writes at device-enumeration time. +// +// This exists because the panel initialisation below discarded every HRESULT it produced +// and returned true unconditionally. A display that failed to open therefore generated no +// error, no crash and no log entry - the monitor simply stayed on the desktop, which is +// indistinguishable from the display never having been asked to open at all. +// +void HSH_LogInit( const char* fmt, ... ) +{ + char szDir[MAX_PATH]; + char szPath[MAX_PATH]; + char szLine[512]; + DWORD dwWritten=0; + va_list ap; + + szDir[0]='\0'; + GetModuleFileNameA( NULL, szDir, sizeof(szDir) ); + char* pSlash=strrchr( szDir, '\\' ); + if( pSlash ) + *(pSlash+1)='\0'; + else + szDir[0]='\0'; + sprintf( szPath, "%sgos-displays.txt", szDir ); + + HANDLE hFile=CreateFileA( szPath, GENERIC_WRITE, FILE_SHARE_READ, NULL, + OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL ); + if( hFile==INVALID_HANDLE_VALUE ) + return; + SetFilePointer( hFile, 0, NULL, FILE_END ); + + va_start( ap, fmt ); + vsprintf( szLine, fmt, ap ); + va_end( ap ); + + WriteFile( hFile, szLine, (DWORD)strlen(szLine), &dwWritten, NULL ); + CloseHandle( hFile ); +} + +// +// [displaylog] Readable name for the result codes multi-device fullscreen init can return. +// +static const char* HSH_HRName( HRESULT hr ) +{ + switch( hr ) + { + case DD_OK: return "DD_OK"; + case DDERR_ALREADYINITIALIZED: return "DDERR_ALREADYINITIALIZED"; + case DDERR_CANNOTATTACHSURFACE: return "DDERR_CANNOTATTACHSURFACE"; + case DDERR_DIRECTDRAWALREADYCREATED: return "DDERR_DIRECTDRAWALREADYCREATED"; + case DDERR_EXCLUSIVEMODEALREADYSET: return "DDERR_EXCLUSIVEMODEALREADYSET"; + case DDERR_GENERIC: return "DDERR_GENERIC"; + case DDERR_HWNDALREADYSET: return "DDERR_HWNDALREADYSET"; + case DDERR_HWNDSUBCLASSED: return "DDERR_HWNDSUBCLASSED"; + case DDERR_INCOMPATIBLEPRIMARY: return "DDERR_INCOMPATIBLEPRIMARY"; + case DDERR_INVALIDCAPS: return "DDERR_INVALIDCAPS"; + case DDERR_INVALIDMODE: return "DDERR_INVALIDMODE"; + case DDERR_INVALIDOBJECT: return "DDERR_INVALIDOBJECT"; + case DDERR_INVALIDPARAMS: return "DDERR_INVALIDPARAMS"; + case DDERR_INVALIDPIXELFORMAT: return "DDERR_INVALIDPIXELFORMAT"; + case DDERR_INVALIDSURFACETYPE: return "DDERR_INVALIDSURFACETYPE"; + case DDERR_NOCOOPERATIVELEVELSET: return "DDERR_NOCOOPERATIVELEVELSET"; + case DDERR_NODIRECTDRAWHW: return "DDERR_NODIRECTDRAWHW"; + case DDERR_NOEXCLUSIVEMODE: return "DDERR_NOEXCLUSIVEMODE"; + case DDERR_NO3D: return "DDERR_NO3D"; + case DDERR_NOTFOUND: return "DDERR_NOTFOUND"; + case DDERR_OUTOFMEMORY: return "DDERR_OUTOFMEMORY"; + case DDERR_OUTOFVIDEOMEMORY: return "DDERR_OUTOFVIDEOMEMORY"; + case DDERR_PRIMARYSURFACEALREADYEXISTS: return "DDERR_PRIMARYSURFACEALREADYEXISTS"; + case DDERR_SURFACEBUSY: return "DDERR_SURFACEBUSY"; + case DDERR_UNSUPPORTED: return "DDERR_UNSUPPORTED"; + case DDERR_UNSUPPORTEDMODE: return "DDERR_UNSUPPORTEDMODE"; + case DDERR_WRONGMODE: return "DDERR_WRONGMODE"; + } + return "(unrecognised)"; +} + +// [displaylog] Log one DirectDraw call result, flagging failures. +static void HSH_CheckHR( const char* pszWhat, HRESULT hr ) +{ + HSH_LogInit( " %-24s = 0x%08lX %s%s\r\n", + pszWhat, (unsigned long)hr, HSH_HRName(hr), + FAILED(hr) ? " *** FAILED ***" : "" ); +} + bool CHSH_Device::InitFirst(int devicenum,DWORD resx,DWORD resy, CHSH_Device* pOtherHSHD/* = NULL*/) { m_pOtherHSHD = pOtherHSHD; if (pOtherHSHD) { pDD = pOtherHSHD->pDD; + HSH_LogInit( " InitFirst : shares the DirectDraw object of another panel\r\n" ); } else { HRESULT hr; + HSH_LogInit( " InitFirst : device %d, %lux%lu 16bpp 60Hz\r\n", + devicenum, (unsigned long)resx, (unsigned long)resy ); + if( devicenum<0 || devicenum>=(int)NumDevices ) { + HSH_LogInit( " *** device index out of range - this panel cannot open ***\r\n" ); + pDD=0; + size_back.cx=resx; + size_back.cy=resy; + return false; + } //1. Resolution change. - wDirectDrawCreateEx( &DeviceArray[devicenum].DeviceGUID , (VOID**) &(pDD), IID_IDirectDraw7, NULL ); - pDD->SetCooperativeLevel(hWindow, dwFlags ); + hr=wDirectDrawCreateEx( &DeviceArray[devicenum].DeviceGUID , (VOID**) &(pDD), IID_IDirectDraw7, NULL ); + HSH_CheckHR( "DirectDrawCreateEx", hr ); + if( FAILED(hr) || !pDD ) { + pDD=0; + size_back.cx=resx; + size_back.cy=resy; + return false; + } + // [panelcoop] Establish the cooperative level for this panel. + // + // The legacy call asks every panel to be BOTH the process focus window and its own + // device window, using the one shared hWindow, after the main display device has + // already taken exclusive mode on it. XP's DirectDraw and dgVoodoo2 tolerate that; + // Windows 10's does not - it returns DDERR_EXCLUSIVEMODEALREADYSET on the first + // panel and DDERR_INVALIDPARAMS on the rest, after which the primary surface fails + // with DDERR_NOCOOPERATIVELEVELSET. + // + // The main device (DXRasterizer.cpp, EnterFullScreenMode) already uses the correct + // two-call form: SETFOCUSWINDOW on its own, then EXCLUSIVE|FULLSCREEN. The panels + // never were. -tcoop picks the form so the alternatives can be compared on real + // hardware without a rebuild between each one. + switch( g_nHshCoopMode ) + { + case 1: // one call, but stop claiming the focus window + HSH_LogInit( " coop 1: CREATEDEVICEWINDOW|EXCLUSIVE|FULLSCREEN\r\n" ); + hr=pDD->SetCooperativeLevel( hWindow, + DDSCL_CREATEDEVICEWINDOW|DDSCL_ALLOWREBOOT|DDSCL_EXCLUSIVE|DDSCL_FULLSCREEN ); + HSH_CheckHR( "SetCoopLevel", hr ); + break; + + case 2: // two calls, DirectDraw makes a device window for this monitor + HSH_LogInit( " coop 2: SETFOCUSWINDOW, then CREATEDEVICEWINDOW|EXCLUSIVE|FULLSCREEN\r\n" ); + hr=pDD->SetCooperativeLevel( hWindow, DDSCL_SETFOCUSWINDOW ); + HSH_CheckHR( "SetCoopLevel(focus)", hr ); + hr=pDD->SetCooperativeLevel( hWindow, + DDSCL_CREATEDEVICEWINDOW|DDSCL_ALLOWREBOOT|DDSCL_EXCLUSIVE|DDSCL_FULLSCREEN ); + HSH_CheckHR( "SetCoopLevel(device)", hr ); + break; + + case 3: // two calls, exactly what the main display device does + HSH_LogInit( " coop 3: SETFOCUSWINDOW, then EXCLUSIVE|FULLSCREEN\r\n" ); + hr=pDD->SetCooperativeLevel( hWindow, DDSCL_SETFOCUSWINDOW ); + HSH_CheckHR( "SetCoopLevel(focus)", hr ); + hr=pDD->SetCooperativeLevel( hWindow, DDSCL_EXCLUSIVE|DDSCL_FULLSCREEN ); + HSH_CheckHR( "SetCoopLevel(device)", hr ); + break; + + case 4: // exclusive only - the main device has ALREADY set the focus window, + // so re-claiming it is what makes the first panel collide with it. + HSH_LogInit( " coop 4: EXCLUSIVE|FULLSCREEN only (no focus claim)\r\n" ); + hr=pDD->SetCooperativeLevel( hWindow, DDSCL_EXCLUSIVE|DDSCL_FULLSCREEN ); + HSH_CheckHR( "SetCoopLevel", hr ); + break; + + case 5: // as 4, plus ALLOWREBOOT (the legacy flag set minus the two window flags) + HSH_LogInit( " coop 5: ALLOWREBOOT|EXCLUSIVE|FULLSCREEN (no focus claim)\r\n" ); + hr=pDD->SetCooperativeLevel( hWindow, + DDSCL_ALLOWREBOOT|DDSCL_EXCLUSIVE|DDSCL_FULLSCREEN ); + HSH_CheckHR( "SetCoopLevel", hr ); + break; + + default: // 0 = legacy, byte-for-byte the original behaviour + HSH_LogInit( " coop 0: legacy combined call\r\n" ); + hr=pDD->SetCooperativeLevel( hWindow, dwFlags ); + HSH_CheckHR( "SetCoopLevel", hr ); + break; + } hr=pDD->SetDisplayMode( resx, resy, 16, 60, 0 ); + HSH_CheckHR( "SetDisplayMode", hr ); } size_back.cx=resx; size_back.cy=resy; @@ -664,25 +831,50 @@ bool CHSH_Device::InitSecond(DWORD tresx,DWORD tresy) //////Front Buffer DDSURFACEDESC2 ddsd={0,}; DDSCAPS2 ddsCaps; + HRESULT hr; + + // [displaylog] InitFirst can now fail cleanly; without this guard every call below + // dereferences a null IDirectDraw7 and takes the process down. + if( !pDD ) { + HSH_LogInit( " InitSecond: skipped - InitFirst produced no DirectDraw object\r\n" ); + return false; + } ddsd.dwSize = sizeof(ddsd); ddsd.dwFlags = DDSD_BACKBUFFERCOUNT | DDSD_CAPS; ddsd.dwBackBufferCount = 1; ddsd.ddsCaps.dwCaps = DDSCAPS_COMPLEX | DDSCAPS_FLIP | DDSCAPS_PRIMARYSURFACE| DDSCAPS_3DDEVICE; - pDD->CreateSurface( &ddsd, &pDDSFront, NULL ); - + hr=pDD->CreateSurface( &ddsd, &pDDSFront, NULL ); + HSH_CheckHR( "CreateSurface(primary)", hr ); + if( FAILED(hr) || !pDDSFront ) { + pDDSFront=0; + return false; + } //////BackBuffer ZeroMemory( &ddsCaps, sizeof(ddsCaps) ); ddsCaps.dwCaps = DDSCAPS_BACKBUFFER; - pDDSFront->GetAttachedSurface( &ddsCaps, &pDDSBack ); + hr=pDDSFront->GetAttachedSurface( &ddsCaps, &pDDSBack ); + HSH_CheckHR( "GetAttachedSurface", hr ); + if( FAILED(hr) || !pDDSBack ) { + pDDSBack=0; + return false; + } //////3D Device - LPDIRECT3D7 pD3D; - pDD->QueryInterface( IID_IDirect3D7, (VOID**)&(pD3D) ); - pD3D->CreateDevice(IID_IDirect3DHALDevice,pDDSBack,&pD3DDevice); + LPDIRECT3D7 pD3D=0; + hr=pDD->QueryInterface( IID_IDirect3D7, (VOID**)&(pD3D) ); + HSH_CheckHR( "QueryInterface(D3D7)", hr ); + if( FAILED(hr) || !pD3D ) + return false; + hr=pD3D->CreateDevice(IID_IDirect3DHALDevice,pDDSBack,&pD3DDevice); + HSH_CheckHR( "CreateDevice(HAL)", hr ); pD3D->Release(); + if( FAILED(hr) || !pD3DDevice ) { + pD3DDevice=0; + return false; + } D3DVIEWPORT7 vp = { 0, 0, size_back.cx, size_back.cy, 0.0f, 1.0f }; pD3DDevice->SetViewport( &vp ); @@ -1018,7 +1210,10 @@ bool CMR_Device::InitFirst() bool CMR_Device::InitSecond() { map_loaded=false; - CHSH_Device::InitSecond(0,0); // no off screen - do not create Target Texture. + // [panelcoop] see CRadar_Device::InitSecond - do not build surfaces on a device + // that never got a cooperative level. + if( !CHSH_Device::InitSecond(0,0) ) // no off screen - do not create Target Texture. + return false; char temppath[MAX_PATH],temppatha[MAX_PATH]; @@ -1122,7 +1317,10 @@ bool CMFDRight_Device::InitFirst() bool CMFDRight_Device::InitSecond() { // Create flip chain, D3D device, render target (1024x512) and fonts. - CHSH_Device::InitSecond(1024, 512); + // [displaylog] If the device never came up, everything below dereferences a null + // IDirect3DDevice7; bail out and leave the reason in gos-displays.txt. + if( !CHSH_Device::InitSecond(1024, 512) ) + return false; // Independent copy of the MFD texture set owned by THIS DirectDraw object. HSH_CreateMFDTextures(pDD, &pDDSMechTexture, &pDDSTexture); @@ -1192,7 +1390,11 @@ bool CRadar_Device::InitFirst() bool CRadar_Device::InitSecond() { ZeroMemory(recent_state,sizeof(recent_state)); - CHSH_Device::InitSecond(512,1024); // 480 x 640 + // [panelcoop] If the device did not come up, stop here. Continuing would create + // surfaces on a DirectDraw object with no cooperative level - they come back NULL, + // and the first GetDC on one faults (IDirectDrawSurface7 vtable +0x44). + if( !CHSH_Device::InitSecond(512,1024) ) // 480 x 640 + return false; MapDrawn=false; char temppath[MAX_PATH],temppatha[MAX_PATH]; @@ -1446,9 +1648,11 @@ bool CMFD_Device::InitFirst() // device here so both displays are ready before InitSecond runs. if (g_nTypeOfMFDs == 4) { m_pRightDevice = &mfd_device_right; - CHSH_Device::InitFirst(g_nMFD1, 640, 480); // left device - mfd_device_right.InitFirst(); // right device display mode - return true; + // [panelcoop] Report the REAL result. This used to return true unconditionally, + // which hid a failed panel and let the per-frame code run on null surfaces. + bool bLeft = CHSH_Device::InitFirst(g_nMFD1, 640, 480); + bool bRight = mfd_device_right.InitFirst(); + return (bLeft && bRight); } m_pRightDevice = NULL; return CHSH_Device::InitFirst(g_nDualHead,1280,480); @@ -1456,7 +1660,10 @@ bool CMFD_Device::InitFirst() bool CMFD_Device::InitSecond() { - CHSH_Device::InitSecond(1024,512); // 640 x 480 + // [displaylog] see CMFDRight_Device::InitSecond - a failed device must not fall + // through into the texture/material setup below. + if( !CHSH_Device::InitSecond(1024,512) ) // 640 x 480 + return false; HSH_CreateMFDTextures(pDD, &pDDSMechTexture, &pDDSTexture); @@ -1478,7 +1685,8 @@ bool CMFD_Device::InitSecond() // Mode 4: create flip chain + D3D device for the right 640x480 monitor. if (g_nTypeOfMFDs == 4 && m_pRightDevice) { - mfd_device_right.InitSecond(); + if( !mfd_device_right.InitSecond() ) + return false; } return true; @@ -2716,21 +2924,57 @@ bool IsSecondaryMonitorAvaliable() void HSH_EnterFullScreen2() { + // [displaylog] Record which of the three possible paths was taken and how each + // individual panel fared. Mode 4 opens four fullscreen DirectDraw devices that all + // share one focus window, so a partial failure is both plausible and, until now, + // completely invisible. + HSH_LogInit( "\r\nHSH_EnterFullScreen2\r\n" ); + HSH_LogInit( "--------------------\r\n" ); + if(IsMultimonitorAvaliable()){ - mfd_device.InitFirst(); - radar_device.InitFirst(); + HSH_LogInit( " path: multi-monitor (MFD + radar)\r\n" ); - mfd_device.InitSecond(); - radar_device.InitSecond(); + HSH_LogInit( " [mfd]\r\n" ); + bool bMFD = mfd_device.InitFirst(); + HSH_LogInit( " [radar]\r\n" ); + bool bRadar = radar_device.InitFirst(); - hsh_initialized=true; - if(g_f3dtarget){ - CopyTargetImage(0,mfd_device.pDDSMechTexture); + if( bMFD ) { + HSH_LogInit( " [mfd]\r\n" ); + bMFD = mfd_device.InitSecond(); + } + if( bRadar ) { + HSH_LogInit( " [radar]\r\n" ); + bRadar = radar_device.InitSecond(); + } + + // [panelcoop] Only claim the panels are up if they actually are. + // + // This flag used to be set unconditionally. A panel that failed to initialise + // therefore left null surfaces and a null IDirect3DDevice7 behind, and the + // per-frame render path called straight through them - the process died on a + // null COM vtable call ("Attempt to read from address 0x00000044") rather than + // simply running without MFDs. + if( bMFD && bRadar ) { + hsh_initialized=true; + HSH_LogInit( " result: panels initialised OK\r\n" ); + if(g_f3dtarget){ + CopyTargetImage(0,mfd_device.pDDSMechTexture); + } + } else { + HSH_LogInit( " result: *** FAILED (mfd=%s radar=%s) - continuing WITHOUT panels ***\r\n", + bMFD ? "ok" : "failed", bRadar ? "ok" : "failed" ); + mfd_device.Release(); + radar_device.Release(); + hsh_initialized=false; } }else if(IsSecondaryMonitorAvaliable()){ + HSH_LogInit( " path: single secondary monitor (mr_device)\r\n" ); mr_device.InitFirst(); mr_device.InitSecond(); hsh_mrdev_initialized=true; + }else{ + HSH_LogInit( " path: none - no secondary displays opened\r\n" ); } } diff --git a/Gameleap/code/mw4/Code/MW4Application/MW4Application.cpp b/Gameleap/code/mw4/Code/MW4Application/MW4Application.cpp index 5cf46209..594e5c55 100644 --- a/Gameleap/code/mw4/Code/MW4Application/MW4Application.cpp +++ b/Gameleap/code/mw4/Code/MW4Application/MW4Application.cpp @@ -92,6 +92,12 @@ extern int g_nTypeOfMFDs; // 0 - no MFDs, 1 - orginal(5+1=dual & single), 2 - B& extern int g_naMonitorOverride[4]; extern int g_nRIOType; extern DWORD g_dwRIOBaud; // [tbaud] +// [fpslog] Non-zero enables the per-second frame pacing report (gos-fps.txt). +// Defined in CoreTech GameOS WinMain.cpp. +extern int g_nFpsLog; +// [panelcoop] SetCooperativeLevel form used by the radar/MFD panels; 0 = legacy. +// Defined in CoreTech GameOS render.cpp. +extern int g_nHshCoopMode; extern LONG g_ThrottleDir; extern bool g_bCanSuicideAllways; extern bool g_f3dtarget; @@ -862,6 +868,19 @@ static const char* const g_apszCommandLineHelp[] = " Separators may be ',' '/' or ':'.", " MFD1 and MFD2 only apply with -tmfds 4.", " Example: -tmon 1,2,4,3 swaps the two MFD panels.", +" -tcoop <0-5> How the radar/MFD panels claim their displays.", +" 0 = legacy single call (as shipped; works on XP", +" and under dgVoodoo2, fails on Windows 10).", +" 1 = drop SETFOCUSWINDOW, let DirectDraw create", +" the device window.", +" 2 = SETFOCUSWINDOW first, then CREATEDEVICEWINDOW.", +" 3 = SETFOCUSWINDOW first, then EXCLUSIVE only,", +" matching what the main display already does.", +" 4 = EXCLUSIVE|FULLSCREEN only. The main display", +" has already set the focus window, so the", +" panels never contend for it.", +" 5 = as 4, plus ALLOWREBOOT.", +" Results are reported in gos-displays.txt.", " -mechview <1|2> Show the rotating mech view. 1 = on the radar", " -mv <1|2> screen, 2 = on the main screen. Default: off.", " -3dt Draw the MFD target as a live 3D model instead of", @@ -941,6 +960,12 @@ static const char* const g_apszCommandLineHelp[] = " -zlogname ZLog output file name.", " -olog Write the legacy .log match report instead of the", " newer .rpt format.", +" -fps Write a per-second frame pacing report to", +" gos-fps.txt next to the executable: average fps,", +" 1% low, worst frame in ms, and a count of frames", +" taking over twice the average (hitches). Useful", +" for confirming smooth pacing on the MFD modes.", +" Off unless this switch is given.", " -gamestats Save game statistics.", " -help Show this reference and exit without starting.", "", @@ -1675,6 +1700,17 @@ int WINAPI WinMain( HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR lpCmdLine, int n if ((9600 <= n) && (n <= 921600)) g_dwRIOBaud = (DWORD)n; } + // [panelcoop] Cooperative-level form for the radar/MFD panels. 0 = legacy combined + // call (what shipped); 1-3 are forms compatible with strict modern DirectDraw. + token = strstr(all_lower, "-tcoop "); + if (token && token[7]) + { + int n = atoi(&token[7]); + if ((0 <= n) && (n <= 5)) + g_nHshCoopMode = n; + } + // [fpslog] Enable the per-second frame pacing report written to gos-fps.txt. + g_nFpsLog = (strstr(all_lower, "-fps") != NULL) ? 1 : 0; g_bCanSuicideAllways = (strstr(all_lower, "-suicide") != NULL); g_f3dtarget = (strstr(all_lower, "-3dt") != NULL); g_bOldLOG = (strstr(all_lower, "-olog") != NULL); diff --git a/build-env/deploy-mw4.ps1 b/build-env/deploy-mw4.ps1 index cda5125a..4193cd92 100644 --- a/build-env/deploy-mw4.ps1 +++ b/build-env/deploy-mw4.ps1 @@ -199,6 +199,22 @@ if (-not (Test-Path $layers)) { New-Item -Path $layers -Force | Out-Null } Set-ItemProperty -Path $layers -Name "$Dest\MW4.exe" -Value "DWM8And16BitMitigation HIGHDPIAWARE" -Type String Write-Host " set: $Dest\MW4.exe = DWM8And16BitMitigation HIGHDPIAWARE" +# 4b-2) Ship the standalone shim installer with the game. +# The AppCompat layer is keyed on the executable's FULL PATH, so any copy of this +# deployment made to another folder or machine silently loses it -- and the resulting +# failure ("Another application is preventing use of full screen mode") points at the +# wrong cause entirely. set-appcompat.bat re-applies the shim for whatever folder it +# is sitting in, so every install can fix itself with one click. +foreach ($f in @("set-appcompat.ps1", "set-appcompat.bat")) { + $src = Join-Path $PSScriptRoot $f + if (Test-Path $src) { + Copy-Item $src (Join-Path $Dest $f) -Force + Write-Host " shipped: $f" + } else { + Write-Warning " missing: $src" + } +} + # 4c) Optimize hsh\mechs preview BMPs to 256-color (8bpp). The dev tree stores them as 24-bit # (~600 KB each, ~23 MB total); production ships 256-color (~200 KB). Converts the DEPLOYED # copies only -- source 24-bit originals are untouched. Idempotent (skips already-8bpp). diff --git a/build-env/set-appcompat.bat b/build-env/set-appcompat.bat new file mode 100644 index 00000000..06fe9f00 --- /dev/null +++ b/build-env/set-appcompat.bat @@ -0,0 +1,14 @@ +@echo off +REM One-click wrapper for set-appcompat.ps1. +REM +REM Bypasses the PowerShell execution policy for this run only (it does not change any +REM machine setting), passes through any arguments such as -Remove or -WhatIfOnly, and +REM keeps the window open so the result is readable. +REM +REM To cover every account on the machine, right-click this file and choose +REM "Run as administrator". + +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0set-appcompat.ps1" %* + +echo. +pause diff --git a/build-env/set-appcompat.ps1 b/build-env/set-appcompat.ps1 new file mode 100644 index 00000000..b04e1296 --- /dev/null +++ b/build-env/set-appcompat.ps1 @@ -0,0 +1,175 @@ +<# + set-appcompat.ps1 - apply the DirectDraw compatibility shim to the MW4 executables + sitting NEXT TO this script, wherever that copy of the game happens to live. + + WHY THIS EXISTS + MW4 renders at bitdepth=16. Modern GPUs report ZERO 16-bit display modes, so the + exclusive-fullscreen path cannot set a mode and fails. The DWM8And16BitMitigation + AppCompat layer is what makes 8/16-bit fullscreen work under the desktop compositor. + + Without it GameOS reports: + "Another application is preventing use of full screen mode." + That message is a catch-all raised after every SetDisplayMode attempt has failed -- + it even scans for NetMeeting -- so it sends you hunting for a conflicting program + that does not exist. The real cause is the missing shim. + + The layer is keyed on the executable's FULL PATH. Every install needs its own entry, + and copying or moving an install silently loses it. Hence this script: run it from + inside any deployment and it fixes that deployment. + + USAGE + Double-click set-appcompat.bat (recommended - one click, no execution-policy fuss) + or: .\set-appcompat.ps1 apply the shim + .\set-appcompat.ps1 -Remove remove the entries this script adds + .\set-appcompat.ps1 -WhatIfOnly report current state, change nothing + + Run as Administrator to also write the all-users (HKLM) entry. Without elevation the + per-user (HKCU) entry is written, which is enough for the account that runs the game. +#> + +param( + [string]$Layer = "DWM8And16BitMitigation HIGHDPIAWARE", + [switch]$Remove, + [switch]$WhatIfOnly +) + +$ErrorActionPreference = "Stop" + +# --- Locate ourselves ------------------------------------------------------------- +# $PSScriptRoot is empty on very old hosts and when dot-sourced, so fall back. +$here = $PSScriptRoot +if ([string]::IsNullOrEmpty($here)) { + $here = Split-Path -Parent $MyInvocation.MyCommand.Path +} + +$hkcuLayers = "HKCU:\Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers" +$hklmLayers = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers" + +$isAdmin = ([Security.Principal.WindowsPrincipal] ` + [Security.Principal.WindowsIdentity]::GetCurrent() + ).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) + +Write-Host "" +Write-Host "MW4 DirectDraw compatibility shim" -ForegroundColor Cyan +Write-Host "---------------------------------" -ForegroundColor Cyan +Write-Host " folder : $here" +Write-Host " elevated : $(if ($isAdmin) { 'yes (HKCU + HKLM)' } else { 'no (HKCU only)' })" +Write-Host "" + +# --- Find the executables that need it -------------------------------------------- +# Only the game/editor executables drive DirectDraw. Launcher/autoconfig/mw4print do not. +$wanted = @("MW4.exe", "MW4pro.exe", "MW4Ed2.exe") +$targets = @() +foreach ($name in $wanted) { + $p = Join-Path $here $name + if (Test-Path -LiteralPath $p) { $targets += $p } +} + +if ($targets.Count -eq 0) { + Write-Host "No MW4 executables found next to this script." -ForegroundColor Red + Write-Host "Expected one of: $($wanted -join ', ')" + Write-Host "Put this script in the same folder as MW4.exe and run it again." + exit 1 +} + +# --- Helpers ---------------------------------------------------------------------- +function Get-Layer([string]$root, [string]$exe) { + if (-not (Test-Path -LiteralPath $root)) { return $null } + $item = Get-ItemProperty -LiteralPath $root -ErrorAction SilentlyContinue + if ($null -eq $item) { return $null } + $prop = $item.PSObject.Properties | Where-Object { $_.Name -eq $exe } + if ($null -eq $prop) { return $null } + return $prop.Value +} + +function Set-Layer([string]$root, [string]$exe, [string]$value) { + if (-not (Test-Path -LiteralPath $root)) { New-Item -Path $root -Force | Out-Null } + Set-ItemProperty -LiteralPath $root -Name $exe -Value $value -Type String +} + +$failed = 0 + +foreach ($exe in $targets) { + + Write-Host $exe -ForegroundColor White + + $before = Get-Layer $hkcuLayers $exe + if ($null -ne $before) { + Write-Host " existing HKCU : $before" -ForegroundColor DarkGray + # The historical failure mode: a HIGHDPIAWARE-only entry, which suppresses the + # automatic shim and produces exactly the fullscreen failure this script fixes. + if (-not $Remove -and $before -notmatch "DWM8And16BitMitigation") { + Write-Host " NOTE: entry exists but has no DWM8And16BitMitigation -" -ForegroundColor Yellow + Write-Host " this specific state BLOCKS fullscreen. Replacing it." -ForegroundColor Yellow + } + } + + if ($WhatIfOnly) { + $lm = Get-Layer $hklmLayers $exe + Write-Host " existing HKLM : $(if ($null -ne $lm) { $lm } else { '(none)' })" -ForegroundColor DarkGray + Write-Host "" + continue + } + + if ($Remove) { + Remove-ItemProperty -LiteralPath $hkcuLayers -Name $exe -ErrorAction SilentlyContinue + if ($isAdmin) { + Remove-ItemProperty -LiteralPath $hklmLayers -Name $exe -ErrorAction SilentlyContinue + } + Write-Host " removed" -ForegroundColor Yellow + Write-Host "" + continue + } + + # Per-user. No admin needed, and applies to the account that actually runs the game. + Set-Layer $hkcuLayers $exe $Layer + + # All-users, if we can. Note the different value format: the HKLM entries use a + # leading "$ " marker. Do not copy one format into the other key. + if ($isAdmin) { + Set-Layer $hklmLayers $exe ("$ " + ($Layer -replace '\s*HIGHDPIAWARE\s*', '')) + } + + # Verify by reading back rather than trusting the write. + $after = Get-Layer $hkcuLayers $exe + if ($after -eq $Layer) { + Write-Host " HKCU set : $after" -ForegroundColor Green + } else { + Write-Host " HKCU FAILED : got '$after'" -ForegroundColor Red + $failed++ + } + + if ($isAdmin) { + $afterM = Get-Layer $hklmLayers $exe + if ($null -ne $afterM) { + Write-Host " HKLM set : $afterM" -ForegroundColor Green + } else { + Write-Host " HKLM FAILED" -ForegroundColor Red + $failed++ + } + } + Write-Host "" +} + +if ($WhatIfOnly) { + Write-Host "Report only - nothing was changed." -ForegroundColor Cyan + exit 0 +} + +if ($failed -gt 0) { + Write-Host "$failed registry write(s) failed." -ForegroundColor Red + exit 1 +} + +if ($Remove) { + Write-Host "Shim removed. Fullscreen will fail on modern GPUs until it is re-applied." -ForegroundColor Yellow +} else { + Write-Host "Done. The shim is read when the process starts -" -ForegroundColor Green + Write-Host "restart MW4 to pick it up. No reboot needed." -ForegroundColor Green + if (-not $isAdmin) { + Write-Host "" + Write-Host "Applied for the current user only. To cover all accounts on this" -ForegroundColor DarkGray + Write-Host "machine, run this script again as Administrator." -ForegroundColor DarkGray + } +} +exit 0