Resource Objects: Buffers and Textures

  • Buffers Are Typeless Allocations of Memory
  • Textures Are Formatted Image Data
  • Creating a Sampler States Object for Texture Lookup
  • Maintaining Coherency Between CPU and GPU Memory

  • 本章介绍存储无格式内存和有格式图片数据的metal resource object(MTLResource),分为2种类型:

    MTLBuffer 表示无格式内存,可以包含任何类型的数据。常被用于vertex、shader、compute state data

    MTLTexture 表示有格式的图片数据,包含一个特定的贴图类型和像素格式。texture被用于作为vertex、fragment、compute function的输入,也可以作为一个attachment用于存储graphic rendering ouput

    本章还会讨论 MTLSamplerState,虽然sample并非resource,它们会在执行lookup texture的时候被使用

    Buffers Are Typeless Allocations of Memory

    MTLBuffer 包含一块内存用来存放任意类型的数据

    Creating a Buffer Object

    可以使用MTLDevice的下列方法创建MTLBuffer

    • newBufferWithLength:options:用于创建一个分配了内存的MTLBuffer
    • newBufferWithBytes:length:options:用于从已有内存(一个cpu地址的指针)复制到一个新的内存块的方式创建一个MTLBuffer
    • newBufferWithBytesNoCopy:length:options:deallocator:用于根据一个已有内存创建一个MTLBuffer(不分配新内存)

    以上所有的函数都包含输入参数length,来表示分配内存的尺寸,单位是bytes。所有的函数也都可以通过参数options传入一个MTLResourceOptions来修改buffer的behavior。如果options为0,则将使用默认值。

    Buffer Methods

    MTLBuffer 有如下函数

    • contents 获取buffer对应的cpu地址
    • newTextureWithDescriptor:offset:bytesPerRow: 使用buffer data创建一个texture

    Textures Are Formatted Image Data

    MTLTexture 表示有格式的图片数据,被用于作为vertex、fragment、compute function的输入,也可以作为render destination。MTLTexture 可以是如下结构体

    • A 1D, 2D, or 3D image
    • An array of 1D or 2D images
    • A cube of six 2D images

    MTLPixelFormat 用于指定 MTLTexture 的像素结构。

    Creating a Texture Object

    可以使用下列方法创建 MTLTexture

    • MTLDevice 的 newTextureWithDescriptor:方法使用一块新的用于存放texture image data的内存来创建 MTLTexture,该api中通过 MTLTextureDescriptor 来描述texture的属性
    • MTLTexture 的 newTextureViewWithPixelFormat: 方法创建一个 MTLTexture 与原MTLTexture共享同一块内存。由于它们共享同一块内存,所以改动任意一个 texture都会影响另外一个。newTextureViewWithPixelFormat:创建的新的 texture,将按照新的像素格式重新解释 老的texture已有的texture image data。新的贴图的 MTLPixelFormat 必须和原始贴图的 MTLPixelFormat 兼容(后面章节会详细介绍普通的、packed、compressed pixel format)
    • MTLBuffer 的 newTextureWithDescriptor:offset:bytesPerRow: 方法创建一个 MTLTexture 与原MMTLBuffer共享同一块内存。由于它们共享同一块内存,所以改动任意一个都会影响另外一个。buffer和texture共享内存,会导致texture的优化失效,比如pixel swizzling or tilling

    Creating a Texture Object with a Texture Descriptor

    创建 MTLTexture的时候 MTLTextureDescriptor 被用于定义属性,包括图像尺寸(宽、高、深度)、pixel format、arrangement(array、cubemap)以及mipmap的数量。MTLTextureDescriptor 值在创建 MTLTexture 的时候有用,当创建完毕后, 改变 MTLTextureDescriptor 的属性将对已经创建的 texture 没有任何影响

    使用 descriptor 创建一个或者多个 texture

    • 创建一个包含 texture 属性的 MTLTextureDescriptor :
      • textureType 表示 texture的dimensionality 和 arrangement(array or cube)
      • width、height、depth用于表明 texture base level mipmap中每一个dimension的pixel size
      • pixelFormat表明 texture中的像素存储方式
      • arrayLength 表明 MTLTextureType1DArray or MTLTextureType2DArray 类型 texture的数组元素的数量
      • mipmapLevelCount 表明texture mipmap的数量
      • sampleCount 表明每个pixel的对应的sample数量
      • resourceOptions 表明内存分配的方式
    • 通过 MTLDevice 的 newTextureWithDescriptor: 方法根据 MTLTextureDescriptor 创建一个texture。创建完毕后,调用 replaceRegion:mipmapLevel:slice:withBytes:bytesPerRow:bytesPerImage: 方法来加载texture image data
    • 如果要创建更多的MTLTexture,可以重复使用这个 MTLTextureDescriptor,如果需要,还可以修改这个object的属性

    下面代码用于创建一个texture descriptor txDesc,将其属性设置为 3D,64*64*64,并创建对应的 texture

        	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];
        

    Working with Texture Slices

    一个slice代表一个 1D、2D、3D texture image以及它关联的所有mipmaps,每个slice:

    • base level mipmap的size是由 MTLTextureDescriptor 的 width, height, and depth 属性指定
    • mipmap i的尺寸的计算方式是:max(1, floor(width / 2i)) , max(1, floor(height / 2i)) ,max(1, floor(depth / 2i))。最大的mipmap level是第一个达到1*1*1尺寸的mipmap level
    • 一个slice的mipmap数量的计算方式是:floor(log2(max(width, height, depth)))+1

    每个texture至少包含一个slice。cube、array 可能会包含多个slices。当写入或者读取textureimage data的时候,slice是从0开始计算的输入参数。1D、2D、3D texture,只有一个slice,所以slice为0.一个cube texture包含6 2D slice,slice从0到5。1DArray和2Darray,每个数组元素表示一个slice。比如,一个2DArray texture,arrayLength=10,代表有10个slice,从0-9。从一个texture structure选择一个1D、2D、3D image,首先先选择一个slice,然后再选择该slice的一个mipmap

    Creating a Texture Descriptor with Convenience Methods

    创建普通的2D和cube texture的时候,使用下面的convenience 方法来创建 MTLTextureDescriptor,其中一些属性将被自动设置

    • texture2DDescriptorWithPixelFormat:width:height:mipmapped: 方法用于创建一个2D texture所需要的 MTLTextureDescriptor,其中width和height定义2D texture的dimension。type会被自动设置为MTLTextureType2D,depth和arrayLength为1
    • textureCubeDescriptorWithPixelFormat:size:mipmapped: 方法用于创建一个 cube texture所需要的 MTLTextureDescriptor,其中width和height定义尺寸。type会被自动设置为 MTLTextureTypeCube ,depth和arrayLength为1

    以上两个方法都可以传入 pixelFormat 用于定义texture 的pixe format。也都可以穿入 mipmapped,用于定义texture是否被mipmap(如果mipmapped为YES,则texture被mipmapped)

    下面代码使用 texture2DDescriptorWithPixelFormat:width:height:mipmapped: 方法用于创建一个64*64 的 没有mipmapped的2D texture 所对应的texture descriptor

        	MTLTextureDescriptor *texDesc = [MTLTextureDescriptor 
             texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm 
             width:64 height:64 mipmapped:NO];
    		id <.MTLTexture> myTexture = [device newTextureWithDescriptor:texDesc];
        

    Copying Image Data to and from a Texture

    从一个MTLTexture中copy data或者写入data,可以使用下面方法

    • replaceRegion:mipmapLevel:slice:withBytes:bytesPerRow:bytesPerImage: 从调用者的指针将数据复制到指定texture slice的storage中。replaceRegion:mipmapLevel:withBytes:bytesPerRow:是一个类似的convenience 方法,将pixel data复制到默认的slice中,assume slice-related参数为默认值(slice=0,bytesPerImage=0)
    • getBytes:bytesPerRow:bytesPerImage:fromRegion:mipmapLevel:slice: 从指定texture slice中获取一个区域的pixel data。getBytes:bytesPerRow:fromRegion:mipmapLevel:是一个类似的convenience 方法,从默认的slice中获取一个区域的pixel data,assume slice-related参数为默认值(slice=0,bytesPerImage=0)

    下面代码使用 replaceRegion:mipmapLevel:slice:withBytes:bytesPerRow:bytesPerImage: 方法使用system memory中的source data textureData 指定slice 0、mipmap 0的texture image

        	//  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];
        

    Pixel Formats for Textures

    MTLPixelFormat 指定 MTLTexture中一个像素的color、depth、stencil数据存储。有三种类型的pixel format:普通、packed、compressed

    • 普通格式拥有传统的8-、16-、32- color component。每个component的内存都是按照第一个componeng放在最低位,其它依次提升分配。比如。MTLPixelFormatRGBA8Unorm是32bit类型,每个color component拥有8bit。lowest address存放red,其次是greed,以此类推。相应的MTLPixelFormatBGRA8Unorm,最低位保存blue,其次是green,以此类推。
    • packed format将多个component合并到一个16-bit或者一个32-bit值中,component的存储方式是least to most significant bit(LSB to MSB)。比如 MTLPixelFormatRGB10A2Uint 是一个32-bit的packed format包含3个10-bit channel(R、G、B)以及2-bit(alpha)
    • compressed format是按照像素块的方式保存,每个block的layout为pixel format。compressed pixel format只能用于2D、2D Array以及cube texture。不能用于1D、2DMultisample、3D texture

    MTLPixelFormatGBGR422 and MTLPixelFormatBGRG422 是特殊的pixel format,被用于存放yuv color space的pixel。这种类型只用于2D texture(也不支持2D Array和cube ),不含mipmap,and an even width

    一些pixel format使用sRGB color space的值存储color component(比如 MTLPixelFormatRGBA8Unorm_sRGB or MTLPixelFormatETC2_RGB8_sRGB)。当采样这个贴图的时候,metal implementation会在sample操作之前将sRGB color spacecomponent convert to a linear color space。sRGB的S convert to a linear component L的公式如下:

    • If S <= 0.04045, L = S/12.92
    • If S > 0.04045, L = pow(((S+0.055)/1.055), 2.4)

    相应的,如果旋绕到一个sRGB pixel format的RT上时,implementation 将linear color值转为sRGB

    • If L <= 0.0031308, S = L * 12.92
    • If L > 0.0031308, S = (1.055 * L0.41667) - 0.055

    Creating a Sampler States Object for Texture Lookup

    MTLSamplerState 定义了 addressing、filter以及其他属性,用于graphics或者compute function对MTLTexture执行采样texture的操作。sampler descriptor定义了一个sampler state object的属性。创建一个sampler state object分为以下几步:

    • 创建一个 MTLSamplerDescriptor object
    • 设置 MTLSamplerDescriptor 的值,包含 filter、addressing、maximun anisotropy、lod参数
    • 通过 MTLDevice 的 newSamplerStateWithDescriptor: 方法根据sampler descriptor创建一个 MTLSamplerState object

    可以重复使用sampler descriptor object来创建更多的 MTLSamplerState object,可以根据需要修改descriptor 的属性。descriptor的属性只在创建的时候有用。创建完毕后,修改descriptor的属性将不会影响已经创建的sampler object

    下面代码先创建 MTLSamplerDescriptor ,配置它 ,然后使用它来创建 MTLSamplerState。desriptor object的filter和address 属性没有默认值。 newSamplerStateWithDescriptor: 方法被用于通过sampler descriptor 来创建sampler 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];
        

    Maintaining Coherency Between CPU and GPU Memory

    CPU和GPU都可以访问MTLResource的storage。然而GPU的操作与host CPU 异步,所以在host CPU访问资源storage的时候,需要注意以下几点

    当执行一个 MTLCommandBuffer 的时候,host CPU对MTLResource的修改,必须要在MTLCommandBuffer被commit之前,才会被MTLDevice检测到。也就是说MTLCommandbuffer被commit后(MTLCommandBuffer 的属性为 MTLCommandBufferStatusCommitted ),host CPU对resource的修改,MTLDevice会忽略。

    类似的,当MTLDevice 执行完毕一个MTLCommandBuffer后,host CPU只会检测 command buffer被执行完毕之后(MTLCommandBuffer 的属性为 MTLCommandBufferStatusCompleted),resource的修改

    本节教程就到此结束,希望大家继续阅读我之后的教程。

    谢谢大家,再见!


    原创技术文章,撰写不易,转载请注明出处:电子设备中的画家|王烁 于 2022 年 4 月 22 日发表,原文链接(http://geekfaner.com/shineengine/blog48_MetalProgrammingGuide_3.html)