Поделиться через


Working with event messaging and CoreWindow (DirectX and C++)

[ This article is for Windows 8.x and Windows Phone 8.x developers writing Windows Runtime apps. If you’re developing for Windows 10, see the latest documentation ]

This topic covers input and draw event messaging using the CoreWindow type in your Windows Runtime app using DirectX with C++.

Event-driven and timer-driven Windows Runtime apps using DirectX

In most games and real-time graphics apps, a sense of control and fluidity is incredibly important. To achieve that, there must be an immediate and obvious coupling between the user's input and the feedback provided as output (usually graphics). As a developer of high-performance Windows Runtime apps using DirectX, you have a decision to make: do you tie the display of the updated graphics directly to input events as they occur, or do you display the updated graphics on a separate timer-driven interval?

Both approaches have their benefits, and some games are better suited to one model than the other. By and large, you can reduce code complexity by presenting the swap chain every time an event comes in. (The swap chain contains the current transformed and projected graphics as determined by the game state and render objects.) It's one less timer to track, after all! If your game or app has simple controls with largely discrete behaviors, where a few touch events are generated (for example, with a turn-based strategy game, or a chess game, or a model viewer), this approach is fine.

In this case, you call CoreDispatcher::ProcessEvents with CoreProcessEventsOption.ProcessUntilQuit, which continues to queue up and dispatch input event messages until the corresponding CoreWindow exits. The HTML and Windows Store app using XAML models handle input events in this fashion.

Code-wise, it follows this pattern:

void MyApp::Run()
{
    // ...
    m_coreWindow->Dispatcher->ProcessEvents(CoreProcessEventsOption.ProcessUntilQuit);
    // ...
}

void MyApp::OnInputEvent()
{
    // Process input and update scene accordingly ...
    Render(); // perform the transformation and projection of the scene into a 2D buffer
    Present(); // draw the 2D buffer to the screen by presenting the swap chain that holds it
}

This model also automatically puts the UI thread to sleep when there are no input events in the message queue.

For most DirectX games, however, it's best to use a timer and CoreProcessEventsOption.ProcessAllIfPresent. With this approach, your app handles the input events that arrive on the timer's interval and presents the swap chain on that interval as well. More on this later.

For more info about CoreWindow event dispatch, see The app object and DirectX.

Handling high frequency input events

Every input event, be it a touch pointer, mouse pointer, or keyboard event, generates a message to be processed by the corresponding event handler. Modern touch digitizers and gaming peripherals can report input for these events at a minimum of 100 Hz per pointer, which means that your app can receive a maximum of 100 events per second, per pointer (or keystroke). This rate of updates can be effectively multiplied if multiple pointers (or keystrokes) are happening concurrently. Potentially, the event message queue, as processed by the CoreDispatcher instance for your app's CoreWindow, could fill up VERY quickly.

More importantly, if the presentation of your current swap chain is tied to the input event handlers, you can potentially "overdraw" the content, because the input events are coming with much greater frequency than the output device's refresh signal. The result is that some actions don't receive a frame that updates with the results of that action, and the screen updates appear to lag behind the input.

So what do you do?

The most straightforward approach is to align the input event with the output interval by using a separate loop or timer. In optimized cases, this timer will be synchronized approximately to the refresh signal for the output device (every 1/60th or 1/120th of a second, typically). However, for our purposes, we'll use a simple while loop.

In the simplest case, you can add this loop to your implementation of the IFrameworkView::Run method. In it, you test for the status of the CoreWindow: is it activated, and is it snapped or not? If it's not activated, or if it's snapped, the app should not render and present the scene. It can also use this opportunity to call ProcessEvents with CoreProcessEventsOption::ProcessOneAndAllPending and quietly dispatch the pending events, and then fall back into a wait state.

If, however, your app's CoreWindow instance is activated, call ProcessEvents with CoreProcessEventsOption::ProcessAllIfPresent, which in turn calls the handlers for each input event currently in the message queue since the last time ProcessEvents was called.

This pseudocode illustrates this approach:

[In the following pseudocode, m_coreWindowClosed is a Boolean value that contains the status of the current CoreWindow object, m_renderNeeded is a Boolean value that indicates whether rendering should occur, and m_updateState is the status of the game app process with respect to the app object itself.]

void MyApp::Run()
{
    while (!m_coreWindowClosed)
    {
        switch (m_updateState)
        {
            case UpdateEngineState::Deactivated: // the app's process is not active (it is suspended)
            case UpdateEngineState::Snapped: // the app's window is snapped
                if (!m_renderNeeded)
                {
                    CoreWindow::GetForCurrentThread()->Dispatcher->
                        ProcessEvents(CoreProcessEventsOption::ProcessOneAndAllPending);
                    break;
                }
            default: // the app is active and not snapped
                    CoreWindow::GetForCurrentThread()->Dispatcher->
                        ProcessEvents(CoreProcessEventsOption::ProcessAllIfPresent);
                    m_myGameObject->Render();
                    m_myGameObject->PresentSwapChain(); // calls IDXGISwapChain::Present()
                    m_renderNeeded = false;
                }
        }
    }
}

As noted previously, you can also do this with a timer object that you have synchronized to the display device's refresh signal. In the Run method, create a timer object and register a callback for it on the specified interval. In the callback, you can test the status of the CoreWindow instance, query the game object and see if rendering has been requested, and call ProcessEvents with CoreProcessEventsOption::ProcessAllIfPresent.

Of course, this approach is not without its disadvantages:

First, a certain percentage of input event messages won't be processed. (This can be lessened by having a very tight coupling between the timer and the maximum input event frequency.) In most cases, this is allowable; however, if you have game physics on a separate high-fidelity timer, and if they require a faster rate of update than the display, this might introduce issues that you'll need to calibrate your physics model for. (For example, the driving model in a racing simulation game.)

This approach can also introduce a minor form of input lag. In this case, input event messages are dropped while the current output frame is rendered and presented.

Lastly, for power-conscious apps, this model keeps the CPU active with input event processing even while the CoreWindow instance is inactive and can drain the device battery quicker.