> ## Documentation Index
> Fetch the complete documentation index at: https://devdocs.xbox.com/llms.txt
> Use this file to discover all available pages before exploring further.

# XBOX 游戏生命周期

> 介绍游戏生命周期事件的状态、转换及其处理方式，以及可用于调试的工具。

# XBOX 游戏生命周期

本主题介绍构成游戏生命周期的概念与事件，演示如何在你的游戏中实现游戏状态与状态变化事件，以为用户提供流畅体验；同时讲解如何满足认证要求，并介绍可用于调试这些事件的工具与功能。

Microsoft Game Development Kit (GDK) 游戏在整个生命周期中会在多种资源状态之间切换。这些状态之间的切换让 XBOX 主机能够提供用户所期望的响应式多任务体验，同时也确保用户在玩游戏时能从主机获得尽可能多的资源。为此，Microsoft Game Development Kit (GDK) 游戏必须为几个必须处理的事件与交互做好准备。

<br />

[简介](#introduction)

[游戏生命周期状态](#game-life-cycle-states)

[生命周期状态转换](#life-cycle-state-transitions)

[生命周期事件回调](#life-cycle-event-callbacks)

[响应 Suspend 与 Resume](#responding-to-suspend-and-resume)

[响应 Constrain 与 Unconstrain](#responding-to-constrain-and-unconstrain)

[启动](#launching)

[与生命周期相关的事件](#events-related-to-life-cycle)

[使用 GDK 的超时机制](#timeouts)

[Quick Resume](#quick-resume)

[调试工具与特性](#debugging-tools-and-features)

[附录](#appendix)

<br />

<a id="introduction" />

## 简介

游戏生命周期状态与事件让用户能够在多个游戏、XBOX shell 及其他应用之间快速高效地切换。此过程需要游戏和应用做出一些动作来处理生命周期状态之间的转换。如果处理得当，就能在用户切换到你的游戏或离开你的游戏时提供顺畅、无缝的体验。本主题解释了管理游戏生命周期的各个方面，并介绍了支持这些工作的可用工具。如果你熟悉 XBOX One 软件开发工具包或通用 Windows 平台 (UWP) 应用中的进程生存期管理 (Process Lifetime Management, PLM)，本主题的信息你会觉得很熟悉。请注意，GDK 应用在此方面有一些关键差异与改进。

本主题从以下内容开始：

* 讨论各种生命周期状态。
* 状态之间的转换。
* 与之关联的回调。

接下来，本主题将说明成功处理这些转换以通过 XBOX 要求 (XR) 的具体要求，并讨论大多数游戏都应考虑的常见用例。

最后，本主题介绍便于理解和修复状态转换问题的工具与调试特性。

<br />

<a id="game-life-cycle-states" />

## 游戏生命周期状态

安装在 XBOX 主机上的 Microsoft Game Development Kit (GDK) 游戏始终处于以下五种可能状态之一。

1. Running（运行中）
2. Constrained（受限）
3. Suspending（挂起中）
4. Suspended（已挂起）
5. Not Running（未运行）

你可以通过检查以下位置来确定游戏所处的状态：

* [XBOX Manager](/zh-CN/tools/tools-console/xbom/xbom) **Home** 视图中列出的 **State**。
* [Dev Home](/zh-CN/tools/tools-console/devhome/devhome) 的 **Games & apps** 视图中游戏旁列出的值。
* [XBOX Device Portal](/zh-CN/tools/tools-console/wdp/wdp)。
* [xbapp query](/zh-CN/tools/tools-console/commandlinetools/xbapp) 命令行工具。

### Running

当游戏处于 Running 状态时，它以完整的资源可用性运行。游戏拥有输入焦点，其窗口对用户可见。完整的资源可用性意味着游戏使用以下资源运行：

* 六个物理 CPU 核心 (core0-core5)，在 XBOX One 家族设备上再加上 CPU core6 的 50%–90%；在 XBOX Series 主机上则是 core6 的 100%。
* 97%–100% 的 GPU 使用率。注意操作系统可能会占用最多 3% 的 GPU。
* 5–13 GB 的内存，具体取决于 MicrosoftGame.config 中的设置以及游戏所运行的主机。有关更多信息，请参见 [配置游戏内存](/zh-CN/build/console-features/memory/system-memory-tooling)。

为了更好地说明 Running 状态与 Constrained 状态之间 CPU 资源的变化，请参考下图，它展示了游戏处于 Running 状态时各 CPU 核心的情况。

<img src="https://mintcdn.com/microsoft-4404708b/-_0FrcE2bDxzGxv-/images/gdk/lifecycle/cpu-resource-diagram-x1-running.png?fit=max&auto=format&n=-_0FrcE2bDxzGxv-&q=85&s=860d4002788745eb605483ca4d85c1d3" alt="展示游戏处于 Running 状态时各 CPU 核心情况的示意图" width="600" height="171" data-path="images/gdk/lifecycle/cpu-resource-diagram-x1-running.png" />

XBOX Series 主机可启用同时多线程 (SMT)，这会略微改变示意图。有关 SMT 的更多信息，请参阅 XBOX One 与 XBOX Series X|S 的 CPU 和内存，以及 [附录](#smt-diagrams) 中使用 SMT 的 CPU 核心示意图。

### Constrained

当游戏运行在 Constrained 执行状态时，它以较低的资源可用性运行。此时游戏不能接收用户输入；游戏窗口可能被部分遮挡，或对用户不可见。当用户与 Guide 等菜单、Settings 等系统应用，或类似 Account Picker 的可由游戏调用的 UI (TCUI) 交互，而不是与活动游戏交互时，游戏就会进入此状态。当游戏处于 Constrained 状态时，其可用资源被降至以下水平。

* 四个物理 CPU 核心。线程按以下方式折叠：
  * core0 与 core1 上的线程不受影响。
  * core2 与 core3 上的线程被时分复用到一个物理 CPU 核心上。
  * core4、core5 与 core6 上的线程被时分复用到一个物理 CPU 核心上。
* 大约 45% 的 GPU。
* 与 Running 状态相同的可用内存。注意，处于 Constrained 并不改变游戏可用的内存量。

下图展示了游戏处于 Constrained 状态时 CPU 核心的情况。

<img src="https://mintcdn.com/microsoft-4404708b/-_0FrcE2bDxzGxv-/images/gdk/lifecycle/cpu-resource-diagram-x1-constrained.png?fit=max&auto=format&n=-_0FrcE2bDxzGxv-&q=85&s=208b833caebeb949daa6877a76ceed6c" alt="展示游戏处于 Constrained 状态时各 CPU 核心情况的示意图" width="595" height="128" data-path="images/gdk/lifecycle/cpu-resource-diagram-x1-constrained.png" />

### Suspending

当游戏运行在 Suspending 执行状态时，其可用资源与 Constrained 状态相同，只是表示主机已开始将游戏转换到 Suspended 状态。Suspending 是一个瞬态状态；因此，依你查询执行状态的频率不同，观察到 Suspending 状态可能比较困难。

### Suspended

当游戏处于 Suspended 执行状态时，它仍驻留在内存中但不再执行。其线程未被调度，因此不再使用任何 CPU 或 GPU 时间。如果一个游戏被挂起，游戏运行所在的整个 Game OS 也同样被挂起，操作系统与内存的状态在下一次状态转换发生前保持不变。

### Not Running

这是既未执行、也未加载到内存、也未使用任何主机资源的游戏所处的状态。请注意，有些工具（如 `xbapp query`）可能把此执行状态报告为 package execution state 0（未知）。

<br />

<a id="life-cycle-state-transitions" />

## 生命周期状态转换

本节解释每一种生命周期状态转换，并给出各自的示例触发原因。用户与主机的某些交互可能导致多个此类事件按序发生，有时还非常快速，但游戏仍会依次经过所有这些转换。下图展示了各生命周期状态之间的转换。为简单起见，图中仅显示非错误路径的转换。请注意，游戏在崩溃时也可能从任意状态转换到 Not Running 状态。

<img src="https://mintcdn.com/microsoft-4404708b/-_0FrcE2bDxzGxv-/images/gdk/lifecycle/gdk-life-cycle-state-transition-diagram.png?fit=max&auto=format&n=-_0FrcE2bDxzGxv-&q=85&s=45f4a5b0c2d951728a177354e447153d" alt="展示每种生命周期状态转换的示意图" width="624" height="552" data-path="images/gdk/lifecycle/gdk-life-cycle-state-transition-diagram.png" />

### 从 Running 到 Constrained 的转换（Constrain）

此转换表示游戏可用资源的下降。游戏可通过调用 [RegisterAppConstrainChangeNotification](#life-cycle-event-callbacks) 注册在发生该转换时收到回调。发生该转换的情形包括：

* 用户打开 Guide。
* 展示 TCUI（如 Account Picker）。
* 用户返回 Home。
* 游戏失去焦点。

### 从 Constrained 到 Suspending 的转换（挂起开始）

当游戏处于被挂起过程中时，它会先转换到 Suspending 状态。通过 [RegisterAppStateChangeNotification](#life-cycle-event-callbacks) 注册的所有回调都会被触发。游戏会一直处于 Suspending 状态，直到所有回调处理器返回，或直到一秒钟的超时到期。如果处理器在超时前返回，游戏就会完成挂起；否则游戏会被终止。游戏在以下情况下开始挂起：

* 主机进入 Connected Standby。
* 游戏在过去 10 分钟内都不可见。
* 终止游戏的第一阶段发生，例如启动了一个新游戏。

<Note>
  这个转换有时也被称为 *quiesce*。
</Note>

### 从 Suspending 到 Suspended 的转换（挂起完成）

当处于 Suspending 状态的游戏从其通过 [RegisterAppStateChangeNotification](#life-cycle-event-callbacks) 注册的回调处理器返回后，该转换自动发生。游戏一旦进入此状态，其线程即停止执行。

### 从 Suspended 到 Not Running 的转换（终止）

此转换代表游戏被从内存中移除。由于已挂起的游戏并未执行，因此对该转换没有任何控制或交互能力。终止会在以下情况下发生：

* 用户从 Home 选择退出游戏。
* 主机关机（不是进入 Connected Standby）。
* 关闭游戏的最后阶段，例如启动新游戏时。
* 游戏存档数据与云端不同步。详见 [Connected Storage 系统触发的终止](#termination-connected-storage)。

### 从 Not Running 到 Running 的转换（启动）

此状态转换代表加载游戏并在 Game OS 中执行它，通常也伴随着 Game OS 自身的启动。以下情形下会触发此转换：

* 用户选择游戏图块。
* 用户接受游戏邀请。
* 用户与 XBOX shell 及系统中指向该游戏的引用发生交互。

### 从 Suspended 到 Constrained 的转换（Resume）

此状态转换后紧接着通常是 Unconstrain 事件。它代表游戏线程重新开始执行，转换开始时会先回调通过 [RegisterAppStateChangeNotification](#life-cycle-event-callbacks) 注册的处理器。当游戏之前处于 Suspended 状态并被带回前台（通常与从 Not Running 状态启动游戏是同样的交互）时，就会发生 Resume。

### 从 Constrained 到 Running 的转换（Unconstrain）

此状态转换是 Constrain 转换的反向操作。它代表游戏可用资源被重新提升到 Running 状态的完整水平。此转换也会触发通过 [RegisterAppConstrainChangeNotification](#life-cycle-event-callbacks) 注册的处理器回调，并在游戏成为前台应用时发生。

当游戏因主机的典型交互（如启动另一个游戏或关机）被终止时，它仍会先转换到 Suspended 状态再终止。这确保正在运行的游戏在被关闭前有机会保存状态。但如果游戏崩溃或挂起失败，则会直接转换到 Not Running 状态。

<a id="termination-connected-storage" />

### Connected Storage 系统触发的终止

如果游戏的本地存档数据与云端不同步，Connected Storage 系统会在游戏下一次挂起时终止它，以便在再次启动时同步到最新的存档数据。这种终止不是崩溃，也不会导致 [XR-001](#xrs) 未通过。

这种情形并不常见，但可能发生在你在主机未连接到 XBOX network（也称 XBOX Live）时启动游戏，之后又在游戏运行时重新连上网。开发过程中如果 Connected Storage 未正确为游戏配置，也可能出现此情况。

你可以通过在 [xbWatson](/zh-CN/tools/tools-console/xbWatson/xbwatson) 或 Visual Studio 的 XBOX System Monitor 窗口中查找如下内容来识别此类情况：

```text theme={null}
 [ConsoleEtw] [1] SuspendComplete
 [ConsoleEtw] TerminateApplicationAfterSuspend <package name>
 [ConsoleEtw] Eviction of package <package name> because -2138890207
 [ConsoleEtw] [1] TerminateBegin Successful
 [ConsoleEtw] [1] TerminateComplete
```

<code>SuspendComplete</code> 表示游戏成功挂起，未崩溃也未超时。<code>TerminateApplicationAfterSuspend</code> 表示终止是系统在成功挂起后主动发起的。值 -2138890207 是一个 HRESULT (CS\_E\_TERMINATEDTITLE\_NOLOCK)，表明终止的原因。

<br />

<a id="life-cycle-event-callbacks" />

## 生命周期事件回调

本节解释游戏如何在状态之间转换，如何判断这些转换的发生，以及如何响应这些转换。

对于 Microsoft Game Development Kit (GDK) 游戏，你可以注册两个事件来获取状态变化信息，如下代码所示。

```cpp theme={null}
ULONG RegisterAppStateChangeNotification(
    _In_ PAPPSTATE_CHANGE_ROUTINE Routine,
    _In_opt_ PVOID Context,
    _Out_ PAPPSTATE_REGISTRATION* Registration
    );

ULONG RegisterAppConstrainedChangeNotification(
    _In_ PAPPCONSTRAIN_CHANGE_ROUTINE Routine,
    _In_opt_ PVOID Context,
    _Out_ PAPPCONSTRAIN_REGISTRATION* Registration
    );
```

有关更多信息，请参见 [RegisterAppStateChangeNotification](https://learn.microsoft.com/windows/win32/api/appnotify/nf-appnotify-registerappstatechangenotification)。

<Note>
  Constrained 变化通知是 XBOX 主机特有的，目前没有对应的 API 参考页。该 API 与 State 版本非常相似，只是将 "State" 替换为 "Constrained"。
</Note>

`PVOID Context` 可以是任意值，在调用 `Routine` 函数时会传给它。你可以保留 `Registration` 以便使用 `UnregisterAppStateChangeNotification` 和 `UnregisterAppConstrainedChangeNotification` 从这些事件中注销。

传入注册函数的 `Routine` 会在关联事件发生时由游戏内部默认线程池的一个线程调用。通过该函数注册的所有 routine 都是在各自独立的线程池线程上异步调用的，这可能发生在一帧内的任意时刻。对于 `RegisterAppStateChangeNotification`，当主机将游戏设为 Suspending 状态并开始挂起超时计时后，`Routine` 会以 Boolean 值 `true` 被调用；当从 Suspended 恢复到 Running 时，会以 Boolean 值 `false` 调用。

对于 `RegisterAppConstrainedChangeNotification`，当主机将游戏设为 Constrained 状态时，`Routine` 以 `true` 被调用；从 Constrained 解除到 Running 时，以 `false` 被调用。

有关如何设置并处理这些事件、并确保在游戏帧的一致节点由主线程处理的示例，请参见 [面向 XBOX One 的 Microsoft Game Development Kit 移植指南](/zh-CN/build/console-features/porting-from-xbox-one-era/porting-guide-for-existing-era) 中的示例代码。你也可以查看 SimplePLM 示例的 main.cpp，或 Microsoft Game Development Kit (GDK) 中 Visual Studio 里 Direct3D 12 XBOX Game 项目模板对应的文件。示例可从 [XBOX Developer Downloads](https://aka.ms/gdkdl) 下载。

在分析 quiesce 挂起的小型转储 (minidump) 时，你可能会遇到看不到任何线程栈里出现你注册的回调的情形。这是由于尾调用优化（tail call optimization）。要绕过这一问题，只需将你的回调包在下列指令内：

```cpp theme={null}
#pragma optimize( "", off ) 
/* ... */
#pragma optimize( "", on )
```

<Note>
  在 Windows PC 上运行的 Microsoft Game Development Kit (GDK) 游戏不会被挂起。在 Windows PC 上，游戏仍可调用 `RegisterAppStateChangeNotification`，但请注意 `PAPPSTATE_CHANGE_ROUTINE` 回调不会被触发。
</Note>

<br />

<a id="responding-to-suspend-and-resume" />

## 响应 Suspend 与 Resume

为了让游戏在挂起时能为用户提供无缝体验并通过 XR，游戏在挂起时需要完成一些事情。

### 挂起超时

要完成挂起（这是通过 [XR-001](#xrs) 的必要条件），游戏必须至少通过 `RegisterAppStateChangeNotification` 注册一个回调 routine。当回调触发时，所有已注册 routine 都会在独立的默认线程池线程上异步调用。游戏有一秒钟时间从所有已注册 routine 返回，以此告知系统可以将游戏转到 Suspended 状态。如果游戏未能在一秒内从挂起 routine 返回，或根本没有注册回调，游戏会被终止并转到 Not Running 状态（会导致 [XR-001](#xrs) 未通过）。此过程可确保主机能快速切换游戏，同时保证游戏在进入 Suspended 状态前已做好挂起准备。

### Direct3D

在挂起 routine 被调用后并从其返回之前，游戏必须在其渲染线程上对其 `ID3D12CommandQueue` 调用 SuspendX。这样才能保存 GPU 状态以准备挂起。恢复时，游戏必须调用 ResumeX 以恢复状态。在 `SuspendX` 与 `ResumeX` 之间调用任何 Direct3D API 都会抛出异常，导致崩溃。若在挂起 routine 返回前未调用 `SuspendX`，会导致挂起失败并终止游戏（同样会导致 [XR-001](#xrs) 未通过）。有关更多信息，请参阅 Direct3D 对 suspend 与 resume 的支持。

### XGameSave API

当游戏收到挂起通知时，它有一秒钟的执行时间需要用于挂起。游戏一旦处于 Suspended 状态，可能在没有任何额外执行时间的情况下转到 Not Running 状态。这意味着在该一秒的挂起时间内，游戏必须在进入 Suspended 前保存所有未保存的用户数据。

[XGameSave](/zh-CN/build/core-features/common/game-save/game-saves-overview) API 就是为了在如此有限的时间内支持保存而设计的。`XGameSave` API 将预留内存之外的 RAM 作为第一站存储，以便在很短的挂起时间内最大化游戏写入速度。[XR-052: User State](#xrs) 规定，游戏不得导致用户数据的意外丢失，包括适当情况下的用户进度和状态。一般而言，游戏必须在挂起处理的一部分中保存用户数据。同时最好不要仅在挂起时保存，还应更频繁地保存，以避免必须在超时时间内准备较大的存档，或因断电等意外造成大量用户数据丢失。有关更多信息，请参见 [游戏存档概述](/zh-CN/build/core-features/common/game-save/game-saves-overview) 及 [GameSave 示例](https://aka.ms/gdkdl)。

请注意，优先使用 `XGameSave` API 的异步版本会更好。这样游戏可以在系统写出存档数据时继续执行挂起的其余代码。示例请参考 XGameSaveSubmitUpdateAsync。

在恢复游戏前，操作系统会检查游戏挂起期间存档是否在另一台设备上被修改。若设备在线且检查显示存档已被修改，游戏会被终止并重新启动，以让游戏进入一致状态。若检查时设备离线，游戏仍会被允许启动。

在恢复时，游戏应通过调用 XGameSaveInitializeProvider 或 XGameSaveInitializeProviderAsync 重新获取存档 provider。重新初始化 provider 可确保获取到最新的存档数据。

### 用户与用户-手柄配对

[XR-112: Establishing a User and Controller During Initial Activation and Resume](#xrs) 规定，游戏在被恢复时必须校验用户/手柄配对，并相应地恢复之前用户的会话或获取新用户（一个或多个）。具体行为取决于游戏如何处理用户与手柄。重要的是不要依赖挂起前有关用户或手柄状态的任何信息，恢复时应重新建立它们的状态。你应保留 `XUserHandle` 跨越 suspend/resume 周期，因为游戏正是通过它获取用户状态更新。此外，通过 XUserRegisterForChangeEvent 与 XUserRegisterForDeviceAssociationChanged 注册的回调会在 resume 期间触发，用来通知游戏已发生的状态变化。有关管理用户与设备的更多信息，请参见 User identity and XUser 与 Users and Input Devices。示例请参考 [UserManagement 示例](https://aka.ms/gdkdl)。

### 权限与权限项

游戏被挂起期间，用户可能会更改账户设置，从而改变其在线许可与与他人通信的方式。游戏恢复时，应重新检查所有相关权限或权限项，然后再启用或禁用相关功能，让游戏行为反映出用户当前的设置。[XR-015: Managing Player Communication](#xrs) 详述了文字与语音通信的权限。[XR-045: XBOX Live and Account Privileges](#xrs) 详述了允许或阻止与 XBOX 服务相关操作的权限项。

### 网络

因为已挂起的游戏不会调度任何线程，恢复时应预期网络通信会受到影响。有关如何处理此情况的指导，请参见 [网络 API 简介](/zh-CN/build/console-features/networking/introduction-networking)。有关 XBOX 服务的具体细节，请参见 XBOX 服务 API 入门。有关多人游戏会话的更多信息，请参见 Multiplayer Session advanced topics。

### 异步 API

某些需要与 System OS 通信的异步 API 如果在游戏挂起时正在进行，可能会失败。这时，`Xasync` API 应以 `E_ABORT` 失败。这与任何 XR 无关，只是调用异步 API 时需要注意的一点。请确保有错误处理来优雅地处理该情况。发生此类失败时，通常最好在游戏恢复后简单重试。

### Gaming Runtime API

游戏完成 AppStateChangedNotification 的挂起处理器后，Gaming Runtime 会被自动挂起。运行时会在调用游戏的 resume 处理器之前立即恢复。如果在 runtime 处于挂起状态时其他线程调用任何 Gaming Runtime API，都会失败并返回 E\_GAMERUNTIME\_SUSPENDED。这种情况可能出现在挂起处理器刚结束不久便调用了异步 API 的场景中——回调可能会在 runtime 已挂起、但游戏尚未停止调度线程的窗口内触发。

在可能的情况下，请避免在此时段（即 Gaming Runtime 挂起期间）进行 Gaming Runtime 调用。如果无法避免，你可以检测该错误并在收到 resume 事件的 AppStateChangedNotification 后重试调用。

### 音频

挂起时，游戏不应持有任何从 [IMMDeviceEnumerator](https://learn.microsoft.com/windows/win32/api/mmdeviceapi/nn-mmdeviceapi-immdeviceenumerator) 获取的对象，包括 `IMMDevice`、`IMMDeviceCollection` 或 `IMMEndpointDevice`。在 resume 后如果不先通过 `IMMDeviceEnumerator` 刷新这些接口就使用它们，行为未定义。但如果游戏持有 `IMMNotificationClient`，则该客户端在 suspend 与 resume 之间仍可正常工作。此外，虽然不是强制要求，最佳实践是在 suspend 期间停止所有音频流，并在 resume 后重新启动它们。

在 resume 过程中，游戏的音频流也会失效，需要重新初始化。游戏应在 resume 处理器执行期间或之后处理这些失效恢复。有关更多信息，请参见 Comparison of XBOX One Software Development Kit and Microsoft Game Development Kit audio API。

<Note>
  在 resume 处理器开始运行前就尝试重新初始化音频流可能导致行为未定义。
</Note>

<a id="dealing-with-time" />

### 处理挂起期间的时间变化

游戏挂起期间，可能发生很多事情。唯一可以肯定的是时间会在挂起期间继续推进。游戏需要考虑到这一点，在计算诸如"玩游戏时长"这类指标时，确保不会意外把挂起期间的时间也算进去。

自 2021 年 6 月的 GDK 起，游戏被挂起时时间戳计数器 (Time Stamp Counter, TSC) 会被冻结，直至游戏后来恢复。此外，TSC 不会包含挂起期间的时间。这意味着游戏可以依赖以下方式来准确报告运行期间的时间：

* \_\_rdtscp
* QueryPerformanceCounter

尽管挂起期间 TSC 不会推进，但墙钟时间仍会推进。这意味着如果游戏调用 [GetSystemTime](https://learn.microsoft.com/windows/win32/api/sysinfoapi/nf-sysinfoapi-getsystemtime) 或 [GetLocalTime](https://learn.microsoft.com/windows/win32/api/sysinfoapi/nf-sysinfoapi-getlocaltime)，会看到时间已经向前推进了。

<br />

<a id="responding-to-constrain-and-unconstrain" />

## 响应 Constrain 与 Unconstrain

与挂起通知不同（挂起通知要求游戏执行特定动作，且主机会等待 routine 返回），对于 app constrained 变化通知，游戏不需要做任何特定动作作为响应。当事件到来时，向 Constrained 或 Running 状态的转换其实已经发生了。相反，此事件通知游戏其可用资源已变化，因此应相应地减少或提升其资源使用。

在决定游戏应如何处理 Constrained 时，请记住只有当游戏没有输入焦点时才会被 Constrained。因此用户在 Constrained 期间无法与游戏交互。基于此，大多数游戏在 Constrained 时应暂停玩法（或最接近暂停的行为）。但由于游戏在 Constrained 时仍可能可见，因此继续渲染一些内容以便用户看到游戏状态非常重要。同时用户很可能很快就会把游戏切回前台，因此维持多人对战与其他网络通信通常会带来更好的用户体验。

最终，处理 Constrain 的唯一目标是让游戏在 CPU 和 GPU 时间被削减的情况下继续执行。达到这一目标的最佳方式取决于你游戏的具体需求。

<br />

<a id="launching" />

## 启动

Microsoft Game Development Kit (GDK) 游戏在设计上属于 Win32，具有传统的 Windows 消息循环。因此在启动时，面向开发者的第一个代码入口是传入 `WinMain` 的参数。或者，游戏也可以通过 `GetCommandLine` 函数查询启动它时使用的参数。与 XBOX One ERA 或 UWP 不同，Microsoft Game Development Kit (GDK) 游戏不受激活超时约束，也没有 Activated 事件处理器。同样与 XBOX One ERA 或 UWP 不同，Microsoft Game Development Kit (GDK) 游戏不会通过激活或启动参数来处理游戏邀请，而应使用 `XGameInviteRegisterForEvent` 注册相应事件。游戏完成注册后即可获得任何可用的待处理邀请。

<br />

<a id="events-related-to-life-cycle" />

## 与生命周期相关的事件

这些事件本身并不是生命周期事件，而是与状态转换的成因和条件相关的事件。它们对识别这些事件并做出相应响应可能很有用。

#### 输入焦点

要知道游戏何时获得输入焦点，Microsoft Game Development Kit (GDK) 游戏可以在 `WindowProc` 回调中监听 `WM_ACTIVATEAPP` 窗口通知。这与 Win32 的 Windows PC 实现相同，可参考同样的文档。有关更多信息，请参见 [WM\_ACTIVATEAPP 消息](https://learn.microsoft.com/windows/win32/winmsg/wm-activateapp)。

以下代码示例演示了如何监听该事件。

```cpp theme={null}
// Windows procedure.
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_ACTIVATEAPP:
        OutputDebugString((wParam) ? L"Activated\n" : L"Deactivated\n");
        break;
    ...
```

#### 可见性

要判断游戏窗口是否可见（类似于输入焦点），Microsoft Game Development Kit (GDK) 游戏可以在 `WindowProc` 回调中监听 `WM_SHOWWINDOW` 窗口通知。有关更多信息，请参见 [WM\_SHOWWINDOW 消息](https://learn.microsoft.com/windows/win32/winmsg/wm-showwindow)。

以下代码示例演示了如何在 `WindowProc` 回调中监听 `WM_SHOWWINDOW` 窗口通知。

```cpp theme={null}
// Windows procedure.
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_SHOWWINDOW:
        OutputDebugString((wParam) ? L"Visible\n" : L"Not Visible\n");
        break;
    ...
```

<br />

<a id="timeouts" />

## 使用 GDK 的超时机制

XBOX One ERA 与 UWP 开发者常与激活、挂起以及挂起检测等多种超时打交道。Microsoft Game Development Kit (GDK) 游戏在设计上属于 Win32，大部分情况下没有可对应的机制——只有在主机上运行且需要改变资源状态来为运行中游戏提供尽可能多资源的场景下才存在类似机制。下表比较了不同平台间的各种行为。

| 处理器/超时 | Microsoft Game Development Kit (GDK) | XBOX One ERA | UWP |
| - | - | - | - |
| Activation handler | 不适用 | `CoreApplicationView:: Activated` | `CoreApplicationView::Activated` |
| Activation 超时 | 不适用 | 45 s | 10 s |
| Suspend handler | `RegisterAppStateChangeNotification` | `CoreApplication:: Suspending` | `CoreApplication:: Suspending` |
| Suspend 超时 | 1 s | 1 s | 5 s |
| Resume handler | `RegisterAppStateChangeNotification` | `CoreApplication:: Resuming` | `CoreApplication:: Resuming` |
| Resume 超时 | 不适用 | 不适用 | 不适用 |
| Constrain handler | `RegisterAppConstrainedChangeNotification` | `CoreApplication:: ResourceAvailabilityChanged` | 不适用 |
| Constrain 超时 | 不适用 | 不适用 | 不适用 |
| Hang 超时 | 不适用 | 2 s | 2 s |

<br />

<a id="quick-resume" />

## Quick Resume

Quick Resume 是 XBOX Series 主机 2020 年 6 月 recovery 中新引入的功能。它让用户可以比以往更快地在游戏间切换：在另一个游戏运行时保持前一个游戏处于挂起状态。在所有 XBOX One 家族设备以及 2020 年 6 月 recovery 之前的 XBOX Series 主机上，当一个游戏正在运行（或处于 Constrained/Suspended）而用户选择新游戏时，第一个游戏会被终止并启动新游戏。而在 2020 年 6 月及以后的 XBOX Series 主机上，第一个游戏可以先被挂起，然后整个 Game OS 及其内存都可以在挂起状态下被保存。当用户切换回来时，Game OS 会被恢复，游戏再从挂起状态恢复。所保存的 Game OS 甚至可以在主机重启后恢复。从游戏的角度看，这与常规的 suspend/resume 完全一样。不需要额外步骤或代码。如果一款游戏能成功执行 suspend/resume，它也就完全支持 Quick Resume。

由于 Quick Resume 的存在，游戏在单次启动会话中运行数百小时变得更容易。游戏可能被挂起数天、数周甚至数月。这意味着重要的是：不要假设挂起前建立的状态信息（尤其是关于用户的信息）在 resume 后仍然成立。例如，如果你的游戏充分利用了深度社交关系，可能已经添加或删除了新的好友。如果你使用好友的存在感 (presence) 信息，恢复时他们的行为不太可能仍与上周完全相同。类似的问题也适用于无障碍偏好、权限项以及排行榜。

<Warning>
  务必对可能随时间变化的状态重新查询。
</Warning>

<br />

<a id="debugging-tools-and-features" />

## 调试工具与特性

本节介绍一些有助于调试和测试游戏生命周期的工具、日志和特性。

### 生命周期转换工具

在调试和测试时，了解游戏所处的状态、并能触发游戏在各状态之间转换非常重要。有几种工具可以查询当前生命周期状态并执行状态转换。[XBOX Manager](/zh-CN/tools/tools-console/xbom/xbom)、[XBOX Device Portal](/zh-CN/tools/tools-console/wdp/wdp) 以及 [xbapp](/zh-CN/tools/tools-console/commandlinetools/xbapp) 命令行工具都会列出应用的当前状态，并提供执行各种转换的命令。例如，下方截图展示了 XBOX Manager 中的 State 与 Actions 菜单。

<img src="https://mintcdn.com/microsoft-4404708b/-_0FrcE2bDxzGxv-/images/gdk/lifecycle/xbom-life-cycle-tools.png?fit=max&auto=format&n=-_0FrcE2bDxzGxv-&q=85&s=bca79514d50eceeaaadd88e30150d4fe" alt="展示 XBOX Manager 中 State 与 Actions 菜单的截图" width="1024" height="443" data-path="images/gdk/lifecycle/xbom-life-cycle-tools.png" />

<Note>
  为提供更快的迭代速度并支持测试的灵活性，对未打包的游戏使用 'terminate' 命令时，会直接转换到 Not Running 状态，而不会先挂起。要以典型方式终止游戏，可先用 `suspend` 命令将其挂起。
</Note>

从 running 到 suspended 的转换也可以通过 devkit 前面板的按钮触发。Microsoft Game Development Kit (GDK) 中随附的 *Suspend\_Title* 脚本可以通过 [XBOX Manager](/zh-CN/tools/tools-console/xbom/xbom) 或 [XBOX Device Portal](/zh-CN/tools/tools-console/wdp/wdp) 映射到前面板按钮。请注意，恢复到 resumed 状态无法通过前面板按钮触发。Resume 必须通过主机 shell（从 Home 或 My games & apps 选择游戏图块）或者通过其他工具（如 [xbApp](/zh-CN/tools/tools-console/commandlinetools/xbapp)、[Visual Studio](/zh-CN/tools/tools-console/visualstudio/debugging-with-visualstudio) 或 [XBOX Manager](/zh-CN/tools/tools-console/xbom/xbom)）来完成。

### xbWatson 与 Visual Studio 中的状态转换日志

知道状态转换何时发生与了解当前生命周期状态同样重要。[xbWatson](/zh-CN/tools/tools-console/xbWatson/xbwatson) 与 Visual Studio 中的 XBOX System Monitor 窗口对生命周期状态转换提供了大量日志支持。现在每当状态发生转换时，都会与应用相关的其他事件一同记录，例如应用启动阶段以及整个系统中的焦点变化。

下方截图展示了通过 xbWatson 观察状态转换发生的时机。

<img src="https://mintcdn.com/microsoft-4404708b/-_0FrcE2bDxzGxv-/images/gdk/lifecycle/xbwatson-state-transition-logging.png?fit=max&auto=format&n=-_0FrcE2bDxzGxv-&q=85&s=ce4fb9b46af97e85c05c3c975306cbf4" alt="使用 xbWatson 展示状态转换发生时机的截图" width="742" height="516" data-path="images/gdk/lifecycle/xbwatson-state-transition-logging.png" />

新的日志功能还包括生命周期事件的错误与失败信息。查看该日志可帮助识别问题发生的时刻，或对任何错误情况提供进一步信息。若出错并报告了 `HRESULT`，你可以通过 xberror 命令行工具查找更多信息。在上方截图的错误情形中，`xberror` 显示游戏在其挂起处理器期间超时 (`E_GAME_SUSPEND_TITLE_TIMEOUT`)。

### 在挂起超时时中断

2020 年 6 月的 Microsoft Game Development Kit (GDK) 引入的新功能是：Visual Studio 可以在调试 Microsoft Game Development Kit (GDK) 游戏时，如果因一秒超时而挂起失败，就自动中断进来。此设置默认关闭，但可在项目的调试属性中更改，如下方截图所示。

<img src="https://mintcdn.com/microsoft-4404708b/-_0FrcE2bDxzGxv-/images/gdk/lifecycle/vs-break-on-suspend-timeout.png?fit=max&auto=format&n=-_0FrcE2bDxzGxv-&q=85&s=455bf244914b06fd15d160da94a39317" alt="展示&#x22;挂起超时时中断&#x22;设置的截图" width="832" height="245" data-path="images/gdk/lifecycle/vs-break-on-suspend-timeout.png" />

即使没有打开对应项目，你也可以通过 XBOX Gaming Explorer 窗口为已安装的游戏更改此设置。在 XBOX Gaming Explorer 中选择该游戏，然后在 **Properties** 工具窗口中更改 **Break on Suspend Timeout** 设置。更多细节请参阅 [XBOX Gaming Explorer](/zh-CN/tools/tools-console/visualstudio/xbox-gaming-explorer)。

在调试挂起失败时，此功能非常有用——比如挂起过程存在挂起或其某一部分耗时超出预期时。调试器通过向游戏进程注入一个带 INT3 的新线程来触发中断。这意味着调试器起初会在一个看似空的线程上中断，但其他线程会在一秒超时到期时停在它们当时所在的位置。有关使用 Visual Studio 调试游戏生命周期状态的更多信息，请参见 [使用 Visual Studio 调试 XBOX 项目](/zh-CN/tools/tools-console/visualstudio/debugging-with-visualstudio)。

<a id="automatic-suspend-failure-dumps" />

### 挂起失败的自动转储

每当发生挂起失败时，都会自动在主机上记录一份诊断崩溃转储。转储也会上传到开发者门户。这些转储保存在主机的 *D:\LocalDumps\\* 中。自 2020 年 6 月更新起，[xbWatson](/zh-CN/tools/tools-console/xbWatson/xbwatson) 也会列出主机上可用的转储，菜单中还提供下载选项。这些被称为 *quiesce memory dump*，因为 quiesce 是挂起的另一种称呼。下方截图展示了主机上可用的 quiesce memory dump。

<img src="https://mintcdn.com/microsoft-4404708b/-_0FrcE2bDxzGxv-/images/gdk/lifecycle/xbwatson-quiesce-memory-dumps.png?fit=max&auto=format&n=-_0FrcE2bDxzGxv-&q=85&s=bc878ec0e3a3f69493b625fb98c86233" alt="展示主机上可用 quiesce memory dump 的截图" width="940" height="637" data-path="images/gdk/lifecycle/xbwatson-quiesce-memory-dumps.png" />

在本应调用挂起回调 (`PAPPSTATE_CHANGE_ROUTINE`) 但未注册挂起处理器时，也会创建诊断转储。若在挂起处理器退出前未调用 `ID3D12CommandQueue::SuspendX`，也会创建诊断转储。

### 手动触发状态转换

你可以通过与主机交互手动触发每一种状态转换。下表列出了在没有任何工具支持的情况下触发各种状态转换的方法（前提是游戏本身处于该转换合适的起始状态）。

| 转换 | 操作 |
| - | - |
| Launch | 从 **Home** 或 **My games & apps** 选择游戏图块。 |
| Constrain | 按下手柄上的 **XBOX** 按钮打开 **Guide**。 |
| Suspend | 在电源模式设为 **Instant-on** 时关闭主机。 |
| Terminate | 选中游戏图块后，按手柄上的 **Menu** 按钮，然后选择 **Quit**。 |
| Resume | 从 **Home** 或 **My games & apps** 选择游戏图块。若要同时避免 unconstrain，可在转换过程中打开 **Guide**。 |
| Unconstrain | 按下手柄上的 **XBOX** 按钮关闭 **Guide**。 |

<br />

<a id="appendix" />

## 附录

<a id="xrs" />

### 查看 XR

#### 下载 XR 列表

1. 前往 [XBOX Developer Downloads](https://aka.ms/gdkdl)。
2. 在 file type 处选择 **Partner, Publishing, and Release Management Information**。
3. 选择 **Confirm**。
4. 在 build/version number 处选择与你正在使用的发行版本相匹配的 XGD Partner Documentation 版本。
5. 选择 **Confirm**。**Download Now** 按钮会出现，提示你下载文档。

### 生命周期事件的诊断遥测

主机平台会为下表所示的事件发送遥测数据。

| 事件 | 说明 |
| - | - |
| Launch time | 从调用 `CreateProcess` 启动游戏到游戏首次调用 present 之间的时间。 |
| Resume time | 从游戏分区被恢复到游戏下一次 present 之间的时间。 |

<a id="smt-diagrams" />

### 启用 SMT 的 XBOX Series 主机的 CPU 资源示意图

有关 SMT 的更多信息，请参见 XBOX One 与 XBOX Series X|S 的 CPU 与内存。

#### Running

下图展示了 Running 状态下 CPU 核心与虚拟核心之间的映射关系。

<img src="https://mintcdn.com/microsoft-4404708b/-_0FrcE2bDxzGxv-/images/gdk/lifecycle/cpu-resource-diagram-smt-running.png?fit=max&auto=format&n=-_0FrcE2bDxzGxv-&q=85&s=f74e206f1a1c658a3cfcac5513c9a381" alt="展示 Running 状态下 CPU 核心与虚拟核心映射关系的示意图" width="603" height="135" data-path="images/gdk/lifecycle/cpu-resource-diagram-smt-running.png" />

#### Constrained

下图展示了 Constrained 状态下 CPU 核心与虚拟核心之间的映射关系。

<img src="https://mintcdn.com/microsoft-4404708b/-_0FrcE2bDxzGxv-/images/gdk/lifecycle/cpu-resource-diagram-smt-constrained.png?fit=max&auto=format&n=-_0FrcE2bDxzGxv-&q=85&s=c479b8f5ea2e3834c68edd7745b0ffe7" alt="展示 Constrained 状态下 CPU 核心与虚拟核心映射关系的示意图" width="593" height="130" data-path="images/gdk/lifecycle/cpu-resource-diagram-smt-constrained.png" />


## Related topics

- [IXtfApplicationClient::Launch](/zh-CN/reference/tools/xtf/xtfapplication/classes/IXtfApplicationClient/methods/launch-ixtfapplicationclient-xtfapplication-xbox-microsoft-m.md)
- [IXtfApplicationClient::Uninstall](/zh-CN/reference/tools/xtf/xtfapplication/classes/IXtfApplicationClient/methods/uninstall-ixtfapplicationclient-xtfapplication-xbox-microsoft-m.md)
- [IXtfApplicationClient::QueryExecutionState](/zh-CN/reference/tools/xtf/xtfapplication/classes/IXtfApplicationClient/methods/queryexecutionstate-ixtfapplicationclient-xtfapplication-xbox-microsoft-m.md)
- [IXtfApplicationClient::Unconstrain](/zh-CN/reference/tools/xtf/xtfapplication/classes/IXtfApplicationClient/methods/unconstrain-ixtfapplicationclient-xtfapplication-xbox-windows-m.md)
- [XGameUiShowErrorDialogAsync](/zh-CN/reference/system/xgameui/functions/xgameuishowerrordialogasync.md)
