Fix constant rhythmic stutter in MFD mode 4 (split dual 640x480)

-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>
This commit is contained in:
2026-07-24 23:39:42 -05:00
co-authored by Claude Opus 5 GitHub Copilot
parent 29aee4f71b
commit 0a657b5998
2 changed files with 177 additions and 96 deletions
@@ -613,6 +613,46 @@ char *mechnames[]={
};
#define ARRAYSIZE(x) (sizeof(x)/sizeof(x[0]))
// [mode 4 split-MFD fix]
// Build the MFD texture set (mech image atlas + sprite atlas) on a given DirectDraw object.
//
// Mode 4 drives two physically separate 640x480 monitors. DirectDraw binds one
// IDirectDraw7 to one monitor, so the right-hand panel necessarily has its own
// IDirectDraw7 *and* its own IDirect3DDevice7. A DirectDraw surface belongs to the
// IDirectDraw7 that created it - NOT to the physical GPU - so a texture made by the left
// device can never be sampled by the right device's D3D device, even when both monitors
// hang off the same card. Attempting it makes the runtime emulate the access with a
// VRAM->system-memory readback every time, which stalls the whole GPU. Hence: each
// device gets its own complete copy of these textures.
static void HSH_CreateMFDTextures(LPDIRECTDRAW7 pdd,
LPDIRECTDRAWSURFACE7* ppMechTexture,
LPDIRECTDRAWSURFACE7* ppSpriteTexture)
{
char temppath[MAX_PATH];
if(g_f3dtarget){
*ppMechTexture=CreatePixelFormatTexture(pdd,128,128,&DDPF_R5G6B5);//Texture for storing 3D mech image...
}else{
// MSL 5.02 Target MFD Image
*ppMechTexture=CreatePixelFormatTexture(pdd,1024,1024,&DDPF_R5G6B5);//Texture for storing 2D mech image...
//Store images for all mechs.
for(int mech=0;mech<ARRAYSIZE(mechnames);mech++){
int x=(mech%8)*128;
int y=(mech/8)*128;
sprintf(temppath,"%s\\hsh\\mfd\\%s.bmp",AssetsDirectory1,mechnames[mech]);
DrawBitmapToSurface(*ppMechTexture,x,y,temppath);
}
}
sprintf(temppath,"%s\\hsh\\%s",AssetsDirectory1,"mfd_texture.bmp");
*ppSpriteTexture=CreateATextureFromFile(pdd, temppath);
}
// [mode 4 split-MFD fix] exchange one data member between two devices.
#define HSH_SWAP_MEMBER(type, a, b) { type _hsh_swaptmp = (a); (a) = (b); (b) = _hsh_swaptmp; }
bool CHSH_Device::InitSecond(DWORD tresx,DWORD tresy)
{
if (m_pOtherHSHD) {
@@ -1068,9 +1108,9 @@ bool CMR_Device::LoadMRTargetTexture(const char *mechtexturename)
CMR_Device mr_device;
// Mode 4: the right-hand 640x480 MFD device. Shares VRAM with mfd_device
// (same GPU), so mfd_device's pDDSTarget texture can be passed to this
// device's pD3DDevice->SetTexture without a copy.
// Mode 4: the right-hand 640x480 MFD device. It owns a SEPARATE IDirectDraw7 and
// IDirect3DDevice7 (one DirectDraw device is bound to one monitor), so it also owns a
// complete, independent copy of the MFD texture set - see HSH_CreateMFDTextures.
CMFDRight_Device mfd_device_right;
bool CMFDRight_Device::InitFirst()
@@ -1081,8 +1121,32 @@ bool CMFDRight_Device::InitFirst()
bool CMFDRight_Device::InitSecond()
{
// Create flip chain, D3D device and a render-target slot (1024x512).
return CHSH_Device::InitSecond(1024, 512);
// Create flip chain, D3D device, render target (1024x512) and fonts.
CHSH_Device::InitSecond(1024, 512);
// Independent copy of the MFD texture set owned by THIS DirectDraw object.
HSH_CreateMFDTextures(pDD, &pDDSMechTexture, &pDDSTexture);
pDDSDamageTexture = 0; // created on demand by LoadDamageTexture
pDDSTargetTexture = 0; // created on demand by LoadTargetTexture
tw = 512;
th = 512;
D3DMATERIAL7 mtrl;
ZeroMemory( &mtrl, sizeof(mtrl) );
mtrl.diffuse.r = mtrl.diffuse.g = mtrl.diffuse.b = 1.0f;
mtrl.ambient.r = mtrl.ambient.g = mtrl.ambient.b = 1.0f;
pD3DDevice->SetMaterial( &mtrl );
pD3DDevice->SetRenderState( D3DRENDERSTATE_AMBIENT, 0xffffffff );
return true;
}
bool CMFDRight_Device::Release()
{
SAFE_RELEASE(pDDSMechTexture);
SAFE_RELEASE(pDDSDamageTexture);
SAFE_RELEASE(pDDSTargetTexture);
return CHSH_Device::Release();
}
/////////////////////////////////// CRadar_Device ///////////////////////////////////
@@ -1368,7 +1432,7 @@ static DWORD channel_color[6]={
CMFD_Device::CMFD_Device()
: ch((DWORD)-1), m_pRightDevice(NULL)
: ch((DWORD)-1), m_pRightDevice(NULL), m_bSwappedToRight(false)
{
}
@@ -1393,38 +1457,13 @@ bool CMFD_Device::InitFirst()
bool CMFD_Device::InitSecond()
{
CHSH_Device::InitSecond(1024,512); // 640 x 480
if(g_f3dtarget){
pDDSMechTexture=CreatePixelFormatTexture(pDD,128,128,&DDPF_R5G6B5);//Texture for storing 3D mech image...
}else{
// MSL 5.02 Target MFD Image
// pDDSMechTexture=CreatePixelFormatTexture(pDD,512,512,&DDPF_R5G6B5);//texture for storing 2D mech image...
pDDSMechTexture=CreatePixelFormatTexture(pDD,1024,1024,&DDPF_R5G6B5);//Texture for storing 2D mech image...
//Store images for all mechs.
for(int mech=0;mech<ARRAYSIZE(mechnames);mech++){
// MSL 5.02 Target MFD Image
// int x=(mech%4)*128;
// int y=(mech/4)*128;
int x=(mech%8)*128;
int y=(mech/8)*128;
char temppath[MAX_PATH];
sprintf(temppath,"%s\\hsh\\mfd\\%s.bmp",AssetsDirectory1,mechnames[mech]);
HSH_CreateMFDTextures(pDD, &pDDSMechTexture, &pDDSTexture);
DrawBitmapToSurface(pDDSMechTexture,x,y,temppath);
}
}
//if(pDDSMechTexture==NULL)MessageBeep(0);
//pDDSMechTexture=0;
//pDDSDamageTexture=CreatePixelFormatTexture(pDD,512,512,&DDPF_R4G4B4A4);//damage image..
pDDSDamageTexture=0;//On Reset: recreated... no need to create now.
// MSL 5.03 Target Damage Display
pDDSTargetTexture=0;
//////Texture
char temppath[MAX_PATH];
sprintf(temppath,"%s\\hsh\\%s",AssetsDirectory1,"mfd_texture.bmp");
pDDSTexture=CreateATextureFromFile(pDD, temppath);
//pDDSTexture=0;
tw=512;
th=512;
@@ -1445,6 +1484,43 @@ bool CMFD_Device::InitSecond()
return true;
}
// [mode 4 split-MFD fix]
// Exchange this object's DATA members with the right-hand device's, so that every
// existing "mfd_device.<member>" drawing call in hudchat/huddamage/hudweapon/radar
// transparently targets whichever physical 640x480 panel the current channel belongs to.
// Only data is exchanged - never the vtable pointer, so virtual dispatch is unaffected.
void CMFD_Device::SwapRightState()
{
if (!m_pRightDevice)
return;
CMFDRight_Device* r = m_pRightDevice;
HSH_SWAP_MEMBER(LPDIRECTDRAW7, pDD, r->pDD);
HSH_SWAP_MEMBER(LPDIRECTDRAWSURFACE7, pDDSFront, r->pDDSFront);
HSH_SWAP_MEMBER(LPDIRECTDRAWSURFACE7, pDDSBack, r->pDDSBack);
HSH_SWAP_MEMBER(LPDIRECTDRAWSURFACE7, pDDSTarget, r->pDDSTarget);
HSH_SWAP_MEMBER(LPDIRECTDRAWSURFACE7, pDDSTexture, r->pDDSTexture);
HSH_SWAP_MEMBER(LPDIRECT3DDEVICE7, pD3DDevice, r->pD3DDevice);
HSH_SWAP_MEMBER(SIZE, size_back, r->size_back);
HSH_SWAP_MEMBER(SIZE, size_target, r->size_target);
HSH_SWAP_MEMBER(float, tw, r->tw);
HSH_SWAP_MEMBER(float, th, r->th);
HSH_SWAP_MEMBER(LPDIRECTDRAWSURFACE7, pDDSMechTexture, r->pDDSMechTexture);
HSH_SWAP_MEMBER(LPDIRECTDRAWSURFACE7, pDDSDamageTexture, r->pDDSDamageTexture);
HSH_SWAP_MEMBER(LPDIRECTDRAWSURFACE7, pDDSTargetTexture, r->pDDSTargetTexture);
// CHSHFont has no virtual functions, so a raw byte swap is safe here and avoids
// running constructor/destructor side effects on a temporary copy.
for (int i = 0; i < 4; i++) {
char fonttmp[sizeof(CHSHFont)];
memcpy(fonttmp, &pFont[i], sizeof(CHSHFont));
memcpy(&pFont[i], &r->pFont[i], sizeof(CHSHFont));
memcpy(&r->pFont[i], fonttmp, sizeof(CHSHFont));
}
m_bSwappedToRight = !m_bSwappedToRight;
}
bool CMFD_Device::BeginChannel(DWORD channel)
{
if (!sh_game_started)
@@ -1491,6 +1567,13 @@ bool CMFD_Device::BeginChannel(DWORD channel)
}
ch=channel;
// [mode 4 split-MFD fix] Channels 3-4 belong to the right-hand 640x480 monitor,
// which is a separate DirectDraw object with its own D3D device and its own copies
// of the MFD textures and fonts. Swap our members over to it for the duration of
// the channel so all existing drawing code targets it, and swap back in EndChannel.
if (g_nTypeOfMFDs == 4 && m_pRightDevice && channel >= 3)
SwapRightState();
//Set the Render Target.
SetRenderTargetTexture();
pD3DDevice->Clear(0,NULL,D3DCLEAR_TARGET ,0,0,0);
@@ -1513,72 +1596,56 @@ bool CMFD_Device::EndChannel()
gCaptureScreen = 0;
}
// Mode 4: route this channel's blit to the correct physical monitor.
// Handled here, before the modes 1-3 SetRenderTargetBackbuffer/BeginScene
// block, so each path has exactly one matched Begin/EndScene pair.
// Channels 0-2 go to the LEFT device (this, g_nMFD1).
// Channels 3-4 go to the RIGHT device (m_pRightDevice, g_nMFD2).
// pDDSTarget was created by this device's pDD; both devices are on the
// same GPU so VRAM textures are mutually accessible.
if (g_nTypeOfMFDs == 4 && m_pRightDevice) {
CHSH_Device* target = (ch >= 3) ? m_pRightDevice : static_cast<CHSH_Device*>(this);
target->SetRenderTargetBackbuffer();
target->pD3DDevice->BeginScene();
// [mode 4 split-MFD fix] One composite path for every mode. In mode 4 BeginChannel
// has already swapped our members over to the device that owns this channel, so
// pDDSTarget and pD3DDevice always belong to the SAME DirectDraw object. The old
// code sampled the LEFT device's pDDSTarget from the RIGHT device's D3D device,
// which DirectDraw cannot do natively and emulated with a VRAM->system-memory
// readback on channels 3 and 4 - a full GPU stall twice every 7-frame cycle, which
// is what produced the constant rhythmic stutter in mode 4.
SetRenderTargetBackbuffer();
pD3DDevice->BeginScene();
target->pD3DDevice->SetRenderState(D3DRENDERSTATE_ALPHABLENDENABLE,TRUE);
target->pD3DDevice->SetRenderState(D3DRENDERSTATE_SRCBLEND,D3DBLEND_ONE);
target->pD3DDevice->SetRenderState(D3DRENDERSTATE_DESTBLEND,D3DBLEND_ONE);
pD3DDevice->SetRenderState(D3DRENDERSTATE_ALPHABLENDENABLE,TRUE);
pD3DDevice->SetRenderState(D3DRENDERSTATE_SRCBLEND,D3DBLEND_ONE);
pD3DDevice->SetRenderState(D3DRENDERSTATE_DESTBLEND,D3DBLEND_ONE);
target->pD3DDevice->SetTexture(0, pDDSTarget);
pD3DDevice->SetTexture( 0, pDDSTarget );
DWORD color4 = channel_color[ch];
float w4 = (float)target->size_back.cx; // 640
float h4 = (float)target->size_back.cy; // 480
float w2_4 = (float)size_target.cx; // 1024
float h2_4 = (float)size_target.cy; // 512
D3DTLVERTEX V4[4];
V4[0]=D3DTLVERTEX(D3DVECTOR( -0.5f,h4-0.5f,0.9f),1.0f,color4,0, 0/w2_4,h4/h2_4);
V4[1]=D3DTLVERTEX(D3DVECTOR( -0.5f, -0.5f,0.9f),1.0f,color4,0, 0/w2_4, 0/h2_4);
V4[2]=D3DTLVERTEX(D3DVECTOR(w4-0.5f,h4-0.5f,0.9f),1.0f,color4,0,w4/w2_4,h4/h2_4);
V4[3]=D3DTLVERTEX(D3DVECTOR(w4-0.5f, -0.5f,0.9f),1.0f,color4,0,w4/w2_4, 0/h2_4);
target->pD3DDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP,D3DFVF_TLVERTEX,V4,4,NULL);
DWORD color;
target->pD3DDevice->EndScene();
ch = (DWORD)-1;
color = channel_color[ch];
D3DTLVERTEX Vertices[4]; // Vertices for the cube
// Modes 1-3 pack two 640-wide panels into a single backbuffer, so channels 0-2 go
// to the left half and 3-4 to the right half. In mode 4 each device IS a
// standalone 640x480 panel, so every channel composites full width at x=0.
float w, x1;
if (g_nTypeOfMFDs == 4) {
w = (float)size_back.cx;
x1 = 0.0f;
} else {
// Modes 1-3: blit to half of the single 1280-wide (or 640-wide) backbuffer.
SetRenderTargetBackbuffer();
pD3DDevice->BeginScene();
pD3DDevice->SetRenderState(D3DRENDERSTATE_ALPHABLENDENABLE,TRUE);
pD3DDevice->SetRenderState(D3DRENDERSTATE_SRCBLEND,D3DBLEND_ONE);
pD3DDevice->SetRenderState(D3DRENDERSTATE_DESTBLEND,D3DBLEND_ONE);
pD3DDevice->SetTexture( 0, pDDSTarget );
DWORD color;
color = channel_color[ch];
//color = 0xFFFFFFFF;
D3DTLVERTEX Vertices[4]; // Vertices for the cube
float w=(float)size_back.cx/2;//<==NOTE: this differs from radar.
float x1=(ch/3)*w;
float x2=x1+w;
float h=(float)size_back.cy;
float w2=(float)size_target.cx;
float h2=(float)size_target.cy;
Vertices[0] = D3DTLVERTEX(D3DVECTOR(x1-0.5f,h-0.5f,0.9f),1.0f,color,0, 0/w2, h/h2);
Vertices[1] = D3DTLVERTEX(D3DVECTOR(x1-0.5f,0-0.5f,0.9f),1.0f,color,0, 0/w2, 0/h2);
Vertices[2] = D3DTLVERTEX(D3DVECTOR(x2-0.5f,h-0.5f,0.9f),1.0f,color,0, w/w2, h/h2);
Vertices[3] = D3DTLVERTEX(D3DVECTOR(x2-0.5f,0-0.5f,0.9f),1.0f,color,0, w/w2, 0/h2);
pD3DDevice->DrawPrimitive( D3DPT_TRIANGLESTRIP, D3DFVF_TLVERTEX , Vertices, 4, NULL );
pD3DDevice->EndScene();
ch = (DWORD)-1;
w = (float)size_back.cx/2;//<==NOTE: this differs from radar.
x1 = (ch/3)*w;
}
float x2=x1+w;
float h=(float)size_back.cy;
float w2=(float)size_target.cx;
float h2=(float)size_target.cy;
Vertices[0] = D3DTLVERTEX(D3DVECTOR(x1-0.5f,h-0.5f,0.9f),1.0f,color,0, 0/w2, h/h2);
Vertices[1] = D3DTLVERTEX(D3DVECTOR(x1-0.5f,0-0.5f,0.9f),1.0f,color,0, 0/w2, 0/h2);
Vertices[2] = D3DTLVERTEX(D3DVECTOR(x2-0.5f,h-0.5f,0.9f),1.0f,color,0, w/w2, h/h2);
Vertices[3] = D3DTLVERTEX(D3DVECTOR(x2-0.5f,0-0.5f,0.9f),1.0f,color,0, w/w2, 0/h2);
pD3DDevice->DrawPrimitive( D3DPT_TRIANGLESTRIP, D3DFVF_TLVERTEX , Vertices, 4, NULL );
pD3DDevice->EndScene();
ch = (DWORD)-1;
// Hand our members back to the left device if this channel was routed right.
if (m_bSwappedToRight)
SwapRightState();
}
return true;
}
@@ -159,6 +159,10 @@ public:
bool MapDrawn;
};
// Mode 4 (split dual 640x480): the right-hand MFD lives on its own DirectDraw object,
// so CMFD_Device keeps a typed pointer to it.
class CMFDRight_Device;
class CMFD_Device:public CHSH_Device
{
public:
@@ -169,7 +173,8 @@ public:
LPDIRECTDRAWSURFACE7 pDDSTargetTexture;//Target damage texture..
DWORD ch; //current channel....
CHSH_Device* m_pRightDevice; // mode 4: pointer to right 640x480 device (NULL in modes 1-3)
CMFDRight_Device* m_pRightDevice; // mode 4: right 640x480 device (NULL in modes 1-3)
bool m_bSwappedToRight; // mode 4: true while our members point at the right device
public:
CMFD_Device();
~CMFD_Device();
@@ -177,6 +182,7 @@ public:
bool InitSecond();
bool BeginChannel(DWORD channel);// Select channel 0-4.
bool EndChannel();
void SwapRightState(); // mode 4: exchange data members with the right device
virtual bool BeginScene(); //Performs normal BeginScene/EndScene.
bool BeginSceneRight(); // mode 4: clear+grid on right device at sh_step==1
virtual bool EndScene(); //Performs the Flip.
@@ -192,16 +198,24 @@ public:
};
// Mode 4 (split dual 640x480): right-side MFD device on its own 640x480 monitor.
// Same GPU as the left device (mfd_device), so VRAM textures are shared.
// It owns a SEPARATE IDirectDraw7 and IDirect3DDevice7 (a DirectDraw device is bound to
// one monitor), therefore it must also own a complete, independent copy of the MFD
// texture set: a surface belongs to the IDirectDraw7 that created it, not to the GPU,
// so surfaces can NOT be shared between the two devices even on the same card.
class CMFDRight_Device : public CHSH_Device
{
public:
CMFDRight_Device() {}
LPDIRECTDRAWSURFACE7 pDDSMechTexture;
LPDIRECTDRAWSURFACE7 pDDSDamageTexture;
LPDIRECTDRAWSURFACE7 pDDSTargetTexture;
public:
CMFDRight_Device()
: pDDSMechTexture(0), pDDSDamageTexture(0), pDDSTargetTexture(0) {}
bool InitFirst();
bool InitSecond();
virtual bool BeginScene() { return true; }
virtual bool EndScene() { return true; }
virtual bool Release() { return CHSH_Device::Release(); }
virtual bool Release();
};
class CMR_Device:public CHSH_Device