120Hz·144Hz 가변 주사율 환경에서 입력 지연(Input Latency)을 극소화하는 프레임 루프 설계법

120Hz·144Hz 가변 주사율 환경에서 입력 지연(Input Latency)을 극소화하는 프레임 루프 설계법

120Hz·144Hz 고주사율 및 VRR(G-Sync/FreeSync) 환경에서 발생하는 입력 지연의 구조적 원인을 분석하고 Late Polling과 프레임 페이싱을 결합한 최적의 게임 클라이언트 루프 설계 기법을 상세히 정리합니다.

TL;DR

고주사율(120Hz/144Hz) 및 가변 주사율(VRR) 환경에서 입력 반응성을 극대화하려면 입력 수집(Input Polling) 시점을 프레임 시작부가 아닌 렌더 제출 직전으로 늦추는 Late Polling, GPU 큐 적체를 방지하는 프레임 페이싱(NVIDIA Reflex / Low Latency API) 그리고 고정 물리 시뮬레이션과 렌더 프레임을 분리하는 서브틱(Sub-tick) 보간 루프를 구축해야 합니다.

이를 통해 전통적인 프레임 루프 대비 엔드투엔드(Input-to-Photon) 지연 시간을 최대 50% 이상 단축할 수 있습니다.


고주사율(120Hz/144Hz)과 VRR 환경에서 입력 지연은 왜 발생하는가?

디스플레이 주사율이 60Hz(16.67 ms16.67\text{ ms})에서 120Hz(8.33 ms8.33\text{ ms}), 144Hz(6.94 ms6.94\text{ ms})로 높아질수록 화면 갱신 주기는 급격히 줄어듭니다. 이론상 디스플레이 갱신 주기가 짧아진 만큼 사용자의 조작이 화면에 더 빠르게 반영되어야 하지만 실제 클라이언트 프레임 루프가 고주사율 파이프라인에 최적화되어 있지 않다면 체감 지연(Input-to-Photon Latency)은 기대만큼 줄어들지 않습니다.

입력 신호가 모니터 픽셀의 변화로 이어지기까지의 전체 지연 시간 TlatencyT_{\text{latency}}는 다음과 같은 파이프라인 단계의 합으로 정의됩니다.

Tlatency=Tpoll_to_sim+Tsim+Trender_queue+Tgpu+TscanoutT_{\text{latency}} = T_{\text{poll\_to\_sim}} + T_{\text{sim}} + T_{\text{render\_queue}} + T_{\text{gpu}} + T_{\text{scanout}}

일반적인 게임 엔진의 기본 루프에서는 프레임의 가장 첫 단계에서 OS 메시지 큐를 비우며 입력을 수집(Early Polling)합니다. 이후 게임 로직 연산, 씬 그래프 업데이트, 애니메이션 평가, 렌더 커맨드 생성(Draw Call), GPU 렌더링, 프레젠트 큐 대기, 최종 디스플레이 주사선 출력(Scanout)이 순차적으로 진행됩니다.

이 구조에서 주사율이 높아지고 GPU 바운드 상황이 발생하면 다음과 같은 병목이 발생합니다:

  1. Early Polling 샘플링 에이징: 프레임 초반에 읽어 들인 입력 데이터가 실제 GPU 렌더링을 거쳐 화면에 출력되기까지 최소 1.5~2프레임 이상의 시간 동안 방치됩니다.
  2. 백버퍼 큐 적체(Queue Backpressure): 더블/트리플 버퍼링 환경에서 CPU 렌더 스레드가 GPU 완료 시점보다 앞서 나가면 렌더 큐에 1~2프레임 분량의 커맨드가 누적되어 입력 지연이 누적 배수로 증가합니다.
  3. 물리 고정 틱(TfixedT_{\text{fixed}})과 가변 주사율의 위상 불일치: 50Hz/60Hz로 고정된 FixedUpdate 주기와 144Hz 가변 프레임이 불일치할 때 샘플링 지터(Jitter)와 프레임 드롭 착시가 발생합니다.

입력 샘플링부터 화면 표시까지의 파이프라인 지연 구조


프레임 루프 아키텍처별 입력 지연 비교

