DirectStorage 概述

简介

本主题仅概述了 Xbox Series X|S 主机的 DirectStorage API。 请参阅桌面版 DirectStorage,了解桌面版 DirectStorage 的详细信息。

使用 PCIe 总线连接的最新 NVMe 存储设备可以实现很高级别的吞吐量和 IOPS(每秒 I/O 请求数)。 Win32 API 的开销意味着即使可以利用可用的存储带宽,但利用它可能会导致 CPU 利用率过高。 当工作负载包含大量小请求时,尤其如此。

DirectStorage API 旨在通过与基础 NVMe 硬件进行紧密交互来消除操作系统的大部分开销。 这样可以较低的 CPU 使用量实现较高的带宽。 目标是每秒最多可处理 50,000 个请求,而最多只使用单个 CPU 内核的 10%。

现有问题

随着每一代主机对更高分辨率资源的需求的增加,游戏内容变得越来越大。 现有的 Xbox One 硬件和软件存在多个限制,这些限制束缚了开发者将这些下一代内容的数据从硬盘驱动器获取到内存的能力。

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,并具有重排最终内容的功能。 这些格式并不相互排斥。 可以将所有三种格式应用到数据。 这让游戏可以选择哪种方法可以为它们提供最佳的压缩率和性能。 不同的资源可以使用不同的压缩和重排设置。

队列深度

之前的建议是在轮流驱动上一次仅保留 12–16 个未完成的异步请求。 更多对性能没有好处,更少则会严重影响性能。 这会导致游戏执行额外的工作来平衡其未完成的读取请求,使其保持在推荐目标之内。

由于 DirectStorage 目标允许游戏达到 50,000 IOPS,因此此建议已改变。 游戏不再需要尝试在未完成的工作和队列深度之间取得平衡。 游戏应提交其所有未完成的请求。 保留某些请求没有任何好处。 在许多情况下,由于硬件会因等待新请求而停止,因此保留请求可能会损害性能。

在某些情况下(例如,处理磁盘碎片),仍然需要操作系统将较大的读取请求分解为几个较小的请求。 不过,DirectStorage 体系结构中考虑了这一点。 50,000 IOPS 的设计目标基于 IO 操作的游戏计数,而不是传入硬件的最终请求。

通知

在 Win32 体系结构中,大量的开销消耗在读取完成的通知上。 游戏可以轮询 OVERLAPPED 结构、等待关联的 Event 处理程序或执行同步阻止读取。 总体上,这增加了每个读取请求的资源需求。

DirectStorage 会保留通知的两个异步概念,同时添加第三种方法。 DirectStorage 不支持同步阻止读取,游戏可以实现自己的系统,但不建议这样做。

第一个异步方法是通过状态块实现的,状态块在关联的请求完成时设置。 游戏可以根据需要轮询此块,以确定何时完成读取。 这类似于 Win32 方法,用于轮询 OVERLAPPED 结构以完成。

第二个异步方法是使用 Windows Event 对象来完成信号。 这类似于将 OVERLAPPED 结构与相应的 Event 对象一起使用。 游戏可以使用 WaitForSingleObject 方法将调用线程挂起,直到读取操作完成。

第三个异步方法是使用 ID3D12Fence 实现的。 游戏可以暂停等待围栏,也可以根据需要轮询围栏。 GPU 还可以使用围栏直接通知已完成的请求,这是另外一个好处。

DirectStorage 通知系统不绑定到单个读取请求。 它是放置在队列中的一个条目,当前面的所有读取请求完成时会发出信号。 这让游戏可以控制它们需要通知具有的粒度数量。 通知始终按队列顺序发出信号。 此队列可以被视为 FIFO(先进先出)队列。 游戏仅需要查询最后一个相关通知。 保证之前排入队列的所有请求都已完成。

内存到内存解压缩

DirectStorage 提供队列类型以调用解压缩硬件,并且解压缩源是内存而非磁盘文件。 如果压缩资源不是源自文件,或者以前曾源自文件并作为缓存保留在内存中,这允许使用解压缩硬件。 源自内存的队列仅接受源自内存的请求,而源自文件的队列仅接受源自文件的请求。

如果源自内存的请求中未指定任何解压缩选项,则解压缩硬件也可以充当 DMA 复制引擎。

尽管 DirectStorage 保证完成通知按顺序排列,但是 DirectStorage 不能保证何时开始处理请求。 因此,挂起的请求之间必须没有数据相关性。 即,请求 A 的目标不能用作请求 B 的源,除非请求 B 在请求 A 完成后排队。

