> ## 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.

# DirectStorage 概述

> 适用于 XBOX Series X|S 的 DirectStorage API，可在处理大量小型请求时以低 CPU 开销提供高吞吐量的 NVMe 存储 I/O。

## 简介

本文概述仅适用于 XBOX Series X|S 主机的 DirectStorage API。有关桌面版 DirectStorage 的详细信息，请参阅 [Desktop 上的 DirectStorage](https://aka.ms/directstorage)。

通过 PCIe 总线连接的最新 NVMe 存储设备可以达到非常高的吞吐量和 IOPS（每秒 I/O 请求数）。Win32 API 的开销意味着，即使可以利用可用的存储带宽，充分利用它可能会带来无法接受的高 CPU 占用。当工作负载由大量小请求组成时尤其如此。

DirectStorage API 通过与底层 NVMe 硬件紧密交互，旨在移除操作系统的大部分开销。这使得可以在更低的 CPU 使用率下获得更高的带宽。目标是在最多占用单个 CPU 核心 10% 的情况下处理最多 50,000 次每秒请求。

### 现有问题

随着每一代主机对更高分辨率资产的需求增加，游戏内容变得越来越大。现有 XBOX One 硬件和软件存在若干限制，妨碍了开发者将下一代内容从硬盘传输到内存的能力。

* CPU 使用率高
  * 现有 Win32 API 的开销可能需要整个 CPU 核心。
  * 这取决于游戏发出的请求数量。

* 从磁盘获取的最大带宽不足
  * [Maximizing File Performance on XBOX Series X|S (NDA 主题)](https://learn.microsoft.com/gaming/gdk/docs/gdk-dev/console-dev/overviews/file-system/Maximizing_File_Performance_Win32_Scarlett) 和 [Maximizing File Performance on XBOX One (NDA 主题)](https://learn.microsoft.com/gaming/gdk/docs/gdk-dev/console-dev/overviews/file-system/Maximizing_File_Performance_Win32_Xbox_One) 白皮书对此有详细介绍。

* 无法对磁盘请求进行优先级排序
  * 没有对游戏请求进行优先级排序的能力，很难构建响应式流式加载系统。

* 无法取消磁盘请求
  * 没有取消请求的能力，很难构建推测式读取系统。

* 没有硬件加速的解压
  * 没有硬件加速的解压，在软件中执行解压会占用大量 CPU 资源。

DirectStorage API 集合直接解决了上述每个问题。整体效果是 XBOX 文件系统的性能大幅提升。

### CPU 使用率

DirectStorage 的主要设计目标是让游戏能持续保持 50K IOPS，同时只占用单个 CPU 核心的 5% 到 10%。这使得游戏可以从 NVMe 存储子系统获得最大带宽，同时 CPU 可用于其他游戏需求。

DirectStorage 还新增了对硬件解压的支持。每个读取请求都可以直接从 NVMe 驱动器路由到内建的硬件解压块。这样游戏无需在解压上耗费 CPU 资源。

### 队列化的管线模型

DirectStorage 使用批处理方法，将多个请求添加到队列中。在稍后的某个时点，将队列刷新到下一管线阶段。这立即降低了管线阶段之间转换的总体 CPU 成本。在现有 Win32 API 集下，每个请求都要经过一次转换。DirectStorage 队列使用无锁算法以尽量减少争用。游戏可以控制每个队列何时被刷新。

在许多情况下，Win32 API 需要将从磁盘读到的数据复制到另一个缓冲区。在某些情况下数据甚至可能需要复制不止一次。DirectStorage 通过将游戏提供的目标缓冲区直接映射到每个管线层来解决此问题。硬件将直接写入游戏提供的缓冲区。

这些变化显著降低了 CPU 开销。

### 解压

硬件解压数据的能力得到了提升。它现在支持更多种类的格式，并且解压速度超过 NVMe 子系统提供的数据速度。此外，DirectStorage 支持就地解压，无需为压缩和未压缩数据管理独立缓冲区。

硬件支持 `BCPACK`、`DEFLATE`，并提供对最终内容进行 swizzle 的能力。这些格式并不互斥。可以对数据同时应用三种。这让游戏可以选择哪种方法能提供最佳压缩率和性能。不同资产可以使用不同的压缩和 swizzle 设置。

### 队列深度

之前的建议是在机械硬盘上一次只保持 12-16 个异步请求同时进行。更大对性能没有帮助，更小则会显著降低性能。这导致游戏需要额外工作来平衡未完成的读取请求以保持在建议目标内。

由于 DirectStorage 的目标是让游戏达到 50,000 IOPS，我们的建议已经改变。游戏不再需要在未完成工作和队列深度之间平衡。游戏应提交它所有未完成的请求。保留部分请求没有任何好处。在许多情况下，保留请求反而会因为硬件停下来等待新请求而损害性能。

操作系统仍需在某些情况下将较大的读取请求拆分为几个较小的请求（例如处理磁盘碎片）。但这在 DirectStorage 架构中已被考虑。50,000 IOPS 的设计目标是基于游戏的 IO 操作数，而不是最终发到硬件的请求数。

### 通知

在 Win32 架构中，读取完成通知消耗了大量开销。游戏可以轮询 *OVERLAPPED* 结构、等待关联的 Event 句柄，或者执行同步阻塞读取。总的来说，这增加了每个读取请求的资源需求。

DirectStorage 保留了两种异步通知概念，同时添加了第三种方式。DirectStorage 不支持同步阻塞读取——游戏可以自行实现其自己的系统，但不建议这样做。

第一种异步方式通过一个在关联请求完成时被设置的状态块实现。游戏可以按需轮询该块以确定读取是否完成。这类似于 Win32 通过轮询 *OVERLAPPED* 结构完成状态的方法。

第二种异步方式是使用 Windows `Event` 对象来通知完成。这类似于将 *OVERLAPPED* 结构与相应的 `Event` 对象一同使用。游戏可以使用 [WaitForSingleObject](https://learn.microsoft.com/windows/win32/api/synchapi/nf-synchapi-waitforsingleobject) 方法让调用线程挂起，直到读取操作完成。

第三种异步方式使用 [ID3D12Fence](https://learn.microsoft.com/windows/win32/api/d3d12/nn-d3d12-id3d12fence) 实现。游戏可以挂起等待 fence，或按需轮询 fence。GPU 也可以直接使用该 fence 来接收完成通知。

DirectStorage 的通知系统并不绑定到单个读取请求。它是放入队列的一个条目，只有在其之前的所有读取请求都完成时才被触发。这让游戏可以控制所需的通知粒度。通知总是按队列顺序被触发。队列可以视为一个 FIFO（先进先出）队列。游戏只需查询最后一个相关的通知，之前所有已入队请求保证已经完成。

### 内存到内存的解压

DirectStorage 提供了一种队列类型，可以调用解压硬件，其解压源为内存而非磁盘文件。这允许在压缩资产不是从文件读取或先前已被读取并保存在内存作为缓存时，仍能利用解压硬件。内存源队列只接受内存源请求，文件源队列只接受文件源请求。

如果内存源请求未指定任何解压选项，解压硬件也可以作为 DMA 复制引擎使用。

尽管 DirectStorage 保证完成通知按顺序，但 DirectStorage 不保证请求何时开始处理。因此，未完成请求之间不能存在数据依赖。也就是说，请求 A 的目标不能作为请求 B 的源，除非请求 B 是在请求 A 完成后才入队的。

内存源队列必须以 real-time 优先级创建。此外，内存源 real-time 请求总是先于需要解压的磁盘源请求由解压硬件处理。如果磁盘源队列没有任何解压请求，则两种队列类型完全并行处理，互不影响。

### 优先级

DirectStorage 允许每个队列分配一个优先级级别。队列中的每个条目继承队列的优先级。共提供四种不同优先级：real-time、high、normal 和 low。请求以加权轮询方式处理。例如，在处理 X 个高优先级请求后再处理一个 normal 优先级请求；处理 Y 个 normal 优先级请求后再处理一个 low 优先级请求。

优先级加权按每个请求的大小计算。各优先级之间的默认权重大约为 10 倍。这意味着每处理 1 KB 的低优先级请求，就会处理 10 KB 的中等优先级请求和 100 KB 的高优先级请求。

现有 Win32 读取请求通过同一优先级系统路由。所有 Win32 请求被视为 normal 优先级。

内存源队列必须以 real-time 优先级创建。

### 取消

每个 DirectStorage 读取请求都有一个由游戏提供的 64 位掩码与之关联。这是为了支持取消未完成的读取请求。游戏可以取消匹配掩码中特定标志集的请求。

即使支持取消，读取请求仍有可能被硬件处理完。游戏的取消请求是尽力而为的。如果请求已经被硬件主动处理，就不能被取消。

由于取消请求是尽力而为，游戏必须等到收到读取请求已完成处理的通知。在此之前，游戏不能释放任何相关资源，直到收到队列中的后续通知。但在此期间，与先前取消请求所用标志匹配的新请求可以入队，且不会被取消。

被取消的请求完成时，视为 **成功**，即使它已被取消并未产生完整结果。也就是说，如果尝试取消某个请求，游戏就不能再消费该可能被取消的请求完成后的结果。

### 保证

XBOX One 和 XBOX One S 主机的最低保证为 40 MB/s。XBOX One X 主机将最低保证提高到 60 MB/s。这些数字远低于实际的硬件极限，实际约在 130 MB/s。这完全由于操作系统带来的开销。

DirectStorage 消除了操作系统造成的大部分开销。这使得最低保证接近硬件极限。新的最低性能保证是 250 ms 窗口内的原始数据 2.0 GB/s。使用内容解压将使最终带宽更高。

未来的 XBOX 主机将支持动态用户可安装驱动器（同样基于 NVMe）。为内部驱动器提供的相同最低性能保证也适用于用户可安装驱动器。

## API 概述

DirectStorage 接口遵循与 Direct3D 接口相同的模式。游戏先获取一个单例工厂。工厂用于创建请求队列和打开文件——这些对象直接映射到硬件。然后将单独的请求入队到队列，以便提交给硬件。

### IDStorageFactoryX

`IDStorageFactoryX` 是创建队列、打开文件和提交待处理请求的主要接口。

`IDStorageFactoryX` 对象有以下方法。

* [OpenFile](/reference/system/dstorage/interfaces/IDStorageFactoryX/methods/idstoragefactoryx_openfile)
  * 创建一个 `IDStorageFileX` 对象，代表一个文件。

* [CreateQueue](/reference/system/dstorage/interfaces/IDStorageFactoryX/methods/idstoragefactoryx_createqueue)
  * 创建一个 `IDStorageQueueX` 对象。用于创建读取请求。

* [CreateStatusArray](/reference/system/dstorage/interfaces/IDStorageFactoryX/methods/idstoragefactoryx_createstatusarray)
  * 创建一个 `IDStorageStatusArray` 对象，用于管理完成状态标志。

* [SetCPUAffinity](/reference/system/dstorage/interfaces/IDStorageFactoryX/methods/idstoragefactoryx_setcpuaffinity)
  * 将 DirectStorage 不在调用线程内的工作限制到游戏定义的一组 CPU 核心。
  * **注意** DirectStorage 尝试在调用线程中完成大部分工作。非调用线程的工作只有在调用线程无法完成时才会发生。例如：
    * 底层资源管线在 `IDStorageQueueX::Submit` 时已满，队列中的请求无法全部向前推进。其余请求将在资源释放后由 DirectStorage 工作线程处理。
    * 处理请求完成到 `ID3DFence` 或 `IDStorageStatusArray` 的过程。

* [SetDebugFlags](/reference/system/dstorage/interfaces/IDStorageFactoryX/methods/idstoragefactoryx_setdebugflags)
  * 控制 DirectStorage 是否在请求入队时进行额外校验以辅助调试。

* [SetStagingBufferSize](/reference/system/dstorage/interfaces/IDStorageFactoryX/methods/idstoragefactoryx_setstagingbuffersize)
  * 设置暂存缓冲区大小，用于在解密/解压之前暂存从存储设备加载的内容。如果只使用内存源队列，暂存缓冲区可以大小为 0。

### IDStorageFactoryX1

`IDStorageFactoryX1` 接口在 `IDStorageFactoryX` 的基础上扩展了一个 [GetStats](/reference/system/dstorage/interfaces/IDStorageFactoryX1/methods/idstoragefactoryx1_getstats) 方法。

* [GetStats](/reference/system/dstorage/interfaces/IDStorageFactoryX1/methods/idstoragefactoryx1_getstats)
  * 获取 DirectStorage 的统计信息。此函数可用于将 DirectStorage 与现有的诊断和遥测管线集成。它做的处理极少，因此可以频繁调用。统计信息不包括 Win32 文件 IO 操作。

### IDStorageFactoryX2

`IDStorageFactoryX2` 接口在 `IDStorageFactoryX1` 的基础上扩展了一个 [CreateQueue1](/reference/system/dstorage/interfaces/IDStorageFactoryX2/methods/idstoragefactoryx_createqueue1) 方法。

* [CreateQueue1](/reference/system/dstorage/interfaces/IDStorageFactoryX2/methods/idstoragefactoryx_createqueue1)
  * 创建一个 `IDStorageQueueX2` 对象。使用新结构以在创建队列时提供额外选项，包括覆盖队列自动提交特性的能力。

### IDStorageFileX

所有文件都需要通过 `IDStorageFactoryX` 对象由 DirectStorage 首次打开。这相当于在 Win32 API 接口中使用 `CreateFile`。

文件以 `FILE_SHARED_READ` 权限打开。如需要，游戏可以在遵守相应权限的前提下同时使用 Win32 API 打开该文件。开发期间同时支持 loose 部署和已打包部署。

关闭文件方式有两种：显式调用文件对象上的 [Close](/reference/system/dstorage/interfaces/IDStorageFileX/methods/idstoragefilex_close) 函数，或释放对匹配的 `IDStorageFileX` 对象的最后一个引用。但在文件关闭前，所有未完成的 I/O 操作必须先完成。这意味着关闭文件的两种方式都会阻塞，直到该文件上的所有未完成 I/O 操作完成。

游戏可以通过调用 [GetHandle](/reference/system/dstorage/interfaces/IDStorageFileX/methods/idstoragefilex_gethandle) 函数获取 `IDStorageFileX` 对象所代表文件的 win32 句柄。该句柄以 `GENERIC_READ` 权限和 `FILE_SHARE_READ` 共享模式打开。可用于查询文件大小等。不再需要时应使用 `CloseHandle()` 关闭。

### IDStorageQueueX

读取请求通过 `IDStorageQueueX` 对象提交到 NVMe。但直到游戏调用队列上的 [Submit](/reference/system/dstorage/interfaces/IDStorageQueueX/methods/idstoragequeuex_submit)，或某次入队方法使队列容量超过一半后自动提交，请求才会被提交给设备。提交作为向管线下一阶段的一次转换处理。这让游戏能控制游戏与内核之间转换发生 CPU 成本的时机。

`IDStorageQueueX` 对象有四个属性。

* [SourceType](/reference/system/dstorage/enums/dstorage_request_source_type)
  * 指定队列可以接收文件源请求还是内存源请求。

* [Priority](/reference/system/dstorage/enums/dstorage_priority)
  * 提交到队列的所有请求的优先级：real-time、high、normal 或 low。
  * 内存源队列必须以 real-time 优先级创建。
  * 请求按优先级以加权轮询顺序处理。
  * Win32 请求以 normal 优先级处理。

* **Capacity**
  * 队列可容纳的未完成请求最大数量。
  * 队列已满时尝试入队请求会阻塞，直到硬件完成了一些条目。
  * 队列所需内存约为队列容量乘以 `DSTORAGE_REQUEST` 的大小。

* **Name**
  * 仅用于辅助调试。DirectStorage 代码不使用该名称，但它会显示在开发者工具中，例如 [PIX (NDA 主题)](/tools/tools-console/pix/pix-directstorage)。

硬件会异步处理请求以获得最高吞吐量。但与 Win32 不同，游戏会按 FIFO 顺序收到完成通知。收到完成通知时，保证同一队列上的所有之前请求也都已完成。

### IDStorageQueueX1

`IDStorageQueueX1` 接口在 `IDStorageQueueX` 的基础上扩展了 [EnqueueSetEvent](/reference/system/dstorage/interfaces/IDStorageQueueX1/methods/idstoragequeuex_enqueuesetevent) 方法。

### IDStorageQueueX2

`IDStorageQueueX2` 接口在 `IDStorageQueueX1` 的基础上扩展了 [CreateQueue1](/reference/system/dstorage/interfaces/IDStorageFactoryX2/methods/idstoragefactoryx_createqueue1) 方法。

以及一个额外属性。

* [Options](/reference/system/dstorage/structs/dstorage_queue_options)
  * 用于控制队列行为的标志，包括禁用自动提交。

### EnqueueRequest

此接口在功能上与 Win32 `ReadFile` 接口相同。单独创建读取请求并提交到队列。主要区别在于 DirectStorage 允许在提交前排入许多请求，支持硬件解压和取消。

请求有几个主要属性。

**请求的源**
根据 `Options.SourceType` 和 `Options.SourceIsPhysicalPages` 的组合，DirectStorage 使用以下三组属性之一来指定源数据所在位置。

* **File** 与 **FileOffset**
  * 当 `Options.SourceType` 为 `DSTORAGE_REQUEST_SOURCE_FILE` 时使用此组。
  * **File** 由之前的 `IDStorageFactoryX::OpenFile` 打开。
  * 若使用解压，**FileOffset** 需要 16 字节对齐；不使用解压时无对齐要求。
    * 这与 Win32 相比是一大变化，Win32 对异步读取要求文件内 4 KiB 对齐。

* **Source**
  * 当 `Options.SourceType` 为 `DSTORAGE_REQUEST_SOURCE_MEMORY` 且 `Options.SourceIsPhysicalPages` 为 `FALSE` 时使用此组。
  * 保存待解压数据的内存缓冲区。

* **SourcePageArray** 与 **SourcePageOffset**
  * 当 `Options.SourceType` 为 `DSTORAGE_REQUEST_SOURCE_MEMORY` 且 `Options.SourceIsPhysicalPages` 为 `TRUE` 时使用此组。
  * 与 `Source` 类似，但以 64 KB 物理页数组和首页中的字节偏移的形式提供源内存缓冲区。
  * 64 KB 物理页可通过 `XMemAllocatePhysicalPages` 分配。

**SourceSize**

* 要读取的源数据字节数，来自内存缓冲区或文件。

**IntermediateSize**

* 当此请求同时启用了 `zlib` 与 `BCPACK` 解压时，`IntermediateSize` 用于指定源数据经 zlib 解压（供 BCPACK 解压使用）后的中间大小。
* 否则应设为 0。

**请求的目标**
根据 `Options.DestinationIsPhysicalPages`，DirectStorage 使用以下两组属性之一来指定目标所在位置。

* **Destination**
  * 当 `Options.DestinationIsPhysicalPages` 为 `FALSE` 时使用此组。
  * 最终已加载数据的目标缓冲区。
  * 解压使用共享的内部缓冲区进行，可视为就地解压。

* **DestinationPageArray** 与 **DestinationPageOffset**
  * 当 `Options.DestinationIsPhysicalPages` 为 `TRUE` 时使用此组。
  * 与 `Destination` 类似，但以 64 KB 物理页数组和首页中的字节偏移的形式提供目标内存缓冲区。
  * 64 KB 物理页可通过 `XMemAllocatePhysicalPages` 分配。

**DestinationSize**

* 最终已加载内容的预期字节数。目标必须包含足以容纳该操作的空间。
* 未使用解压时，大小必须等于 **SourceSize**；使用解压时，大小必须大于 **SourceSize**。

**CancellationTag**

* 由游戏定义的任意 64 位 tag。
* 该 tag 用作取消请求的掩码。

**Name**

* 可选字符串，用于辅助调试。Name 可以出现在开发者工具中，例如 [PIX (NDA 主题)](/tools/tools-console/pix/pix-directstorage)，或出现在通过 `IDStorageQueueX::RetrieveErrorRecord` 获取的错误记录中。`Name` 字符串在请求整个生命周期内必须可访问。

**Options**

* **ZlibDecompress**
  * 指示数据需要按 RFC 1950 解压标准解压。
* **BcpackMode**
  * 指示应使用哪种 `BCPACK` 模式对数据进行解压。
  * None 是有效选项，表示数据未使用 `BCPACK` 压缩。
* **SwizzleMode**
  * 指示最终数据在内存中应如何 swizzle。
* **DestinationIsPhysicalPages**
  * 指示目标缓冲区使用 **DestinationPageArray** 与 **DestinationPageOffset** 指定，而不是 **Destination**。
* **SourceType**
  * 请求可以是内存源（具有 `Source`/`SourcePageArray` 和 `SourcePageOffset` 属性），也可以是文件源（具有 `File`/`FileOffset` 属性）。
* **SourceIsPhysicalPages**
  * 指示源缓冲区使用 **SourcePageArray** 与 **SourcePageOffset** 指定，而不是 **Source**。

### EnqueueStatus/EnqueueSignal/EnqueueSetEvent

请求可以入队并作为一系列相关请求处理。方法是通过在处理到达队列某个位置时入队通知。只有当所有先前的读取请求完成后才处理通知。这保证之前所有请求的数据都立即可用。

游戏有两种轮询方法和一种等待方法用于通知。游戏可以插入 `ID3D12Fence` 对象、`IDStorageStatusArrayX` 对象或设置事件操作。`ID3D12Fence` 的行为与 `ID3D12Fence` 对象一致。游戏线程可以等待 `Event`、CPU 可以轮询 fence、GPU 也可以轮询 fence。`IDStorageStatusArrayX` 对象允许 CPU 轮询完成状态并访问可能的读取失败。`EnqueueSetEvent` 方法允许游戏线程等待指定事件而不是轮询。这与 `ID3D12Fence::SetEventOnCompletion` 不同——XBOX 上的 `ID3D12Fence::SetEventOnCompletion` 实现会在 fence 上自旋直到被触发，从而占用 CPU 硬件线程；而 `EnqueueSetEvent` 允许游戏线程使用 `WaitForSingleObject`/`WaitForMultipleObjects` 将 CPU 让给其他线程，直到事件被触发。

如前所述，所有请求都按顺序完成，即使底层硬件为了性能而重新排序也是如此。通知只有在队列上所有先前已入队的请求都完成后才会被触发。

### 解压

解压由专用硬件处理，消除了传统解压算法带来的 CPU 开销。DirectStorage 在初始化期间分配固定的一块内存作为解压的工作缓冲区。这支持就地解压，无需在内存中同时保存压缩和未压缩数据。

解压硬件支持三种操作模式。这些模式并不互斥，可以指定任意组合。解压模式按以下顺序应用：`DEFLATE`、`BCPACK`、`Swizzle`。

* `ZLibDecompress`
  * 这是 [IETF RFC 1950](https://www.ietf.org/rfc/rfc1950.txt) 压缩标准。

* `BCPack`
  * `BCPack` 是专为 BCn 数据设计的定制熵编码器。一般来说，这意味着颜色端点与调色板索引（即权重）被分离并使用 rANS 算法压缩。

* `Swizzle`
  * `Swizzle` 与 shuffle 模式可在内容管线中提供额外优化。

当高熵数据被压缩时，压缩后大小实际上可能变得更大。相反地，相应的解压会缩小尺寸。DirectStorage 不允许缩小型解压，需要游戏自行检测高熵数据（不可压缩）并避免对此类资产进行压缩。有关详细信息，请参阅 [使用 DirectStorage 和 XBTC 优化压缩内容 (NDA 主题)](/build/console-features/storage/directstorage/directstorage-compression)。

### 暂存缓冲区

DirectStorage 内部使用缓冲区暂存从原始 NVMe 存储读取的所有内容，然后再进行解密和解压等操作。此暂存缓冲区让 NVMe 驱动器与解密/解压硅芯片可以并行流水工作。它默认为 32 MiB，在获取第一个 DirectStorage 工厂指针时分配。

如果游戏只使用 DirectStorage 进行内存到内存的解压操作，则不需要暂存缓冲区，可调用 [SetStagingBufferSize](/reference/system/dstorage/interfaces/IDStorageFactoryX/methods/idstoragefactoryx_setstagingbuffersize) 将暂存缓冲区大小设为 0。

当前 DirectStorage 发行版支持的暂存缓冲区大小为 0、16、20、24、28 和 32 MiB。比默认值小的尺寸可为游戏节省内存，但可能影响整体读取性能。

**注意：** 仅当不存在 `IDStorageQueueX` 对象和 `IDStorageFileX` 对象时才能调用 [SetStagingBufferSize](/reference/system/dstorage/interfaces/IDStorageFactoryX/methods/idstoragefactoryx_setstagingbuffersize)，否则会报错。

### CancelRequestsWithTag

DirectStorage 支持取消请求。每个请求都关联一个由游戏定义的 64 位 tag，其作用是作为要取消的请求的位掩码。游戏提供掩码和取消值。队列会尝试取消所有满足条件 `tag & mask == value` 的请求。

取消是尽力而为的操作。取决于请求在管线中的位置，可能无法取消。例如请求可能正在被硬件解压，无法取消。API 立即返回，不会阻塞等待所有被取消请求处理完毕。游戏必须等待队列中后续通知被触发后，才能释放与被取消请求相关的资源。

必须小心避免在调用 [CancelRequestsWithTag](/reference/system/dstorage/interfaces/IDStorageQueueX/methods/idstoragequeuex_cancelrequestswithtag) 的同时向队列添加请求。在此情况下行为未定义。但在 [CancelRequestsWithTag](/reference/system/dstorage/interfaces/IDStorageQueueX/methods/idstoragequeuex_cancelrequestswithtag) 调用返回之后添加到队列的请求，即使匹配条件也不会被取消，只有先前已入队的请求才会被取消。

### GetErrorEvent/RetrieveErrorRecord

如果某次读取导致错误，该读取会被标记为已完成。队列中未来的通知不会因此被阻塞而无法触发。错误通知通过与队列关联的 `Event` 对象进行，可通过 [GetErrorEvent](/reference/system/dstorage/interfaces/IDStorageQueueX/methods/idstoragequeuex_geterrorevent) 获取。游戏可以在 [GetErrorEvent](/reference/system/dstorage/interfaces/IDStorageQueueX/methods/idstoragequeuex_geterrorevent) 返回的 `Event` 上使用 **WaitForSingleObject**。如果事件被触发，游戏可以通过调用 [RetrieveErrorRecord](/reference/system/dstorage/interfaces/IDStorageQueueX/methods/idstoragequeuex_retrieveerrorrecord) 函数获取自上次调用 [RetrieveErrorRecord](/reference/system/dstorage/interfaces/IDStorageQueueX/methods/idstoragequeuex_retrieveerrorrecord) 以来的第一个错误。

[RetrieveErrorRecord](/reference/system/dstorage/interfaces/IDStorageQueueX/methods/idstoragequeuex_retrieveerrorrecord) 返回的错误记录仅包含自上次 [RetrieveErrorRecord](/reference/system/dstorage/interfaces/IDStorageQueueX/methods/idstoragequeuex_retrieveerrorrecord) 以来队列中第一个失败请求的数据。如果错误 `Event` 未被触发或数据已被检索，则错误记录中的数据未定义。

### Query

获取队列的信息。它包含用于创建队列的 [DSTORAGE\_QUEUE\_DESC](/reference/system/dstorage/structs/dstorage_queue_desc) 或 [DSTORAGE\_QUEUE\_DESC1](/reference/system/dstorage/structs/dstorage_queue_desc1) 结构，以及空槽数和还需入队多少条目才会触发自动提交。

## 最佳实践

与 Win32 相同的最佳实践建议同样适用于 DirectStorage。最佳性能的阈值发生了根本变化。

### 读取大小

机械硬盘的原始建议是每次至少读取 128 KiB 块。随着块大小增大，性能持续提升。512 KiB 的块大小性能最佳。

NVMe 没有活动部件，因此阈值低得多。从 32 KiB 读取开始读取性能有大幅提升，从 64 KiB 开始达到平台期。更大的读取不会带来性能提升。这意味着为了最佳性能，无需过多努力将数据合并成更大的块。

在 512 KiB 以上，如果使用解压，优先使用较小但更多的并行请求，而不是单个巨大请求。单个巨大请求会强制解压串行化，而多个并发请求允许多个解压硬件单元并行工作，达到全吞吐量。

在 October 2022 版本的 Microsoft 游戏开发工具包 (GDK) 中，单个请求的最大大小已从 32 MiB 目标增至 1 GiB（源加目标）合并内存使用。它的提供是为方便从允许较大读取尺寸的其他存储 API 移植。但为达到最大吞吐量的先前尺寸推荐仍然相同，因为大请求不允许多个解压硬件单元并行工作。

### 顺序

之前对于机械硬盘，需要花力气排列读取在磁盘上的位置。理想情况是从磁盘上的顺序位置读取。这样可以最小化磁头的移动，从而消除寻道时间因素。这样做可以带来一个数量级的性能提升。即便是按位置排序的随机读取也有好处，某些情况下能快 2 倍。

对 NVMe 驱动器，让读取请求尽量顺序也仍然有用。NVMe 驱动器以 64 KiB 对齐的块读取。因此可能会浪费带宽读取 64 KiB 块中未使用的部分。如果读取请求只有 4 KiB，就会有 60 KiB 的带宽被浪费。NVMe 会尽可能重用这多出的 60 KiB 以满足其他挂起的请求。例如，如果你有两次顺序读取，一次 32 KiB 接着 8 KiB，那么从驱动器上仍只需要一次 64 KiB 的读取。

### 队列管理

对于机械硬盘的建议是队列大小为 12 到 16。更大的队列深度没有好处，较小的队列深度性能会显著降低。

NVMe 规范说 NVMe 驱动器应支持每队列最多 65,536 项的多个队列。DirectStorage 支持这一要求，因此允许游戏一次性提交数千个请求。

以前对于机械硬盘，游戏会缓冲待处理请求以保持队列深度在 12 到 16 范围内。DirectStorage 的建议是不要缓冲请求，而是请求一创建就入队。整个系统是一个管线，游戏侧缓冲会在管线中制造气泡，严重损害性能。

另一个建议是创建容量至少为每帧创建的最大请求数四倍的队列。这样应能提供足够容量以在等待现有请求完成时不因入队新请求而阻塞。

### 通知管理

一般来说，加入队列的通知请求越少越好。建议在游戏需求与保持已入队通知请求尽量少之间寻找平衡。在每个请求后都入队通知只会因这些通知处理的开销而损害整体性能。

一个例子可能是按内容分组。例如 SFS 纹理、地形需求（例如网格与纹理）以及角色需求（例如网格、纹理与动画）。这样单个通知就能绑定到创建一个对象所需的所有资产的可用性。

在使用 `ID3D12Fence` 与状态数组之间的选择取决于游戏需求。数据是否需要立刻由 GPU 使用？让检查线程挂起直到读取完成是否可接受？由于数据只能在帧的某些点处理，定期轮询是否已经足够？

### 注意事项

在有可能同时有更多请求在途的情况下，必须小心避免在游戏其他部分产生瓶颈。游戏管理请求的成本可能很快压过 DirectStorage 节省的开销。建议查看每个请求的所有支持代码，确定哪些可以最小化。

每个请求是否都需要分配新的内存块？

* 内存系统需要开销来寻找新块并更新内部列表。
* 考虑尽可能重用内存块。

更新管理器是否需要锁？

* 更新越多，争用越多。
* 尽可能使用无锁方案。

是否使用推测式加载？

* 支持取消，这可能导致创建更多请求。
* 但推测需要可用内存。
* 考虑对推测阈值设置硬限制。

## 另请参阅

[DirectStorage](/build/console-features/storage/directstorage-toc)
[DirectStorage 使用方式与内部细节 (NDA 主题)](/build/console-features/storage/directstorage/directstorage-white-paper)
[使用 DirectStorage 和 XBTC 优化压缩内容 (NDA 主题)](/build/console-features/storage/directstorage/directstorage-compression)
[分析 DirectStorage 性能 (NDA 主题)](/tools/tools-console/pix/pix-directstorage)


## Related topics

- [DirectStorage](/zh-CN/build/console-features/storage/directstorage-toc.md)
- [IDStorageFactoryX](/zh-CN/reference/system/dstorage/interfaces/IDStorageFactoryX/idstoragefactoryx.md)
- [IDStorageFactoryX1](/zh-CN/reference/system/dstorage/interfaces/IDStorageFactoryX1/idstoragefactoryx1.md)
- [DStorageGetFactory](/zh-CN/reference/system/dstorage/functions/dstoragegetfactory.md)
- [IDStorageFactoryX2](/zh-CN/reference/system/dstorage/interfaces/IDStorageFactoryX2/idstoragefactoryx2.md)