프레임 루프의 구조적 차이에 따른 144Hz(6.94 ms6.94\text{ ms} 기준) 환경에서의 지연 특성을 정리하면 다음과 같습니다.

프레임 루프 방식입력 수집 시점렌더 큐 제어144Hz 기준 평균 지연 (GPU 바운드 시)주요 장단점
전통적 동기 루프프레임 시작점 (Frame Start)미제어 (1~2 Frame 대기)20.8 ms27.7 ms20.8\text{ ms} \sim 27.7\text{ ms} (3~4 프레임)구현이 단순하나 큐 적체 시 지연 심화
분리형 틱 + 보간 루프틱 시작점세마포어 기반 1프레임 제한13.8 ms17.3 ms13.8\text{ ms} \sim 17.3\text{ ms} (2~2.5 프레임)물리 안정성 우수, 샘플링 지연 일부 잔존
Late Polling + 저지연 SDK 루프렌더 커맨드 제출 직전Just-In-Time 프레임 슬립 제어8.0 ms10.5 ms8.0\text{ ms} \sim 10.5\text{ ms} (1.1~1.5 프레임)최고 수준 반응성, 카메라/조준 로직 분리 필요

초저지연 프레임 루프를 구현하는 3단계 엔지니어링 가이드

1단계: Late Input Polling(지연 샘플링) 파이프라인 구축

게임의 모든 시스템이 동일한 시점의 입력을 필요로 하지는 않습니다. 예를 들어 인벤토리 UI 토글, 퀘스트 수락 등의 비동기 이벤트는 프레임 시작 시 처리해도 무방하지만 1인칭/3인칭 카메라 회전, 조준선(Crosshair), 즉발 사격 로직은 GPU로 렌더 커맨드를 넘기기 직전의 최신 입력이어야 합니다.

flowchart LR
    subgraph TraditionalLoop [전통적인 프레임 루프]
        direction TB
        A1["Input Polling (Early)"] --> B1["Gameplay Logic"]
        B1 --> C1["Render Command Gen"]
        C1 --> D1["GPU Render & Present"]
    end

    subgraph LowLatencyLoop [저지연 최적화 프레임 루프]
        direction TB
        A2["Gameplay / Physics"]
        A2 --> B2["Late Input Polling (Camera/Aim)"]
        B2 --> C2["Render Transform Update"]
        C2 --> D2["Just-In-Time Present (Reflex Sleep)"]
    end

    %% 서브그래프 간 투명 링크를 주어 좌->우 배치를 강제
    TraditionalLoop ~~~ LowLatencyLoop

다음은 C# 기반 커스텀 루프에서 렌더링 직전 카메라 Transform을 최신 원시 입력(Raw Input)으로 갱신하는 Late Update 구조입니다.

public sealed class LowLatencyFrameLoop
{
    private readonly IRawInputDevice mouseDevice;
    private readonly CameraSystem cameraSystem;
    private readonly RenderPipeline renderPipeline;

    public void ExecuteFrame(double deltaTime)
    {
        // 1. 일반 게임 로직 및 애니메이션 업데이트
        UpdateGameLogic(deltaTime);

        // 2. Late Input Polling: 렌더링 커맨드 생성 직전 최신 마우스 델타 수집
        mouseDevice.PollLatestEvents();
        Vector2 latestLookDelta = mouseDevice.ConsumeLookDelta();

        // 3. 카메라 뷰 매트릭스 및 즉발 조준 보정
        cameraSystem.ApplyLateRotation(latestLookDelta);
        cameraSystem.UpdateViewProjectionMatrix();

        // 4. 저지연 렌더링 제출
        renderPipeline.RenderScene(cameraSystem.CurrentViewMatrix);
    }
}

2단계: 프레임 페이싱과 Just-In-Time 렌더 제출 제어

GPU 렌더링 시간이 CPU 프레임 생성 시간보다 길어지는 GPU 바운드 상황에서는 CPU가 쉬지 않고 렌더 커맨드를 제출하여 드라이버 큐에 프레임이 쌓이게 됩니다. 이를 방지하기 위해 렌더 스레드가 GPU 시작 직전에 정확히 깨어나도록 유도하는 Just-In-Time(JIT) 슬립 제어가 필요합니다.

