Conceptual Model
Asynchronous programming in the Microsoft Game Development Kit (GDK) is split into 2 main components: Tasks and Task Queues. While there is more functionality in the libraries, the whole conceptual model utilizes these 2 main components. A task is a single set of async work that can be started, have its status checked, potentially canceled, completed, and return its completion information. For the Microsoft Game Development Kit (GDK) model, tasks are comprised of two bodies: the work callback and the completion callback. This allows for more control such as full parallel processing or parallel work combined with single-threaded completion. A task queue is a container that enqueues both work and completion callbacks for later execution. There are two internal queues in a task queue, called ports, that handle the work and completion callbacks separately. These are called the work port and completion port. Figure 1. Diagram of a Task and Task Queue Each port of the task queue is configured differently at creation time to create different callback execution behavior. For example, the work port could be configured to be asynchronous and the completion port could be configured to run serially on a main thread. A manual setting can be set to enable complete control over the execution behavior. Port configuration modes are explained below. When an async task is started, the callbacks aren’t enqueued onto the task queue immediately. An async provider handles state changes to ensure that the work is enqueued and dispatched before the completion callback is enqueued and dispatched. The task queue doesn’t handle threading directly itself. Instead, it relies on an outside call to dispatch its ports. The external calls determine the threading and concurrency behavior. The task queue itself is completely thread-safe. Figure 2. Port Being Dispatched onto Multiple Threads That’s essentially it! The callbacks of a task are enqueued onto a task queue’s work and completion ports and that task queue has those callbacks dispatched in some manner. The API contains a whole suite of functionality to manage task queues, check status of callbacks, track work data, create custom task handling, and more. Microsoft Game Development Kit (GDK) asynchronous API calls always implement the work callback internally and completion callbacks are always optional. For usage beyond Microsoft Game Development Kit (GDK) async calls, you must supply the work callback.Requirements
Game developers have listed the following requirements for API calls.- Prefer synchronous calls over asynchronous calls
- Provide async with polling
- Provide async with callbacks
- Provide control over what thread the async work executes on
- Provide control over what thread completion callbacks are executed on
Types of APIs
The Microsoft Game Development Kit (GDK) strives to be very straightforward in its API design. Game developers are experts at fine-tuning their code to maximize the use of the hardware. We give them control whenever possible. API implementations break down into the following types.- Time Sensitive Safe: A time sensitive safe API is one that can be called on a time sensitive thread. Note that while this usually means the API is trivial or very fast, the key concept is that the API’s performance characteristics are consistent. They are always synchronous and never need to have an asynchronous version. These APIs should be documented as time-sensitive-safe.
- Not Time Sensitive Safe: These APIs aren’t safe to call from the render thread. Their performance characteristics may vary widely. Most APIs fall into this category.
- Asynchronous: These APIs are asynchronous in nature, such as a web service call. They use the asynchronous pattern described in this topic. Asynchronous APIs aren’t as common in the Microsoft Game Development Kit (GDK) as they are in the XBOX One ERA programming model - an asynchronous API is generally long-running and cancelable. Except for a few specific use cases, asynchronous APIs will have a not-time-critical-safe synchronous version. Calling an asynchronous API should always be time-critical-safe.
- Notifications: Notifications are periodic in nature and have no defined ending. They are related to asynchronous APIs, but because of their periodic nature, they should look and act differently to developers. Registering for a notification should always be time-critical-safe.
Async API pattern
The Microsoft Game Development Kit (GDK) introduces a general purpose async API pattern that Microsoft Game Development Kit (GDK) components can use to provide consistent async support. At the core is a structure similar to OVERLAPPED called XAsyncBlock:
The Internal fields are used by the system and shouldn’t be modified.
The user-settable fields in this structure shouldn’t be modified
during an async operation. An XAsyncBlock must remain in memory for the
lifetime of the async operation. If the XAsyncBlock is dynamically allocated,
the completion callback is the earliest time that it can be deleted.
In addition to XAsyncBlock, there are a small number of helper APIs, shown as follows.
Async API Usage
First, let’s look at a synchronous API in the following code example.Controlling work dispatching
What thread did the async work in the previous calls? What thread invoked the completion callback? That’s decided by the task queue assigned to the XAsyncBlock. Task queues have two “ports”: a work port and a completion port. Each port has a dispatch mode that determines how callbacks queued to a port are processed. There are several dispatch modes.- Thread pool: Callbacks queued to a thread pool queue are executed on the system thread pool. The thread pool invokes the calls in parallel, taking a call to execute from the queue in turn as thread pool threads become available.
- Serialized thread pool: Callbacks are queued and run on the thread pool but run one at a time.
- Manual: Callbacks queued to a manual queue aren’t automatically dispatched. It’s up to the developer to dispatch them on any thread they want.
- Immediate: The immediate dispatch mode doesn’t queue at all. It immediately executes the call on the thread that submitted the callback.
Notifications
A notification might not have an ending, and it might be called many times. Notifications should support a subset of the requirements of an async call.- Async with polling
- Async with callbacks
- Control over which thread callbacks happen on
- A Register method that takes any call-specific parameters, a task queue, an optional void context, and a strongly typed callback pointer. The last parameter is an out parameter that returns a token.
- An Unregister method that takes any call-specific context and the token.
- Polling is supported by adding a separate method that’s not related to the notification callback.
Async Library
To make it easier to create consistent APIs that support the async pattern, we provide a library that can be used to implement the “async plumbing” of an API. The API for the library looks like the following.- Call XAsyncBegin with the async block passed by the caller, and provide a callback that provides the implementation.
- Perform the async work for the call. If you need to run the work on a worker thread, call XAsyncSchedule. If you can perform the work by using OS async primitives and set up those primitives fast enough to remain time-critical-safe, that’s preferred.
- If you need to invoke other async work from a worker thread callback, you can return E_PENDING from the worker. You can also call XAsyncSchedule from inside a worker to reschedule additional work.
- When all the work is complete, call XAsyncComplete.
- Provide a strongly typed wrapper around XAsyncGetResult to return the results.
- If your async call has no data payload, you should provide a strongly typed wrapper around XAsyncGetStatus and pass zero as the required buffer size to XAsyncComplete.
- Begin An async provider is invoked with this opcode during XAsyncBegin. If the provider implements this op code, they should start their asynchronous task by calling XAsyncSchedule or through exterior means. This callback is called synchronously in the XAsyncBegin call chain, so it should never block.
- DoWork Called in cases where XAsyncSchedule was called to schedule async work by using the task queue. The provider function does any work it needs to. When complete, it calls XAsyncComplete with the result code and data payload size, which can be zero if there’s no data payload from the call. If more asynchronous work needs to be done, the provider can schedule that work and should return E_PENDING.
- GetResult Called to fetch the result of the call. Because the data size is passed into XAsyncComplete during call completion, no argument checking is needed here - all buffers and buffer sizes have been verified by the library.
- Cancel Called when the user cancels an async call. If the call can be canceled, cancel it and call XAsyncComplete with E_ABORT as the result code.
- Cleanup Called when the call has completely finished, and the provider can delete any dynamic memory.
Reference API documentation
- XAsync (API contents)
- Functions
- Structures
- XAsyncProvider (API contents)
- XTaskQueue (API contents)
- Functions
- xgamesave (API contents)