必须通过实时优先级创建源自内存的队列。 此外,在发出要求解压缩的源自磁盘的请求之前,源自内存的实时请求始终由解压缩硬件进行处理。 如果源自磁盘的队列没有任何解压缩请求,这两种队列类型将完全并行处理,不会影响其他类型。

优先级

DirectStorage 允许为每个队列分配优先级。 队列中的每个条目都继承队列的优先级。 提供了四个不同的优先级:实时、高、正常和低。 按加权的轮循机制方式处理请求。 例如,先处理高优先级的 X 个请求,再处理普通优先级的一个请求。 先处理普通优先级的 Y 个请求,再处理低优先级的一个请求。

优先级权重将根据每个请求的大小进行计数。 每个优先级之间的默认权重大约是 10 倍。 这意味着,对于每个 1KB 的低优先级请求 ,10KB 的中等优先级请求和 100KB 的高优先级请求都将得到处理。

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

必须通过实时优先级创建源自内存的队列。

取消

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

即使支持取消,仍然可以由硬件处理读取请求。 游戏取消请求是一次尽力而为的尝试。 如果请求已经被硬件先加以处理,则无法取消该请求。

由于取消请求是一项尽力而为的工作,因此游戏必须等待,直到收到读取请求已完成处理的通知。 在队列中收到后来的通知之前,游戏无法释放所需的任何资源。 但是,在此期间,与先前取消请求中使用的标志匹配的新请求可以排入队列,而且不会被取消。

当取消的请求完成时,它将视为成功,即使它已取消并且未产生完整的结果也是如此。 换句话说,如果对某个请求尝试取消,则该游戏在完成后将无法再消耗可能取消的请求的结果。

性能保证

Xbox One 和 Xbox One S 主机都有 40 MB/s 的最低保证。 Xbox One X 主机将最低保证提高到了 60 MB/s。 这些数字远低于 130 MB/s 范围内的实际硬件限制。 这完全是因为操作系统造成的开销。

DirectStorage 消除了由操作系统引起的大部分开销。 这样可以使最低保证更接近硬件限制。 新的最低性能保证是在 250 毫秒的原始数据窗口内达到 2.0 GB/s。 对内容使用解压缩将提高最终带宽。

未来的 Xbox 主机将支持添加同时基于 NVMe 的动态用户可安装驱动器。 用户可安装驱动器也提供了与内部驱动器相同的最低性能保证。

API 概述

DirectStorage 接口采用与 Direct3D 接口相同的模式。 游戏最初获取单一实例工厂。 该工厂用于创建请求队列和打开文件,这些对象中的每一个都有直接到硬件的映射。 然后将各个请求排入队列,以提交到硬件。

IDStorageFactoryX

IDStorageFactoryX 是用于创建队列、打开文件和提交挂起请求的主接口。

IDStorageFactoryX 对象具有以下方法。

  • OpenFile

    • 创建代表一个文件的 IDStorageFileX 对象。
  • CreateQueue

    • 创建 IDStorageQueueX 对象。 用于创建读取请求。
  • CreateStatusArray

    • 创建管理完成状态标志的 IDStorageStatusArray 对象。
  • SetCPUAffinity

    • 将 DirectStorage 的调用线程外工作限制为游戏定义的 CPU 内核集。
    • 注意 DirectStorage 尝试在调用线程中完成大部分工作。 仅当不能在调用线程中完成时,调用线程以外的工作才会发生。 示例如下所示:
      • 基础资源管道在 IDStorageQueueX::Submit 期间是满的,不是队列中的所有请求都可以向前推送。 其余请求将在这些资源可用时进行处理,并且在 DirectStorage 工作线程中完成。
      • 在 ID3DFence 或 IDStorageStatusArray 中完成请求处理。
  • SetDebugFlags

    • 控制 DirectStorage 是否将在请求排队时间执行额外的验证以帮助调试。
  • SetStagingBufferSize

    • 设置暂存缓冲区的大小,该暂存缓冲区用于在解密/解压缩从存储设备加载的内容之前先临时存储这些内容。 如果仅使用内存源队列,则暂存缓冲区的大小可以为 0。

IDStorageFactoryX1

IDStorageFactoryX1 界面通过 GetStats 方法扩展了 IDStorageFactoryX 界面。

  • GetStats
    • 获取 DirectStorage 统计信息。 该功能可以用来将 DirectStorage 与现有的诊断和遥测管道相整合。 它所做的处理最少,因此可以经常调用。 这些统计数据不包括 Win32 文件的 IO 操作。

