From 0a657b59989df52255a3c19d5c9aeda96735b437 Mon Sep 17 00:00:00 2001 From: RT Date: Fri, 24 Jul 2026 23:39:42 -0500 Subject: [PATCH] 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) Co-authored-by: GitHub Copilot --- .../code/CoreTech/Libraries/GameOS/render.cpp | 251 +++++++++++------- .../code/CoreTech/Libraries/GameOS/render.hpp | 22 +- 2 files changed, 177 insertions(+), 96 deletions(-) diff --git a/Gameleap/code/CoreTech/Libraries/GameOS/render.cpp b/Gameleap/code/CoreTech/Libraries/GameOS/render.cpp index 841e403b..4138258c 100644 --- a/Gameleap/code/CoreTech/Libraries/GameOS/render.cpp +++ b/Gameleap/code/CoreTech/Libraries/GameOS/render.cpp @@ -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;mechSetTexture 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" 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(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; } diff --git a/Gameleap/code/CoreTech/Libraries/GameOS/render.hpp b/Gameleap/code/CoreTech/Libraries/GameOS/render.hpp index f2d55c84..ffd1a5f9 100644 --- a/Gameleap/code/CoreTech/Libraries/GameOS/render.hpp +++ b/Gameleap/code/CoreTech/Libraries/GameOS/render.hpp @@ -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