iOS Metal Spritebatch - 更新顶点VS更新制服

时间:2015-12-10 23:43:14

标签: ios swift performance metal spritebatch

拥有2d渲染器中级经验的每个人都知道精灵批处理器具有需要更新的图形API特定缓冲区内的数据,我们总是寻找最快的方式来更新它。现在我陷入了两难境地 - 对于Metal和Swift来说,更新最聪明的事情是什么,最聪明的做法是什么?更具体地说,我应该在将顶点发送到GPU之前更新顶点(在CPU上执行顶点和tex坐标转换),或创建变换矩阵,创建tex坐标参数,并将其发送到一个实例化的统一缓冲区中( GPU上的顶点和tex坐标转换)。我这样做的方式目前涉及实例化渲染和一个与8字节对齐的巨型制服缓冲区。

静态数据

static let spritesPerBatch: Int = 1024
static var spritesData: [Float] = [Float](count: spritesPerBatch * BufferConstants.SIZE_OF_SPRITE_INSTANCE_UNIFORMS / sizeof(Float), repeatedValue: 0.0)

排队精灵数据

方法:SpriteBatch.begin()

spritesInBatch = 0

方法:SpriteBatch.submit(sprite)

let offset: Int = spritesInBatch * BufferConstants.SIZE_OF_SPRITE_INSTANCE_UNIFORMS / sizeof(Float)
// transform matrix (3x2)
spritesData[offset + 0] = wsx * cosMetaRot * xOrtho
spritesData[offset + 1] = wsx * sinMetaRot * yOrtho
spritesData[offset + 2] = -hsy * sinMetaRot * xOrtho
spritesData[offset + 3] = hsy * cosMetaRot * yOrtho
spritesData[offset + 4] = (tx * cosNegCameraRotation - ty * sinNegCameraRotation) * xOrtho
spritesData[offset + 5] = (tx * sinNegCameraRotation + ty * cosNegCameraRotation) * yOrtho

// tex coords and lengths
spritesData[offset + 6] = sprite.getU()
spritesData[offset + 7] = sprite.getV()
spritesData[offset + 8] = sprite.getUVW()
spritesData[offset + 9] = sprite.getUVH()

// which texture to use out of the 16 that could be bound
spritesData[offset + 10] = Float(targetTextureIDIndex)

spritesInBatch++

将精灵数据复制到制服缓冲区

方法:SpriteBatch.end()

instancedUniformsBuffer = device.newBufferWithLength(length: spritesPerBatch * BufferConstants.SIZE_OF_SPRITE_INSTANCE_UNIFORMS, options: MTLResourceOptions.CPUCacheModeWriteCombined)
instancedUniformsPointer = instancedUniformsBuffer.contents()
memcpy(instancedUniformsPointer, spritesData, instancedUniformsBuffer.length)
Renderer.renderSpriteBatch()

精灵批量渲染方法

方法:Renderer.renderSpriteBatch()

Shaders.setShaderProgram(Shaders.SPRITE)

let textureIDs: [TextureID] = SpriteBatch.getTextureIDs()
for (var i: Int = 0; i < textureIDs.count; i++) {
    renderEncoder.setFragmentTexture(TextureManager.getTexture(textureIDs[i]).texture, atIndex: i)
}

let instancedUniformsBuffer: MTLBuffer = SpriteBatch.getInstancedUniformsBuffer().buffer
renderEncoder.setVertexBuffer(VertexBuffers.SPRITE.buffer, offset: 0, atIndex: 0)
renderEncoder.setVertexBuffer(instancedUniformsBuffer, offset: 0, atIndex: 1)
renderEncoder.drawIndexedPrimitives(MTLPrimitiveType.Triangle, indexCount: BufferConstants.SPRITE_INDEX_COUNT, indexType: MTLIndexType.UInt16, indexBuffer: IndexBuffers.SPRITE.buffer, indexBufferOffset: 0, instanceCount: SpriteBatch.getSpritesInBatch())

我目前能够在iPhone 5s上以60 fps的速度获得大约1400个大小为32x64的精灵和8个单独的纹理。我对此非常满意,并且能用这个数字完成我的iOS游戏。但是,我想推动边界,以便我可以在游戏中使用更好的效果。为了重申这个问题,我还没有说清楚,我想知道两个特定于绩效的重要事项。

  1. 更好的想法是拥有一个更大的顶点缓冲区(与我当前的方法相反:为所有精灵共享一个顶点和索引缓冲区),其中我使用内存副本设置每个顶点的位置和纹理坐标CPU方面?这也意味着不使用实例化绘制调用。
  2. 如果没有,是否有更快的方法来准备和复制精灵数据?
  3. 感谢和抱歉超长的帖子! :)

1 个答案:

答案 0 :(得分:0)

只是一些想法......

  1. 我会使用乐器来查看游戏循环中花费最多的时间。然而,'Time Profiler'可能无法帮助你解决GPU方面的问题。

  2. 查看XCode中的GPU报告,它应该显示GPU和CPU上每帧的花费时间。如果它已经在16毫秒左右徘徊,那么将更多的工作转移到GPU是毫无意义的。

  3. 请注意用memory buffer that is shared across the GPU and CPU替换memcpy。这样你就可以简单地在Swift中写入数组,并且GPU可以使用它,而无需复制内存。

  4. 您可以在Metal计算着色器中重写SpriteBatch.submit(sprite),但如果您只执行了几千次,则该方法的计算成本似乎并不昂贵。输出MTLBuffer将包含可以直接送入渲染编码器的所有spritesData。您仍然需要将输入数据从CPU获取到GPU(计算)。

  5. 你的观点1很有意思。我认为你不想在CPU上转换顶点,但这可能是计算着色器的一个很好的候选者。这类似于我前一段时间做过的boid模拟。金属计算着色器更新每个boid位置和速度,它还创建一个每个boid变换矩阵,然后用于变换构成boids视觉表示的6个顶点位置(用2个三角形绘制的简单鱼)。我的场景是在SceneKit中构建的,因此使用实例化绘制调用实际上并不是一种选择。