IDStorageFileX

所有文件最初都需要由 DirectStorage 通过 IDStorageFactoryX 对象打开。 这等效于在 Win32 API 接口中使用 CreateFile。

文件使用 FILE_SHARED_READ 权限打开。 如果需要,游戏可以使用 Win32 API 同时打开文件,前提是要具有适当的权限。 开发过程中,既支持松散部署也支持打包部署。

可以通过显式调用文件对象上的 Close 函数或在释放对匹配的 IDStorageFileX 对象的最后一个引用时关闭文件。 但是,所有未完成的 I/O 操作必须全部完成才能关闭文件。 这意味着关闭文件的两种方法都将阻塞,直到该文件上所有未完成的 I/O 操作完成。

游戏可以通过调用 GetHandle 函数,获得 IDStorageFileX 对象表示的文件的 win32 句柄。 该句柄以 GENERIC_READ 权限和 FILE_SHARE_READ 共享模式打开。 它可用于查询文件的大小等。当不再需要该句柄时,应使用 CloseHandle() 将其关闭。

IDStorageQueueX

读取请求通过 IDStorageQueueX 对象提交到 NVMe。 但是,在游戏在队列中调用 提交 之前,请求不会提交到设备,或者其中一个“排队”方法会填充自上次提交以来一半以上的队列容量,并触发自动提交。 提交将作为进入管道中下一个阶段的单个转换进行处理。 这允许游戏控制在游戏和内核之间进行转换时何时产生 CPU 成本。

IDStorageQueueX 对象具有四个属性。

  • SourceType

    • 指定队列是否可以接收源自文件的请求或源自内存的请求。
  • 优先级

    • 提交到队列的所有请求的优先级:实时、高、普通或低。
    • 必须通过实时优先级创建源自内存的队列。
    • 根据优先级,按加权的轮循机制顺序处理请求。
    • Win32 请求均在普通优先级进行处理。
  • 容量

    • 队列可以容纳的未完成请求的最大数量。
    • 当队列已满时,尝试将请求排入队列将无效,直到硬件完成各个条目为止。
    • 队列所需的内存量约为队列容量与 DSTORAGE_REQUEST 大小相乘得出的值。
  • 名称

    • 名称的作用只是为了帮助调试。 名称不会被任何 DirectStorage 代码使用,但会在开发者工具(如 PIX(NDA 主题)要求授权)中显示。

硬件将异步处理请求以实现最高吞吐量。 但是,与 Win32 不同,游戏按 FIFO 顺序通知完成信息。 收到完成通知时,可以确认同一队列的所有之前的请求也已完成。

IDStorageQueueX1

IDStorageQueueX1 接口使用 EnqueueSetEvent 方法扩展 IDStorageQueueX 接口。

EnqueueRequest

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

请求具有以下几个主要属性。

请求的来源 具体取决于 Options.SourceType 和 Options.SourceIsPhysicalPages 的组合。DirectStorage 使用以下 3 组属性之一来指定源数据所在的位置:

  • FileFileOffset

    • Options.SourceType 是 DSTORAGE_REQUEST_SOURCE_FILE 时使用该组。
    • File之前已使用 IDStorageFactoryX::OpenFile 打开。
    • FileOffset使用了解压缩,则需要为 16 字节对齐,如果未使用解压缩,则不会有任何对齐要求。
      • 这是 Win32 的一项主要更改,Win32 之前要求在文件内进行 4 KiB 对齐以进行异步读取。
    • 当 Options.SourceType 为 DSTORAGE_REQUEST_SOURCE_MEMORY 和 Options.SourceIsPhysicalPages 为 FALSE 时,将使用该组。
    • 存放要解压缩数据的内存缓冲区。
  • SourcePageArraySourcePageOffset

    • 当 Options.SourceType 为 DSTORAGE_REQUEST_SOURCE_MEMORY 和 Options.SourceIsPhysicalPages 为 TRUE 时,将使用该组。
    • 类似于源,但以 64KB 物理页面数组的形式提供源内存缓冲区,并在第一页中提供字节偏移量。
    • XMemAllocatePhysicalPages 可以分配物理 64KB 页面。

SourceSize

  • 从内存缓冲区或从文件中读取的源数据的大小(以字节为单位)。

IntermediateSize

  • 当在此请求中启用 zlib 和 BCPACK 解压缩时,IntermediateSize 用于指定源数据 zlib 解压缩到的(以及从 BCPACK 解压缩的)中间大小。
  • 否则,它应该设置为 0。

