Highlight
GPU 软件工程师 Jaideep Joshi 介绍了 Metal 3 的 Fast Resource Loading 功能,这是一个专门为游戏和图形密集型 App 设计的异步资源加载 API。
核心内容
大型游戏的资源加载有一个很直接的问题:资源越精细,玩家等待越久。关卡开始前先把纹理、音频、几何数据全部读进内存,可以避免运行中卡顿,但会拉长 loading screen,也会占用更多内存。
Metal 3 的 Fast Resource Loading 解决的是这条链路。它把资源加载变成异步 IO 命令,App 发出加载请求后不必阻塞当前线程,可以继续编码渲染或计算工作。IO 命令队列还可以并发执行多个 command buffer,更适合高速 SSD 和 Apple Silicon 的统一内存架构。
这场 session 的核心场景是流式加载。玩家靠近黑板时,游戏再加载更高分辨率的纹理;开放世界地图按区域预加载;音效比大纹理更紧急时,音频资源走高优先级队列。共同目标是让游戏只加载当前需要的数据,并尽量在玩家察觉前完成。
另一个重点是粒度。Apple 建议把加载请求拆小,例如用 sparse texture 按 tile 加载可见区域,而不是一次加载完整 mip level。请求数量会变多,但现代存储硬件可以并发处理,Metal 3 的 IO 队列正是为这种模式设计的。
详细内容
打开资源文件
(05:19)Fast Resource Loading 的第一步是创建文件句柄。这个句柄来自 MTLDevice,后续所有 load 命令都通过它读取文件内容。
// Create an Metal File IO Handle
// Create handle to an existing file
var fileIOHandle: MTLIOFileHandle!
do {
try fileHandle = device.makeIOHandle(url: filePath)
} catch {
print(error)
}
关键点:
MTLIOFileHandle表示 Metal IO 系统可以读取的文件句柄。device.makeIOHandle(url:)用 Metal device 打开已有文件。catch分支处理文件打开失败,例如路径错误或资源不可访问。- 官方片段里声明名是
fileIOHandle,赋值处写成fileHandle;实际项目中应保持变量名一致。
创建 IO 命令队列
(06:49)加载命令不会直接提交到渲染队列,而是提交到 MTLIOCommandQueue。默认队列是 concurrent,也可以改成 serial,让 command buffer 按顺序执行。
// Create a Metal IO command queue
let commandQueueDescriptor = MTLIOCommandQueueDescriptor()
commandQueueDescriptor.type = MTLIOCommandQueueType.concurrent // or serial
var ioCommandQueue: MTLIOCommandQueue!
do {
try ioCommandQueue = device.makeIOCommandQueue(descriptor: commandQueueDescriptor)
} catch {
print(error)
}
关键点:
MTLIOCommandQueueDescriptor用来描述 IO 队列的行为。type = .concurrent允许多个 IO command buffer 并发执行并乱序完成。- 如果资源有严格顺序要求,可以使用 serial 队列。
device.makeIOCommandQueue(descriptor:)根据描述符创建 Metal IO 队列。
提交纹理和缓冲区加载
(07:17)IO 队列创建后,App 从队列生成 command buffer,再向里面编码加载命令。Metal 3 支持三类加载:加载到 texture、加载到 buffer、加载到 CPU 可访问内存。
// Create Metal IO Command Buffer
let ioCommandBuffer = ioCommandQueue.makeCommandBuffer()
// Encode load commands
// Encode load texture and load buffer commands
ioCommandBuffer.load(texture, slice: 0, level: 0, size: size,
sourceBytesPerRow:bytesPerRow, sourceBytesPerImage: bytesPerImage,
destinationOrigin: destOrigin,
sourceHandle: fileHandle, sourceHandleOffset: 0)
ioCommandBuffer.load(buffer, offset: 0, size: size,
sourceHandle: fileHandle, sourceHandleOffset: 0)
// Commit command buffer for execution
ioCommandBuffer.commit()
关键点:
makeCommandBuffer()从 IO 队列创建一个加载批次。- 第一个
load把文件数据加载到 Metal texture,可用于纹理流式加载。 slice、level、destinationOrigin指定写入纹理的区域。- 第二个
load把文件数据加载到 Metal buffer,可用于几何数据或场景数据。 sourceHandleOffset指定从文件的哪个偏移开始读取。commit()提交加载工作,调用线程不需要等待加载完成。
用 shared event 同步加载和渲染
(08:51)异步加载带来一个新问题:渲染不能在资源就绪前读取它。Metal 3 使用 MTLSharedEvent 在 IO command buffer 和图形 command buffer 之间建立同步。
var sharedEvent: MTLSharedEvent!
sharedEvent = device.makeSharedEvent()
// Create Metal IO command buffer
let ioCommandBuffer = ioCommandQueue.makeCommandBuffer()
ioCommandBuffer.waitForEvent(sharedEvent, value: waitVal)
// Encode load commands
ioCommandBuffer.signalEvent(sharedEvent, value: signalVal)
ioCommandBuffer.commit()
// Graphics work waits for the IO command buffer to signal
关键点:
device.makeSharedEvent()创建一个可以跨队列使用的同步对象。waitForEvent(_:value:)让 IO command buffer 等待某个事件值后再执行。- 加载命令编码在 wait 和 signal 之间。
signalEvent(_:value:)会在 IO command buffer 内所有加载命令完成后发出信号。- 对应的图形 command buffer 等待同一个事件值,就能避免读取未加载完成的资源。
取消不再需要的加载
(10:29)开放世界游戏常会预加载玩家可能前往的区域。玩家改变方向后,某些预加载就失去价值。Metal 3 允许在 command buffer 粒度尝试取消这些 IO 工作。
// Player in the center region
// Encode loads for the North-West region
ioCommandBufferNW.commit()
// Encode loads for the West region
ioCommandBufferW.commit()
// Encode loads for the South-West region
ioCommandBufferSW.commit()
// Player in the western region and heading south
// tryCancel NW command buffer
ioCommandBufferNW.tryCancel()
// ..
// ..
func regionNWCancelled() -> Bool {
return ioCommandBufferNW.status == MTLIOStatus.cancelled
}
关键点:
- 每个区域的预加载可以放进独立 IO command buffer。
- 玩家还在中心区域时,游戏同时提交西北、西、西南三个方向的加载。
- 玩家向南移动后,
tryCancel()尝试取消西北区域的加载。 status == .cancelled用来判断 command buffer 是否确实被取消。- 取消粒度是 command buffer,不是单条 load 命令。
给紧急资源更高优先级
(12:28)纹理和音频都可以通过 Fast Resource Loading 读取,但它们的延迟要求不同。玩家传送到新场景时,大量纹理可以稍后完成,传送音效必须马上播放。解决方法是为音频创建高优先级 IO 队列。
// Create a Metal IO command queue
let commandQueueDescriptor = MTLIOCommandQueueDescriptor()
commandQueueDescriptor.type = MTLIOCommandQueueType.concurrent // or serial
// Set Metal IO command queue Priority
commandQueueDescriptor.priority = MTLIOPriority.high // or normal or low
var ioCommandQueue: MTLIOCommandQueue!
do {
try ioCommandQueue = device.makeIOCommandQueue(descriptor: commandQueueDescriptor)
} catch {
print(error)
}
关键点:
priority可以设为 high、normal 或 low。- 高优先级队列适合音频这类对延迟敏感的资源。
- 纹理加载可以继续走普通优先级队列。
- 队列创建后不能再修改优先级,因此需要提前按资源类型设计队列。
离线压缩资源包
(14:04)Fast Resource Loading 还支持压缩文件。Apple 建议在离线阶段生成压缩包,运行时由 Metal 3 根据偏移找到需要解压的 chunk,并把解压后的数据加载到资源中。
// Create a compressed file
// Create compression context
let chunkSize = 64 * 1024
let compressionMethod = MTLIOCompressionMethod.zlib
let compressionContext = MTLIOCreateCompressionContext(compressedFilePath, compressionMethod, chunkSize)
// Append uncompressed file data to the compression context
// Get uncompressed file data
MTLIOCompressionContextAppendData(compressionContext, filedata.bytes, filedata.length)
// Write the compressed file
MTLIOFlushAndDestroyCompressionContext(compressionContext)
关键点:
chunkSize决定压缩包内部的分块大小,session 示例使用 64K。MTLIOCompressionMethod.zlib选择 ZLib 压缩方法。MTLIOCreateCompressionContext创建离线压缩上下文。MTLIOCompressionContextAppendData可以多次调用,把不同文件的数据追加进同一个压缩上下文。MTLIOFlushAndDestroyCompressionContext写出压缩文件并销毁上下文。
读取压缩文件
(15:05)运行时打开压缩文件时,需要把创建压缩包时使用的压缩方法传给 makeIOHandle。Metal 3 后续会在 load 命令执行时做 inline decompression。
// Create an Metal File IO Handle
// Create handle to a compressed file
var compressedFileIOHandle : MTLIOFileHandle!
do {
try compressedFileHandle = device.makeIOHandle(url: compressedFilePath, compressionMethod: MTLIOCompressionMethod.zlib)
} catch {
print(error)
}
关键点:
- 压缩文件同样通过
MTLIOFileHandle进入加载管线。 compressionMethod必须匹配离线压缩时使用的方法。- Metal 3 会把文件偏移转换成需要解压的 chunk 列表。
- 官方片段里声明名是
compressedFileIOHandle,赋值处写成compressedFileHandle;实际项目中应保持变量名一致。
核心启发
-
做一个按视距加载的贴图系统。 远处只加载低分辨率 mip,玩家靠近时再用
load(texture:slice:level:size:...)加载更高分辨率数据。这样可以缩短初始加载时间,并减少一开始占用的显存。 -
做开放世界区域预加载。 把地图切成区域,每个区域对应一个 IO command buffer。玩家移动方向变化时,对不再需要的区域调用
tryCancel(),把存储带宽留给新的目标区域。 -
给音频建立高优先级 IO 队列。 传送、战斗提示、关键 UI 音效可以用
loadBytes加载到 App 分配的内存,并放到MTLIOPriority.high队列中,降低被大纹理加载拖慢的风险。 -
把资源打成 Metal IO 压缩包。 构建阶段用
MTLIOCreateCompressionContext生成压缩文件,运行时用带compressionMethod的makeIOHandle打开。适合纹理、几何数据和音频都很多的游戏项目。 -
用 Metal System Trace 检查加载是否足够早。 session 提到 Xcode 14 的 Metal System Trace 可以观察 load 操作的编码和执行时间。把红色未加载 tile 的出现时间和 IO 队列活动对齐,就能判断加载请求是否发得太晚。
关联 Session
- Discover Metal 3 — 先看 Metal 3 总览,了解 Fast Resource Loading 在 Metal 3 新能力中的位置。
- Target and optimize GPU binaries with Metal 3 — 资源加载解决 IO 等待,GPU 二进制优化解决 shader 编译等待,两者都面向首屏和关卡加载卡顿。
- Boost performance with MetalFX Upscaling — Fast Resource Loading 负责更快送入高质量资源,MetalFX 负责用更低渲染成本输出高分辨率画面。
- Maximize your Metal ray tracing performance — 同属 Metal 3 游戏性能主题,适合继续研究光线追踪场景的加载、构建和调试成本。
评论
GitHub Issues · utterances