Я пытаюсь перенести свои проекты с OpenGL на Metal на iOS. Но, похоже, я уперся в преграду. Задача простая ...
У меня текстура большая (больше 3000х3000 пикселей). На котором мне нужно нарисовать несколько (несколько сотен) небольших текстур (скажем, 124x124) для каждого события touchMoved. И это при включении определенной функции смешивания. Это в основном похоже на малярную кисть. А затем отобразите большую текстуру. Это примерно задача.
В OpenGL работает довольно быстро. Я получаю около 60 кадров в секунду. Когда я портировал тот же код на Metal, мне удалось получить только 15 кадров в секунду.
Я создал два образца проекта с минимумом, чтобы продемонстрировать проблему. Вот проекты (OpenGL и Metal) ...
https://drive.google.com/file/d/12MPt1nMzE2UL_s4oXEUoTCXYiTz42r4b/view?usp=sharing
Это примерно то, что я делаю в OpenGL ...
- (void) renderBrush:(GLuint)brush on:(GLuint)fbo ofSize:(CGSize)size at:(CGPoint)point {
GLfloat brushCoordinates[] = {
0.0f, 0.0f,
1.0f, 0.0f,
0.0f, 1.0f,
1.0f, 1.0f,
};
GLfloat imageVertices[] = {
-1.0f, -1.0f,
1.0f, -1.0f,
-1.0f, 1.0f,
1.0f, 1.0f,
};
int brushSize = 124;
CGRect rect = CGRectMake(point.x - brushSize/2, point.y - brushSize/2, brushSize, brushSize);
rect.origin.x /= size.width;
rect.origin.y /= size.height;
rect.size.width /= size.width;
rect.size.height /= size.height;
[self convertImageVertices:imageVertices toProjectionRect:rect onImageOfSize:size];
int currentFBO;
glGetIntegerv(GL_FRAMEBUFFER_BINDING, ¤tFBO);
[_Program use];
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glViewport(0, 0, (int)size.width, (int)size.height);
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, brush);
glUniform1i(brushTextureLocation, 2);
glVertexAttribPointer(positionLocation, 2, GL_FLOAT, 0, 0, imageVertices);
glVertexAttribPointer(brushCoordinateLocation, 2, GL_FLOAT, 0, 0, brushCoordinates);
glEnable(GL_BLEND);
glBlendEquation(GL_FUNC_ADD);
glBlendFuncSeparate(GL_ONE, GL_ZERO, GL_ONE, GL_ONE);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glDisable(GL_BLEND);
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, 0);
glBindFramebuffer(GL_FRAMEBUFFER, currentFBO);
}
Я запускаю этот код в цикле (около 200-500) на событие касания. Работает довольно быстро.
Вот так я перенес код на Metal ...
- (void) renderBrush:(id<MTLTexture>)brush onTarget:(id<MTLTexture>)target at:(CGPoint)point withCommandBuffer:(id<MTLCommandBuffer>)commandBuffer {
int brushSize = 124;
CGRect rect = CGRectMake(point.x - brushSize/2, point.y - brushSize/2, brushSize, brushSize);
rect.origin.x /= target.width;
rect.origin.y /= target.height;
rect.size.width /= target.width;
rect.size.height /= target.height;
Float32 imageVertices[8];
// Calculate the vertices (basically the rectangle that we need to draw) on the target texture that we are going to draw
// We are not drawing on the entire target texture, only on a square around the point
[self composeImageVertices:imageVertices toProjectionRect:rect onImageOfSize:CGSizeMake(target.width, target.height)];
// We use different one vertexBuffer per pass. This is because this is run on a loop and the subsequent calls will overwrite
// The values. Other buffers also get overwritten but that is ok for now, we only need to demonstrate the performance.
id<MTLBuffer> vertexBuffer = [_vertexArray lastObject];
memcpy([vertexBuffer contents], imageVertices, 8 * sizeof(Float32));
id<MTLRenderCommandEncoder> commandEncoder = [commandBuffer renderCommandEncoderWithDescriptor:mRenderPassDescriptor];
commandEncoder.label = @"DrawCE";
[commandEncoder setRenderPipelineState:mPipelineState];
[commandEncoder setVertexBuffer:vertexBuffer offset:0 atIndex:0];
[commandEncoder setVertexBuffer:mBrushTextureBuffer offset:0 atIndex:1];
[commandEncoder setFragmentTexture:brush atIndex:0];
[commandEncoder setFragmentSamplerState:mSampleState atIndex:0];
[commandEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4];
[commandEncoder endEncoding];
}
А затем запустите этот код в цикле с одним MTLCommandBuffer для каждого события касания, например ...
id<MTLCommandBuffer> commandBuffer = [MetalContext.defaultContext.commandQueue commandBuffer];
commandBuffer.label = @"DrawCB";
dispatch_semaphore_wait(_inFlightSemaphore, DISPATCH_TIME_FOREVER);
mRenderPassDescriptor.colorAttachments[0].texture = target;
__block dispatch_semaphore_t block_sema = _inFlightSemaphore;
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> buffer) {
dispatch_semaphore_signal(block_sema);
}];
_vertexArray = [[NSMutableArray alloc] init];
for (int i = 0; i < strokes; i++) {
id<MTLBuffer> vertexBuffer = [MetalContext.defaultContext.device newBufferWithLength:8 * sizeof(Float32) options:0];
[_vertexArray addObject:vertexBuffer];
id<MTLTexture> brush = [_brushes objectAtIndex:rand()%_brushes.count];
[self renderBrush:brush onTarget:target at:CGPointMake(x, y) withCommandBuffer:commandBuffer];
x += deltaX;
y += deltaY;
}
[commandBuffer commit];
В прилагаемом примере кода я заменил события касания циклом таймера, чтобы упростить задачу.
На iPhone 7 Plus я получаю 60 кадров в секунду с OpenGL и 15 кадров в секунду с Metal. Может я здесь что-то ужасно не так делаю?





