Metal: support efficient BufferUsage::STREAM usage (#4525)
This commit is contained in:
@@ -51,9 +51,19 @@ public:
|
||||
*
|
||||
* @return The MTLBuffer representing the current state of the buffer to bind, or nil if there
|
||||
* is no device allocation.
|
||||
*
|
||||
* For STREAM buffers, getGpuBufferStreamOffset() should be called to retrieve the correct
|
||||
* buffer offset.
|
||||
*
|
||||
*/
|
||||
id<MTLBuffer> getGpuBufferForDraw(id<MTLCommandBuffer> cmdBuffer) noexcept;
|
||||
|
||||
/**
|
||||
* Returns the offset into the buffer returned by getGpuBufferForDraw. This is always 0 for
|
||||
* non-STREAM buffers.
|
||||
*/
|
||||
size_t getGpuBufferStreamOffset() noexcept { return mCurrentStreamOffset; }
|
||||
|
||||
void* getCpuBuffer() const noexcept { return mCpuBuffer; }
|
||||
|
||||
enum Stage {
|
||||
@@ -73,11 +83,14 @@ public:
|
||||
|
||||
private:
|
||||
|
||||
BufferUsage mUsage;
|
||||
size_t mBufferSize = 0;
|
||||
size_t mCurrentStreamOffset = 0;
|
||||
const MetalBufferPoolEntry* mBufferPoolEntry = nullptr;
|
||||
void* mCpuBuffer = nullptr;
|
||||
MetalContext& mContext;
|
||||
|
||||
void copyIntoStreamBuffer(void* src, size_t size);
|
||||
};
|
||||
|
||||
} // namespace metal
|
||||
|
||||
@@ -28,11 +28,19 @@ MetalBuffer::MetalBuffer(MetalContext& context, BufferUsage usage, size_t size,
|
||||
: mBufferSize(size), mContext(context) {
|
||||
// If the buffer is less than 4K in size and is updated frequently, we don't use an explicit
|
||||
// buffer. Instead, we use immediate command encoder methods like setVertexBytes:length:atIndex:.
|
||||
const bool dynamicUsage = usage == BufferUsage::DYNAMIC || usage == BufferUsage::STREAM;
|
||||
if (size <= 4 * 1024 && dynamicUsage && !forceGpuBuffer) {
|
||||
mBufferPoolEntry = nullptr;
|
||||
mCpuBuffer = malloc(size);
|
||||
if (size <= 4 * 1024) {
|
||||
// We'll take the same approach for STREAM buffers under 4K.
|
||||
if (usage == BufferUsage::STREAM) {
|
||||
usage = BufferUsage::DYNAMIC;
|
||||
}
|
||||
|
||||
if (usage == BufferUsage::DYNAMIC && !forceGpuBuffer) {
|
||||
mBufferPoolEntry = nullptr;
|
||||
mCpuBuffer = malloc(size);
|
||||
}
|
||||
}
|
||||
|
||||
mUsage = usage;
|
||||
}
|
||||
|
||||
MetalBuffer::~MetalBuffer() {
|
||||
@@ -54,6 +62,13 @@ void MetalBuffer::copyIntoBuffer(void* src, size_t size, size_t byteOffset) {
|
||||
"Attempting to copy %d bytes into a buffer of size %d at offset %d",
|
||||
size, mBufferSize, byteOffset);
|
||||
|
||||
if (mUsage == BufferUsage::STREAM) {
|
||||
// byteOffset must be 0 for STREAM buffers.
|
||||
assert_invariant(byteOffset == 0);
|
||||
copyIntoStreamBuffer(src, size);
|
||||
return;
|
||||
}
|
||||
|
||||
// Either copy into the Metal buffer or into our cpu buffer.
|
||||
if (mCpuBuffer) {
|
||||
memcpy(static_cast<uint8_t*>(mCpuBuffer) + byteOffset, src, size);
|
||||
@@ -70,6 +85,30 @@ void MetalBuffer::copyIntoBuffer(void* src, size_t size, size_t byteOffset) {
|
||||
memcpy(static_cast<uint8_t*>(mBufferPoolEntry->buffer.contents) + byteOffset, src, size);
|
||||
}
|
||||
|
||||
void MetalBuffer::copyIntoStreamBuffer(void* src, size_t size) {
|
||||
// Stream buffers are handled a bit differently. In STREAM mode, we have a single large buffer.
|
||||
// At each update we "advance" into the buffer, keeping track of the where we're at, and bind
|
||||
// the buffer at that location. Effectively, "byteOffset" is managed automatically.
|
||||
|
||||
assert_invariant(size <= mBufferSize);
|
||||
assert_invariant(!mCpuBuffer);
|
||||
|
||||
if (mCurrentStreamOffset + size > mBufferSize) {
|
||||
// Allocate a new buffer and reset the stream offset.
|
||||
mCurrentStreamOffset = 0;
|
||||
|
||||
if (mBufferPoolEntry) { mContext.bufferPool->releaseBuffer(mBufferPoolEntry); }
|
||||
mBufferPoolEntry = mContext.bufferPool->acquireBuffer(mBufferSize);
|
||||
}
|
||||
|
||||
// Lazily acquire a new buffer if we haven't already.
|
||||
if (!mBufferPoolEntry) {
|
||||
mBufferPoolEntry = mContext.bufferPool->acquireBuffer(mBufferSize);
|
||||
}
|
||||
|
||||
memcpy(static_cast<uint8_t*>(mBufferPoolEntry->buffer.contents) + mCurrentStreamOffset, src, size);
|
||||
}
|
||||
|
||||
id<MTLBuffer> MetalBuffer::getGpuBufferForDraw(id<MTLCommandBuffer> cmdBuffer) noexcept {
|
||||
if (!mBufferPoolEntry) {
|
||||
// If there's a CPU buffer, then we return nil here, as the CPU-side buffer will be bound
|
||||
@@ -121,7 +160,7 @@ void MetalBuffer::bindBuffers(id<MTLCommandBuffer> cmdBuffer, id<MTLRenderComman
|
||||
continue;
|
||||
}
|
||||
metalBuffers[b] = gpuBuffer;
|
||||
metalOffsets[b] = offsets[b];
|
||||
metalOffsets[b] = offsets[b] + buffer->mCurrentStreamOffset;
|
||||
}
|
||||
|
||||
if (stages & Stage::VERTEX) {
|
||||
|
||||
@@ -1339,11 +1339,12 @@ void MetalDriver::draw(backend::PipelineState ps, Handle<HwRenderPrimitive> rph)
|
||||
|
||||
id<MTLCommandBuffer> cmdBuffer = getPendingCommandBuffer(mContext);
|
||||
id<MTLBuffer> metalIndexBuffer = indexBuffer->buffer.getGpuBufferForDraw(cmdBuffer);
|
||||
size_t offset = indexBuffer->buffer.getGpuBufferStreamOffset();
|
||||
[mContext->currentRenderPassEncoder drawIndexedPrimitives:getMetalPrimitiveType(primitive->type)
|
||||
indexCount:primitive->count
|
||||
indexType:getIndexType(indexBuffer->elementSize)
|
||||
indexBuffer:metalIndexBuffer
|
||||
indexBufferOffset:primitive->offset];
|
||||
indexBufferOffset:primitive->offset + offset];
|
||||
}
|
||||
|
||||
void MetalDriver::beginTimerQuery(Handle<HwTimerQuery> tqh) {
|
||||
|
||||
Reference in New Issue
Block a user