Vulkan. Guía del desarrollador. Representación

Realizo traducciones técnicas en CG Tribe, empresa de TI de Izhevsk, y sigo publicando la traducción de las lecciones del Tutorial Vulkan al ruso. El texto original de la guía se puede encontrar aquí .



Mi publicación de hoy trata sobre los dos primeros artículos de la sección Dibujo, Búferes de cuadros y Búferes de comandos.



Contenido
1.



2.



3.



4.







  1. (pipeline)



5.



  1. Staging


6. Uniform-



  1. layout
  2. sets


7.



  1. Image view image sampler
  2. image sampler


8.



9.



10. -



11. Multisampling



FAQ









Framebuffers



En los últimos capítulos, hablamos mucho sobre framebuffers y configuramos un pase de renderizado para un framebuffer con el mismo formato que la imagen de la cadena de intercambio. Sin embargo, el framebuffer en sí todavía no se ha creado.



Los adjuntos a los que hicimos referencia al crear el pase de renderizado deben estar envueltos en un VkFramebuffer . Apunta a todos los objetos VkImageView que usaremos como objetivos. En nuestro caso, solo hay un búfer: el búfer de color. Sin embargo, la cadena de intercambio contiene muchas imágenes, y debemos usar exactamente la que obtuvimos de la cadena de intercambio para dibujar. En otras palabras, necesitamos crear framebuffers para cada imagen de la cadena de intercambio y usar el framebuffer al que se adjunta la imagen de interés.



Para hacer esto, agregue un miembro más de la clase:



std::vector<VkFramebuffer> swapChainFramebuffers;
      
      





Agreguemos una función createFramebuffers



y llamémosla justo después de crear la canalización de gráficos. Dentro de este método, crearemos objetos para nuestra matriz:



void initVulkan() {
    createInstance();
    setupDebugMessenger();
    createSurface();
    pickPhysicalDevice();
    createLogicalDevice();
    createSwapChain();
    createImageViews();
    createRenderPass();
    createGraphicsPipeline();
    createFramebuffers();
}

...

void createFramebuffers() {

}
      
      





Primero, asignemos el espacio necesario en el contenedor para almacenar framebuffers:



void createFramebuffers() {
    swapChainFramebuffers.resize(swapChainImageViews.size());
}
      
      





Luego, revisamos todas las vistas de imágenes y creamos framebuffers a partir de ellas:



for (size_t i = 0; i < swapChainImageViews.size(); i++) {
    VkImageView attachments[] = {
        swapChainImageViews[i]
    };

    VkFramebufferCreateInfo framebufferInfo{};
    framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
    framebufferInfo.renderPass = renderPass;
    framebufferInfo.attachmentCount = 1;
    framebufferInfo.pAttachments = attachments;
    framebufferInfo.width = swapChainExtent.width;
    framebufferInfo.height = swapChainExtent.height;
    framebufferInfo.layers = 1;

    if (vkCreateFramebuffer(device, &framebufferInfo, nullptr, &swapChainFramebuffers[i]) != VK_SUCCESS) {
        throw std::runtime_error("failed to create framebuffer!");
    }
}
      
      





Como puede ver, no es difícil crear un framebuffer. Primero debe especificar con cuál renderPass



debería ser compatible. Los framebuffers solo se pueden usar con pases de renderizado compatibles, es decir, usan el mismo número y tipo de búferes.



Los parámetros attachmentCount



y pAttachments



apuntan a los objetos VkImageView que deben coincidir con la descripción pAttachments



utilizada al crear el pase de renderizado.



Parámetros width



y height



no causan problemas. El campo layers



especifica el número de capas para imágenes. Nuestras imágenes solo tienen una capa, entonces capas = 1



.



Los framebuffers deben eliminarse antes de las vistas de la imagen y el pase de renderizado, pero solo después de que se complete el renderizado:



void cleanup() {
    for (auto framebuffer : swapChainFramebuffers) {
        vkDestroyFramebuffer(device, framebuffer, nullptr);
    }

    ...
}
      
      





Ahora tenemos todos los objetos que necesitamos renderizar. En el próximo capítulo, ya podremos escribir los primeros comandos de dibujo.



Código C ++ / Vertex Shader / Fragment Shader





Búferes de comando





Los comandos en Vulkan, como dibujar y moverse en la memoria, no se ejecutan directamente cuando se llama a la función correspondiente. Todas las operaciones necesarias deben escribirse en el búfer. Esto es conveniente porque el complejo proceso de configuración de comandos se puede realizar por adelantado y en múltiples subprocesos. En el ciclo principal, todo lo que tienes que hacer es decirle a Vulkan que ejecute comandos.



Grupo de equipo



Antes de continuar con la creación del búfer de comandos, debemos crear el grupo de comandos. El grupo de comandos administra la memoria que se utiliza para almacenar búferes. Agreguemos



un nuevo miembro de la clase VkCommandPool .



VkCommandPool commandPool;
      
      





Ahora creemos una nueva función createCommandPool