NVIDIA Reflex 또는 AMD Anti-Lag 2와 같은 SDK를 연동하면 드라이버 레벨에서 Reflex_Sleep() 함수를 호출하여 CPU 렌더 커맨드 생성을 GPU 유휴 직전 시점까지 지연시킵니다.

// C++ DirectX 12 / Vulkan 프레임 루프
void GameEngine::RunFrame()
{
    // NVIDIA Reflex 프레임 마커 시작: GPU 렌더링 타이밍에 맞춰 CPU 대기
    NvReflex_SetMarker(pDevice, NV_LATENCY_MARKER_SIMULATION_START, frameID);

    // 1. 게임플레이 시뮬레이션
    SimulatePhysicsAndEntities();

    NvReflex_SetMarker(pDevice, NV_LATENCY_MARKER_SIMULATION_END, frameID);

    // 2. Late Input Polling & 렌더 커맨드 생성
    NvReflex_SetMarker(pDevice, NV_LATENCY_MARKER_RENDERSUBMIT_START, frameID);
    PollRawMouseAndKeyboard();
    UpdateCameraTransform();
    RecordCommandBuffers();

    // 3. 렌더 커맨드 제출 및 Present
    ExecuteCommandLists();
    NvReflex_SetMarker(pDevice, NV_LATENCY_MARKER_RENDERSUBMIT_END, frameID);

    pSwapChain->Present(0, 0); // VSync OFF (VRR 환경에서 드라이버 처리)
    NvReflex_SetMarker(pDevice, NV_LATENCY_MARKER_PRESENT_END, frameID);
}

3단계: 가변 주사율(VRR)과 고정 물리 틱 간 서브틱(Sub-tick) 보간

G-Sync 및 FreeSync 환경에서는 모니터 주사율이 프레임 레이트에 맞춰 동적으로 변동합니다(60 Hz144 Hz60\text{ Hz} \sim 144\text{ Hz}).

물리 엔진의 fixedDeltaTime0.02 s0.02\text{ s}(50Hz)로 고정했을 때 144Hz 화면 갱신 주기(0.00694 s\approx 0.00694\text{ s})와의 위상 차이로 인해 화면이 튀는 현상(Stutter)을 방지하려면 누적 잉여 시간(α\alpha)을 이용한 상태 보간(Interpolation)을 구현해야 합니다.

보간 계수 α\alpha는 다음과 같이 계산됩니다.

α=AccumulatorΔtfixed\alpha = \frac{\text{Accumulator}}{\Delta t_{\text{fixed}}}

0α<10 \le \alpha < 1 값을 렌더링 직전 오브젝트의 이전 물리 상태(St1S_{t-1})와 현재 물리 상태(StS_t) 사이에 적용하여 중간 렌더 위치를 결정합니다.

Srender=(1α)St1+αStS_{\text{render}} = (1 - \alpha) S_{t-1} + \alpha S_t

저지연 프레임 루프에서의 입력 샘플링 타이밍 비교


상용 엔진(Unity / Unreal) 적용 방법

Unity 엔진 설정 가이드

  • Input System 패키지 설정: Edit > Project Settings > Input System Package에서 Update ModeProcess Events In Dynamic Update로 설정하여 고정 틱이 아닌 렌더 프레임 단위로 최신 이벤트를 처리합니다.
  • PlayerLoop 커스텀 수정: PlayerLoop.GetCurrentPlayerLoop()를 가져와 Update 또는 PreLateUpdate 단계의 뒤쪽, 즉 PreUpdate.InputUpdate와 별개로 카메라 갱신 직전에 커스텀 인풋 핸들러를 삽입합니다.
  • NVIDIA Reflex 패키지 활성화: com.unity.modules.lowlatency 또는 최신 Unity 렌더 파이프라인의 Reflex 옵션을 활성화하여 GPU 큐 지연을 제거합니다.

