Search code examples
iosmetalmtlbuffer

Reading contents from a generic MTLBuffer?


Within my app, I have a MTLBuffer which is being instantiated using a generic type. In one particular case, the buffer will hold values as related to particles in a point cloud, and is defined as such;

struct ParticleUniforms {
    simd_float3 position;
    simd_float3 color;
    float confidence;
};

I am instantiating my MTLBuffer like so;

guard let buffer = device.makeBuffer(length: MemoryLayout<Element>.stride * count, options: options) else {
   fatalError("Failed to create MTLBuffer.")
}

Where I am struggling, however, is to understand how to read the contents of the buffer. More-so, I am looking to copy one element of each item in the buffer to an array on the CPU, which I will use at a later time.

Effectively, the buffer holds a collection of ParticleUniforms, and I would like to access the position value of each item, saving that position to a separate array.

All of the examples I've seen here on Stack Overflow seem to show the MTLBuffer as holding a collection of Floats, though I've not seen any that use a generic type.


Solution

  • It seems what you are looking to achieve can only be done with C structures which hold each member in a contiguous block (arrays of C structs are not necessarily contiguous, but MemoryLayout<Type>.stride will account for any potential padding). Swift structure properties may not be contiguous, so the below method for accessing member values would not work in a practical manner. Unfortunately, when working with void* you need to know what the data describes, which isn't particularly suited for Swift generic types. However, I will offer a potential solution.

    C file:

    #ifndef Test_h
    #define Test_h
    
    #include <simd/simd.h>
    
    typedef struct {
        vector_float3 testA;
        vector_float3 testB;
    } CustomC;
    
    #endif /* Test_h */
    

    Swift file (bridging header assumed)

    import Metal
    
    // MARK: Convenience
    typealias MTLCStructMemberFormat = MTLVertexFormat
    
    @_functionBuilder
    struct ArrayLayout { static func buildBlock<T>(_ arr: T...) -> [T] { arr } }
    
    extension MTLCStructMemberFormat {
        var stride: Int {
            switch self {
            case .float2:  return MemoryLayout<simd_float2>.stride
            case .float3:  return MemoryLayout<simd_float3>.stride
            default:       fatalError("Case unaccounted for")
            }
        }
    }
    
    // MARK: Custom Protocol
    protocol CMetalStruct {
        /// Returns the type of the `ith` member
        static var memoryLayouts: [MTLCStructMemberFormat] { get }
    }
    
    // Custom Allocator
    class CustomBufferAllocator<Element> where Element: CMetalStruct {
        
        var buffer: MTLBuffer!
        var count: Int
        
        init(bytes: UnsafeMutableRawPointer, count: Int, options: MTLResourceOptions = []) {
            guard let buffer = device.makeBuffer(bytes: bytes, length: count * MemoryLayout<Element>.stride, options: options) else {
                fatalError("Failed to create MTLBuffer.")
            }
            self.buffer = buffer
            self.count = count
        }
        
        func readBufferContents<T>(element_position_in_array n: Int, memberID: Int, expectedType type: T.Type = T.self)
            -> T {
            let pointerAddition = n * MemoryLayout<Element>.stride
                let valueToIncrement = Element.memoryLayouts[0..<memberID].reduce(0) { $0 + $1.stride }
            return buffer.contents().advanced(by: pointerAddition + valueToIncrement).bindMemory(to: T.self, capacity: 1).pointee
        }
        
        func extractMembers<T>(memberID: Int, expectedType type: T.Type = T.self) -> [T] {
            var array: [T] = []
     
            for n in 0..<count {
                let pointerAddition = n * MemoryLayout<Element>.stride
                let valueToIncrement = Element.memoryLayouts[0..<memberID].reduce(0) { $0 + $1.stride }
                let contents = buffer.contents().advanced(by: pointerAddition + valueToIncrement).bindMemory(to: T.self, capacity: 1).pointee
                array.append(contents)
            }
            
            return array
        }
    }
    
    // Example
    
    // First extend the custom struct to conform to out type
    extension CustomC: CMetalStruct {
        @ArrayLayout static var memoryLayouts: [MTLCStructMemberFormat] {
            MTLCStructMemberFormat.float3
            MTLCStructMemberFormat.float3
        }
    }
    
    let device = MTLCreateSystemDefaultDevice()!
    var CTypes = [CustomC(testA: .init(59, 99, 0), testB: .init(102, 111, 52)), CustomC(testA: .init(10, 11, 5), testB: .one), CustomC(testA: .zero, testB: .init(5, 5, 5))]
    
    let allocator = CustomBufferAllocator<CustomC>(bytes: &CTypes, count: 3)
    let value = allocator.readBufferContents(element_position_in_array: 1, memberID: 0, expectedType: simd_float3.self)
    print(value)
    
    // Prints SIMD3<Float>(10.0, 11.0, 5.0)
    
    let group = allocator.extractMembers(memberID: 1, expectedType: simd_float3.self)
    print(group)
    
    // Prints [SIMD3<Float>(102.0, 111.0, 52.0), SIMD3<Float>(1.0, 1.0, 1.0), SIMD3<Float>(5.0, 5.0, 5.0)]
    

    This is similar to a MTLVertexDescriptor, except the memory is accessed manually and not via the [[stage_in]] attribute and the argument table passed to each instance of a vertex of fragment shader. You could even extend the allocator to accept a string parameter with the name of the property and hold some dictionary which maps to member IDs.