资源对象:缓存和纹理
本章节描述 Metal 的资源对象 (MTLResource
) ,这些对象可以存储非格式化的内存数据或者格式化的图像数据,有如下两种 MTLResource
类型的对象:
MTLBuffer
表示一块非格式化的内存,它可以存放任何类型的数据。它通常用于存放定点数据,着色器数据和计算状态数据。MTLTexture
表示一块有格式的图像数据,有特定的纹理类型和像素格式。纹理对象通常被用作定点着色程序,片段着色程序,或是并行计算着色程序的纹理源,它也可以存放图形渲染的输出结果。
MTLSamplerState
类型的对象也将在这个章节讲述。虽然采样器不是资源,但它总是用于纹理对象的查找计算。
缓存是无类型的内存判断
一个 MTLBuffer
对象表示一个一个装载任一类型数据的内存片段。
创建一个缓存对象
下面的 MTLDevice
方法用于创建并返回一个 MTLBuffer
对象:
newBufferWithLength:options:
该方法分配一块新的内存来创建一个MTLBuffer
对象。newBufferWithBytes:length:options:
该方法分配一块新的内存来创建一个MTLBuffer
对象,并从已有的存储中 (由 CPU 内存地址指针
指定) 拷贝数据到新分配的内存中。newBufferWithBytesNoCopy:length:options:deallocator:
该方法使用一个已经存在的内存来创建一个MTLBuffer
对象,不会为这个缓存对象分配新的内存。
以上所有的创建方法都有个叫做 length
的入参,它用来指定分配存储的大小(单位是 byte)。 它们还有个叫做 options
的入参,它是一个 MTLResourceOptions
类型对象,用来设定缓存创建的行为。options
的默认值是0。
缓存对象方法
MTLBuffer
协议有如下方法:
contents
该方法返回缓存对象对应内存的 CPU 地址。- The
newTextureWithDescriptor:offset:bytesPerRow:
该方法创建某种特定类型的纹理对象。这个方法在 Creating a Texture Object 中详细介绍。
纹理是格式化的图像数据
一个 MTLTexture
对象代表了一个格式化的图像数据内存片段,它在顶点着色程序、片段着色程序、并行计算着色程序中被作为输入源,或是作为渲染操作的目标输出。一个 MTLTexture
对象有如下之一结构:
- 一个 1D, 2D, 或者 3D 图像
- 一个含有 1D 或者 2D 图像的数组
- 拥有6个 2D 图像的立方体
MTLPixelFormat
属性描述了一个 MTLTexture
对象中每个单独像素的组织排列。详见 Pixel Formats for Textures.
创建纹理对象
下列的方法用于创建并返回一个 MTLTexture
对象:
newTextureWithDescriptor:
这是一个MTLDevice
方法,该方法新分配内存创建一个个MTLTexture
对象,创建时需要传入的MTLTextureDescriptor
类型的参数描述了纹理的属性。newTextureViewWithPixelFormat:
这是一个MTLTexture
方法,该方法创建出来的MTLTexture
对象和调用源对象共享数据。因为共享存储,所以新创建出来的对象的像素发生了任何改变,调用源对象的像素也随之变化,反之亦然。对于新创建出来的纹理对象,newTextureViewWithPixelFormat:
方法重新解释了调用源纹理对象对应的内存中的图像数据是一种什么格式。MTLPixelFormat
类型的入参,必须和调用源纹理对象的像素格式属性相适配。 (详见 Pixel Formats for Textures.)newTextureWithDescriptor:offset:bytesPerRow:
这是一个MTLBuffer
方法,它创建一个MTLTexture
对象,共享调用源对象的内存,作为它自己的图像数据。因为共享存储,所以新创建出来的对象的像素发生了任何改变,调用源对象的像素也随之变化,反之亦然。纹理对象和缓存对象共享内存将阻碍实施某些纹理优化操作,比如像素混合( pixel swizzling )或分片( tiling )。
通过纹理 Descriptor 创建纹理对象
MTLTextureDescriptor
描述用于创建一个 MTLTexture
对象的各属性。包括图形尺寸(宽、高、深),像素格式,组合模式(数组或是立方体)还有 mipmaps 的数量。这些 MTLTextureDescriptor
属性都只用在创建 MTLTexture
对象的过程中。当纹理对象创建完毕后, MTLTextureDescriptor
对象值的改变不再影响之前有它创建的那个纹理对象。
通过一个 descriptor 创建一个或者多个纹理对象:
- 创建一个
MTLTextureDescriptor
对象,它包含各种描述纹理数据的属性:textureType
,该属性用来指定纹理的维度和组合模式(数组或是立方)。width
,height
,depth
,这些属性指定了基层 mipmap 纹理在各个维度的尺寸。pixelFormat
,该属性指定了一个纹理中每个像素的存储方式。arrayLength
,对于MTLTextureType1DArray
或者MTLTextureType2DArray
类型的纹理对象,这个属性指定数组中元素的数量。mipmapLevelCount
,该属性指定 mipmap 的层数。sampleCount
,该属性指定每个像素的样本数。resourceOptions
,该属性指定内存分配的方式。
- 通过
MTLDevice
对象的newTextureWithDescriptor:
方法创建一个新的纹理对象,首先要提供一个MTLTextureDescriptor
对象。 当纹理创建完毕后,调用replaceRegion:mipmapLevel:slice:withBytes:bytesPerRow:bytesPerImage:
方法载入纹理图像数据。详见 Copying Image Data to and from a Texture. - 同一个
MTLTextureDescriptor
对象,在做必要的修改后,可以继续用来创建其他MTLTexture
对象。
列表 3-1 展示了一段代码,创建纹理 descriptor 对象 txDesc
,并且设置它为 3D 纹理,长宽高都是64。
列表 3-1 通过纹理 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 <MTLTexture> aTexture = [device newTextureWithDescriptor:txDesc];
纹理切片
一个纹理切片是一个单独的 1D, 2D, 或者 3D 纹理图像以及和它相关联的 mipmaps,对于每一个切片:
- 其基层的 mipmap 的尺寸由
MTLTextureDescriptor
对象的width
,height
, 以及depth
属性决定。 - mipmap 第 i 层的尺寸计算公式是 max(1, floor(
width
/ 2^i)) x max(1, floor(height
/ 2^i)) x max(1, floor(depth
/ 2^i))。mipmap 层数的最大值,是当尺寸为 1 x 1 x 1 时,对应的 i 值。 - 在一个切片中 mipmap 层数的计算公式为floor(log2(max(
width
,height
,depth
)))+1.
所有的纹理对象都至少有一个切片。立方和数组类型的纹理可能有多个切片。在读写纹理图像数据的方法(详见 Copying Image Data to and from a Texture)里, slice
是一个0-based 的值。对应 1D, 2D, 或者 3D 类型的纹理,它们只有一个切片,所以它们的 slice
值必须是0。一个立方纹理有6个 2D 切片,地址从0到5。对于 1DArray 和 2DArray 类型的纹理,每个数组元素代表一个切片。比如一个 arrayLength
= 10 的 2DArray 类型的纹理,它拥有10个切片,地址从0到9。从一个整体纹理结构中选择一个单独的图像(1D, 2D, 或者 3D 图像),首先要指定一个切片,然后再选择切片上的 mipmap 层级。
通过快捷(convenience)方法创建纹理 Descriptor
对于一个 2D 或者立方纹理来说,可以使用如下的快捷方法创建 MTLTextureDescriptor
对象,并且自动设置多个属性值:
texture2DDescriptorWithPixelFormat:width:height:mipmapped:
,该方法创建一个描述 2D 纹理的MTLTextureDescriptor
对象。入参width
和height
定义 2D 纹理的尺寸。 descriptor 的type
属性自动设置为MTLTextureType2D
,属性depth
和arrayLength
自动设置为1。textureCubeDescriptorWithPixelFormat:size:mipmapped:
,该方法创建一个描述立方纹理的MTLTextureDescriptor
对象,type
属性被自动设置为MTLTextureTypeCube
,width
和height
由入参 size 设置,depthh
和arrayLength
属性自动设置为1。
上面两个 MTLTextureDescriptor
的快捷方法都接受一个入参 pixelFormat
,它定义了纹理的像素格式。这两个快捷方法还都接受一个入参 mipmapped
,它指定纹理是否支持 mipmap。 (如果 mipmapped
值为 YES
,表示支持 mipmap。)
列表 3-2 使用 texture2DDescriptorWithPixelFormat:width:height:mipmapped:
方法创建了一个 64x64
的不支持 mipmap 的 2D 纹理对象。
Listing 3-2 通过快捷方法 创建纹理对象
MTLTextureDescriptor *texDesc = [MTLTextureDescriptor
texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm
width:64 height:64 mipmapped:NO];
id <MTLTexture> myTexture = [device newTextureWithDescriptor:texDesc];
拷贝图像数据进出纹理对象
可以使用如下方法,来以同步方式实现拷贝图像数据进出纹理对象的内存:
replaceRegion:mipmapLevel:slice:withBytes:bytesPerRow:bytesPerImage:
从 withBytes 参数表示的资源对象中拷贝指定区域的像素数据到一个指定的纹理切片的指定内存区域。replaceRegion:mipmapLevel:withBytes:bytesPerRow:
方法和前一个方法类似,只是 slice-related 参数的默认值不同。 (i.e.,slice
= 0 以及bytesPerImage
= 0).getBytes:bytesPerRow:bytesPerImage:fromRegion:mipmapLevel:slice:
获取指定纹理切片的指定区域的像素数据。getBytes:bytesPerRow:fromRegion:mipmapLevel:
方法和前一个方法类似,只是 slice-related 参数的默认值不同。(slice
= 0 以及bytesPerImage
= 0).
列表 3-3 展示如何调用 replaceRegion:mipmapLevel:slice:withBytes:bytesPerRow:bytesPerImage:
方法将系统内存中的资源对象 textureData
对应的图像数据,设定到纹理对象 tex 的 slice 0
以及 mipmap 第 0
层中。
列表 3-3 拷贝图像数据到纹理中
// 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];
纹理的像素格式
MTLTexture
对象的 MTLPixelFormat
属性指定颜色、深度和模板缓存数据中每一个像素如何组织。有3种像素格式:原生,紧密填充和压缩。
- 原生格式只有8、16或是32位颜色值。每个分量以升序内存地址排序,第一个分量处于最低的内存地址处。举个例子,
MTLPixelFormatRGBA8Unorm
是一个32位格式颜色值,每8位表示一个颜色分量,那么最低位内存地址保存红色分量,接下去的地址保存绿色分量。而对于MTLPixelFormatBGRA8Unorm
类型的颜色值,最低位内存地址保存的是蓝色分量,接下去的地址是绿色分量。 - 紧密填充格式把多个颜色分量结合起来存放在一个16位或是32位的值中,这些颜色分量从最低位(LSB)向最高位(MSB)填充。例如:
MTLPixelFormatRGB10A2Uint
是一个32位的填充格式颜色值,它含有3个10位长的颜色通道(分别存放RGB分量)以及一个2位的 alpha 分量。 压缩格式用于把像素排列成块,每个块的布局被设定为这种像素格式。压缩格式只能被用于 2D,2D 数组或是立方类型的纹理。它不能被用于创建 1D,2D 多重采样或是 3D 类型纹理。
MTLPixelFormatGBGR422
和MTLPixelFormatBGRG422
是两种特殊的像素格式,他们用于存储 YUV 颜色空间的像素。 这种格式只支持不含 mipmap 并且width
为偶数的 2D 纹理(不包括 2D 数组 和立方纹理)。
还有几种支持 sRGB 颜色空间的像素格式 (比如 MTLPixelFormatRGBA8Unorm_sRGB
和 MTLPixelFormatETC2_RGB8_sRGB
)。当采用操作用到具有 sRGB 格式的纹理时,Metal 将在采用操作前把 sRGB 颜色空间的颜色分量转换成线性颜色空间分量。转换公式如下:(S 表示 sRGB颜色, L表示线性颜色空间颜色
- If S <= 0.04045, L = S/12.92
- If S > 0.04045, L = ((S+0.055)/1.055)2.4
相反的,当使用 sRGB 像素格式渲染一个 attachment 的时候,会按照下面的公式将线性颜色空间颜色值转回为 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.
创建一个用于纹理查找的采样器 State (Sampler States )
一个 MTLSamplerState
对象定义了寻址、过滤等其他属性,用于一个图形着色程序或者并行计算程序对一个 MTLTexture
对象实施采样操作。一个采样器 Descriptor 定义了一个采样器 State 对象的属性。创建一个采样器 State 的具体步骤如下:
- 创建一个
MTLSamplerDescriptor
类型对象,来定义采样器 state 属性。( 原文:Call thenewSamplerStateWithDescriptor:
method of aMTLDevice
object to create aMTLSamplerDescriptor
object. PS: 这里新版本的文档应该描述有误,旧版本的为:First create a MTLSamplerDescriptor to define the sampler state properties,这里按照旧版本的翻译,才能解释的通。) - 为这个
MTLSamplerDescriptor
对象设置相应的值,包括过滤操作(filtering options),寻找方式 (addressing modes),最大的异向性 (maximum anisotropy),以及细节层次 (level-of-detail) 的参数。 - 调用
MTLDevice
对象的newSamplerStateWithDescriptor:
方法,使用先前创建的 descriptor,就可以创建一个MTLSamplerState
对象。
你可以仅仅修改 descriptor 的必要属性的值后,重用它创建更多的 MTLSamplerState
对象。descriptor 的属性值仅仅在创建采样器 state 时生效,当一个采样器 state 创建完毕后,改变 descriptor 的属性值不会影响已经创建的采样器 state 对象。
列表 3-4 示例代码展示了如果创建并设置一个 MTLSamplerDescriptor
对象,然后用它来创建一个 MTLSamplerState
对象。 descriptor 对象的过滤(filter)和寻址模式(address mode)属性没有默认值。最后 newSamplerStateWithDescriptor:
方法使用创建的 descriptor 对象,创建一个采样 state 对象。
列表 3-4 创建一个采样器 state 对象
// 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 <MTLSamplerState> sampler = [device newSamplerStateWithDescriptor:desc];
在 CPU 和 GPU 内存间保持一致性
CPU和 GPU 都可以访问一个 MTLResource
对象管理的存储数据。但是 GPU 和 CPU 的操作都是异步进行的,所以当 CPU 访问这些资源对应的存储时,有如下注意事项:
当执行一个 MTLCommandBuffer
对象, MTLDevice
对象只能保证观察到由 CPU 引起的和 MTLCommandBuffer
对象相关的那些 MTLResource
对象存储上产生的变化,并且这些变化是在 MTLCommandBuffer
对象被提交之前产生的。也就说,在 MTLCommandBuffer
对象被提交之后 (这时,MTLCommandBuffer
对象的 status
属性值为 MTLCommandBufferStatusCommitted
),MTLDevice
对象就观察不到由 CPU 引起的这些资源的变化情况。
类似的,当 MTLDevice
对象执行完一个 MTLCommandBuffer
对象后(这时 MTLCommandBuffer
对象的 status
属性值为 MTLCommandBufferStatusCompleted
),CPU 只保证能观察到由 MTLDevice
对象引起的 command buffer 相关的那些资源文件存储上的变化。