Files
firestorm/build-env/set-appcompat.ps1
T
9c67dddd83 Display diagnostics + -fps/-tcoop; native multi-monitor MFD ruled out
Four-monitor MFD bring-up on the new bench (MR_new: AMD FirePro W4100, 4 outputs,
Win10). Adds permanent display diagnostics, a Release-capable frame pacing logger,
a self-locating AppCompat shim installer, and settles -- empirically -- whether
dgVoodoo2 can be dropped for the multi-monitor MFD modes. It cannot.

WHY THIS WAS HARD
-----------------
CHSH_Device::InitFirst/InitSecond discarded EVERY 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 just stayed on the desktop. SPEW is
compiled out of shipping builds, so none of it was visible. Restoring that
visibility is what unblocked everything else.

DIAGNOSTICS ADDED (keep these)
------------------------------
* VideoCard.cpp -- LogDisplayDevices() writes gos-displays.txt next to the exe:
  NumDevices/NumHWDevices/NumMonitors, every DirectDraw device with its
  hw_rasterization flag, the role assignment (FullScreenDevice / g_nNonDualHead /
  g_nDualHead / g_nDualHead2 / g_nMFD1 / g_nMFD2), per-slot -tmon APPLIED/REJECTED
  (previously silent), and whether mode 4's "BOTH mfd1 and mfd2" requirement is met.
* render.cpp -- HSH_LogInit()/HSH_CheckHR()/HSH_HRName() log every DirectDraw call
  in the panel init path with its HRESULT decoded by name (27 DDERR_* codes, all
  verified present and collision-free against build-env/dx7asdk/include/ddraw.h).
  Each line is opened/appended/closed individually so the log survives a crash.
* WinMain.cpp -- GOS_LogFrameRate() writes gos-fps.txt: per second, frame count,
  average fps, 1% low (mean of the worst 1% of frames), worst frame in ms, and a
  count of frames exceeding 2x average ("hitches"). Average fps alone cannot
  distinguish 60fps from 60fps-with-a-dropped-frame-every-second; the 1% low can.
  The engine's own FrameRate readout is #ifdef LAB_ONLY (MWMission.cpp) so it only
  exists in MW4pro.exe; this works in Release, which is what runs on the pods.

NEW SWITCHES (both documented in -help)
---------------------------------------
* -fps    Enable the frame pacing report. Off by default: the gate is the first
          statement in GOS_LogFrameRate, so an unflagged run does no arithmetic
          and does not even create the file. The file handle is held open for the
          process lifetime -- opening/closing it every second would put a syscall
          of unpredictable latency on the render thread, i.e. the measurement tool
          perturbing what it measures.
* -tcoop <0-5>  Selects the SetCooperativeLevel form used by the radar/MFD panels.
          0 = legacy (unchanged shipped behaviour, remains the default).
          Exists so every candidate fix could be compared on real hardware without
          a rebuild between attempts.

CRASH-SAFETY FIX
----------------
hsh_initialized was set unconditionally after panel init, so a failed panel 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 its real result instead of always returning true,
  - hsh_initialized is only set when the panels genuinely came up.
A display failure now leaves the game running without MFDs instead of bombing to
desktop. Crash signature for the record: `call [ecx+0x44]` with ECX=0 is
IDirectDrawSurface7::GetDC on a never-created surface (vtable offset confirmed
against the DX7 header), reported as "Attempt to read from address 0x00000044".

APPCOMPAT SHIM INSTALLER (new)
------------------------------
build-env/set-appcompat.ps1 + set-appcompat.bat. Self-locating via $PSScriptRoot:
applies DWM8And16BitMitigation to the MW4 executables sitting next to it, wherever
that install lives. HKCU always, HKLM too when elevated (the HKLM value format
differs -- it carries a leading "$" marker -- so the two must not be interchanged).
Verifies by reading back; detects the HIGHDPIAWARE-only entry that SUPPRESSES the
automatic shim; supports -Remove and -WhatIfOnly. deploy-mw4.ps1 now ships both
files into every deployment.

This matters because the layer is keyed on the executable's FULL PATH -- any copy
of an install to another folder or machine silently loses it, and the resulting
error is actively misleading (see below).

WHAT WE LEARNED
---------------
* The AppCompat shim SYNTHESISES 16-bit display modes. Proved directly: the crash
  dump shows "16 bit modes :" EMPTY without it and fully populated with it. MW4
  renders at bitdepth=16 and modern GPUs expose no 16-bit modes at all.
