Db732707a2d88a6021ba17a685eecef1
Metal【10】—— 增高 & MTLHeap

前一阵都在介绍具体效果的实现,主要集中在 shader 部分,对 Metal 本身介绍的比较少。

这次,我们同样是实现一个新的效果,增高。不过,会找一个很好的切入点,来介绍一类新的对象:MTLHeap,拓展一下 Metal 部分的内容。

效果如下:

PS:

订阅后的朋友,可以加我微信:wxidlongze,拉你进群。交流,扯淡,学习资源分享~

最后,源码在文末~

另外,这是第 19 篇文章了。下一篇(20)发布的时候,订阅价格会有一定的涨幅,如果对内容感兴趣,可以考虑现在订阅哈~

那么,开始吧~


1. 增高

所谓增高,说白了就是纵向拉伸局部区域,使得该区域占整个画面的比例提高,从而实现“变高了”的视觉效果。

这背后,不涉及什么基础知识,也没有什么复杂的概念需要介绍,就是一个拉伸...

我们会有这么几个调整参数:

  • beginY:拉伸区域起始位置
  • endY:拉伸区域结束位置
  • scale:拉伸比例

那么,不妨先思考下,结合我们之前一起学习的,你会怎么做?

PS:
​ 思

​ 考

​ 分

​ 隔

​ 符

​ /

​ /

​ /

​ /

最直接的,和我们之前实现效果的方式一样,自定义一个 fragment function,然后把对应的参数 beginY、endY、scale 当做 uniform 参数传入,然后在 fragment function 中,通过计算,得到对应的结果值,大体意思如下:

if (currentY <= beginY || currentY >= endY) {
    return originColor;
} else {
    // 通过线性插值计算
    // ...
    return newColor;
}

当然,这种方式,同样也能实现对应的效果,虽然可自定义程度比较高,但是其实绕了个弯,不太直接。

我们在传递顶点坐标和纹理坐标的时候,本身就已经定义了他们的映射方式。如果在 shader 里面再进行一次转换计算,其实是没有必要的,虽然在这个简单的例子里,没什么影响。

我们完全可以在设置纹理坐标的时候,就考虑上。

结合增高,具体看下。如下是前后效果图:

假设,我们现在识别到腿部区域是在 [0.65,0.8] 这个范围内,我们要将其拉伸 1.5 倍,结合之前提到的三个控制参数:beginY、endY、scale,放入具体场景中,如下:

可以看到,拉伸区域,将原图分割成了三个区域。其中,除了拉伸区域外的上下两个区域,它们的内容是完全保持一致的,只有拉伸区域本身,做了拉伸操作。

我们原先处理这张纹理的时候,都是以图像整体区域为单位,通过两个三角形来绘制的。但是如果结合上面提到的区域划分,我们可以将不变的,继续保持不变,只动态改动拉伸区域,如下:

我们将一张纹理图分割成三个部分。蓝色和黄色是不可变的,红色是拉伸区域

从而需要 8 个顶点,6 个三角形来完成。

通过将拉伸、无拉伸区域的分离,我们可以通过对应的顶点坐标以及纹理坐标的设置,来让 fragment 采样的时候,自动帮我们完成拉伸的操作。

而 shader 部分,我们无须使用新的,完全可以沿用之前的 PassthroughFragment 即可。

接下去,结合代码,看下具体的实现。

public class StretchFilter: BasicOperation {

    public var heightFactor: Float =  1.0;
    public var texCoordBeginY: Float = 0.6;
    public var texCoordEndY: Float = 0.8;

      public init() {
        super.init(fragmentFunctionName:FunctionName.PassthroughFragment, numberOfInputs:1)
    }

