-tmfds 4 splits the 1280x480 MFD span into two independent 640x480 monitors so
the span hardware is no longer required. It rendered correctly but stuttered
rhythmically and constantly, making the game unplayable. -tmfds 1 on the same
binary was flawless.
Root cause: a cross-DirectDraw-object texture read, twice every 7-frame cycle.
CHSH_Device::InitFirst with no pOtherHSHD peer creates its OWN IDirectDraw7 (via
wDirectDrawCreateEx on that monitor's device GUID) and InitSecond creates its OWN
primary flip chain and IDirect3DDevice7. CMFDRight_Device::InitFirst passes no
peer, so the right MFD is an entirely separate DirectDraw object.
EndChannel then did, for channels 3-4:
target->pD3DDevice->SetTexture(0, pDDSTarget);
where target->pD3DDevice belonged to the RIGHT device but pDDSTarget was the LEFT
device's render-target texture. The old code comment asserted "both devices are
on the same GPU so VRAM textures are mutually accessible" -- that premise is
wrong. In DirectDraw 7 a surface belongs to the IDirectDraw7 that created it, not
to the physical GPU, so it is not a valid texture on another object's D3D device.
The MFDs still displayed, which means the runtime was emulating the access with a
VRAM -> system-memory readback and re-upload of the 1024x512 16-bit render target.
That forces a full GPU pipeline stall, and it happened on channels 3 and 4 (that
is, sh_step 5 and 6) -- twice per 7-frame cycle, on the same GPU drawing the main
view. Hence a fixed-period hitch in the whole game, forever.
This also explains why the earlier stagger work (eaa5fd3, BeginSceneRight) did not
help: it only moved a Flip from sh_step 0 to 1, and the flips already used
DDFLIP_DONOTWAIT|DDFLIP_NOVSYNC and never blocked. The flips were never the
problem.
Fix: give the right device everything it draws with, and render channels 3-4
entirely on it.
- New HSH_CreateMFDTextures() builds the mech image atlas and the MFD sprite atlas
on a caller-supplied IDirectDraw7. Both devices now call it, so each owns a
complete independent texture set. CMFD_Device::InitSecond was refactored onto it.
- CMFDRight_Device gained its own pDDSMechTexture / pDDSDamageTexture /
pDDSTargetTexture plus a Release() override, and its InitSecond now sets up
tw/th and the material/render state exactly like the left device.
- New CMFD_Device::SwapRightState() exchanges this object's DATA members with the
right device's. BeginChannel swaps in when channel >= 3 in mode 4; EndChannel
swaps back. This routes all existing drawing to the correct monitor without
touching the ~233 mfd_device.* call sites in hudchat/huddamage/hudweapon/
GUIRadarManager. The vtable pointer is deliberately never swapped, so virtual
dispatch is unaffected; CHSHFont has no virtual functions so its array is
swapped bytewise to avoid ctor/dtor side effects on a temporary.
- EndChannel's composite is now a single path for all modes. Mode 4 composites
full 640 width at x=0 (each device is a standalone panel); modes 1-3 keep the
half-width (ch/3)*w packing into one backbuffer.
Side effects: startup builds the 65-bitmap mech atlas twice (once per device), and
VRAM use rises by a few MB. Modes 0-3 are behaviourally unchanged.
Known cosmetic leftover, deliberately not changed: huddamage.cpp lines ~1319 and
~1901 call LoadTargetTexture outside the channel-3 block, so those loads land on
the left device and go unused. The in-channel call at ~2209 runs every frame in
that branch and correctly populates the right device's copy, so behaviour is
correct -- it is just a redundant load on target change.
Requires rebuild: MW4.exe (GameOS changes recompile the engine library).
Verified: compiles clean, console launches. Two-monitor testing pending.
Co-authored-by: Claude Opus 5 (Anthropic) <noreply@anthropic.com>
Co-authored-by: GitHub Copilot <copilot@github.com>