Vulkan API — урок 45. Промежуточное изображение

Теперь создадим изображение в общедоступной памяти. Пиксели в объекте изображения известны как тексели (texel(s)), так и и будем называть впредь. Добавим следующие две следующие переменные в функцию createTextureImage:

VDeleter<VkImage> stagingImage{device, vkDestroyImage};
VDeleter<VkDeviceMemory> stagingImageMemory{device, vkFreeMemory};

Параметры для изображения определяются в структуре VkImageCreateInfo.

Тип изображения, определенный в поле imageType, говорит Vulkan’у о том, какого рода система координат будет использоваться для адресации текселей в изображении. Возможно создавать 1D, 2D и 3D изображения. Одномерное изображение может быть использовано для хранения массива данных или градиента, двумерное изображение в основном используется для текстур, а трехмерное изображение может быть использовано для хранения вексельных значений, например.

Поле extent определяет размеры изображения, основываясь на том, сколько текселей по каждой оси. Потому значение поля depth должно быть 1. Данное изображение не будет массивом и не будет использоваться для мипмаппинга.

VkImageCreateInfo imageInfo = {};
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imageInfo.imageType = VK_IMAGE_TYPE_2D;
imageInfo.extent.width = texWidth;
imageInfo.extent.height = texHeight;
imageInfo.extent.depth = 1;
imageInfo.mipLevels = 1;
imageInfo.arrayLayers = 1;

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

imageInfo.format = VK_FORMAT_R8G8B8A8_UNORM;

Полю tiling можно задать одно из двух значений:

  • VK_IMAGE_TILING_LINEAR: Тексели располагаются в виде основного ряда (row-major order), как массив пикселей
  • VK_IMAGE_TILING_OPTIMAL: Тексели располагаются в заданном порядке для оптимального доступа row-major

Если вы хотите получить доступ непосредственно к текселям непосредственно в памяти изображения, тогда нужно использовать VK_IMAGE_TILING_LINEAR. В нашем случае нужно иметь доступ для непосредственного копирования данных в pixels в память промежуточного изображения, так что нужно использовать данный вариант. В отличии от layout изображения, tiling не может быть изменен в дальнейшем. Для финального изображения будет использоваться VK_IMAGE_TILING_OPTIMAL.

VK_IMAGE_TILING_OPTIMAL

Для поля initialLayout есть только два возможных значения:

  • VK_IMAGE_LAYOUT_UNDEFINED: Не и используется GPU и первое изменение (transition) отбросит все тексели.
  • VK_IMAGE_LAYOUT_PREINITIALIZED: Не и используется GPU, но первое изменение (transition) сохранит тексели.

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

imageInfo.initialLayout = VK_IMAGE_LAYOUT_PREINITIALIZED;

Поле usage имеет то же смысл, что и при создании буфера. Промежуточное изображение будет скопировано в окончательное изображение текстуры, потому и должно быть установлен «статус» промежуточного источника:

imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT;

Промежуточное изображение будет использовано только семейством, поддерживающим операции переноса:

imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;

Флаг samples относится к такой штуке, как multisampling (вид сглаживания). Это актуально для изображений, используемых в качестве вложений, так что придерживаемся one sample. Есть некоторые дополнительные флаги для изображений, связанных с разреженными изображений (sparse images). Разреженные изображения – это изображения, в которых только определенные области записаны в память. Если используются 3D текстуры для воксельной местности, к приеру, тогда следует использовать эту функцию, дабы избежать выделения памяти для хранения больших объемов значений «воздуха». В этой серии уроков данный функционал использоваться не будет, потому оставляем значение по умолчанию:

imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageInfo.flags = 0; // Optional

Теперь можно создать изображение используя vkCreateImage, который не имеет никаких особенных параметров. Возможна ситуация, когда формат VK_FORMAT_R8G8B8A8_UNORM не поддерживается видеокартой. Вам необходимо иметь список альтернатив, что бы подобрать наиболее подходящий поддерживаемый аналог. Но поддержка этого формата столь широка, что опустим этот шаг в этой серии уроков. Использование различных форматов потребует множественных конверсий, вернемся к этому во время обсуждения глубинного буфера (depth buffer).