请求的目标 具体取决于 Options.DestinationIsPhysicalPages,DirectStorage 使用以下 2 组属性之一来指定目标所在的位置:

  • 目标

    • 当 Options.DestinationIsPhysicalPages 为 FALSE 时,将使用该组。
    • 最终加载的数据的目标缓冲区。
    • 使用共享内部缓冲区进行解压缩,可以将其视为就地解压缩。
  • DestinationPageArrayDestinationPageOffset

    • 当 Options.DestinationIsPhysicalPages 为 TRUE 时,将使用该组。
    • 类似于目标,但以 64KB 物理页面数组的形式提供目标内存缓冲区,并在第一页中提供字节偏移量。
    • XMemAllocatePhysicalPages 可以分配物理 64KB 页面。

DestinationSize

  • 最终加载内容的预期大小,以字节为单位。 配置必须有足够的空间来容纳操作。
  • 当使用解压缩时,大小必须等于SourceSize,或当使用解压缩时,大小必须大于SourceSize

CancellationTag

  • 游戏定义的任意 64 位标签。
  • 此标签用作取消请求的掩码。

名称

  • 帮助调试的可选字符串。 名称可在开发人员工具中显示,例如PIX(NDA 主题)要求授权,或在从 IDStorageQueueX::RetrieveErrorRecord 中获取的错误记录中显示。 需要在请求的生存期内访问名称字符串。

选项

  • ZlibDecompress
    • 指示需要使用 RFC 1950 解压缩标准对数据进行解压缩。
  • BcpackMode
    • 指示应使用哪种 BCPACK 模式来解压缩数据。
    • “无”是有效选项,表示未对数据进行 BCPACK 压缩。
  • SwizzleMode
    • 指示应如何在内存中重排最终数据。 必须为当前版本中的 DSTORAGE_SWIZZLE_MODE_NONE。
  • DestinationIsPhysicalPages
    • 指示使用DestinationPageArrayDestinationPageOffset而非Destination指定目标缓冲区。
  • SourceType
    • 请求可能是源自内存的,因此具有 Source/SourcePageArray & SourcePageOffset 属性,或是源自文件的,因此具有 File/FileOffset 属性。
  • SourceIsPhysicalPages
    • 指示使用SourcePageArraySourcePageOffset而不是Source指定源缓冲区。

EnqueueStatus/EnqueueSignal/EnqueueSetEvent

可以将请求排入队列并将其视为一系列相关请求。 在处理到达队列中的某个点时,通过排入队列发出通知的方式来完成此操作。 仅当所有先前的读取请求均已完成时,才处理通知。 这样可以确保所有前请求中的数据立即可用。

游戏有两个轮询方法和一个通知等待方法。 游戏可以插入 ID3D12Fence 对象、IDStorageStatusArrayX 对象或“设置事件”操作。 ID3D12Fence 的行为与 ID3D12Fence 对象应是相同的。 游戏线程可以等待事件,CPU 可以轮询围栏,GPU 可以轮询围栏。 IDStorageStatusArrayX 对象允许轮询 CPU 执行的完成以及访问可能的读取失败。 EnqueueSetEvent 方法允许游戏线程等待指定事件,而非轮询。 这与 ID3D12Fence::SetEventOnCompletion 不同,因为 ID3D12Fence::SetEventOnCompletion 的 Xbox 实现在围栏上旋转,直到收到信号, 因此,使用 CPU 硬件线程直到收到信号,而 EnqueueSetEvent 则允许游戏线程使用 WaitForSingleObject / WaitForMultipleObjects 将 CPU 生成到其他线程,直到事件收到信号。

如前所述,即使基础硬件决定重新排序请求以提高性能,所有请求也会按顺序完成。 在队列上所有之前排入队列的请求完成之前,不会发出通知信号。

解压缩

解压缩是通过专用硬件处理的。 这将消除传统解压缩算法中的 CPU 开销。 DirectStorage 在初始化期间分配固定的内存块,以用作解压缩的工作缓冲区。 这样就可以进行就地解压缩,而无需同时将压缩数据和解压缩数据保留在内存中。

解压缩硬件支持三种操作模式。 这些模式不是互斥的,因此可以指定任何一种模式组合。 解压缩模式按以下顺序应用:DEFLATE、BCPACK 和 Swizzle。

  • ZLibDecompress

  • BCPack

    • BCPack 是专门为 BCn 数据设计的自定义熵编码器。 通常,这意味着将颜色终结点与调色板指数(即权重)分开并使用 rANS 算法进行压缩。
  • Swizzle

    • Swizzle 和随机模式可以在内容管道中提供其他优化。

