다음을 통해 공유


UWP(유니버설 Windows 플랫폼) DirectX 게임에 대한 입력 대기 시간 최적화

입력 대기 시간은 게임 환경에 큰 영향을 미칠 수 있으며, 이를 최적화하면 게임이 더욱 세련되게 느껴질 수 있습니다. 또한 적절한 입력 이벤트 최적화는 배터리 수명을 향상시킬 수 있습니다. 올바른 CoreDispatcher 입력 이벤트 처리 옵션을 선택하여 게임이 입력을 최대한 원활하게 처리하도록 하는 방법을 알아봅니다.

입력 대기 시간

입력 대기 시간은 시스템이 사용자 입력에 응답하는 데 걸리는 시간입니다. 응답은 화면에 표시되는 내용이나 오디오 피드백을 통해 수신되는 내용의 변경 사항인 경우가 많습니다.

터치 포인터, 마우스 포인터 또는 키보드에서 가져온 모든 입력 이벤트는 이벤트 처리기에서 처리할 메시지를 생성합니다. 최신 터치 디지타이저 및 게임 주변 장치는 포인터당 최소 100Hz의 입력 이벤트를 보고합니다. 즉, 앱은 포인터(또는 키 입력)당 초당 100개 이상의 이벤트를 받을 수 있습니다. 여러 포인터가 동시에 발생하거나 더 높은 정밀도 입력 디바이스(예: 게임 마우스)를 사용하는 경우 이 업데이트 속도가 증폭됩니다. 이벤트 메시지 큐는 매우 빠르게 채울 수 있습니다.

이벤트가 시나리오에 가장 적합한 방식으로 처리되도록 게임의 입력 대기 시간 요구를 이해하는 것이 중요합니다. 모든 게임에 대한 솔루션은 없습니다.

전력 효율성

입력 대기 시간의 컨텍스트에서 "전원 효율성"은 게임에서 GPU를 사용하는 양을 나타냅니다. GPU 리소스를 적게 사용하는 게임은 전력 효율이 높고 배터리 수명이 길어질 수 있습니다. CPU에 대해서도 마찬가지입니다.

게임이 사용자의 환경을 저하하지 않고 초당 60프레임 미만(현재 대부분의 디스플레이에서 최대 렌더링 속도)으로 전체 화면을 그릴 수 있는 경우 그리는 빈도가 낮아지면 전력 효율이 높아집니다. 일부 게임은 사용자 입력에 대한 응답으로 화면만 업데이트하므로 해당 게임은 초당 60프레임에서 동일한 콘텐츠를 반복적으로 그리면 안 됩니다.

최적화할 항목 선택

DirectX 앱을 디자인할 때는 몇 가지 선택을 해야 합니다. 부드러운 애니메이션을 표시하려면 앱이 초당 60프레임 렌더링을 해야 합니까, 아니면 입력에 대한 응답으로만 렌더링해야 합니까? 가능한 입력 대기 시간이 가장 낮아야 합니까, 아니면 약간의 지연을 허용할 수 있나요? 내 사용자가 내 앱이 배터리 사용량에 대해 신중할 것으로 예상합니까?

이러한 질문에 대한 답변은 다음 시나리오 중 하나에 맞게 앱을 정렬할 수 있습니다.

  1. 요청 시 렌더링합니다. 이 범주의 게임은 특정 유형의 입력에 대한 응답으로 화면을 업데이트하기만 하면됩니다. 앱이 동일한 프레임을 반복적으로 렌더링하지 않고 앱이 입력을 기다리는 데 대부분의 시간을 소비하기 때문에 입력 대기 시간이 짧기 때문에 전력 효율성이 뛰어났습니다. 보드 게임 및 뉴스 판독기는 이 범주에 속할 수 있는 앱의 예입니다.
  2. 임시 애니메이션을 사용하여 요청 시 렌더링합니다. 이 시나리오는 특정 유형의 입력이 사용자의 후속 입력에 종속되지 않는 애니메이션을 시작한다는 점을 제외하고 첫 번째 시나리오와 유사합니다. 게임이 동일한 프레임을 반복적으로 렌더링하지 않고 게임에 애니메이션 효과를 주지 않는 동안 입력 대기 시간이 낮기 때문에 전력 효율성이 좋습니다. 각 이동에 애니메이션 효과를 주는 대화형 어린이 게임 및 보드 게임은 이 범주에 속할 수 있는 앱의 예입니다.
  3. 초당 60프레임 렌더링 이 시나리오에서는 게임이 화면을 지속적으로 업데이트합니다. 디스플레이에 표시할 수 있는 최대 프레임 수를 렌더링하므로 전력 효율성이 저하됩니다. 콘텐츠가 표시되는 동안 DirectX가 스레드를 차단하기 때문에 입력 대기 시간이 높습니다. 이렇게 하면 스레드가 사용자에게 표시할 수 있는 것보다 더 많은 프레임을 디스플레이에 보낼 수 없습니다. 1인칭 슈팅 게임, 실시간 전략 게임 및 물리학 기반 게임은 이 범주에 속할 수 있는 앱의 예입니다.
  4. 초당 60 프레임을 렌더링하고 가능한 가장 낮은 입력 대기 시간을 달성합니다. 시나리오 3과 마찬가지로 앱은 화면을 지속적으로 업데이트하므로 전력 효율성이 저하됩니다. 차이점은 입력 처리가 그래픽을 디스플레이에 표시하는 작업으로 차단되지 않도록 게임이 별도의 스레드에서 입력에 응답한다는 것입니다. 온라인 멀티 플레이어 게임, 격투 게임 또는 리듬/타이밍 게임은 매우 타이트한 이벤트 기간 내에서 이동 입력을 지원하기 때문에 이 범주에 속할 수 있습니다.

