Когда безопасно переписать и повторно использовать MTLBuffer или другой вершинный буфер Metal? - PullRequest
1 голос
/ 01 февраля 2020

Я только начинаю работать с металлом, и мне трудно понять некоторые базовые вещи c. Я читал целую кучу веб-страниц о металле, прорабатывал примеры Apple и так далее, но пробелы в моем понимании остаются. Я думаю, что мой ключевой момент путаницы заключается в следующем: как правильно обращаться с буферами вершин и как узнать, когда их можно безопасно использовать повторно? Эта путаница проявляется несколькими способами, как я опишу ниже, и, возможно, к различным проявлениям моей путаницы нужно подходить по-разному.

Чтобы быть более точным c, я использую MTKView подкласс в Objective- C на macOS для отображения очень простых 2D-фигур: общий кадр для вида с цветом фона внутри, 0+ прямоугольных angular подкадров внутри этого общего кадра с другим цветом фона внутри них, а затем 0 + плоские квадраты разных цветов внутри каждого подрамника. Моя вершинная функция - просто простое преобразование координат, а моя фрагментная функция просто проходит через цвет, который она получает, на основе демонстрационного приложения Apple в виде треугольника. У меня это работает нормально для одного подкадра с одним квадратом. Пока все хорошо.

Есть несколько вещей, которые меня озадачивают.

Один: я мог бы спроектировать свой код так, чтобы он целиком отображался с помощью одного буфера вершин и одного вызова drawPrimitives: , рисуя все (под) рамки и квадраты в одном большом взрыве. Однако это не оптимально, поскольку нарушает инкапсуляцию моего кода, в котором каждый подкадр представляет состояние одного объекта (объекта, который содержит квадраты 0+); Я бы хотел, чтобы каждый объект отвечал за отрисовку своего содержимого. Поэтому было бы неплохо, чтобы каждый объект устанавливал буфер вершин и выполнял свой собственный вызов drawPrimitives:. Но поскольку объекты будут отрисовываться последовательно (это однопоточное приложение), я хотел бы повторно использовать один и тот же буфер вершин во всех этих операциях рисования, вместо того чтобы каждый объект должен был выделять и владеть отдельным буфером вершин. Но я могу сделать это? После того, как я вызову drawPrimitives:, я предполагаю, что содержимое буфера вершин должно быть скопировано в графический процессор, и я предполагаю (?), Что это не делается синхронно, поэтому было бы небезопасно немедленно начинать модифицировать буфер вершин для рисования следующего объекта. Итак: как я узнаю, когда Metal завершит работу с буфером, и я смогу начать его изменение снова?

Два: Даже если у # 1 есть четко определенный ответ, такой, что я могу блокировать, пока Metal не закончится с буфер и затем начните изменять его для следующего вызова drawPrimitives:, это разумный дизайн? Я предполагаю, что это будет означать, что мой поток ЦП будет постоянно блокироваться, чтобы ждать передачи памяти, что не очень хорошо. Так что это в значительной степени подталкивает меня к дизайну, где каждый объект имеет свой собственный буфер вершин?

Три: ОК, предположим, что у каждого объекта есть свой собственный буфер вершин, или я выполняю один рендеринг "большого взрыва" всего этого с одним большим буфером вершин (я думаю, этот вопрос относится к обоим проектам). После того, как я вызову presentDrawable: и затем commit в моем буфере команд, мое приложение go отключится и выполнит небольшую работу, а затем попытается обновить отображение, поэтому мой код для рисования теперь выполняется снова. Я хотел бы повторно использовать буферы вершин, которые я выделил ранее, перезаписывая данные в них, чтобы сделать новое, обновленное отображение. Но опять же: как мне узнать, когда это безопасно? Насколько я понимаю, тот факт, что commit вернулся в мой код, не означает, что Metal уже завершил копирование моих вершинных буферов в GPU, и в общем случае я должен предположить, что это может занять произвольно много времени, поэтому может быть еще не сделано, когда я повторно введу свой код для рисования. Что правильно сказать? И снова: должен ли я просто блокировать ожидание, пока они не станут доступны (однако я должен это делать), или у меня должен быть второй набор буферов вершин, который я могу использовать в случае, если Metal все еще занят первым набором? (Похоже, что это просто выталкивает проблему вниз, так как, когда мой код рисования введен для третьего обновления, оба ранее использованных набора буферов могут быть еще недоступны, верно? Так что тогда я мог бы добавить третий набор буферов вершин, но затем четвертое обновление ...)

Четыре: для рисования кадра и подкадров, я хотел бы просто написать повторно используемая функция типа «drawFrame», которую могут вызывать все, но я немного озадачен правильным дизайном. С OpenGL это было легко:

