Vulkan. Developer's guide. Non-programmable pipeline stages

I work as a translator for CG Tribe in Izhevsk and here I publish translations of the Vulkan Tutorial (original - vulkan-tutorial.com ) into Russian.



Today I want to present a translation of a new chapter in the section on the Graphics pipeline basics called Fixed functions.



Content
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









Non-programmable pipeline stages





Early graphics APIs used default state for most stages of the graphics pipeline. In Vulkan, all states must be described explicitly, starting with the viewport size and ending with the color mixing function. In this chapter, we will set up the non-programmable pipeline stages.



Vertex input



The VkPipelineVertexInputStateCreateInfo structure describes the format of the vertex data that is passed to the vertex shader. There are two types of descriptions:



  • Description of attributes: data type passed to the vertex shader, binding to the data buffer and offset in it
  • Binding: the distance between data items and how the data and the output geometry are bound (per-instance or vertex binding) (see Geometry instancing )


Since we hardcoded the vertex data in the vertex shader, we will indicate that there is no data to load. To do this, let's fill in the structure VkPipelineVertexInputStateCreateInfo



. We'll come back to this question later in the chapter on vertex buffers.



VkPipelineVertexInputStateCreateInfo vertexInputInfo{};
vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
vertexInputInfo.vertexBindingDescriptionCount = 0;
vertexInputInfo.pVertexBindingDescriptions = nullptr; // Optional
vertexInputInfo.vertexAttributeDescriptionCount = 0;
vertexInputInfo.pVertexAttributeDescriptions = nullptr; // Optional
      
      





Members pVertexBindingDescriptions



and pVertexAttributeDescriptions



point to an array of structures that describe the above data for loading vertex attributes. Add this structure to the function createGraphicsPipeline



right after shaderStages



.



Input assembler



The VkPipelineInputAssemblyStateCreateInfo structure describes 2 things: what geometry is formed from vertices and whether restarting of geometry is allowed for geometries such as line strip and triangle strip. Geometry is specified in the field topology



and can have the following values:



  • VK_PRIMITIVE_TOPOLOGY_POINT_LIST



    : geometry is drawn as separate points, each vertex is a separate point
  • VK_PRIMITIVE_TOPOLOGY_LINE_LIST



    : the geometry is drawn as a set of line segments, each pair of vertices forms a separate line
  • VK_PRIMITIVE_TOPOLOGY_LINE_STRIP



    : the geometry is drawn as a continuous polyline, each subsequent vertex adds one segment to the polyline
  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST



    : the geometry is drawn as a set of triangles, with every 3 vertices forming an independent triangle
  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP



    : ,


Usually vertices are loaded sequentially in the order in which you place them in the vertex buffer. However, with an index buffer, you can change the loading order. This allows for optimizations such as reusing vertices. If you primitiveRestartEnable



specify a value in the field VK_TRUE



, you can interrupt the lines and triangles with topology VK_PRIMITIVE_TOPOLOGY_LINE_STRIP



and VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP



and start drawing new primitives using the special index 0xFFFF



or 0xFFFFFFFF



.



In the tutorial, we will be drawing individual triangles, so we will use the following structure:



VkPipelineInputAssemblyStateCreateInfo inputAssembly{};
inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
inputAssembly.primitiveRestartEnable = VK_FALSE;
      
      





Viewport and scissors



The viewport describes the area of ​​the framebuffer to which the output is rendered. Almost always the coordinates from (0, 0)



to are set for the viewport (width, height)



.



VkViewport viewport{};
viewport.x = 0.0f;
viewport.y = 0.0f;
viewport.width = (float) swapChainExtent.width;
viewport.height = (float) swapChainExtent.height;
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;
      
      





Be aware that the size of the swap chain and images may differ from the values WIDTH



and the HEIGHT



window. Later, the images from the swap chain will be used as framebuffers, so we must use exactly their size.



minDepth



and maxDepth



determine the range of depth values ​​for the framebuffer. These values ​​must be in the range [0,0f, 1,0f]



, and there minDepth



may be more maxDepth



. Use the default values ​​- 0.0f



and 1.0f



if you're not going to do anything out of the ordinary.



If the viewport determines how the image will be stretched in the framebuffer, then scissor determines which pixels will be saved. All pixels outside the scissor rectangle will be discarded during rasterization. The clipping rectangle is used to crop the image, not transform it. The difference is shown in the pictures below. Please note that the clipping rectangle on the left is just one of many possible options for obtaining such an image, as long as its size is larger than the size of the viewport.







In this tutorial, we want to render the image to the entire framebuffer, so we will specify that the scissor rectangle completely overlaps the viewport:



VkRect2D scissor{};
scissor.offset = {0, 0};
scissor.extent = swapChainExtent;
      
      