이행

대부분의 DirectX 게임은 게임 루프로 알려진 것에 의해 구동됩니다. 기본 알고리즘은 사용자가 게임 또는 앱을 종료할 때까지 다음 단계를 수행하는 것입니다.

  1. 프로세스 입력
  2. 게임 상태 업데이트
  3. 게임 콘텐츠 그리기

DirectX 게임의 콘텐츠가 렌더링되고 화면에 표시될 준비가 되면 게임 루프는 GPU가 새 프레임을 받을 준비가 될 때까지 기다린 후 다시 입력을 처리합니다.

간단한 직소 퍼즐 게임을 반복하여 앞에서 언급한 각 시나리오에 대한 게임 루프의 구현을 보여 드리겠습니다. 각 구현과 논의된 의사 결정 지점, 이점 및 장단점은 짧은 대기 시간 입력 및 전력 효율성을 위해 앱을 최적화하는 데 도움이 되는 가이드 역할을 할 수 있습니다.

시나리오 1: 주문형 렌더링

직소 퍼즐 게임의 첫 번째 반복은 사용자가 퍼즐 조각을 이동할 때만 화면을 업데이트합니다. 사용자는 퍼즐 조각을 제자리에 끌거나 퍼즐 조각을 선택한 다음 올바른 대상을 터치하여 제자리에 맞출 수 있습니다. 두 번째 경우, 퍼즐 조각은 애니메이션이나 효과없이 대상으로 이동합니다.

코드에는 IFrameworkView::Run 메서드 내에 CoreProcessEventsOption::ProcessOneAndAllPending를 사용하는 단일 스레드 게임 루프가 있습니다. 이 옵션을 사용하면 큐에서 현재 사용 가능한 모든 이벤트가 처리됩니다. 보류 중인 이벤트가 없으면 게임 루프는 이벤트가 나타날 때까지 기다립니다.

void App::Run()
{
    
    while (!m_windowClosed)
    {
        // Wait for system events or input from the user.
        // ProcessOneAndAllPending will block the thread until events appear and are processed.
        CoreWindow::GetForCurrentThread()->Dispatcher->ProcessEvents(CoreProcessEventsOption::ProcessOneAndAllPending);

        // If any of the events processed resulted in a need to redraw the window contents, then we will re-render the
        // scene and present it to the display.
        if (m_updateWindow || m_state->StateChanged())
        {
            m_main->Render();
            m_deviceResources->Present();

            m_updateWindow = false;
            m_state->Validate();
        }
    }
}

시나리오 2: 임시 애니메이션을 사용하여 주문형 렌더링

두 번째 반복에서는 사용자가 퍼즐 조각을 선택한 다음 해당 조각의 올바른 대상을 터치할 때 대상에 도달할 때까지 화면에 애니메이션 효과를 주도록 게임이 수정됩니다.

이전과 마찬가지로 코드에는 ProcessOneAndAllPending 사용하여 큐에 입력 이벤트를 디스패치하는 단일 스레드 게임 루프가 있습니다. 이제 애니메이션 중에 루프가 CoreProcessEventsOption::P rocessAllIfPresent 사용하여 새 입력 이벤트를 기다리지 않도록 변경된다는 점이 다릅니다. 보류 중인 이벤트가 없으면 ProcessEvents 즉시 반환되고 앱이 애니메이션의 다음 프레임을 표시할 수 있습니다. 애니메이션이 완료되면, 루프가 ProcessOneAndAllPending로 다시 전환되어 화면 업데이트를 제한합니다.

