Marathon Input Issues Fix: Architettare giochi PC per sopravvivere ai conflitti di Overlay
Ogni sviluppatore di multiplayer competitivi conosce la sensazione di vedere un netcode perfettamente architettato rompersi a causa di un overlay di Discord. Passi mesi a ottimizzare l'infrastruttura server per una latenza inferiore a 30ms, solo per scoprire che i giocatori mancano i colpi perché un'app di streaming in background sta soffocando l'input thread del gioco.
Questo scenario si sta verificando nel Server Slam di Marathon, l'imminente extraction shooter di Bungie. I giocatori PC segnalano mouse lag, input persi e movimenti lenti della telecamera. Bungie ha confermato il problema, indicando utility di terze parti, overlay e metodi di cattura finestra come principali colpevoli.
Sui forum, migliaia di utenti cercano un marathon input issues fix. La soluzione temporanea lato utente è semplice: trasmettere l'intero monitor invece della singola finestra e disattivare gli overlay.
Ma per i developer, la domanda è architettonica: perché le app di streaming dirottano gli input e come possiamo prevenirlo?
In questo deep dive tecnico, analizzeremo l'input handling di Windows, perché OBS Game Capture causa problemi di frame pacing e come deaccoppiare gli input thread.
La causa: come gli Overlay dirottano la Render Pipeline
Per capire perché lo streaming rovina il mouse input, dobbiamo guardare come i giochi PC arrivano a schermo.
Quando un utente usa OBS o Discord, queste app iniettano una DLL nel processo del gioco. Questa DLL intercetta le chiamate alle API grafiche, facendo hooking della funzione Present in DirectX o vkQueuePresentKHR in Vulkan.
Il collo di bottiglia della Swap Chain
Facendo hooking della swap chain, l'overlay costringe l'engine ad aspettare mentre copia il frame buffer nella propria memoria. Se la CPU o GPU è carica per l'encoder, l'operazione rallenta. Un frame da 16.6ms (60 FPS) passa a 24ms. Poiché molti engine interrogano l'input Windows (WM_MOUSEMOVE) nel main thread prima del render tick, ritardare il rendering ritarda l'input polling.
Il mouse a 1000Hz viene interrogato a intervalli irregolari, causando una sensazione di mira pesante o fluttuante.
Fullscreen vs. Windowed Capture
Bungie ha notato che i problemi avvengono soprattutto con la cattura della finestra. Questo dipende da come il Windows Desktop Window Manager (DWM) gestisce la composizione.
In Exclusive Fullscreen o con il DXGI Flip Model, il gioco bypassa il DWM. Ma se un'app forza un hook di cattura finestra, spesso riporta il gioco in modalità composited, aggiungendo da 1 a 3 frame di latenza (16ms a 48ms) prima che il frame arrivi al monitor.
Architettare Input imbattibili: Raw Input API
Per immunizzare il gioco, bisogna abbandonare i message pump standard di Windows per gli input critici. Usare WM_MOUSEMOVE è letale per uno shooter competitivo.
Implementate la Raw Input API (WM_INPUT) e spostate l'input polling su un thread dedicato ad alta priorità, deaccoppiato dal render loop.
Implementare Raw Input in C++ (Win32)
Ecco come bypassare le code standard per registrare dati raw dal mouse:
// Inizializzazione Raw Input C++ Win32
void InitializeRawInput(HWND hwnd) {
RAWINPUTDEVICE Rid[1];
Rid[0].usUsagePage = 0x01;
Rid[0].usUsage = 0x02;
Rid[0].dwFlags = 0;
Rid[0].hwndTarget = hwnd;
if (RegisterRawInputDevices(Rid, 1, sizeof(Rid[0])) == FALSE) {
DWORD error = GetLastError();
LogWarning("Errore registrazione raw input: %d", error);
}
}
// Nel WndProc
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
case WM_INPUT: {
UINT dwSize = 0;
GetRawInputData((HRAWINPUT)lParam, RID_INPUT, NULL, &dwSize, sizeof(RAWINPUTHEADER));
if (dwSize == 0) break;
LPBYTE lpb = new BYTE[dwSize];
GetRawInputData((HRAWINPUT)lParam, RID_INPUT, lpb, &dwSize, sizeof(RAWINPUTHEADER));
RAWINPUT* raw = (RAWINPUT*)lpb;
if (raw->header.dwType == RIM_TYPEMOUSE) {
int deltaX = raw->data.mouse.lLastX;
int deltaY = raw->data.mouse.lLastY;
InputManager::GetInstance().AccumulateMouseDelta(deltaX, deltaY);
}
delete[] lpb;
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
Unreal Engine: Disabilitare lo Smoothing
Unreal gestisce il Raw Input internamente, ma spesso applica uno smoothing che peggiora il lag. Disabilitatelo nel PlayerController:
void ACompetitivePlayerController::SetupInputComponent() {
Super::SetupInputComponent();
bEnableMouseSmoothing = false;
LocalPlayer->AspectRatioAxisConstraint = AspectRatio_MaintainYFOV;
}
Effetto a cascata: Client Lag vs. Server Rollback
L'input lag non è solo locale. In un'architettura server autoritativa, se un overlay blocca il loop del client, i pacchetti di input arrivano in ritardo. Il server deve accelerare la simulazione e, se il ritardo è eccessivo, la reconciliation forza una correzione brusca (rubberbanding), come descritto in How To Fix Player Location Desync In Uefn And Unreal Engine Multiplayer.
Gestione lato Server
Il server deve usare un buffer di input per assorbire il jitter del client.
void ServerProcessClientInputs(ClientConnection* client) {
const int TARGET_BUFFER_SIZE = 2;
int currentBufferSize = client->InputQueue.Size();
if (currentBufferSize > TARGET_BUFFER_SIZE + 3) {
client->InputQueue.DropOldest(currentBufferSize - TARGET_BUFFER_SIZE);
}
if (!client->InputQueue.IsEmpty()) {
InputCommand cmd = client->InputQueue.Pop();
SimulatePlayerMovement(client->PlayerEntity, cmd);
}
}
Con horizOn, puoi distribuire server ottimizzati con protezione dal desync, lasciando che il team si concentri sul client loop.
5 Best Practice per l'Input PC
- Deaccoppiare Input e Rendering: Usa un thread ad alta priorità a 1000Hz.
- Forzare DXGI Flip Model: Usa
DXGI_SWAP_EFFECT_FLIP_DISCARDper bypassare il DWM. - Hardware Cursor: Usa
SetCursordell'OS invece di quad testurizzati. - Avvisare per Hook Injection: Rileva DLL di overlay e avvisa l'utente.
- Fixed Timestep Simulation: Lega fisica e input a un passo temporale fisso.
Conclusione
Il caso Marathon ricorda quanto sia ostile l'ecosistema PC. Mentre gli utenti cercano un marathon input issues fix, i developer devono implementare Raw Input e deaccoppiare i thread. Per il backend, usa horizOn per gestire l'orchestrazione server e il desync.