    public override func newTextureAvailable(_ texture: Texture, fromSourceIndex: UInt) {
        let _ = textureInputSemaphore.wait(timeout:DispatchTime.distantFuture)
        defer {
            textureInputSemaphore.signal()
        }

        inputTextures[fromSourceIndex] = texture

        if (UInt(inputTextures.count) >= maximumInputs) {
            let scaleHeightFactor: Float = (texCoordEndY - texCoordBeginY) * (heightFactor - 1.0) + 1.0;
            let verticesBeginY: Float = texCoordBeginY / scaleHeightFactor
            let verticesEndY: Float = (texCoordBeginY + (texCoordEndY - texCoordBeginY) * heightFactor) / scaleHeightFactor

            let imageVertices: [Float] = [-1.0, 1.0, 1.0, 1.0,
                                          -1.0, -2.0 * verticesBeginY + 1.0, 1.0, -2.0 * verticesBeginY + 1.0,
                                          -1.0, -2.0 * verticesEndY + 1.0, 1.0, -2.0 * verticesEndY + 1.0,
                                          -1.0, -1.0, 1.0, -1.0]

            let textureCoordinates: [Float] = [0.0, 0.0, 1.0, 0.0,
                                               0.0, texCoordBeginY, 1.0, texCoordBeginY,
                                               0.0, texCoordEndY, 1.0, texCoordEndY,
                                               0.0, 1.0, 1.0, 1.0]

            let outputWidth: Int
            var outputHeight: Int

            let firstInputTexture = inputTextures[0]!
            outputWidth = firstInputTexture.texture.width
            outputHeight = Int(Float(firstInputTexture.texture.height) * scaleHeightFactor)

            guard let commandBuffer = sharedContext.commandQueue.makeCommandBuffer() else {
                return
            }

            var outputTexture: Texture(width: outputWidth, height: outputHeight)

            commandBuffer.renderQuad(pipelineState: renderPipelineState, uniformSettings: uniformSettings, inputTextures: inputTextures, outputTexture: outputTexture, clearColor: RenderColor.clearColor, imageVertices: imageVertices, textureCoordinates: textureCoordinates)
            commandBuffer.commit()

            updateTargetsWithTexture(outputTexture)
        }
    }
}

可能你已经注意到了,这和我们之前自定义 Filter 时候,Filter 类的代码不太一样,这里重写了 newTextureAvailable 方法,因为我们的处理方式,和通用的一些滤镜不太一致,需要额外处理。

其中,这三个对应我们之前提到的可调整参数:

public var heightFactor: Float =  1.0;
public var texCoordBeginY: Float = 0.6;
public var texCoordEndY: Float = 0.8;

init 方法很简单,没有新的 shader,沿用旧的 PassthroughFragment 即可。

public init() {
    super.init(fragmentFunctionName:FunctionName.PassthroughFragment, numberOfInputs:1)
}

newTextureAvailable 里面,大体都是一致的,不一致的,体现的坐标的计算,如下:

let scaleHeightFactor: Float = (texCoordEndY - texCoordBeginY) * (heightFactor - 1.0) + 1.0;
let verticesBeginY: Float = texCoordBeginY / scaleHeightFactor
let verticesEndY: Float = (texCoordBeginY + (texCoordEndY - texCoordBeginY) * heightFactor) / scaleHeightFactor

let imageVertices: [Float] = [-1.0, 1.0, 1.0, 1.0,
                              -1.0, -2.0 * verticesBeginY + 1.0, 1.0, -2.0 * verticesBeginY + 1.0,
                              -1.0, -2.0 * verticesEndY + 1.0, 1.0, -2.0 * verticesEndY + 1.0,
                              -1.0, -1.0, 1.0, -1.0]

let textureCoordinates: [Float] = [0.0, 0.0, 1.0, 0.0,
                                   0.0, texCoordBeginY, 1.0, texCoordBeginY,
                                   0.0, texCoordEndY, 1.0, texCoordEndY,
                                   0.0, 1.0, 1.0, 1.0]

let outputWidth: Int
var outputHeight: Int

let firstInputTexture = inputTextures[0]!
outputWidth = firstInputTexture.texture.width
outputHeight = Int(Float(firstInputTexture.texture.height) * scaleHeightFactor)

其中,scaleHeightFactor 表示,经过拉伸后,整张图片放大的比例

比如,原图的高度是 4000,其中,[0.6,0.8] 区域,拉伸了 1.5 倍,则:

scaleHeightFactor = (0.8 - 0.6) * (1.5 - 1.0) + 1.0 = 1.1,

同理,拉伸后的图片高度是 outputHeight = 4000 * 1.1 = 4400。