y llamemos initVulkan



después de crear los framebuffers.



void initVulkan() {
    createInstance();
    setupDebugMessenger();
    createSurface();
    pickPhysicalDevice();
    createLogicalDevice();
    createSwapChain();
    createImageViews();
    createRenderPass();
    createGraphicsPipeline();
    createFramebuffers();
    createCommandPool();
}

...

void createCommandPool() {

}
      
      





Solo necesita dos parámetros para crear un grupo de comandos:



QueueFamilyIndices queueFamilyIndices = findQueueFamilies(physicalDevice);

VkCommandPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
poolInfo.queueFamilyIndex = queueFamilyIndices.graphicsFamily.value();
poolInfo.flags = 0; // Optional
      
      





Para ejecutar búferes de comandos, deben enviarse a una cola, como una cola de gráficos o de visualización. El grupo de comandos solo puede asignar búferes de comandos para una familia de colas. Necesitamos escribir los comandos de dibujo, por lo que usamos una familia de colas habilitadas para gráficos.



Hay dos posibles indicadores para el grupo de comandos:



  • VK_COMMAND_POOL_CREATE_TRANSIENT_BIT



    : una pista que le dice a Vulkan que los búferes del grupo se vacían y se asignan con frecuencia (puede cambiar el comportamiento del grupo al asignar memoria)
  • VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT



    : le permite sobrescribir búferes de forma independiente; si la bandera no está configurada, será posible restablecer los búferes solo todos y simultáneamente


Escribiremos búferes de comandos solo al comienzo del programa, y ​​luego los ejecutaremos repetidamente en el bucle principal, por lo que ninguno de estos indicadores nos conviene.



if (vkCreateCommandPool(device, &poolInfo, nullptr, &commandPool) != VK_SUCCESS) {
    throw std::runtime_error("failed to create command pool!");
}
      
      





Completemos la creación del grupo de comandos con la función vkCreateCommandPool . No hay parámetros especiales en él. Los comandos se utilizarán durante todo el ciclo de vida del programa, por lo que el grupo debe destruirse al final:



void cleanup() {
    vkDestroyCommandPool(device, commandPool, nullptr);

    ...
}
      
      







Asignar memoria para búferes de comandos



Ahora podemos asignar memoria para búferes de comandos y escribirles comandos de dibujo. Dado que cada uno de los comandos de dibujo está vinculado a un VkFrameBuffer correspondiente , debemos escribir un búfer de comando para cada imagen en la cadena de intercambio. Para hacer esto, creemos una lista de objetos VkCommandBuffer como miembro de la clase. Una vez que se destruye el grupo de comandos, los búferes de comandos se liberan automáticamente, por lo que no es necesario vaciarlos explícitamente.



std::vector<VkCommandBuffer> commandBuffers;
      
      





Pasemos a la función createCommandBuffers



que asigna memoria y escribe comandos para cada imagen de la cadena de intercambio.



void initVulkan() {
    createInstance();
    setupDebugMessenger();
    createSurface();
    pickPhysicalDevice();
    createLogicalDevice();
    createSwapChain();
    createImageViews();
    createRenderPass();
    createGraphicsPipeline();
    createFramebuffers();
    createCommandPool();
    createCommandBuffers();
}

...

void createCommandBuffers() {
    commandBuffers.resize(swapChainFramebuffers.size());
}
      
      





Los búferes de comando se crean utilizando la función vkAllocateCommandBuffers , que toma una estructura VkCommandBufferAllocateInfo como parámetro. La estructura indica el grupo de comandos y el número de búferes:



VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = (uint32_t) commandBuffers.size();

if (vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data()) != VK_SUCCESS) {
    throw std::runtime_error("failed to allocate command buffers!");
}
      
      





El parámetro level



determina si los búferes de comando son primarios o secundarios.



  • VK_COMMAND_BUFFER_LEVEL_PRIMARY



    : Los búferes primarios se pueden poner en cola, pero no se pueden llamar desde otros búferes de comando.
  • VK_COMMAND_BUFFER_LEVEL_SECONDARY



    : Los búferes secundarios no se envían directamente a la cola, pero se pueden llamar desde los búferes de comandos primarios.


No usaremos búferes de comandos secundarios, aunque a veces puede ser conveniente transferir a ellos algunas secuencias de comandos de uso frecuente y reutilizarlas en el búfer primario.



Búfer de comando de escritura



Comencemos a grabar el búfer de comandos llamando a vkBeginCommandBuffer . Como argumento, pasamos una pequeña estructura VkCommandBufferBeginInfo que contiene información sobre el uso de este búfer.



for (size_t i = 0; i < commandBuffers.size(); i++) {
    VkCommandBufferBeginInfo beginInfo{};
    beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
    beginInfo.flags = 0; // Optional
    beginInfo.pInheritanceInfo = nullptr; // Optional

    if (vkBeginCommandBuffer(commandBuffers[i], &beginInfo) != VK_SUCCESS) {
        throw std::runtime_error("failed to begin recording command buffer!");
    }
}
      
      





