Я разрабатываю приложение Какао, используя Swift 4.0 MetalKit API для macOS 10.13. Все, о чем я здесь сообщаю, было сделано на моем MBPro 2015 года выпуска.
Я успешно реализовал MTKView, который очень хорошо отображает простую геометрию с небольшим количеством вершин (кубы, треугольники и т. Д.). Я реализовал камеру на основе перетаскивания мышью, которая вращается, обтекает и увеличивает. Вот скриншот экрана отладки xcode FPS, пока я вращаю куб:
Однако, когда я пытаюсь загрузить набор данных, который содержит только ~ 1500 вершин (каждая из которых хранится как 7 x 32-битных чисел с плавающей запятой ... то есть: всего 42 кБ), я начинаю получать очень плохую задержку в FPS. Я покажу реализацию кода ниже. Вот скриншот (обратите внимание, что на этом изображении вид охватывает только несколько вершин, которые отображаются как большие точки):
Вот моя реализация:
1) viewDidLoad ():
override func viewDidLoad() {
super.viewDidLoad()
// Initialization of the projection matrix and camera
self.projectionMatrix = float4x4.makePerspectiveViewAngle(float4x4.degrees(toRad: 85.0),
aspectRatio: Float(self.view.bounds.size.width / self.view.bounds.size.height),
nearZ: 0.01, farZ: 100.0)
self.vCam = ViewCamera()
// Initialization of the MTLDevice
metalView.device = MTLCreateSystemDefaultDevice()
device = metalView.device
metalView.colorPixelFormat = .bgra8Unorm
// Initialization of the shader library
let defaultLibrary = device.makeDefaultLibrary()!
let fragmentProgram = defaultLibrary.makeFunction(name: "basic_fragment")
let vertexProgram = defaultLibrary.makeFunction(name: "basic_vertex")
// Initialization of the MTLRenderPipelineState
let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
pipelineStateDescriptor.vertexFunction = vertexProgram
pipelineStateDescriptor.fragmentFunction = fragmentProgram
pipelineStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineStateDescriptor)
// Initialization of the MTLCommandQueue
commandQueue = device.makeCommandQueue()
// Initialization of Delegates and BufferProvider for View and Projection matrix MTLBuffer
self.metalView.delegate = self
self.metalView.eventDelegate = self
self.bufferProvider = BufferProvider(device: device, inflightBuffersCount: 3, sizeOfUniformsBuffer: MemoryLayout<Float>.size * float4x4.numberOfElements() * 2)
}
2) Загрузка MTLBuffer для вершин куба:
private func makeCubeVertexBuffer() {
let cube = Cube()
let vertices = cube.verticesArray
var vertexData = Array<Float>()
for vertex in vertices{
vertexData += vertex.floatBuffer()
}
VDataSize = vertexData.count * MemoryLayout.size(ofValue: vertexData[0])
self.vertexBuffer = device.makeBuffer(bytes: vertexData, length: VDataSize!, options: [])!
self.vertexCount = vertices.count
}
3) Загрузка MTLBuffer для вершин набора данных. Обратите внимание, что я явно объявляю режим хранения этого буфера как Private, чтобы обеспечить эффективный доступ к данным со стороны графического процессора, поскольку процессору не требуется доступ к данным после загрузки буфера. Также обратите внимание, что я загружаю только 1/100 вершин в моем фактическом наборе данных, потому что вся ОС на моем компьютере начинает отставать, когда я пытаюсь загрузить его полностью (всего 4,2 МБ данных).
public func loadDataset(datasetVolume: DatasetVolume) {
// Load dataset vertices
self.datasetVolume = datasetVolume
self.datasetVertexCount = self.datasetVolume!.vertexCount/100
let rgbaVertices = self.datasetVolume!.rgbaPixelVolume[0...(self.datasetVertexCount!-1)]
var vertexData = Array<Float>()
for vertex in rgbaVertices{
vertexData += vertex.floatBuffer()
}
let dataSize = vertexData.count * MemoryLayout.size(ofValue: vertexData[0])
// Make two MTLBuffer's: One with Shared storage mode in which data is initially loaded, and a second one with Private storage mode
self.datasetVertexBuffer = device.makeBuffer(bytes: vertexData, length: dataSize, options: MTLResourceOptions.storageModeShared)
self.datasetVertexBufferGPU = device.makeBuffer(length: dataSize, options: MTLResourceOptions.storageModePrivate)
// Create a MTLCommandBuffer and blit the vertex data from the Shared MTLBuffer to the Private MTLBuffer
let commandBuffer = self.commandQueue.makeCommandBuffer()
let blitEncoder = commandBuffer!.makeBlitCommandEncoder()
blitEncoder!.copy(from: self.datasetVertexBuffer!, sourceOffset: 0, to: self.datasetVertexBufferGPU!, destinationOffset: 0, size: dataSize)
blitEncoder!.endEncoding()
commandBuffer!.commit()
// Clean up
self.datasetLoaded = true
self.datasetVertexBuffer = nil
}
4) Наконец, вот цикл рендеринга. Опять же, здесь используется MetalKit.
func draw(in view: MTKView) {
render(view.currentDrawable)
}
private func render(_ drawable: CAMetalDrawable?) {
guard let drawable = drawable else { return }
// Make sure an MTLBuffer for the View and Projection matrices is available
_ = self.bufferProvider?.availableResourcesSemaphore.wait(timeout: DispatchTime.distantFuture)
// Initialize common RenderPassDescriptor
let renderPassDescriptor = MTLRenderPassDescriptor()
renderPassDescriptor.colorAttachments[0].texture = drawable.texture
renderPassDescriptor.colorAttachments[0].loadAction = .clear
renderPassDescriptor.colorAttachments[0].clearColor = Colors.White
renderPassDescriptor.colorAttachments[0].storeAction = .store
// Initialize a CommandBuffer and add a CompletedHandler to release an MTLBuffer from the BufferProvider once the GPU is done processing this command
let commandBuffer = self.commandQueue.makeCommandBuffer()
commandBuffer?.addCompletedHandler { (_) in
self.bufferProvider?.availableResourcesSemaphore.signal()
}
// Update the View matrix and obtain an MTLBuffer for it and the projection matrix
let camViewMatrix = self.vCam.getLookAtMatrix()
let uniformBuffer = bufferProvider?.nextUniformsBuffer(projectionMatrix: projectionMatrix, camViewMatrix: camViewMatrix)
// Initialize a MTLParallelRenderCommandEncoder
let parallelEncoder = commandBuffer?.makeParallelRenderCommandEncoder(descriptor: renderPassDescriptor)
// Create a CommandEncoder for the cube vertices if its data is loaded
if self.cubeLoaded == true {
let cubeRenderEncoder = parallelEncoder?.makeRenderCommandEncoder()
cubeRenderEncoder!.setCullMode(MTLCullMode.front)
cubeRenderEncoder!.setRenderPipelineState(pipelineState)
cubeRenderEncoder!.setTriangleFillMode(MTLTriangleFillMode.fill)
cubeRenderEncoder!.setVertexBuffer(self.cubeVertexBuffer, offset: 0, index: 0)
cubeRenderEncoder!.setVertexBuffer(uniformBuffer, offset: 0, index: 1)
cubeRenderEncoder!.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertexCount!, instanceCount: self.cubeVertexCount!/3)
cubeRenderEncoder!.endEncoding()
}
// Create a CommandEncoder for the dataset vertices if its data is loaded
if self.datasetLoaded == true {
let rgbaVolumeRenderEncoder = parallelEncoder?.makeRenderCommandEncoder()
rgbaVolumeRenderEncoder!.setRenderPipelineState(pipelineState)
rgbaVolumeRenderEncoder!.setVertexBuffer( self.datasetVertexBufferGPU!, offset: 0, index: 0)
rgbaVolumeRenderEncoder!.setVertexBuffer(uniformBuffer, offset: 0, index: 1)
rgbaVolumeRenderEncoder!.drawPrimitives(type: .point, vertexStart: 0, vertexCount: datasetVertexCount!, instanceCount: datasetVertexCount!)
rgbaVolumeRenderEncoder!.endEncoding()
}
// End CommandBuffer encoding and commit task
parallelEncoder!.endEncoding()
commandBuffer!.present(drawable)
commandBuffer!.commit()
}
Хорошо, вот шаги, которые я сделал, пытаясь выяснить, что вызывает задержку, имея в виду, что эффект задержки пропорционален размеру буфера вершин набора данных:
Сначала я думал, что это было из-за того, что графический процессор не мог достаточно быстро получить доступ к памяти, потому что он был в режиме общего хранилища -> Я изменил набор данных MTLBuffer на режим частного хранилища. Это не решило проблему.
Затем я подумал, что проблема связана с тем, что процессор слишком много времени тратит на мою функцию render (). Это могло быть связано с проблемой с BufferProvider или, может быть, потому что каким-то образом ЦП пытался каким-то образом повторно обрабатывать / перезагружать буфер вершин набора данных каждый кадр -> Чтобы это проверить, я использовал Time Profiler в инструментах xcode. К сожалению, похоже, что проблема в том, что приложение вызывает этот метод рендеринга (другими словами, метод draw () MTKView) очень редко. Вот несколько скриншотов:
- Всплеск ~ 10 секунд - это когда куб загружен.
- Пики между ~ 25-35 секундами - это когда набор данных загружен.
- Это изображение (^) показывает активность в промежутке ~ 10-20 секунд сразу после загрузки куба. Это когда FPS на уровне ~ 60. Вы можете видеть, что основной поток тратит около 53 мсек на функцию render () в течение этих 10 секунд.
- На этом изображении (^) показана активность между ~ 40-50 секундами сразу после загрузки набора данных. Это когда FPS <10. Вы можете видеть, что основной поток тратит около 4 мсек на функцию render () в течение этих 10 секунд. Как видите, ни один из методов, которые обычно вызываются из этой функции, не вызывается (то есть: те, которые мы видим, вызываются, когда загружен только куб, предыдущее изображение). Следует отметить, что когда я загружаю набор данных, таймер профилировщика времени начинает прыгать (то есть: он останавливается на несколько секунд, а затем переходит к текущему времени ... повтор).
Так вот где я. Проблема, похоже, в том, что процессор каким-то образом перегружается этими 42 КБ данных ... рекурсивно. Я также провел тест с распределителем в инструментах xcode. Насколько я могу судить, никаких признаков утечки памяти (вы могли заметить, что многое из этого для меня в новинку).
Извините за запутанный пост, я надеюсь, что следить за ним не так уж и сложно. Заранее всем спасибо за помощь.
Изменить:
Вот мои шейдеры, на случай, если вы захотите их увидеть:
struct VertexIn{
packed_float3 position;
packed_float4 color;
};
struct VertexOut{
float4 position [[position]];
float4 color;
float size [[point_size]];
};
struct Uniforms{
float4x4 cameraMatrix;
float4x4 projectionMatrix;
};
vertex VertexOut basic_vertex(const device VertexIn* vertex_array [[ buffer(0) ]],
constant Uniforms& uniforms [[ buffer(1) ]],
unsigned int vid [[ vertex_id ]]) {
float4x4 cam_Matrix = uniforms.cameraMatrix;
float4x4 proj_Matrix = uniforms.projectionMatrix;
VertexIn VertexIn = vertex_array[vid];
VertexOut VertexOut;
VertexOut.position = proj_Matrix * cam_Matrix * float4(VertexIn.position,1);
VertexOut.color = VertexIn.color;
VertexOut.size = 15;
return VertexOut;
}
fragment half4 basic_fragment(VertexOut interpolated [[stage_in]]) {
return half4(interpolated.color[0], interpolated.color[1], interpolated.color[2], interpolated.color[3]);
}