Металл намного медленнее по сравнению с OpenGL при рендеринге мелких текстур на большой текстуре

Я пытаюсь перенести свои проекты с 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, &currentFBO);

    [_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. Может я здесь что-то ужасно не так делаю?

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
990
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Удалите всю избыточность:

  • Не создавайте буферы во время рендеринга. Выделите достаточное количество буферов во время инициализации.
  • Не создавайте командный кодировщик для каждого квадрата.
  • Используйте один большой буфер вершин с разными (правильно выровненными) смещениями для каждого квадрата. Используйте -setVertexBufferOffset:atIndex:, чтобы установить необходимое смещение, не меняя буфер.
  • composeImageVertices:... может записывать непосредственно в буфер вершин с соответствующим приведением, избегая memcpy.
  • В зависимости от того, что на самом деле делает composeImageVertices:..., и если deltaX и deltaY являются константами, вы можете однажды настроить буфер вершин. При необходимости вершинный шейдер может преобразовывать вершины. Вы должны передать соответствующие данные в виде униформ (либо точку назначения и размер целевого объекта рендеринга, либо даже матрицу преобразования).
  • Предполагая, что они каждый раз одни и те же, не устанавливайте mPipelineState, mBrushTextureBuffer и mSampleState каждый раз.
  • Если какие-либо квадраты имеют одну и ту же текстуру кисти, сгруппируйте их вместе и выполните одну команду рисования, чтобы нарисовать их все. Это может потребовать переключения на примитивы треугольника вместо примитивов полосы треугольника. Однако, если вы выполняете проиндексированное рисование, вы можете использовать примитивный дозорный перезапуск для рисования нескольких полос треугольников в одной команде рисования.
  • Вы даже можете сделать несколько кистей в одной команде рисования, если количество не превышает разрешенное количество текстур (31). Передайте все текстуры кисти во фрагментный шейдер. Он может получать их как массив текстур. Данные вершины будут включать индекс кисти, вершинный шейдер будет передавать его вперед, фрагментный шейдер будет использовать его для поиска текстуры для выборки из массива.
  • Вы можете использовать инстансированное рисование, чтобы нарисовать все с помощью одной команды. Нарисуйте экземпляры одного квадрата stroke. В вершинном шейдере преобразуйте положение на основе идентификатора экземпляра. Вам нужно будет передать deltaX и deltaY как единые данные. Индексы кисти могут быть в одном переданном буфере, и шейдер может искать в нем индекс кисти по идентификатору экземпляра.
  • Рассматривали ли вы использование точечных примитивов вместо четырехугольников? Это уменьшит количество вершин и даст информацию о Metal, которую можно использовать для оптимизации растеризации.

Спасибо, Кен. Я изменил свой пример кода, чтобы использовать точечные примитивы (все точки за один проход), а затем использовал массив текстур кисти (пока количество текстур кисти для меня намного меньше 31) с индексами текстур кисти, прошедших через вершину шейдер, как вы предложили. У меня теперь 60 кадров в секунду! Единственное, что мне нужно будет проверить, это то, совпадают ли результаты смешивания с кодом openGL. В OpenGL я рисую один квад за другим с определенной функцией смешивания. Но здесь я рисую все квадраты сразу с одним и тем же режимом наложения, но нет определенного порядка.

Abix 14.07.2018 12:01

Я использовал жестко запрограммированные координаты текстуры кисти во фрагментном шейдере для теста. Но теперь, когда я пытаюсь получить доступ к координатам текстуры во фрагментном шейдере, я не могу найти металлический эквивалент "gl_PointCoord", когда я использую точечный примитив.

Abix 14.07.2018 19:23

В параметре [[stage_in]] фрагментного шейдера поле, помеченное как [[point_coord]], получает координату точки.

Ken Thomases 14.07.2018 19:37

Или просто отдельный параметр с такой аннотацией.

Ken Thomases 14.07.2018 19:43

Другие вопросы по теме