El parámetro flags



especifica cómo se utilizará el búfer de comando. Están disponibles los siguientes valores:



  • VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT



    : Después de cada inicio, se sobrescribe el búfer de comandos.
  • VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT



    : indica que este será un búfer de comando secundario que está completamente dentro de una pasada de renderizado.
  • VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT



    : el búfer se puede reenviar a la cola incluso si su llamada anterior aún no se ha completado y está en estado pendiente.


De momento, ninguna de las banderas nos conviene.



Este parámetro se pInheritanceInfo



utiliza solo para búferes secundarios de comando. Define el estado heredado de los búferes primarios.



Si el búfer de comando ya se ha escrito una vez, llamar a vkBeginCommandBuffer lo vaciará implícitamente. No puede agregar comandos a un búfer que ya está lleno.



Iniciar pase de renderizado



Comencemos a dibujar el triángulo iniciando el paso de renderizado con vkCmdBeginRenderPass . El pase de representación se configura utilizando algunos parámetros en la estructura VkRenderPassBeginInfo .



VkRenderPassBeginInfo renderPassInfo{};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
renderPassInfo.renderPass = renderPass;
renderPassInfo.framebuffer = swapChainFramebuffers[i];
      
      





Los primeros parámetros definen el paso de renderizado y el framebuffer. Hemos creado un framebuffer para cada imagen en la cadena de intercambio, que se utiliza como buffer de color.



renderPassInfo.renderArea.offset = {0, 0};
renderPassInfo.renderArea.extent = swapChainExtent;
      
      





Los siguientes dos parámetros determinan el tamaño del área de renderizado. El área de renderización define la parte del framebuffer donde los sombreadores pueden guardar y cargar datos. Los píxeles fuera de esta área no estarán definidos. Para un mejor rendimiento, el área de renderizado debe coincidir con el tamaño de los búferes.



VkClearValue clearColor = {0.0f, 0.0f, 0.0f, 1.0f};
renderPassInfo.clearValueCount = 1;
renderPassInfo.pClearValues = &clearColor;
      
      





Los dos últimos parámetros definen los valores para vaciar los búferes cuando se utiliza la operación VK_ATTACHMENT_LOAD_OP_CLEAR



. Llene el framebuffer con negro al 100% de opacidad.



Ahora comencemos el pase de renderizado.



vkCmdBeginRenderPass(commandBuffers[i], &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE);
      
      





Las funciones para escribir comandos en el búfer se pueden encontrar mediante el prefijo vkCmd . Todos regresan void



, por lo que el manejo de errores se realizará solo después del final de la grabación.



El primer parámetro de cada comando es el búfer de comando en el que se escribe el comando. El segundo parámetro es información sobre el pase de renderizado. El último parámetro determina cómo se proporcionarán los comandos en el subpaso. Puede elegir uno de dos valores:



  • VK_SUBPASS_CONTENTS_INLINE



    : los comandos se escribirán en el búfer de comando primario sin activar búferes secundarios.
  • VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS



    : los comandos se ejecutarán desde búferes secundarios de comandos.


No usaremos búferes de comandos, por lo que elegiremos la primera opción.



Comandos de dibujo básicos



Ahora podemos conectar la canalización de gráficos:



vkCmdBindPipeline(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline);
      
      





El segundo parámetro indica qué canalización se utiliza: gráficos o computacional. Queda por dibujar un triángulo:



vkCmdDraw(commandBuffers[i], 3, 1, 0, 0);
      
      





La función vkCmdDraw parece muy modesta, pero es simple debido al hecho de que hemos especificado todos los datos de antemano. Además del búfer de comando, se pasan los siguientes parámetros a la función:



  • vertexCount



    : aunque no estamos usando un búfer de vértices, técnicamente tenemos 3 vértices para dibujar el triángulo.
  • instanceCount



    : se usa cuando es necesario renderizar varias instancias de un objeto (renderizado instanciado); pasar 1



    si no usa esta función.
  • firstVertex



    : utilizado como un desplazamiento en el búfer de vértice, especifica el valor más pequeño gl_VertexIndex



    .
  • firstInstance



    : se utiliza como desplazamiento al dibujar varias instancias del mismo objeto; determina el valor más pequeño gl_InstanceIndex



    .


Terminación



Ahora podemos terminar el pase de renderizado:



vkCmdEndRenderPass(commandBuffers[i]);
      
      





Y termina de escribir el búfer de comando:



if (vkEndCommandBuffer(commandBuffers[i]) != VK_SUCCESS) {
    throw std::runtime_error("failed to record command buffer!");
}
      
      





En el siguiente capítulo, escribiremos el código para el bucle principal, donde la imagen se recupera de la cadena de intercambio, después de lo cual se inicia el búfer de comando correspondiente, y luego la imagen terminada se devuelve a la cadena de intercambio para su visualización.



Código C ++ / Vertex Shader / Fragment Shader



All Articles