Совместима ли математическая библиотека GLM с языком затенения металлов Apple? - PullRequest
0 голосов
/ 28 февраля 2019

Я собираюсь портировать приложение iOS, которое использует OpenGL, написанный на C ++, на Apple Metal.Цель состоит в том, чтобы полностью избавиться от OpenGL и заменить его на Metal.

Код OpenGL является многоуровневым, и я пытаюсь просто заменить средство визуализации, то есть класс, который фактически вызывает функции OpenGL.Однако вся кодовая база использует математическую библиотеку GLM для представления векторов и матриц.

Например, существует класс камеры, который обеспечивает матрицу вида и проекции.Оба они имеют тип glm::mat4 и просто передаются в вершинный шейдер GLSL, где они совместимы с типом данных mat4, заданным GLSL.Я хотел бы использовать этот класс камеры для отправки этих матриц в вершинный шейдер Metal.Теперь я не уверен, совместим ли glm::mat4 с металлом float4x4.

У меня нет рабочего примера, где я могу это проверить, потому что я буквально только начал с Metal и не могу найтичто-нибудь полезное онлайн.

Итак, мои вопросы следующие:

  1. Совместимы ли типы GLM, такие как glm::mat4 и glm::vec4, с металлом float4x4 / float4?
  2. Если ответ на вопрос 1. да, есть ли у меня какие-либо недостатки, если я непосредственно использую типы GLM в металлических шейдерах?SIMD-библиотека Apple, которая предоставляет другой набор типов данных, которые я бы не смог использовать в таком случае, верно?

    Приложение только для iOS, мне вообще не важно запускать Metal на macOS.

    Фрагменты кода (желательно Objective-C (да, без шуток)) были бы очень кстати.

1 Ответ

0 голосов
/ 20 марта 2019

В целом ответ: да , GLM хорошо подходит для приложений, использующих Apple Metal.Тем не менее, есть несколько вещей, которые необходимо учитывать.Некоторые из этих вещей уже упоминались в комментариях.

Прежде всего, в Руководстве по программированию Metal упоминается, что

Metal определяет его нормализованную координату устройства.(NDC) система как куб 2x2x1 с центром в (0, 0, 0.5)

Это означает, что координаты NDC металла отличаются от координат NDC OpenGL, поскольку OpenGL определяет систему координат NDC как 2x2x2куб с центром в (0, 0, 0), т. е. действительные координаты NDC OpenGL должны находиться в пределах

// Valid OpenGL NDC coordinates
-1 <= x <= 1
-1 <= y <= 1
-1 <= z <= 1

Поскольку GLM изначально был адаптирован для OpenGL, его функции glm::ortho и glm::perspective создают матрицы проекций, которые преобразуют координаты вКоординаты OpenGL NDC.Из-за этого необходимо настроить эти координаты на металл.Как это можно сделать, описано в этом сообщении в блоге.

Однако есть более элегантный способ исправить эти координаты.Интересно, что Vulkan использует ту же систему координат NDC, что и Metal, и GLM уже адаптирован для работы с Vulkan (подсказка для этого найдена здесь ).

Путем определенияМакрос препроцессора C / C ++ GLM_FORCE_DEPTH_ZERO_TO_ONE упомянутые матричные функции проекции GLM преобразуют координаты для работы с системой координат Metal / Vulkan NDC.Следовательно, #define решит проблему с различными системами координат NDC.

Далее, при обмене данными между шейдерами Metal и клиентом важно учитывать как размер, так и выравнивание типов данных GLM и Metal.боковой (CPU) код.В спецификации Apple Metal Shading Language указаны размер и выравнивание для некоторых типа данных.

Для типов данных, которые там не указаны, их размер и выравниваниеможно определить с помощью операторов * / 1039 * и alignof в C / C ++.Интересно, что оба оператора поддерживаются в шейдерах Metal.Вот пара примеров как для GLM, так и для Metal:

// Size and alignment of some GLM example data types
glm::vec2 : size:  8, alignment: 4
glm::vec3 : size: 12, alignment: 4
glm::vec4 : size: 16, alignment: 4
glm::mat4 : size: 64, alignment: 4

// Size and alignment of some of Metal example data types
float2        : size:  8, alignment:  8
float3        : size: 16, alignment: 16
float4        : size: 16, alignment: 16
float4x4      : size: 64, alignment: 16
packed_float2 : size:  8, alignment:  4
packed_float3 : size: 12, alignment:  4
packed_float4 : size: 16, alignment:  4

Как видно из приведенной таблицы, векторные типы данных GLM хорошо соответствуют упакованным векторным типам данных Metal как с точки зрения размера, так и с точки зрения выравнивания.Однако обратите внимание, что матричные типы данных 4x4 не совпадают с точки зрения выравнивания.

Согласно этому ответу на другой вопрос SO, выравнивание означает следующее:

Выравнивание - это ограничение, в котором в памяти можно сохранить первый байт значения.(Необходимо повысить производительность процессоров и разрешить использование определенных инструкций, которые работают только с данными с определенным выравниванием, например, SSE необходимо выровнять до 16 байтов, а AVX - до 32 байтов.)

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

Поэтому мы должны быть осторожны, чтобы учитывать различные выравнивания при отправке матриц 4x4 в металлические шейдеры.Давайте рассмотрим пример:

Следующая структура Objective-C служит буфером для хранения унифицированных значений для отправки в вершинный шейдер Metal:

typedef struct
{
  glm::mat4 modelViewProjectionMatrix;
  glm::vec2 windowScale;
  glm::vec4 edgeColor;
  glm::vec4 selectionColor;
} SolidWireframeUniforms;

Эта структура определена взаголовочный файл, который включен везде, где это требуется в коде на стороне клиента (т.е. на стороне процессора).Чтобы использовать эти значения на стороне вершинного шейдера Metal, нам нужна соответствующая структура данных.В этом примере часть вершинного шейдера Metal выглядит следующим образом:

#include <metal_matrix>
#include <metal_stdlib>

using namespace metal;

struct SolidWireframeUniforms
{
  float4x4      modelViewProjectionMatrix;
  packed_float2 windowScale;
  packed_float4 edgeColor;
  packed_float4 selectionColor;
};

// VertexShaderInput struct defined here...

// VertexShaderOutput struct defined here...

vertex VertexShaderOutput solidWireframeVertexShader(VertexShaderInput input [[stage_in]], constant SolidWireframeUniforms &uniforms [[buffer(1)]])
{
  VertexShaderOutput output;
  // vertex shader code
}

Для передачи данных из кода на стороне клиента в шейдер Metal унифицированная структура упаковывается в буфер.Приведенный ниже код показывает, как создать и обновить этот буфер:

- (void)createUniformBuffer
{
  _uniformBuffer = [self.device newBufferWithBytes:(void*)&_uniformData length:sizeof(SolidWireframeUniforms) options:MTLResourceCPUCacheModeDefaultCache];
}


- (void)updateUniforms
{
  dispatch_semaphore_wait(_bufferAccessSemaphore, DISPATCH_TIME_FOREVER);

  SolidWireframeUniforms* uniformBufferContent = (SolidWireframeUniforms*)[_uniformBuffer contents];
  memcpy(uniformBufferContent, &_uniformData, sizeof(SolidWireframeUniforms));

  dispatch_semaphore_signal(_bufferAccessSemaphore);
}

Обратите внимание на вызов memcpy, который используется для обновления буфера.Здесь все может пойти не так, если размер и выравнивание типов данных GLM и Metal не совпадают.Поскольку мы просто копируем каждый байт структуры Objective-C в буфер, а затем на стороне шейдера Metal, снова интерпретируем эти данные, данные будут неверно интерпретированы на стороне шейдера Metal, если структуры данных не совпадают.

В этом примере схема памяти выглядит следующим образом:

                                              104 bytes
           |<--------------------------------------------------------------------------->|
           |                                                                             |
           |         64 bytes              8 bytes         16 bytes         16 bytes     |
           | modelViewProjectionMatrix   windowScale      edgeColor      selectionColor  |
           |<------------------------->|<----------->|<--------------->|<--------------->|
           |                           |             |                 |                 |
           +--+--+--+------------+--+--+--+-------+--+--+-----------+--+--+----------+---+
Byte index | 0| 1| 2|    ...     |62|63|64|  ...  |71|72|    ...    |87|88|   ...    |103|
           +--+--+--+------------+--+--+--+-------+--+--+-----------+--+--+----------+---+
                                        ^             ^                 ^
                                        |             |                 |
                                        |             |                 +-- Is a multiple of 4, aligns with glm::vec4 / packed_float4
                                        |             |
                                        |             +-- Is a multiple of 4, aligns with glm::vec4 / packed_float4
                                        |
                                        +-- Is a multiple of 4, aligns with glm::vec2 / packed_float2

За исключением выравнивания 4x4 matix, все хорошо соответствует.Несоосность матрицы 4х4 здесь не представляет проблемы, как видно на приведенной выше схеме памяти.Однако, если единообразная структура изменяется, выравнивание или размер могут стать проблемой, и для правильной работы может потребоваться заполнение.

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

Это не так в приведенном выше примере, гдедлина буфера составляет 104 байта, что не кратно 16. При запуске приложения непосредственно из Xcode встроенное утверждение выводит следующее сообщение:

validateFunctionArguments: 3478: сбой подтверждения `Функция вершины (solidWireframeVertexShader): аргумент uniforms [0] из буфера (1) со смещением (0) и длиной (104) имеет пространство для 104 байтов, но аргумент имеет длину (112). '

В порядкеЧтобы решить эту проблему, нам нужно сделать размер буфера кратным 16 байтам.Для этого мы просто вычисляем следующее кратное 16 на основе фактической длины, которая нам нужна.Для 104 это будет 112, что также говорит нам приведенное выше утверждение.

Следующая функция вычисляет следующий кратный 16 для указанного целого числа:

- (NSUInteger)roundUpToNextMultipleOf16:(NSUInteger)number
{
  NSUInteger remainder = number % 16;

  if(remainder == 0)
  {
    return number;
  }

  return number + 16 - remainder;
}

Теперь мы вычисляемдлина унифицированного буфера с использованием вышеуказанной функции, которая изменяет метод создания буфера (опубликованный выше) следующим образом:

- (void)createUniformBuffer
{
  NSUInteger bufferLength = [self roundUpToNextMultipleOf16:sizeof(SolidWireframeUniforms)];
  _uniformBuffer = [self.device newBufferWithBytes:(void*)&_uniformData length:bufferLength options:MTLResourceCPUCacheModeDefaultCache];
}

Это должно решить проблему, обнаруженную упомянутым утверждением.

...