高效地将Swift数组复制到内存缓冲区以供iOS Metal使用

7
我正在使用苹果的新Metal框架编写iOS应用程序。我有一个Matrix4对象数组(请参见Ray Wenderlich's tutorial),需要通过MTLDevice.newBufferWithLength()方法传递到着色器中。Matrix4对象利用了苹果的GLKit(它包含一个GLKMatrix4对象)。
我正在使用GPU调用进行实例化。
稍后我将把这个数组更改为一个结构体,该结构体包括每个实例的更多数据(不仅仅是Matrix4对象)。
以下是我的代码子集:
1.如何有效地将[Matrix4]对象数组复制到此缓冲区中?
2.是否有更好的方法来处理这个问题?再次强调,我将来会扩展它以使用包含更多数据的结构体。
let sizeofMatrix4 = sizeof(Float) * Matrix4.numberofElements()

// This returns an array of [Matrix4] objects.
let boxArray = createBoxArray(parentModelViewMatrix)

let sizeOfUniformBuffer = boxArray.count * sizeOfMatrix4
var uniformBuffer = device.newBufferWithLength(sizeofUniformBuffer, options: .CPUCacheModeDefaultCache)
let bufferPointer = uniformBuffer?.contents()

// Ouch - way too slow.  How can I optimize?
for i in 0..<boxArray.count
{
    memcpy(bufferPointer! + (i * sizeOfMatrix4), boxArray[i].raw(), sizeOfMatrix4)
}

renderEncoder.setVertexBuffer(uniformBuffer, offset: 0, atIndex: 2)

注意: 在Objective-C代码中,boxArray[i].raw()方法定义如下:
- (void *)raw {
    return glkMatrix.m;
}

你可以看到我正在循环遍历每个数组对象,然后进行memcpy操作。我这样做是因为在将数组视为连续内存块时遇到了问题。
谢谢!

1
你应该使用simd.float4x4。 - user652038
3个回答

8

Swift中的数组被承诺是连续内存,但你需要确保它真的是一个Swift数组,而不是秘密地一个NSArray。如果你想要完全确定,使用ContiguousArray,即使其中的对象可以进行ObjC桥接,也会确保连续内存。如果你想更多地控制内存,看看ManagedBuffer。

因此,在这种情况下,您应该使用newBufferWithBytesNoCopy(length:options:deallocator)来创建围绕现有内存的MTL缓冲区。


1
Rob,感谢您的反馈。自从您回复后,我一直在尝试解决这个问题,但是一直没有成功。您能否提供源代码,演示如何从[Matrix4]对象数组开始,创建MTLDevice缓冲区,然后将其复制到该缓冲区中? - Dead Pixel
1
Rob,你认为ManagedBuffer提供页面对齐的存储吗?从源代码和文档的初步查看来看,它似乎只强制执行元素对齐(通过遵守MemoryLayout<Element>.alignment),而不是页面对齐。页面对齐可能会浪费更多空间,因此我希望能够明确地看到它被提及。 - ldoogy

4
我使用了一个粒子数组并将其传递给计算着色器来完成这项任务。
简单来说,我定义了一些常量,并声明了一些可变指针和可变缓冲区指针:
let particleCount: Int = 1048576
var particlesMemory:UnsafeMutablePointer<Void> = nil
let alignment:UInt = 0x4000
let particlesMemoryByteSize:UInt = UInt(1048576) * UInt(sizeof(Particle))
var particlesVoidPtr: COpaquePointer!
var particlesParticlePtr: UnsafeMutablePointer<Particle>!

var particlesParticleBufferPtr: UnsafeMutableBufferPointer<Particle>!

当我设置粒子时,我填充指针并使用posix_memalign()来分配内存:
    posix_memalign(&particlesMemory, alignment, particlesMemoryByteSize)

    particlesVoidPtr = COpaquePointer(particlesMemory)
    particlesParticlePtr = UnsafeMutablePointer<Particle>(particlesVoidPtr)

    particlesParticleBufferPtr = UnsafeMutableBufferPointer(start: particlesParticlePtr, count: particleCount)

填充粒子的循环略有不同 - 现在我循环遍历缓冲指针:

    for index in particlesParticleBufferPtr.startIndex ..< particlesParticleBufferPtr.endIndex
    {
        [...]

        let particle = Particle(positionX: positionX, positionY: positionY, velocityX: velocityX, velocityY: velocityY)

        particlesParticleBufferPtr[index] = particle
    }

在applyShader()函数内部,我创建了一个内存的副本,该内存用作输入和输出缓冲区:

    let particlesBufferNoCopy = device.newBufferWithBytesNoCopy(particlesMemory, length: Int(particlesMemoryByteSize),
        options: nil, deallocator: nil)

    commandEncoder.setBuffer(particlesBufferNoCopy, offset: 0, atIndex: 0)

    commandEncoder.setBuffer(particlesBufferNoCopy, offset: 0, atIndex: 1)

在着色器运行后,我将共享内存(particlesMemory)放回缓冲指针中:

    particlesVoidPtr = COpaquePointer(particlesMemory)
    particlesParticlePtr = UnsafeMutablePointer(particlesVoidPtr)

    particlesParticleBufferPtr = UnsafeMutableBufferPointer(start: particlesParticlePtr, count: particleCount)

这里有一个最新的Swift 2.0版本,与此相关,可以在我的GitHub存储库中找到


1
你能概述一下Swift 2的差异吗? - Cameron Lowell Palmer

3
显然,使用共享内存和 MTLDevice.makeBuffer(bytesNoCopy:...) 的目的是为了避免冗余的内存拷贝。因此,理想情况下,我们要寻找一种设计,使我们能够在数据加载到 MTLBuffer 对象后轻松操作数据。
经过一段时间的研究,我决定尝试创建一个半通用解决方案,以允许简化页面对齐内存的分配,将内容加载到该内存中,并随后在共享内存块中操作您的项目。
我创建了一个名为 PageAlignedArray 的 Swift 数组实现,它匹配了内置的 Swift 数组接口和功能,但始终驻留在页面对齐的内存上,因此可以非常轻松地转换为 MTLBuffer。我还添加了一个方便的方法,直接将 PageAlignedArray 转换为 Metal 缓冲区。
当然,您可以在之后继续改变数组,由于共享内存架构的帮助,您的更新将自动可用于 GPU。但是请记住,每当数组的长度发生更改时,您必须重新生成您的 MTLBuffer 对象。 以下是一个快速的代码示例:
  var alignedArray : PageAlignedContiguousArray<matrix_double4x4> = [matrixTest, matrixTest]
  alignedArray.append(item)
  alignedArray.removeFirst() // Behaves just like a built-in array, with all convenience methods

  // When it's time to generate a Metal buffer:
  let testMetalBuffer = device?.makeBufferWithPageAlignedArray(alignedArray)

示例代码使用了 matrix_double4x4,但该数组适用于任何Swift值类型。请注意,如果您使用引用类型(例如任何类型的 class),则该数组将包含指向您的元素的指针,因此不能从GPU代码中使用。


1
太棒了!!!只有一个问题 - 我在考虑是否只创建一个可变数组,使用接受可变缓冲区指针的初始化器 - 你是否考虑过这种方法,如果是,为什么拒绝它? - David H
1
@DavidH 如果按照这种方式设置数组,它会如何增长?我使用了自己的类进行分配,以便允许数组根据需要增长。 - ldoogy
1
当然,你是正确的。我想到了一个固定大小的可变数组,但是当然没有办法防止有人试图添加。再次感谢,非常好的帖子! - David H
如何一次性分配所有内存? 如果我使用本地数组,可以调用以下代码[Float](repeating: 0, count: 40_000_000)以一次分配160MB的内存。但是,使用该库需要循环遍历40_000_000并附加数组。这需要大约40秒的时间。 - Андрей Первушин

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接