void App::Run()
{

    while (!m_windowClosed)
    {
        // 2. Switch to a continuous rendering loop during the animation.
        if (m_state->Animating())
        {
            // Process any system events or input from the user that is currently queued.
            // ProcessAllIfPresent will not block the thread to wait for events. This is the desired behavior when
            // you are trying to present a smooth animation to the user.
            CoreWindow::GetForCurrentThread()->Dispatcher->ProcessEvents(CoreProcessEventsOption::ProcessAllIfPresent);

            m_state->Update();
            m_main->Render();
            m_deviceResources->Present();
        }
        else
        {
            // Wait for system events or input from the user.
            // ProcessOneAndAllPending will block the thread until events appear and are processed.
            CoreWindow::GetForCurrentThread()->Dispatcher->ProcessEvents(CoreProcessEventsOption::ProcessOneAndAllPending);

            // If any of the events processed resulted in a need to redraw the window contents, then we will re-render the
            // scene and present it to the display.
            if (m_updateWindow || m_state->StateChanged())
            {
                m_main->Render();
                m_deviceResources->Present();

                m_updateWindow = false;
                m_state->Validate();
            }
        }
    }
}

ProcessOneAndAllPendingProcessAllIfPresent간의 전환을 지원하려면 앱이 애니메이션 효과를 주는지 알 수 있도록 상태를 추적해야 합니다. 직소 퍼즐 앱에서 GameState 클래스의 게임 루프 중에 호출할 수 있는 새 메서드를 추가하여 이 작업을 수행합니다. 게임 루프의 애니메이션 분기는 GameState의 새 Update 메서드를 호출하여 애니메이션 상태의 업데이트를 구동합니다.

시나리오 3: 초당 60프레임 렌더링

세 번째 반복에서 앱은 사용자가 퍼즐을 얼마나 오래 작업했는지를 보여 주는 타이머를 표시합니다. 경과된 시간을 밀리초까지 표시하므로 디스플레이를 최신 상태로 유지하려면 초당 60프레임을 렌더링해야 합니다.

시나리오 1 및 2와 마찬가지로 앱에는 단일 스레드 게임 루프가 있습니다. 이 시나리오의 차이점은 항상 렌더링되므로 처음 두 시나리오에서 수행한 것처럼 게임 상태의 변경 내용을 더 이상 추적할 필요가 없다는 것입니다. 따라서 기본적으로 ProcessAllIfPresent 사용하여 이벤트를 처리할 수 있습니다. 보류 중인 이벤트가 없으면 ProcessEvents 즉시 반환되고 다음 프레임을 렌더링합니다.

void App::Run()
{

    while (!m_windowClosed)
    {
        if (m_windowVisible)
        {
            // 3. Continuously render frames and process system events and input as they appear in the queue.
            // ProcessAllIfPresent will not block the thread to wait for events. This is the desired behavior when
            // trying to present smooth animations to the user.
            CoreWindow::GetForCurrentThread()->Dispatcher->ProcessEvents(CoreProcessEventsOption::ProcessAllIfPresent);

            m_state->Update();
            m_main->Render();
            m_deviceResources->Present();
        }
        else
        {
            // 3. If the window isn't visible, there is no need to continuously render.
            // Process events as they appear until the window becomes visible again.
            CoreWindow::GetForCurrentThread()->Dispatcher->ProcessEvents(CoreProcessEventsOption::ProcessOneAndAllPending);
        }
    }
}

이 방법은 렌더링 시기를 결정하기 위해 추가 상태를 추적할 필요가 없기 때문에 게임을 작성하는 가장 쉬운 방법입니다. 타이머 간격에서 합리적인 입력 응답성과 함께 가능한 가장 빠른 렌더링을 달성합니다.

그러나 이러한 개발 용이성은 가격과 함께 제공됩니다. 초당 60프레임으로 렌더링하면 주문형 렌더링보다 더 많은 전력이 사용됩니다. 게임이 모든 프레임에 표시되는 내용을 변경할 때 ProcessAllIfPresent 사용하는 것이 가장 좋습니다. 또한 앱이 ProcessEvents대신 디스플레이의 동기화 주기에서 게임 루프를 차단하기 때문에 입력 지연 시간이 최대 16.7ms까지 증가합니다. 큐가 프레임당 한 번만 처리되기 때문에 일부 입력 이벤트가 삭제될 수 있습니다(60Hz).

시나리오 4: 초당 60프레임 렌더링 및 가능한 가장 낮은 입력 대기 시간 달성

일부 게임은 시나리오 3에서 볼 수 있는 입력 대기 시간의 증가를 무시하거나 보정할 수 있습니다. 그러나 낮은 입력 대기 시간이 게임의 경험과 플레이어 피드백 감각에 중요한 경우 초당 60프레임을 렌더링하는 게임은 별도의 스레드에서 입력을 처리해야 합니다.