当压缩高熵数据时,压缩可能会实际增大大小。 相反,相应的解压缩将缩小大小。 DirectStorage 不允许解压缩压缩,并且由游戏来检测不可压缩的高熵数据,并避免在此类资源上压缩。 有关进一步的详细信息,请参阅使用 DirectStorage 和 XBTC 优化压缩后的内容(NDA 主题)要求授权指南。

暂存缓冲区

DirectStorage 在执行解密和解压缩等操作之前,会在内部使用一个缓冲区来暂存从原始 NVMe 存储读取的所有内容。 该暂存缓冲区允许 NVMe 驱动器和解密/解压缩芯片在管道中并行工作。 它默认为 32MiB,并在检索到第一个 DirectStorage 工厂指针时分配。

如果游戏仅将 DirectStorage 用于内存到内存解压缩操作,则不需要暂存缓冲区,可以调用 SetStagingBufferSize 来将暂存缓冲区的大小设置为 0。 SetStagingBufferSize 只能在 IDStorageQueueX 对象和 IDStorageFileX 对象都不存在时调用。

DirectStorage 的当前版本仅支持 0 或 32MiB 暂存缓冲区大小。

CancelRequestsWithTag

DirectStorage 支持取消请求。 每个请求都有一个与之关联的游戏定义的 64 位标签。 目的是用作要取消的请求的位掩码。 游戏提供掩码和取消值。 队列将尝试取消所有符合条件的请求:tag & mask == value

取消是一项尽力而为的操作。 根据请求在管道中的位置,可能无法取消。 例如,请求可能正在被硬件解压缩,因而无法取消。 API 将立即返回,并且不会阻止等待所有已取消的请求得到处理。 游戏必须等待,直到队列中的后续通知发出,然后释放与取消的请求关联的资源。

必须注意避免在调用 CancelRequestsWithTag 的同时将请求添加到队列。 在这种情况下,行为是未定义的。 但是,即使条件匹配,在 CancelRequestsWithTag 调用返回后添加到队列中的请求也不会被取消。 仅之前排入队列的请求会被取消。

GetErrorEvent/RetrieveErrorRecord

如果读取导致错误,则将该读取标记为已完成。 队列中将来的通知不会被阻止发出。 错误通知是通过与队列关联的事件对象处理的,可以通过 GetErrorEvent 进行检索。 游戏可以对 GetErrorEvent 返回的事件使用 WaitForSingleObject。 如果事件发出信号,则游戏可以通过调用 RetrieveErrorRecord 函数获取自上次调用 RetrieveErrorRecord 函数以来发生的第一个错误。

RetrieveErrorRecord 返回的错误记录仅包含自最后一个 RetrieveErrorRecord 以来队列中第一个失败请求的数据。 如果已向错误事件发出信号或已经检索到数据,则错误记录中的数据是不确定的。

查询

获取有关队列的信息。 它包括用于创建队列的 DSTORAGE_QUEUE_DESC 结构,以及空插槽数和需要排队以触发自动提交的条目数。

最佳做法

针对 Win32 的最佳做法建议也适用于 DirectStorage。 最佳性能的阈值已彻底改变。

读取大小

旋转磁盘的最初建议是至少在 128 KiB 的块内读取。 随着块大小增加,性能也不断提高。 512 KiB 大小的块实现了最佳性能。

NVMe 上缺少移动部件会大大降低阈值。 从 32 KiB 读取开始,读取性能有了很大的提高,64 KiB 则开始达到最高。 大于此值的读取不会提高性能。 这意味着只要更少的工作量就可以将数据合并到包中的更大块中,从而获得最佳性能。

在 512 KiB 的情况下,如果使用解压缩,则在一个巨大请求的情况下更喜欢以并行方式对较小但更多请求进行解压缩。 单个大型请求将强制使解压缩实现序列化,而多个并发请求则允许多个解压缩硬件单元并行工作并实现完全吞吐量。

在 2022 年 10 月发布的 Microsoft 游戏开发工具包 (GDK)中,对于组合源 + 目标内存使用量,最大单个请求大小已从 32MiB (作为目标)增加到 1GiB。 提供它的目的是便于从允许大型读取大小的其他存储 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

DirectStorage 使用情况和内部详细信息(NDA 主题)要求授权

使用 DirectStorage 和 XBTC 优化压缩内容(NDA 主题)要求授权

分析 DirectStorage 性能(NDA 主题)要求授权