* Without the shim, GameOS reports "Another application is preventing use of full
  screen mode" (GOS_DXRASTERIZER_NOFULLSCREEN, DXRasterizer.cpp ~1125). That is a
  catch-all fired after every SetDisplayMode attempt fails -- it even scans for
  NetMeeting -- and it sends you looking for a conflicting program that does not
  exist. The real cause is the missing shim.
* Exclusive fullscreen DOES work on Win10 with the system ddraw.dll and dgVoodoo2
  physically removed, once the shim is applied to that exe path.
* Windowed mode works natively with no shim at all: the windowed path sets
  Environment.bitDepth = DesktopBpp (32), so there is no mode switch. Verified for
  the console/shell; a full mission windowed is still untested.
* Native DirectDraw enumerates all four W4100 outputs, so dgVoodoo2 was never
  needed for device enumeration.
* The engine is 4:3 ONLY. ImageHlp.cpp ~464 asserts the complete supported set:
  640x480, 512x384, 800x600, 960x720, 1024x768, 1280x1024 (5:4), 1600x1200. No
  16:9 mode and no aspect correction anywhere in the codebase. On a 16:9 monitor
  the scaler must adapt: plain stretch distorts geometry, keep-aspect pillarboxes.
* -2dt is not a recognised switch anywhere in the codebase, despite appearing in
  production ctcl.ini launch lines. Completely inert.
* 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.

WHAT WE TRIED AND WHY IT FAILED
-------------------------------
The panel 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, the same
author. The panels never were.

Results on real hardware, no dgVoodoo2, shim applied, -tmfds 4:

  -tcoop 0  SETFOCUSWINDOW|CREATEDEVICEWINDOW|ALLOWREBOOT|EXCLUSIVE|FULLSCREEN
            -> DDERR_EXCLUSIVEMODEALREADYSET
  -tcoop 1  CREATEDEVICEWINDOW|EXCLUSIVE|FULLSCREEN (no focus claim)
            -> DDERR_INVALIDPARAMS (CREATEDEVICEWINDOW needs a focus window)
  -tcoop 2  SETFOCUSWINDOW, then CREATEDEVICEWINDOW|EXCLUSIVE|FULLSCREEN
            -> DDERR_INVALIDPARAMS
  -tcoop 3  SETFOCUSWINDOW, then EXCLUSIVE|FULLSCREEN
            -> first panel collides, but that collision STEALS exclusive mode from
               the main display, after which panels 2 and 3 fully initialise (the
               radar reached CreateDevice(HAL) = DD_OK -- a secondary panel running
               entirely on native DirectDraw). Side effect: the desktop was left at
               1920x1080 16bpp. Not viable.
  -tcoop 4  EXCLUSIVE|FULLSCREEN only          -> EXCLUSIVEMODEALREADYSET, all panels
  -tcoop 5  ALLOWREBOOT|EXCLUSIVE|FULLSCREEN   -> EXCLUSIVEMODEALREADYSET, 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 and is not bound by that rule -- it is not papering over a bug we can fix.

=> dgVoodoo2 CANNOT be removed for -tmfds 1/3/4 by correcting these flags. The
default stays -tcoop 0. The switch is retained because it is how this was settled
and it will re-settle it on different hardware.

The only native path is a borderless windowed panel design (DDSCL_NORMAL + clipper
per monitor, no exclusive mode anywhere). Assessment and staged plan are recorded
in CLAUDE.md STEP 10; not started.

WORKING 4-MONITOR CONFIG (with dgVoodoo2)
-----------------------------------------
dgVoodoo2 Scaling mode MUST be "Stretched, Keep Aspect Ratio". Plain "Stretched"
fails silently: main and 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), which also established that device index maps
1:1 to physical monitor on this bench, so -tmon 1,2,3,4 equals auto-detection.
Confirmed working end to end: all three secondary panels present, full mission
played.

KNOWN GAP: the working dgVoodoo.conf is still not versioned in the repo (removed
in 0ceba9c7), so a fresh deploy will reproduce the silent MFD failure.

Behaviour with no new switches supplied is unchanged from the previous build
except on failure paths, which now degrade gracefully instead of crashing.

Co-authored-by: Claude Opus 5 (Anthropic) <noreply@anthropic.com>
Co-authored-by: GitHub Copilot <copilot@github.com>
2026-07-25 18:22:31 -05:00

176 lines
7.1 KiB
PowerShell

<#
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