Unreal Engine 엔진 설정 가이드

  • Tick Group 분리: 플레이어 컨트롤러의 회전 입력은 TG_PrePhysics에서 수집하되 뷰포트 카메라 컴포넌트의 FinalizeViews 직전에 TG_PostUpdateWork 그룹에서 ApplyWorldOffset 및 Late Rotation을 갱신합니다.
  • 저지연 플래그 설정: r.GTSyncType 1을 적용하여 게임 스레드와 렌더 스레드의 프레임 동기화 대기를 최적화합니다.
  • NVIDIA Reflex Plugin 활성화: 프로젝트 설정에서 Reflex 모드를 Enabled + Boost로 활성화하여 GPU 코어 클럭 저하에 의한 지연 스파이크를 방지합니다.

자주 묻는 질문 (FAQ)

Q1. 144Hz 게이밍 모니터에서 VSync를 켜면 왜 입력 지연이 체감될 정도로 늘어나나요?

VSync(수직 동기화)는 화면 찢김(Tearing)을 막기 위해 GPU가 렌더링을 마친 프레임을 모니터의 수직 동기 신호(V-Blank) 주기까지 대기시킵니다. 이 과정에서 백버퍼 큐가 가득 차게 되며 CPU는 새로운 프레임을 생성하지 못하고 대기(Backpressure) 상태에 빠집니다.

그 결과 방금 들어온 입력이 최소 2~3프레임(20 ms45 ms20\text{ ms} \sim 45\text{ ms} 이상) 뒤에 표시됩니다. VRR(G-Sync/FreeSync) 환경에서는 VSync 대신 최대 프레임을 주사율보다 23 FPS2 \sim 3\text{ FPS} 낮게 제한(Frame Capping)하고 저지연 SDK를 사용하는 것이 정석입니다.

Q2. 마우스 폴링 레이트(1000Hz vs 4000Hz/8000Hz)가 프레임 루프에 미치는 영향은 무엇인가요?

1000Hz 마우스는 1 ms1\text{ ms}마다 신호를 전달하며 144Hz 루프(6.94 ms6.94\text{ ms}) 내에 약 7개의 입력 이벤트가 발생합니다. 8000Hz 초고주사율 마우스는 프레임당 약 55개 이상의 인터럽트를 발생시킵니다. 프레임 루프가 OS 메시지 큐의 모든 중간 좌표를 순회하며 무거운 연산을 수행하면 CPU 오버헤드로 인해 오히려 프레임 드롭이 발생합니다. 최적화를 위해서는 중간 이동량을 누적(Accumulate)하여 프레임당 1회의 최종 델타로 압축 처리하는 입력 버퍼링 구조가 필수적입니다.

Q3. Late Polling을 적용할 때 네트워크 동기화 패킷 생성 시점은 어떻게 관리해야 하나요?

클라이언트 렌더링을 위한 Late Polling과 서버 전송용 입력 패킷 생성은 분리해야 합니다. 서버 전송용 커맨드 패킷은 결정론적(Deterministic) 서버 틱 주기에 맞춰 정밀하게 시퀀싱되어 패킹되어야 하며 로컬 뷰포트 렌더링에만 렌더 직전 수집한 Late Polling 데이터를 적용하는 이중 파이프라인(Dual-path Input Routing) 구조를 권장합니다.


요약 및 결론

120Hz·144Hz 이상의 고주사율 게이밍 환경에서는 렌더링 프레임 레이트를 올리는 것만으로 입력 반응성을 완전히 보장할 수 없습니다.

  1. Late Input Polling을 통해 카메라 및 조준 시스템의 입력 데이터 신선도를 확보하고
  2. 저지연 프레임 페이싱 SDK(Reflex/Anti-Lag)로 GPU 백버퍼 대기 큐를 상시 1프레임 미만으로 억제하며
  3. 서브틱 상태 보간을 통해 가변 주사율 환경에서도 부드럽고 일관된 화면 출력을 구현해야 합니다.

클라이언트 아키텍처 레벨에서 프레임 루프를 위 3단계로 재설계함으로써 e스포츠 및 하이엔드 액션 게임에 걸맞은 극상의 조작감을 완성할 수 있습니다.

#게임최적화#프레임루프#입력지연#144Hz#고주사율#Unity#UnrealEngine#VRR

계속 읽어보기

이런 글은 어떠세요?

< Back to Logs