Удалите всю избыточность:
-setVertexBufferOffset:atIndex:, чтобы установить необходимое смещение, не меняя буфер.composeImageVertices:... может записывать непосредственно в буфер вершин с соответствующим приведением, избегая memcpy.composeImageVertices:..., и если deltaX и deltaY являются константами, вы можете однажды настроить буфер вершин. При необходимости вершинный шейдер может преобразовывать вершины. Вы должны передать соответствующие данные в виде униформ (либо точку назначения и размер целевого объекта рендеринга, либо даже матрицу преобразования).mPipelineState, mBrushTextureBuffer и mSampleState каждый раз.stroke. В вершинном шейдере преобразуйте положение на основе идентификатора экземпляра. Вам нужно будет передать deltaX и deltaY как единые данные. Индексы кисти могут быть в одном переданном буфере, и шейдер может искать в нем индекс кисти по идентификатору экземпляра.Я использовал жестко запрограммированные координаты текстуры кисти во фрагментном шейдере для теста. Но теперь, когда я пытаюсь получить доступ к координатам текстуры во фрагментном шейдере, я не могу найти металлический эквивалент "gl_PointCoord", когда я использую точечный примитив.
В параметре [[stage_in]] фрагментного шейдера поле, помеченное как [[point_coord]], получает координату точки.
Или просто отдельный параметр с такой аннотацией.
Спасибо, Кен. Я изменил свой пример кода, чтобы использовать точечные примитивы (все точки за один проход), а затем использовал массив текстур кисти (пока количество текстур кисти для меня намного меньше 31) с индексами текстур кисти, прошедших через вершину шейдер, как вы предложили. У меня теперь 60 кадров в секунду! Единственное, что мне нужно будет проверить, это то, совпадают ли результаты смешивания с кодом openGL. В OpenGL я рисую один квад за другим с определенной функцией смешивания. Но здесь я рисую все квадраты сразу с одним и тем же режимом наложения, но нет определенного порядка.