同样,纹理坐标和顶点坐标,需要对应起来。我们之前提到的拉伸区域 beginY,endY,都是针对原图的比例,而我们真正需要展示的,是拉伸后的结果图。所以对应的点,需要换算成结果图上的,如下:

let verticesBeginY: Float = texCoordBeginY / scaleHeightFactor
let verticesEndY: Float = (texCoordBeginY + (texCoordEndY - texCoordBeginY) * heightFactor) / scaleHeightFactor

这几个关键点得到后,对应的顶点坐标,纹理坐标就出来了,如下:

let imageVertices: [Float] = [-1.0, 1.0, 1.0, 1.0,
                              -1.0, -2.0 * verticesBeginY + 1.0, 1.0, -2.0 * verticesBeginY + 1.0,
                              -1.0, -2.0 * verticesEndY + 1.0, 1.0, -2.0 * verticesEndY + 1.0,
                              -1.0, -1.0, 1.0, -1.0]

let textureCoordinates: [Float] = [0.0, 0.0, 1.0, 0.0,
                                   0.0, texCoordBeginY, 1.0, texCoordBeginY,
                                   0.0, texCoordEndY, 1.0, texCoordEndY,
                                   0.0, 1.0, 1.0, 1.0]

之前提到的 beginY,endY,取值范围都是在 [0,1] 之间,对应的是纹理坐标。所以我们需要额外换算一下顶点坐标,转换成 [-1,1] 之间,即 -2.0 * verticesBeginY + 1.0 操作。

所以,当我们传入这样顶点坐标和纹理坐标后,就实现了增高效果。

怎么样,简单吧,不妨动手试试~


2. 存在问题

到这里,这次的内容就结束了吗?当然不是~

我们认真体验下,当快速拖动滑杆的时候,会感知到明显的卡顿,另外,CPU 占用很高:

持续拖动一段时间后,App 会闪退,提示内存占用过高:

MetalImageProcessing[63657:12169422] Metal GPU Frame Capture Enabled
MetalImageProcessing[63657:12169422] Metal API Validation Enabled
Message from debugger: Terminated due to memory issue

看起来,好像问题不小...

整理一下,我们增高的整个实现过程:

UIImage —— 【1】——> Texture(PictureInput)——>【2】——>Texture(BasicOperation)——>【3】——> CAMetalDrawable(RenderView)

其中,【1】和【3】都是固定的过程,而且【1】只有首次才会执行,之后 Texture 是缓存下来的,【3】里面也不涉及新的 Texture 等资源产生。

唯一可能出现问题的,只能是【2】。

在 StretchFilter 处理过程中,我们每一次处理,都是生成一个新的 Texture,

let outputTexture = Texture(width: outputWidth, height: outputHeight)

除此之外,就是一些基础的变量计算,不太可能引起那么高的 CPU 占用。

那么,具体是什么原因呢,我们不妨跑下 Time Profiler:

可以很直观的看到,耗时操作,基本都集中在了 Metal 相关的资源管理上,尤其是这行:

-[_MTLCommandQueue _submitAvailableCommandBuffers]+0x308

至于 _submitAvailableCommandBuffers 里面,具体是什么操作导致的耗时,暂时无法得知(如果有了解如何进一步分析的,还望能指点一下)。

另外,内存方面,体现在某一瞬间的内存占用过高,停止拖动后,相应内存正常释放。

iOS 11 之后,有一个非常实用的接口,可以帮我们监控当前 device 创建出来所有资源的大小:

/*!
 @property currentAllocatedSize
 @abstract The current size in bytes of all resources allocated by this device
 */
@available(iOS 11.0, *)
public var currentAllocatedSize: Int { get }

我们加入这么一行代码,尝试验证一下:

if #available(iOS 11.0, *) {
    print("memory: " + String(sharedContext.device.currentAllocatedSize))
}

缓慢拖动时,打印如下:

memory: 134152192
memory: 134152192
memory: 134152192
memory: 134152192
memory: 133890048
memory: 134152192
memory: 134152192

整体很平稳, 134152192 / 1024 / 1024 = 127.9375 M。

但是当我们快速持续拖动的时候,打印如下:

top Created with Sketch.