- (void)drawViewFrameInBounds:(NSRect)bounds
{
    int ox = (int)bounds.origin.x, oy = (int)bounds.origin.y;

    glColor3f(0.77f, 0.77f, 0.77f);
    glRecti(ox, oy, ox + 1, oy + (int)bounds.size.height);
    glRecti(ox + 1, oy, ox + (int)bounds.size.width - 1, oy + 1);
    glRecti(ox + (int)bounds.size.width - 1, oy, ox + (int)bounds.size.width, oy + (int)bounds.size.height);
    glRecti(ox + 1, oy + (int)bounds.size.height - 1, ox + (int)bounds.size.width - 1, oy + (int)bounds.size.height);
}

Но с металлом я не уверен, что такое хороший дизайн. Я предполагаю, что функция не может просто иметь свой собственный маленький буфер вершин, объявленный как локальный массив stati c, в который она выбрасывает вершины и затем вызывает drawPrimitives:, потому что, если она вызывается дважды подряд, Metal может еще не иметь скопировал данные вершины из первого вызова, когда второй вызов хочет изменить буфер. Я, очевидно, не хочу выделять новый буфер вершин каждый раз, когда вызывается функция. Я мог бы сделать так, чтобы вызывающая сторона передавала буфер вершин для использования функции, но это только выдвигает проблему на уровень; как тогда вызывающая сторона справится с этой ситуацией? Возможно, я мог бы сделать так, чтобы функция добавляла новые вершины в конец растущего списка вершин в буфере, предоставленном вызывающей стороной; но это, кажется, либо заставляет весь рендер полностью заранее спланировать (так что я могу предварительно выделить большой буфер правильного размера, чтобы он соответствовал всем вершинам, которые будут рисовать все), что требует кода рисования верхнего уровня, чтобы каким-то образом знать, как много вершин, каждый объект будет в конечном итоге генерировать (что нарушает инкапсуляцию), или создать проект, в котором у меня есть расширяющийся буфер вершин, который по-настоящему получает c ', когда его емкость оказывается недостаточной. Я знаю, как делать эти вещи; но никто из них не чувствует себя хорошо. Я борюсь с тем, каков правильный дизайн, потому что я не очень хорошо понимаю модель памяти Metal, я думаю. Любой совет? Извиняюсь за очень длинный многочастный вопрос, но я думаю, что все это сводится к тому же основанию c непониманию.

1 Ответ

2 голосов
/ 02 февраля 2020

Краткий ответ на основной вопрос: вам не следует перезаписывать ресурсы, которые используются командами, добавленными в буфер команд, до тех пор, пока этот буфер команд не завершится. Лучший способ определить это - добавить обработчик завершения. Вы также можете опрашивать свойство status буфера команд, но это не так хорошо.

Во-первых, пока вы не зафиксируете буфер команд, в GPU ничего не копируется. Кроме того, как вы заметили, даже после фиксации командного буфера вы не можете предполагать, что данные полностью скопированы в графический процессор.

Во-вторых, в простом случае вы должны поместить весь чертеж для кадр в единый командный буфер. Создание и фиксация большого количества командных буферов (например, по одному для каждого объекта, который dr aws) добавляет накладные расходы.

Эти два комбинированных пункта означают, что вы обычно не можете повторно использовать ресурс в одном и том же кадре. По сути, вам понадобится двойной или тройной буфер, чтобы получить правильность и хорошую производительность одновременно.

Типичный метод - создать небольшой пул буферов, защищаемых семафором. Первоначально количество семафоров - это количество буферов в пуле. Код, которому нужен буфер, ожидает семафора и, когда это удается, вынимает буфер из пула. Также следует добавить обработчик завершения в буфер команд, который помещает буфер обратно в пул и сигнализирует семафор.

Вы можете использовать пул динамических c буферов. Если код хочет буфер и пул пуст, он создает буфер вместо блокировки. Затем, когда это сделано, он добавляет буфер в пул, эффективно увеличивая размер пула. Однако, как правило, в этом нет никакого смысла. Вам понадобится только более трех буферов, если процессор работает намного впереди графического процессора, и в этом нет реальной выгоды.

Что касается вашего желания, чтобы каждый объект рисовал сам, то это, безусловно, можно сделать. Я бы использовал большой буфер вершин вместе с некоторыми метаданными о том, сколько из них использовалось до сих пор. Каждый объект, который нужно нарисовать, будет добавлять свои данные вершин в буфер и кодировать свои команды рисования, ссылающиеся на эти данные вершин. Вы должны использовать параметр vertexStart, чтобы команда рисования ссылалась на правильное место в буфере вершин.

Вы должны также рассмотреть индексированное рисование со значением примитива перезапуска, поэтому есть только одна команда рисования, которая dr aws все примитивы. Каждый объект будет добавлять свой примитив к данным общих вершин и индексным буферам, а затем какой-то высокоуровневый контроллер будет рисовать.

...