Now we need to combine the information about the viewport and the scissor using the VkPipelineViewportStateCreateInfo structure . On some video cards, several viewports and clipping rectangles can be used simultaneously, so information about them is transmitted as an array. To use several viewports at once, you need to enable the corresponding GPU option.



VkPipelineViewportStateCreateInfo viewportState{};
viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewportState.viewportCount = 1;
viewportState.pViewports = &viewport;
viewportState.scissorCount = 1;
viewportState.pScissors = &scissor;
      
      





Rasterizer



The rasterizer converts geometry from a vertex shader into multiple fragments. The depth test , face culling , scissor test is also performed here, and the method of filling polygons with fragments is configured: filling the entire polygon, or only the edges of polygons (wireframe rendering). All of this is configured in the VkPipelineRasterizationStateCreateInfo structure .



VkPipelineRasterizationStateCreateInfo rasterizer{};
rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
rasterizer.depthClampEnable = VK_FALSE;
      
      





If the field is depthClampEnable



set VK_TRUE



, the fragments of which are outside of the near and far planes, are not cut off, and pushes them. This can be useful, for example, when creating a shadow map. To use this parameter, you must enable the corresponding GPU option.



rasterizer.rasterizerDiscardEnable = VK_FALSE;
      
      





If rasterizerDiscardEnable



set VK_TRUE



, the rasterization stage is disabled and no output is passed to the framebuffer.



rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
      
      





polygonMode



determines how chunks are generated. The following modes are available:



  • VK_POLYGON_MODE_FILL



    : polygons are completely filled with fragments
  • VK_POLYGON_MODE_LINE



    : polygon edges are converted to lines
  • VK_POLYGON_MODE_POINT



    : polygon vertices are drawn as dots


To use these modes, except VK_POLYGON_MODE_FILL



, you need to enable the corresponding GPU option.




rasterizer.lineWidth = 1.0f;
      
      





The field lineWidth



sets the thickness of the segments. The maximum supported chunk width depends on your hardware, and thicker chunks 1,0f



require the GPU option to be enabled wideLines



.



rasterizer.cullMode = VK_CULL_MODE_BACK_BIT;
rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE;
      
      





The parameter cullMode



defines the face culling type. You can turn off clipping entirely, or turn on clipping for front and / or non-front faces. The variable frontFace



determines the order in which the vertices are traversed (clockwise or counterclockwise) to define the front faces.



rasterizer.depthBiasEnable = VK_FALSE;
rasterizer.depthBiasConstantFactor = 0.0f; // Optional
rasterizer.depthBiasClamp = 0.0f; // Optional
rasterizer.depthBiasSlopeFactor = 0.0f; // Optional
      
      





The rasterizer can change the depth values ​​by adding a constant value or offsetting the depth depending on the slope of the fragment. This is usually used when creating a shadow map. We don't need this, so we'll depthBiasEnable



install it for VK_FALSE



.



Multisampling



The VkPipelineMultisampleStateCreateInfo structure configures multisampling - one of the anti-aliasing methods . It works mainly on the edges, combining colors from different polygons that are rasterized into the same pixels. This allows you to get rid of the most visible artifacts. The main advantage of multisampling is that the fragment shader in most cases is executed only once per pixel, which is much better, for example, than rendering at a higher resolution and then reducing the size. To use multisampling, you must enable the corresponding GPU option.



VkPipelineMultisampleStateCreateInfo multisampling{};
multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
multisampling.sampleShadingEnable = VK_FALSE;
multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
multisampling.minSampleShading = 1.0f; // Optional
multisampling.pSampleMask = nullptr; // Optional
multisampling.alphaToCoverageEnable = VK_FALSE; // Optional
multisampling.alphaToOneEnable = VK_FALSE; // Optional
      
      





Until we include it, we will return to it in one of the following articles.



Depth test and stencil test



When using a depth buffer and / or a stencil buffer, you need to configure them using VkPipelineDepthStencilStateCreateInfo . We don't need this yet, so we'll just pass it nullptr



instead of a pointer to this structure. We'll come back to this in the chapter on the depth buffer.



Color mixing



The color returned by the fragment shader needs to be merged with the color already in the framebuffer. This process is called color mixing, and there are two ways to do it:



  • Mix old and new value to get the output color
  • Concatenate old and new value using bitwise operation


Two types of structures are used to configure color mixing: the VkPipelineColorBlendAttachmentState structure contains settings for each connected framebuffer, the VkPipelineColorBlendStateCreateInfo structure contains global color mixing settings. In our case, only one framebuffer is used:



VkPipelineColorBlendAttachmentState colorBlendAttachment{};
colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
colorBlendAttachment.blendEnable = VK_FALSE;
colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_ONE; // Optional
colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ZERO; // Optional
colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; // Optional
colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; // Optional
colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; // Optional
colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; // Optional
      
      





The structure VkPipelineColorBlendAttachmentState



allows you to customize color mixing in the first way. The following pseudocode best demonstrates all the operations performed:



if (blendEnable) {
    finalColor.rgb = (srcColorBlendFactor * newColor.rgb) <colorBlendOp> (dstColorBlendFactor * oldColor.rgb);
    finalColor.a = (srcAlphaBlendFactor * newColor.a) <alphaBlendOp> (dstAlphaBlendFactor * oldColor.a);
} else {
    finalColor = newColor;
}

finalColor = finalColor & colorWriteMask;
      
      





If blendEnable



set VK_FALSE



, the color from the fragment shader is passed unchanged. If set VK_TRUE



, two blending operations are used to compute the new color. The final color is filtered using colorWriteMask



to determine which channels of the output image are being written to.



The most common color blending is alpha blending, where the new color is blended with the old color based on transparency. finalColor



is calculated as follows:



finalColor.rgb = newAlpha * newColor + (1 - newAlpha) * oldColor;
finalColor.a = newAlpha.a;
      
      





This can be configured using the following options:



colorBlendAttachment.blendEnable = VK_TRUE;
colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD;
colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD;
      
      





All possible operations can be found in the VkBlendFactor and VkBlendOp enumerations in the specification.



The second structure refers to an array of structures for all framebuffers and allows mixing constants to be specified that can be used as mixing factors in the above calculations.



VkPipelineColorBlendStateCreateInfo colorBlending{};
colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
colorBlending.logicOpEnable = VK_FALSE;
colorBlending.logicOp = VK_LOGIC_OP_COPY; // Optional
colorBlending.attachmentCount = 1;
colorBlending.pAttachments = &colorBlendAttachment;
colorBlending.blendConstants[0] = 0.0f; // Optional
colorBlending.blendConstants[1] = 0.0f; // Optional
colorBlending.blendConstants[2] = 0.0f; // Optional
colorBlending.blendConstants[3] = 0.0f; // Optional
      
      





If you want to use the second mixing method (bitwise operation), set VK_TRUE



for logicOpEnable



. Then you can specify the bitwise operation in the field logicOp



. Note that the first method will automatically become unavailable, as if each is connected to the framebuffer blendEnable



has been found VK_FALSE



! Note that it colorWriteMask



is also used for bitwise operations to determine which channel content will be changed. You can turn off both modes, as we did, in this case the colors of the fragments will be written to the framebuffer without changes.



Dynamic state



Some states of the graphics pipeline can be changed without re-creating the pipeline, such as viewport size, chunk widths, and blending constants. To do this, fill in the VkPipelineDynamicStateCreateInfo structure :



VkDynamicState dynamicStates[] = {
    VK_DYNAMIC_STATE_VIEWPORT,
    VK_DYNAMIC_STATE_LINE_WIDTH
};

VkPipelineDynamicStateCreateInfo dynamicState{};
dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
dynamicState.dynamicStateCount = 2;
dynamicState.pDynamicStates = dynamicStates;
      
      





As a result, the values ​​of these settings are not taken into account at the stage of creating the pipeline, and you need to specify them right at the time of rendering. We'll come back to this in the next chapters. You can use nullptr



instead of a pointer to this structure if you don't want to use dynamic states.



Pipeline Layout



In shaders, you can use uniform



-variables - global variables that can be changed dynamically to change the behavior of the shaders without having to re-create them. They are typically used to pass a transformation matrix to a vertex shader or to create texture samplers in a fragment shader.



These uniforms must be specified when creating the pipeline using the VkPipelineLayout object . Even though we won't be using these variables for now, we still need to create an empty pipeline layout.



Let's create a member of the class to hold the object, as we will later refer to it from other functions:




VkPipelineLayout pipelineLayout;
      
      





Then let's create an object in a function createGraphicsPipeline



:



VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 0; // Optional
pipelineLayoutInfo.pSetLayouts = nullptr; // Optional
pipelineLayoutInfo.pushConstantRangeCount = 0; // Optional
pipelineLayoutInfo.pPushConstantRanges = nullptr; // Optional

if (vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS) {
    throw std::runtime_error("failed to create pipeline layout!");
}
      
      





The structure also specifies push constants, which are another way to pass dynamic variables to shaders. We will get to know them later. We will use the pipeline throughout the entire life cycle of the program, so we need to destroy it at the very end:



void cleanup() {
    vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
    ...
}
      
      





Conclusion



That's all there is to know about non-programmable states! It took a lot of work to set them up from scratch, but now you know almost everything that happens in the graphics pipeline!



To create a graphics pipeline, it remains to create the last object - the render pass.



All Articles