직소 퍼즐 게임의 네 번째 버전은 시나리오 3을 기반으로 하여, 입력 처리와 그래픽 렌더링을 게임 루프와 분리된 별도의 스레드로 나눕니다. 각각에 대해 별도의 스레드가 있으면 그래픽 출력에 의해 입력이 지연되지 않습니다. 그러나 결과적으로 코드가 더 복잡해집니다. 시나리오 4에서 입력 스레드는 CoreProcessEventsOption::ProcessUntilQuit사용하여 ProcessEvents 호출합니다. 이는 새 이벤트를 대기하고 사용 가능한 모든 이벤트를 디스패치합니다. 창이 닫히거나 게임에서 CoreWindow::Close가 호출될 때까지 이 동작을 계속합니다.

void App::Run()
{
    // 4. Start a thread dedicated to rendering and dedicate the UI thread to input processing.
    m_main->StartRenderThread();

    // ProcessUntilQuit will block the thread and process events as they appear until the App terminates.
    CoreWindow::GetForCurrentThread()->Dispatcher->ProcessEvents(CoreProcessEventsOption::ProcessUntilQuit);
}

void JigsawPuzzleMain::StartRenderThread()
{
    // If the render thread is already running, then do not start another one.
    if (IsRendering())
    {
        return;
    }

    // Create a task that will be run on a background thread.
    auto workItemHandler = ref new WorkItemHandler([this](IAsyncAction^ action)
    {
        // Notify the swap chain that this app intends to render each frame faster
        // than the display's vertical refresh rate (typically 60 Hz). Apps that cannot
        // deliver frames this quickly should set this to 2.
        m_deviceResources->SetMaximumFrameLatency(1);

        // Calculate the updated frame and render once per vertical blanking interval.
        while (action->Status == AsyncStatus::Started)
        {
            // Execute any work items that have been queued by the input thread.
            ProcessPendingWork();

            // Take a snapshot of the current game state. This allows the renderers to work with a
            // set of values that won't be changed while the input thread continues to process events.
            m_state->SnapState();

            m_sceneRenderer->Render();
            m_deviceResources->Present();
        }

        // Ensure that all pending work items have been processed before terminating the thread.
        ProcessPendingWork();
    });

    // Run the task on a dedicated high priority background thread.
    m_renderLoopWorker = ThreadPool::RunAsync(workItemHandler, WorkItemPriority::High, WorkItemOptions::TimeSliced);
}

Microsoft Visual Studio 2015의 DirectX 11 및 XAML 앱(유니버설 Windows) 템플릿은 게임 루프를 비슷한 방식으로 여러 스레드로 분할합니다. Windows::UI::Core::CoreIndependentInputSource 개체를 사용하여 입력 처리 전용 스레드를 시작하고 XAML UI 스레드와 독립적으로 렌더링 스레드를 만듭니다. 이러한 템플릿에 대한 자세한 내용은 템플릿 유니버설 Windows 플랫폼 및 DirectX 게임 프로젝트 만들기을 참조하세요.

입력 대기 시간을 줄이는 추가 방법

대기 가능한 스왑 체인 사용

DirectX 게임은 사용자가 화면에 표시되는 내용을 업데이트하여 사용자 입력에 응답합니다. 60Hz 디스플레이에서 화면은 16.7ms(1초/60프레임)마다 새로 고쳐집니다. 그림 1은 초당 60프레임을 렌더링하는 앱의 VBlank(16.7ms 새로 고침 신호)를 기준으로 입력 이벤트에 대한 대략적인 수명 주기 및 응답을 보여 줍니다.

그림 1

그림 1 DirectX 입력 대기 시간

Windows 8.1에서 DXGI는 스왑 체인에 대한 DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT 플래그를 도입했습니다. 이를 통해 앱이 프레젠트 큐를 비워 두기 위한 복잡한 추론을 구현하지 않고도 프레임 지연 시간을 쉽게 줄일 수 있습니다. 이 플래그를 사용하여 만든 스왑 체인을 대기 가능한 스왑 체인이라고 합니다. 그림 2는 대기 가능한 스왑 체인을 사용할 때 입력 이벤트에 대한 대략적인 수명 주기 및 응답을 보여줍니다.

그림 2

directx 대기 가능 그림2 입력 지연 시간

이러한 다이어그램에서 볼 수 있는 것은 게임이 디스플레이의 새로 고침 속도에 정의된 16.7ms 예산 내에서 각 프레임을 렌더링하고 표시할 수 있는 경우 두 개의 전체 프레임으로 입력 대기 시간을 줄일 수 있다는 것입니다. 직소 퍼즐 샘플은 대기 가능한 스왑 체인을 사용하고 다음을 호출하여 현재 큐 제한을 제어합니다. m_deviceResources->SetMaximumFrameLatency(1);