if (vkCreateImage(device, &imageInfo, nullptr, stagingImage.replace()) != VK_SUCCESS) {
    throw std::runtime_error("failed to create image!");
}

Выделение памяти для изображения работает аналогично выделению памяти для буфера. Используется vkGetImageMemoryRequirements вместо vkGetBufferMemoryRequirements, и vkBindImageMemory вместо vkBindBufferMemory. Стоит помнить, что память должна быть видима процессору, это свойство нужно указать при поиске правильного типа памяти.

VkMemoryRequirements memRequirements;
vkGetImageMemoryRequirements(device, stagingImage, &memRequirements);

VkMemoryAllocateInfo allocInfo = {};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memRequirements.size;
allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);

if (vkAllocateMemory(device, &allocInfo, nullptr, stagingImageMemory.replace()) != VK_SUCCESS) {
    throw std::runtime_error("failed to allocate image memory!");
}

vkBindImageMemory(device, stagingImage, stagingImageMemory, 0);

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

void* data;
vkMapMemory(device, stagingImageMemory, 0, imageSize, 0, &data);

К сожалению мы не можем просто скопировать байты пикселя непосредственно в память изображения при помощи memcpy и предположить, что все сработает корректно. Проблема в том, что там могут быть промежуточные (padding) байты меж рядами пикселей. Другими словами видеокарта может предположить, что одна из строк пикселей не texWidth * 4, а texWidth * 4 + paddingBytes. Для корректрой обработки необходимо запросить, как байты располагаются в промежуточном изображении, используя vkGetImageSubresourceLayout:

VkImageSubresource subresource = {};
subresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
subresource.mipLevel = 0;
subresource.arrayLayer = 0;

VkSubresourceLayout stagingImageLayout;
vkGetImageSubresourceLayout(device, stagingImage, &subresource, &stagingImageLayout);

Изображение содержит один или несколько subresources (подресурсов), которые являются специфическими изображениями в изображении. К примеру есть один подресурс для каждой записи в массиве изображения, в таком случае мы не имеем массива изображения, это просто один подресурс при входе на 0 и базовый уровень мипмаппинга.

Элемент rowPitch структуры VkSubresourceLayout задает общее количество байт каждой строки пикселей в изображении. Если это значение равно texWidth * 4, то нам повезло и можно использоватьe memcpy, потому что промежуточные байты отсутствуют.

if (stagingImageLayout.rowPitch == texWidth * 4) {
    memcpy(data, pixels, (size_t) imageSize);
} else {

}

Так обычно и происходит, если изображение имеет размер два в степени (512, 1024 и т.д.). Иначе же придется копировать пиксели строка за строкой используя смещение:

uint8_t* dataBytes = reinterpret_cast<uint8_t*>(data);

for (int y = 0; y < texHeight; y++) {
    memcpy(
        &dataBytes[y * stagingImageLayout.rowPitch],
        &pixels[y * texWidth * 4],
        texWidth * 4
    );
}

Каждая последующая строка в памяти изображения имеет смещение на и сами пиксели размером texWidth * 4, без промежуточных байт.

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

void* data;
vkMapMemory(device, stagingImageMemory, 0, imageSize, 0, &data);

    if (stagingImageLayout.rowPitch == texWidth * 4) {
        memcpy(data, pixels, (size_t) imageSize);
    } else {
        uint8_t* dataBytes = reinterpret_cast<uint8_t*>(data);

        for (int y = 0; y < texHeight; y++) {
            memcpy(&dataBytes[y * stagingImageLayout.rowPitch], &pixels[y * texWidth * 4], texWidth * 4);
        }
    }

vkUnmapMemory(device, stagingImageMemory);

Не забудьте подчистить оригинальный массив пикселей:

stbi_image_free(pixels);

Main Admin

2 Comments

  1. VDeleter stagingImage{device, vkDestroyImage};
    VDeleter stagingImageMemory{device, vkFreeMemory};/code>

    /code> <—— Косяк

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *