资源对象:缓冲和纹理
这一章节将讨论 Metal 中存储未格式化数据和格式化图片数据的资源对象 MTLResource,它主要分为两类
- MTLBuffer 代表一个未格式化数据的存储空间,能够保存保存任意类型数据,缓冲经常用于顶点、着色器以及计算状态数据
- MTLTexture 代表一个有确定类型和像素格式的格式化图片数据的存储空间,常作为顶点、片段和计算函数的源纹理,或者存储图形渲染的输出(即作为附件)
本章也会讨论 MTLSamplerState。尽管采样器本身并不是资源,但它们常被用于对一个纹理对象执行查找计算。
缓冲是内存的无类型分配
一个 MTLBuffer 对象代表着一个能装载任意类型数据的内存分配空间。
创建一个缓冲对象
下面这些 MTLDevice 方法可以创建并返回一个 MTLBuffer 对象
-
newBufferWithLength:options:
使用新的存储分配空间创建一个 MTLBuffer 对象 -
newBufferWithBytes:length:options:
从一片已知的内存空间(通过 CPU 地址指针寻址)中拷贝数据到新的存储分配空间并创建一个 MTLBuffer 对象 -
newBufferWithBytesNoCopy:length:options:deallocator:
不创建新的存储空间,直接将现有的存储空间创建为一个 MTLBuffer 对象
所有缓冲创建方法都需要一个表示存储空间大小的 length 参数,它的单位是字节,同时所有创建方法还接受一个 MTLResourceOptions 对象作为 options,从而可以修改创建出来的缓冲的特性。如果 options 传 0,则采用默认值。
缓冲方法
MTLBuffer 协议有以下方法
-
contents
方法返回缓冲的存储空间的 CPU 地址 -
newTextureWithDescriptor:offset:bytesPerRow:
方法返回一个特定类型的纹理对象,这个纹理对象指向缓冲内的数据。更多信息请查阅 创建一个纹理对象
纹理是格式化的图像数据
一个 MTLTexture 对象代表了一个格式化后的图像数据的内存空间,它可以被用于顶点着色器、片段着色器和计算函数的资源,或者作为一个渲染目标附件。MTLTexture 支持以下几种数据结构:
- 1D,2D,3D 图像
- 1D,2D,3D 图像数组
- 六个 2D 图像的立方体
MTLPixelFormat 确定了纹理对象中单个像素的结构,详细的像素格式请查阅 纹理中的像素格式
创建一个纹理对象
下面这些方法可以创建并返回一个纹理对象:
- MTLDevice 类的
newTextureWithDescriptor:
方法为纹理图像数据开辟一个新的内存空间并创建一个 MTLTexture 对象,它将根据传入的 MTLTextureDescriptor 对象设置此纹理的属性 - MTLTexture 类的
newTextureViewWithPixelFormat:
方法创建一个与调用者共享内存空间的 MTLTexture 对象,由于它们共享相同的内存空间,所有在新纹理对象上的改动都会反映到调用对象上,反之亦然。对于新创建的纹理,newTextureViewWithPixelFormat:
将会重新按照传入的像素格式诠释调用对象内存储的图像数据。传入的 MTLPixelFormat 必须与原调用对象的 MTLPixelFormat 兼容(查阅 纹理像素格式 了解 ordinary, packed 和 compressed 格式) - MTLBuffer 类的
newTextureWithDescriptor:offset:bytesPerRow:
方法创建一个与调用者共享内存空间的 MTLTexture 对象,由于它们共享相同的内存空间,所有在新纹理对象上的改动都会反映到调用对象上,反之亦然。在纹理和缓冲之间共享存储空间会防止使用某些纹理优化,如像素调整或平铺。
利用纹理描述符创建一个纹理对象
MTLTextureDescriptor 类定义了创建一个纹理对象所需要的属性,包括图片尺寸(宽、高、深度)、像素格式、布局格式(数组或立方体)以及 mipmap 的个数。MTLTextureDescriptor 仅用于创建 MTLTexture 对象的过程中,一旦创建完成,对 MTLTextureDescriptor 的属性修改将不再对 MTLTexture 生效。
利用描述符创建一个或多个纹理对象的方法如下:
1 创建一个 MTLTextureDescriptor 对象,包含描述纹理数据的纹理属性:
- textureType 确定纹理的维度和布局
- width、height、depth 确定基级纹理 mipmap 的每一个维度的像素尺寸
- pixelFormat 确定纹理如何存储一个像素
- arrayLength 确定 MTLTextureType1DArray 或 MTLTextureType2DArray 类型的纹理对象中,数组元素的个数
- mipmapLevelCount 确定 mipmap 层级的个数
- sampleCount 确定每一个像素的样本个数
2 调用 MTLDevice 类的 newTextureWithDescriptor:
方法,利用 MTLTextureDescriptor 描述符创建一个纹理对象。创建完纹理后,调用 replaceRegion:mipmapLevel:slice:withBytes:bytesPerRow:bytesPerImage:
方法加载纹理图片数据,详细查阅 复制图片数据到纹理以及从纹理复制出图片数据
3 可以复用 MTLTextureDescriptor 对象,修改其中属性以创建出更多的纹理
listing 3-1 代码展示了如何创建一个纹理描述符 txDesc,并设置它的属性为 3D, 64x64x64 纹理
//Listing 3-1 Creating a Texture Object with a Custom Texture Descriptor
MTLTextureDescriptor* txDesc = [[MTLTextureDescriptor alloc] init];
txDesc.textureType = MTLTextureType3D;
txDesc.height = 64;
txDesc.width = 64;
txDesc.depth = 64;
txDesc.pixelFormat = MTLPixelFormatBGRA8Unorm;
txDesc.arrayLength = 1;
txDesc.mipmapLevelCount = 1;
id aTexture = [device newTextureWithDescriptor:txDesc];
使用纹理切片
一个纹理切片就是一个单独的 1D、2D、3D 纹理图片及所有与之相关的 mipmap。对于每一个切片:
- 基级 mipmap 的尺寸由纹理描述符 MTLTextureDescriptor 对象的 width、height、depth 属性确定
- i 级 mipmap 的缩放尺寸由
max(1, floor(width / 2i)) x max(1, floor(height / 2i)) x max(1, floor(depth / 2i))
确定,顶级 mipmap 的尺寸为 1X1X1 - 一个切片的 mipmap 层级可以由
floor(log2(max(width, height, depth)))+1
确定
所有纹理都有至少一个切片,立方体和数组类型的纹理可能有多个切片。在 复制图片数据到纹理以及从纹理复制出图片数据 中讨论的读写纹理图片数据的方法里,slice 是一个从 0 开始的值。对于 1D、2D、3D 纹理,由于只有一个切片,所以 slice 必须为 0。对于立方体纹理由于有 6 个 2D 纹理,所以 slice 从 0 到 5。对于 1DArray 和 2DArray 纹理类型,每一个数组元素代表一个切片。从整体纹理结构中选择单个 1D,2D 或 3D 图像时,首先选择一个切片,其次选择一个切片内的 mipmap 层级。
便捷方式创建纹理描述符
对于一般的 2D 和立方体纹理,使用下面的便捷方式会创建一个 MTLTextureDescriptor 对象,并自动填充一些属性:
-
texture2DDescriptorWithPixelFormat:width:height:mipmapped:
方法为 2D 纹理创建一个 MTLTextureDescriptor 对象,其中 width 和 height 参数定义 2D 纹理的尺寸,type 参数自动设置为 MTLTextureType2D,depth 和 arrayLength 被设置为 1 -
textureCubeDescriptorWithPixelFormat:size:mipmapped:
方法为一个立方体纹理创建 MTLTextureDescriptor 对象,type 设置为 MTLTextureTypeCube,width 和 height 被设置为 size 参数的值,depth 和 arrayLength 被设置为 1
两个便捷方法都接受一个 pixFormat 输入参数用于定义纹理的像素格式,一个 mipmapped 参数用于定义纹理是否使用纹理切片。
listing 3-2 使用 texture2DDescriptorWithPixelFormat:width:height:mipmapped:
方法创建了一个 64*64 未使用纹理切片的 2D 纹理描述符对象
MTLTextureDescriptor *texDesc = [MTLTextureDescriptor
texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm
width:64 height:64 mipmapped:NO];
id myTexture = [device newTextureWithDescriptor:texDesc];
复制图片数据到纹理以及从纹理复制出图片数据
下面的方法支持复制图片数据到纹理以及从纹理复制出图片数据:
-
replaceRegion:mipmapLevel:slice:withBytes:bytesPerRow:bytesPerImage:
从调用者指针指向的纹理存储空间中拷贝一部分区域的数据到一个确定的纹理切片中,replaceRegion:mipmapLevel:withBytes:bytesPerRow:
是一个类似的便捷方法,它将拷贝数据到默认切片中,并设置其他相关参数为默认参数(slice = 0,bytesPerImage = 0) -
getBytes:bytesPerRow:bytesPerImage:fromRegion:mipmapLevel:slice:
从一个确定纹理切片中获取一段数据。getBytes:bytesPerRow:fromRegion:mipmapLevel:
是一个类似的便捷方法,它将从默认切片中获取一段数据,并设置其他相关参数为默认参数(slice = 0,bytesPerImage = 0)
listing 3-3 展示了如何调用 replaceRegion:mipmapLevel:slice:withBytes:bytesPerRow:bytesPerImage:
方法来通过系统内存中一段源数据创建一个纹理图片
// pixelSize is the size of one pixel, in bytes
// width, height - number of pixels in each dimension
NSUInteger myRowBytes = width * pixelSize;
NSUInteger myImageBytes = rowBytes * height;
[tex replaceRegion:MTLRegionMake2D(0,0,width,height)
mipmapLevel:0 slice:0 withBytes:textureData
bytesPerRow:myRowBytes bytesPerImage:myImageBytes];
纹理像素格式
MTLPixelFormat 确定了一个 MTLTexture 对象中单个像素数据存储空间中的颜色、深度、模板数据的组织方式,大致有三类像素格式:ordinary,packed,compressed。
- ordinary 格式仅包含正常的 8,16,32 位颜色分量,每个分量都安排在递增的内存地址中,第一个列出的组件位于最低地址。例如, MTLPixelFormatRGBA8Unorm 代表每个颜色分量有 8 比特位,最低地址位是红色分量,接下来是绿色,以此类推。
- packed 格式混合多个分量到一个 16 或 32位值中,分量从低位向高位存储。例如,MTLPixelFormatRGB10A2Uint 是一个 32 位 packed 格式,其中包含 3 个 10 比特位的颜色通道,2 bit 位的 alpha 通道。
- compressed 格式以像素块排列,每个像素块的布局特定于一种像素格式。compressed 格式仅能用于 2D,2D 数组或立方体纹理类型,不能用于 1D,3D 或 2D 多重采样类型。
MTLPixelFormatGBGR422 和 MTLPixelFormatBGRG422 格式用于存储 YUV 颜色空间格式的像素,这些格式仅支持 2D 纹理,不支持 2D 数组或立方体纹理,没有切片,宽度为偶数。
许多像素格式存储的是 sRGB 规格的颜色分量(例如 MTLPixelFormatRGBA8Unorm_sRGB 或 MTLPixelFormatETC2_RGB8_sRGB)。当一个采样操作引用到了一个 sRGB 格式的纹理时,Metal 会将 sRGB 色值转换为线性颜色空间色值,再执行采样操作。从一个 sRGB 格式的分量 S 转换为线性分量 L 的公式如下
- If S <= 0.04045, L = S/12.92
- If S > 0.04045, L = ((S+0.055)/1.055)2.4
相反,当需要渲染到一个使用 sRGB 格式纹理的色彩渲染附件时,需要将线性颜色色值转换为 sRGB 色值
- If L <= 0.0031308, S = L * 12.92
- If L > 0.0031308, S = (1.055 * L0.41667) - 0.055
有关渲染过程中的像素格式请查阅 [创建一个渲染过程描述符](Creating a Render Pass Descriptor)
为纹理查询创建采样状态对象
MTLSamplerState 对象定义了当一个图形或计算函数对一个 MTLTexture 纹理对象执行纹理采样操作时用到的寻址、滤波和其他属性。采样描述符定义了一个采样状态对象的属性,创建一个采样状态对象的过程如下:
- 调用 MTLDevice 的
newSamplerStateWithDescriptor:
方法创建一个 MTLSamplerDescriptor 对象 - 在采样描述符中定义所需属性,包括滤波选项、寻址模式、最大各向异性、细节层次参数
- 通过调用 MTLDevice 的
newSamplerStateWithDescriptor:
方法从一个采样描述符创建一个 MTLSamplerState 对象
你可以重复使用采样描述符来创建多个采样状态对象,还可以修改所需要的描述符的属性。描述符的属性值仅在创建采样状态对象过程中生效。一旦采样状态对象生成了,再修改描述符的属性将不会对采样状态对象产生任何影响。
Listing 3-4 是一个示例代码,通过创建并配置一个 MTLSamplerDescriptor 对象来创建一个 MTLSamplerState 对象,其中定义了这个描述符的滤波和寻址模式属性。然后调用了 newSamplerStateWithDescriptor:
,利用采样描述符创建了一个采样状态对象。
// create MTLSamplerDescriptor
MTLSamplerDescriptor *desc = [[MTLSamplerDescriptor alloc] init];
desc.minFilter = MTLSamplerMinMagFilterLinear;
desc.magFilter = MTLSamplerMinMagFilterLinear;
desc.sAddressMode = MTLSamplerAddressModeRepeat;
desc.tAddressMode = MTLSamplerAddressModeRepeat;
// all properties below have default values
desc.mipFilter = MTLSamplerMipFilterNotMipmapped;
desc.maxAnisotropy = 1U;
desc.normalizedCoords = YES;
desc.lodMinClamp = 0.0f;
desc.lodMaxClamp = FLT_MAX;
// create MTLSamplerState
id sampler = [device newSamplerStateWithDescriptor:desc];
维护 CPU 与 GPU 内存间相干性
CPU 和 GPU 均能访问一个 MTLResource 对象的底层存储,但是 GPU 和主 CPU 的操作是异步的,所以在使用主 CPU 方法访问这些资源的存储时要牢记下面的要点。
当执行一个 MTLCommandBuffer 对象时,MTLDevice 对象仅能保证在 MTLCommandBuffer 对象被提交之前观察到主机 CPU 对这个 MTLCommandBuffer 引用到的 MTLResource 对象所指向的内存空间的任何改动。一旦相应的 MTLCommandBuffer 被提交,则 MTLDevice 就无法再观察到主 CPU 对资源所做的修改(即,MTLCommandBuffer 对象的 status 为 MTLCommandBufferStatusCommitted)。
类似的,在 MTLDevice 执行完一个 MTLCommandBuffer 对象后,主 CPU 仅在命令缓冲完全被完成时,才能保证观察到 MTLDevice 通过命令缓冲对任何引用到的资源内存空间的修改(即,MTLCommandBuffer 对象的 status 为 MTLCommandBufferStatusCompleted)。