Setting up render pipeline
vk.GraphicsPipelineCreateInfo
Building a graphics pipeline is a far more involved task than building a compute pipeline. With the compute pipeline, we only needed a single shader module and pipeline layout, so there was no need of an abstraction layer. But graphics pipelines contain a considerable amount of options, and without a way to simplify it, creating them can be considerably complicated.
For that reason, we will be creating a PipelineBuilder structure, that keeps track of all those options, and will offer some simpler procedures to enable/disable features we want, keeping as much defaulted as possible. A lot of those options are things that we wont be using on the tutorial, so trying to reduce the area will be useful.
Some of the options on a pipeline can be set to be dynamic, which means we will set those options when binding the pipeline and recording draw commands. For example we will put viewport as dynamic, as if we had it "baked in", we would need to create new pipelines if we wanted to change the resolution of our rendering.
Before writing the builder, lets look at what we will need to fill. In the same way creating a
compute pipeline required a vk.ComputePipelineCreateInfo
, a graphics one is a
vk.GraphicsPipelineCreateInfo
structure.
GraphicsPipelineCreateInfo :: struct {
sType: StructureType,
pNext: rawptr,
flags: PipelineCreateFlags,
stageCount: u32,
pStages: [^]PipelineShaderStageCreateInfo,
pVertexInputState: ^PipelineVertexInputStateCreateInfo,
pInputAssemblyState: ^PipelineInputAssemblyStateCreateInfo,
pTessellationState: ^PipelineTessellationStateCreateInfo,
pViewportState: ^PipelineViewportStateCreateInfo,
pRasterizationState: ^PipelineRasterizationStateCreateInfo,
pMultisampleState: ^PipelineMultisampleStateCreateInfo,
pDepthStencilState: ^PipelineDepthStencilStateCreateInfo,
pColorBlendState: ^PipelineColorBlendStateCreateInfo,
pDynamicState: ^PipelineDynamicStateCreateInfo,
layout: PipelineLayout,
renderPass: RenderPass,
subpass: u32,
basePipelineHandle: Pipeline,
basePipelineIndex: i32,
}
Spec page for graphics pipeline can be found here, which can be used to check things in detail.
stageCount
and pStages
contains the ShaderStageCreateInfo that will contain the shader
modules for the different stages on the pipeline. We will be sending here our fragment shader
and vertex shader.
vk.PipelineVertexInputStateCreateInfo
contains the configuration for vertex attribute input
with vertex buffers. If we configure this correctly, our vertex shader will get vertex
properties as input in an optimal way. But we will not be using this, as we are just going to
send a data array to the shader and index it ourselves, which allows techniques that improve
performance and allows more complicated vertex formats that compress data. This is generally
known as "vertex pulling", and even if you are doing equivalent thing as the fixed-hardware
vertex input, on modern gpus it will perform about the same.
vk.PipelineInputAssemblyStateCreateInfo
contains the configuration for triangle topology. We
use this to set the pipeline to draw triangles, points, or lines.
vk.PipelineTessellationStateCreateInfo
is configuration for fixed tesellation. We will not be
using this and will leave it as null.
vk.PipelineViewportStateCreateInfo
contains information about the viewport the pixels will be
rendered into. This lets you set what region of pixels will the pipeline draw. We will default
it, because we will be using dynamic state for this.
vk.PipelineRasterizationStateCreateInfo
has the information on how exactly do the triangles
get rasterized between the vertex shader and the fragment shader. It has options for depth bias
(used when rendering shadows), toggling between wireframe and solid rendering, and the
configuration for drawing or skipping backfaces.
vk.PipelineMultisampleStateCreateInfo
lets us configure Multi Sample antialiasing. Thats a
way of improving the antialiasing of our rendering by rasterizing the fragments more times at
triangle edges. We will default it to no antialiasing, but we will look into using it later.
vk.PipelineDepthStencilStateCreateInfo
contains the depth-testing and stencil configuration.
vk.PipelineColorBlendStateCreateInfo
has the color blending and attachment write information.
Its used to make triangles transparent or other blending configurations.
vk.PipelineDynamicStateCreateInfo
configures dynamic state. One great downside that vulkan
pipelines have is that their configuration is "hardcoded" at creation. So if we want to do
things like toggle depth-testing on and off, we will need 2 pipelines. It even hardcodes
viewport, so if we want to change the size of our render targets, we will also need to rebuild
all pipelines. Building pipelines is a very expensive operation, and we want to minimize the
number of pipelines used as its critical for performance. For that reason, some of the state of
a vulkan pipeline can be set as dynamic, and then the configuration option can be modified at
runtime when recording commands. What dynamic state is supported by a given gpu depends on gpu
vendor, driver version, and other variables. We will be using dynamic state for our viewport
and scissor configuration, as almost all GPUs support that one, and it removes the need to
hardcode the draw image resolution when building the pipelines.
The VkGraphicsPipelineCreateInfo takes a VkPipelineLayout that is the same one used when building compute pipelines.
It also takes a VkRenderPass and subpass index. We will not be using that because we use
dynamic rendering, so all systems related to VkRenderPass will be completely skipped. Instead,
we need to extend the VkGraphicsPipelineCreateInfo with a vk.PipelineRenderingCreateInfo
added into its pNext chain. This structure holds a list of the attachment formats the pipeline
will use.
Pipeline Builder
Lets begin writing the builder. All pipeline code will be on pipelines.odin
. You can find
it on the shared folder if you are checking the chapter code.
// Core
import sa "core:container/small_array"
MAX_SHADER_STAGES :: #config(MAX_SHADER_STAGES, 8)
Shader_Stages :: sa.Small_Array(MAX_SHADER_STAGES, vk.PipelineShaderStageCreateInfo)
Pipeline_Builder :: struct {
shader_stages: Shader_Stages,
input_assembly: vk.PipelineInputAssemblyStateCreateInfo,
rasterizer: vk.PipelineRasterizationStateCreateInfo,
color_blend_attachment: vk.PipelineColorBlendAttachmentState,
multisampling: vk.PipelineMultisampleStateCreateInfo,
pipeline_layout: vk.PipelineLayout,
depth_stencil: vk.PipelineDepthStencilStateCreateInfo,
render_info: vk.PipelineRenderingCreateInfo,
color_attachment_format: vk.Format,
tessellation_state: vk.PipelineTessellationStateCreateInfo,
base_pipeline: vk.Pipeline,
base_pipeline_index: i32,
flags: vk.PipelineCreateFlags,
allocator: runtime.Allocator,
}
The pipeline builder will hold most of the state we need to track of. The actual CreateInfo
structure will be fully filled from the pipeline_builder_build()
procedure. But first, we
need to initialize a builder with some defaults. We have a pipeline_builder_clear()
procedure
that will set everything into empty/default properties. The pipeline_builder_create_default
just returns a builder with the same defaults.
Lets write that pipeline_builder_clear()
procedure first.
pipeline_builder_clear :: proc(self: ^Pipeline_Builder) {
assert(self != nil)
self.input_assembly = {
sType = .PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,
}
self.rasterizer = {
sType = .PIPELINE_RASTERIZATION_STATE_CREATE_INFO,
}
self.multisampling = {
sType = .PIPELINE_MULTISAMPLE_STATE_CREATE_INFO,
}
self.depth_stencil = {
sType = .PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO,
}
self.render_info = {
sType = .PIPELINE_RENDERING_CREATE_INFO,
}
self.tessellation_state = {
sType = .PIPELINE_TESSELLATION_STATE_CREATE_INFO,
}
sa.clear(&self.shader_stages)
self.pipeline_layout = {}
self.base_pipeline = {}
self.base_pipeline_index = -1
self.flags = {}
}
pipeline_builder_create_default :: proc() -> (builder: Pipeline_Builder) {
pipeline_builder_clear(&builder)
return
}
We will set the sType
of every structure here, and leave everything else as 0.
Lets begin writing the pipeline_builder_build
procedure. first we will begin by setting some
of the Info structures we are missing because they wont be configured.
pipeline_builder_build :: proc(
self: ^Pipeline_Builder,
device: vk.Device,
) -> (
pipeline: vk.Pipeline,
ok: bool,
) #optional_ok {
// Make viewport state from our stored viewport and scissor.
// At the moment we wont support multiple viewports or scissors
viewport_state := vk.PipelineViewportStateCreateInfo {
sType = .PIPELINE_VIEWPORT_STATE_CREATE_INFO,
viewportCount = 1,
scissorCount = 1,
}
// Setup dummy color blending. We arent using transparent objects yet,
// the blending is just "no blend", but we do write to the color attachment
color_blending := vk.PipelineColorBlendStateCreateInfo {
sType = .PIPELINE_COLOR_BLEND_STATE_CREATE_INFO,
logicOpEnable = false,
logicOp = .COPY,
attachmentCount = 1,
pAttachments = &self.color_blend_attachment,
}
// Completely clear `VertexInputStateCreateInfo`, as we have no need for it
vertex_input_info := vk.PipelineVertexInputStateCreateInfo {
sType = .PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,
}
dynamic_states := [?]vk.DynamicState{.VIEWPORT, .SCISSOR}
dynamic_info := vk.PipelineDynamicStateCreateInfo {
sType = .PIPELINE_DYNAMIC_STATE_CREATE_INFO,
pDynamicStates = raw_data(dynamic_states[:]),
dynamicStateCount = u32(len(dynamic_states)),
}
return pipeline, true
}
We first fill vk.PipelineViewportStateCreateInfo
with just viewport count and nothing else.
With dynamic viewport state we dont need to fill the viewport or stencil options here.
Then we fill vk.PipelineColorBlendStateCreateInfo
with some default options for logic
blending (we wont use it), and hook the vk.PipelineColorBlendAttachmentState
for the blending
options for a single attachment. We only support rendering to one attachment here, so this is
fine. It can be made into an array of vk.PipelineColorBlendAttachmentState
if drawing to
multiple attachments is needed.
Setting up dynamic state is just filling a vk.PipelineDynamicStateCreateInfo
with an array of
vk.DynamicState
enums. We will use these 2 for now.
To connect all of the configuration structures we have on the builder, lets begin filling the
vk.GraphicsPipelineCreateInfo
and add render_info
into the pNext
of the graphics pipeline
info itself.
// Build the actual pipeline.
// We now use all of the info structs we have been writing into into this one
// to create the pipeline.
pipeline_info := vk.GraphicsPipelineCreateInfo {
sType = .GRAPHICS_PIPELINE_CREATE_INFO,
// Connect the renderInfo to the pNext extension mechanism
pNext = &self.render_info,
flags = self.flags,
stageCount = u32(sa.len(self.shader_stages)),
pStages = raw_data(sa.slice(&self.shader_stages)),
pVertexInputState = &vertex_input_info,
pInputAssemblyState = &self.input_assembly,
pTessellationState = &self.tessellation_state,
pViewportState = &viewport_state,
pRasterizationState = &self.rasterizer,
pMultisampleState = &self.multisampling,
pDepthStencilState = &self.depth_stencil,
pColorBlendState = &color_blending,
pDynamicState = &dynamic_info,
layout = self.pipeline_layout,
basePipelineHandle = self.base_pipeline,
basePipelineIndex = self.base_pipeline_index,
}
This is all we needed for the pipeline, so we can finally call the create procedure.
vk_check(
vk.CreateGraphicsPipelines(device, 0, 1, &pipeline_info, nil, &pipeline),
"Failed to create pipeline",
) or_return
And thats it with the main creation procedure. We now need to actually set the options properly, as right now the entire pipeline is essentially null, which will error as-is due to missing options.
pipeline_builder_add_shader :: proc(
self: ^Pipeline_Builder,
shader: vk.ShaderModule,
stage: vk.ShaderStageFlags,
entry_point: cstring = "main",
) {
create_info := pipeline_shader_stage_create_info(stage, shader, entry_point)
sa.push(&self.shader_stages, create_info)
}
pipeline_builder_set_shaders :: proc(
self: ^Pipeline_Builder,
vertex_shader, fragment_shader: vk.ShaderModule,
) {
pipeline_builder_add_shader(self, vertex_shader, {.VERTEX})
pipeline_builder_add_shader(self, fragment_shader, {.FRAGMENT})
}
We begin by adding a procedure to set the vertex and fragment shaders. We add them into the
shader_stages
array with the proper info creation, which we already had from building the
compute pipeline.
Next we add a procedure to set input topology.
pipeline_builder_set_input_topology :: proc(
self: ^Pipeline_Builder,
topology: vk.PrimitiveTopology,
primitive_restart_enable: bool = false,
) {
self.input_assembly.topology = topology
// we are not going to use primitive restart on the entire tutorial so leave it on false
self.input_assembly.primitiveRestartEnable = b32(primitive_restart_enable)
}
vk.PrimitiveTopology
has the options for TRIANGLE_LIST
, POINT_LIST
, and so on.
PrimitiveRestart is used for triangle strips and line strips, but we dont use it.
The rasterizer state is a big one so we will split it on a few options.
pipeline_builder_set_polygon_mode :: proc(
self: ^Pipeline_Builder,
polygon_mode: vk.PolygonMode,
line_width: f32 = 1.0,
) {
self.rasterizer.polygonMode = polygon_mode
self.rasterizer.lineWidth = line_width
}
We need to have lineWidth as 1.f as default, then we set the polygon mode, which controls wireframe vs solid rendering and point rendering.
pipeline_builder_set_cull_mode :: proc(
self: ^Pipeline_Builder,
cull_mode: vk.CullModeFlags,
front_face: vk.FrontFace,
) {
self.rasterizer.cullMode = cull_mode
self.rasterizer.frontFace = front_face
}
Cull mode will set the front face and the cull mode for backface culling.
Next is setting the multisample state. We will default the structure to multisampling disabled. Later we can add other procedures for enabling different multisampling levels for antialiasing
pipeline_builder_set_multisampling :: proc(
self: ^Pipeline_Builder,
rasterization_samples: vk.SampleCountFlags,
min_sample_shading: f32 = 1.0,
sample_mask: ^vk.SampleMask = nil,
alpha_to_coverage_enable: bool = false,
alpha_to_one_enable: bool = false,
) {
self.multisampling.rasterizationSamples = rasterization_samples
self.multisampling.sampleShadingEnable = min_sample_shading < 1.0
self.multisampling.minSampleShading = min_sample_shading
self.multisampling.pSampleMask = sample_mask
self.multisampling.alphaToCoverageEnable = b32(alpha_to_coverage_enable)
self.multisampling.alphaToOneEnable = b32(alpha_to_one_enable)
}
pipeline_builder_set_multisampling_none :: proc(self: ^Pipeline_Builder) {
pipeline_builder_set_multisampling(self, {._1})
}
Next we will add a procedure for blending mode.
pipeline_builder_set_blend_state :: proc(
self: ^Pipeline_Builder,
blend_enable: bool,
src_color_blend: vk.BlendFactor,
dst_color_blend: vk.BlendFactor,
color_blend_op: vk.BlendOp,
src_alpha_blend: vk.BlendFactor,
dst_alpha_blend: vk.BlendFactor,
alpha_blend_op: vk.BlendOp,
color_write_mask: vk.ColorComponentFlags,
) {
self.color_blend_attachment = {
blendEnable = b32(blend_enable),
srcColorBlendFactor = src_color_blend,
dstColorBlendFactor = dst_color_blend,
colorBlendOp = color_blend_op,
srcAlphaBlendFactor = src_alpha_blend,
dstAlphaBlendFactor = dst_alpha_blend,
alphaBlendOp = alpha_blend_op,
colorWriteMask = color_write_mask,
}
}
pipeline_builder_disable_blending :: proc(self: ^Pipeline_Builder) {
// Default write mask
self.color_blend_attachment.colorWriteMask = {.R, .G, .B, .A}
// No blending
self.color_blend_attachment.blendEnable = false
}
We will have our pipeline_builder_disable_blending()
procedure that sets blendEnable
to
false
but sets the correct write mask. We will add procedures for more blending modes later.
We need to setup a proper colorWriteMask here so that our pixel output will write to the
attachment correctly.
Now we hook our formats, lets add the procedures for both depth testing and color attachment.
pipeline_builder_set_color_attachment_format :: proc(self: ^Pipeline_Builder, format: vk.Format) {
self.color_attachment_format = format
// Connect the format to the `render_info` structure
self.render_info.colorAttachmentCount = 1
self.render_info.pColorAttachmentFormats = &self.color_attachment_format
}
pipeline_builder_set_depth_attachment_format :: proc(self: ^Pipeline_Builder, format: vk.Format) {
self.render_info.depthAttachmentFormat = format
}
On the color attachment, the pipeline needs it by pointer because it wants an array of color attachments. This is useful for things like deferred rendering where you draw to multiple images at once, but we dont need this yet so we can default it to just 1 color format.
The last one we need is a procedure to disable the depth testing logic.
pipeline_builder_disable_depth_test :: proc(self: ^Pipeline_Builder) {
self.depth_stencil.depthTestEnable = false
self.depth_stencil.depthWriteEnable = false
self.depth_stencil.depthCompareOp = .NEVER
self.depth_stencil.depthBoundsTestEnable = false
self.depth_stencil.stencilTestEnable = false
self.depth_stencil.front = {}
self.depth_stencil.back = {}
self.depth_stencil.minDepthBounds = 0.0
self.depth_stencil.maxDepthBounds = 1.0
}
Draw Geometry
With all the basic features for the pipeline builder filled, we can now draw a triangle. For our triangle, we are going to use hardcoded vertex positions in the vertex shader, and the output will be a pure color.
These are the shaders:
struct VSOutput
{
float4 pos : SV_Position;
[vk_location(0)]
float3 color : COLOR0;
};
[shader("vertex")]
VSOutput main(uint vertex_index: SV_VertexID)
{
let positions = float3[3](
float3(1.f, 1.f, 0.0f),
float3(-1.f, 1.f, 0.0f),
float3(0.f, -1.f, 0.0f));
// Array of colors for the triangle
let colors = float3[3](
float3(1.0f, 0.0f, 0.0f), // red
float3(0.0f, 1.0f, 0.0f), // green
float3(0.0f, 0.0f, 1.0f) // blue
);
VSOutput output;
output.pos = float4(positions[vertex_index], 1.0f);
output.color = colors[vertex_index];
return output;
}
struct PSInput
{
[vk_location(0)]
float3 color : COLOR0;
};
struct PSOutput
{
[vk_location(0)]
float4 frag_color : COLOR0;
};
[shader("fragment")]
PSOutput main(PSInput input)
{
PSOutput output;
output.frag_color = float4(input.color, 1.f);
return output;
}
In our Slang vertex shader, we're working with a hardcoded array of positions and colors to
create a colored triangle. The shader function is marked with [shader("vertex")]
to indicate
its type.
For each vertex shader invocation, we use the vertex_index
to access the corresponding
position and color from our arrays. This parameter automatically increments for each vertex
processed, similar to how LocalThreadID
works in compute shaders.
The shader outputs both position (tagged with SV_Position
) and color (with a custom Vulkan
location specified by [vk_location(0)]
). Since we only defined three vertices, attempting to
render more than one triangle would cause an error.
In our fragment shader (marked with [shader("fragment")]
), we take the interpolated color
from the vertex shader as input. We then output a 4-component color vector at the same Vulkan
location (0), which connects to the render attachments in the render pass. We simply pass
through the interpolated color from the vertex shader and add an alpha value of 1.0 for full
opacity.
Lets now create the pipeline and layout we need to draw this triangle.
On Engine
structure, we add a couple of fields to hold the pipeline and its layout. We also
add a engine_init_triangle_pipeline()
procedure.
Engine :: struct {
triangle_pipeline_layout: vk.PipelineLayout,
triangle_pipeline: vk.Pipeline,
}
engine_init_triangle_pipeline :: proc(self: ^Engine) -> (ok: bool) {
return true
}
We will call this engine_init_triangle_pipeline()
from engine_init_pipelines()
procedure.
Lets write that procedure We will start by loading the 2 shaders into vk.ShaderModules
, like
we did with the compute shader, but this time more shaders.
engine_init_triangle_pipeline :: proc(self: ^Engine) -> (ok: bool) {
triangle_frag_shader := create_shader_module(
self.vk_device,
#load("./../../shaders/compiled/colored_triangle.frag.spv"),
) or_return
defer vk.DestroyShaderModule(self.vk_device, triangle_frag_shader, nil)
triangle_vert_shader := create_shader_module(
self.vk_device,
#load("./../../shaders/compiled/colored_triangle.vert.spv"),
) or_return
defer vk.DestroyShaderModule(self.vk_device, triangle_vert_shader, nil)
// Build the pipeline layout that controls the inputs/outputs of the shader, we are not
// using descriptor sets or other systems yet, so no need to use anything other than empty
// default
pipeline_layout_info := pipeline_layout_create_info()
vk_check(
vk.CreatePipelineLayout(
self.vk_device,
&pipeline_layout_info,
nil,
&self.triangle_pipeline_layout,
),
) or_return
deletion_queue_push(&self.main_deletion_queue, self.triangle_pipeline_layout)
return true
}
We also create the pipeline layout. Unlike with the compute shader before, this time we have no push constants and no descriptor bindings on here, so its really just a completely empty layout.
Now we create the pipeline, using the Pipeline Builder created before.
engine_init_triangle_pipeline :: proc(self: ^Engine) -> (ok: bool) {
// Other code ---
builder := pipeline_builder_create_default()
// Use the triangle layout we created
builder.pipeline_layout = self.triangle_pipeline_layout
// Add the vertex and pixel shaders to the pipeline
pipeline_builder_set_shaders(&builder, triangle_vert_shader, triangle_frag_shader)
// It will draw triangles
pipeline_builder_set_input_topology(&builder, .TRIANGLE_LIST)
// Filled triangles
pipeline_builder_set_polygon_mode(&builder, .FILL)
// No backface culling
pipeline_builder_set_cull_mode(&builder, vk.CullModeFlags_NONE, .CLOCKWISE)
// No multisampling
pipeline_builder_set_multisampling_none(&builder)
// No blending
pipeline_builder_disable_blending(&builder)
// No depth testing
pipeline_builder_disable_depth_test(&builder)
// Connect the image format we will draw into, from draw image
pipeline_builder_set_color_attachment_format(&builder, self.draw_image.image_format)
pipeline_builder_set_depth_attachment_format(&builder, .UNDEFINED)
// Finally build the pipeline
self.triangle_pipeline = pipeline_builder_build(&builder, self.vk_device) or_return
deletion_queue_push(&self.main_deletion_queue, self.triangle_pipeline)
return true
}
With the pipeline built, we can draw our triangle as part of the command buffer we create every frame.
The compute shader we run for the background needed to draw into GENERAL
image layout, but
when doing geometry rendering, we need to use COLOR_ATTACHMENT_OPTIMAL
. It is possible to
draw into GENERAL
layout with graphics pipelines, but its lower performance and the
validation layers will complain. We will create a new procedure, engine_draw_geometry()
, to
hold these graphics commands.
Lets update the draw loop first.
// Start the command buffer recording
vk_check(vk.BeginCommandBuffer(cmd, &cmd_begin_info)) or_return
// Transition our main draw image into general layout so we can write into it
// we will overwrite it all so we dont care about what was the older layout
transition_image(cmd, self.draw_image.image, .UNDEFINED, .GENERAL)
// Clear the image
engine_draw_background(self, cmd) or_return
transition_image(cmd, self.draw_image.image, .GENERAL, .COLOR_ATTACHMENT_OPTIMAL)
// Draw the triangle
engine_draw_geometry(self, cmd) or_return
// Transition the draw image and the swapchain image into their correct transfer layouts
transition_image(cmd, self.draw_image.image, .COLOR_ATTACHMENT_OPTIMAL, .TRANSFER_SRC_OPTIMAL)
transition_image(
cmd,
self.swapchain_images[frame.swapchain_image_index],
.UNDEFINED,
.TRANSFER_DST_OPTIMAL,
)
Now fill the draw_geometry procedure
engine_draw_geometry :: proc(self: ^Engine, cmd: vk.CommandBuffer) -> (ok: bool) {
// Begin a render pass connected to our draw image
color_attachment := attachment_info(self.draw_image.image_view, nil, .COLOR_ATTACHMENT_OPTIMAL)
render_info := rendering_info(self.draw_extent, &color_attachment, nil)
vk.CmdBeginRendering(cmd, &render_info)
vk.CmdBindPipeline(cmd, .GRAPHICS, self.triangle_pipeline)
// Set dynamic viewport and scissor
viewport := vk.Viewport {
x = 0,
y = 0,
width = f32(self.draw_extent.width),
height = f32(self.draw_extent.height),
minDepth = 0.0,
maxDepth = 1.0,
}
vk.CmdSetViewport(cmd, 0, 1, &viewport)
scissor := vk.Rect2D {
offset = {x = 0, y = 0},
extent = {width = self.draw_extent.width, height = self.draw_extent.height},
}
vk.CmdSetScissor(cmd, 0, 1, &scissor)
// Launch a draw command to draw 3 vertices
vk.CmdDraw(cmd, 3, 1, 0, 0)
vk.CmdEndRendering(cmd)
return true
}
To draw our triangle we need to begin a renderpass with cmdBeginRendering. This is the same we were doing for imgui last chapter, but this time we are pointing it into our _drawImage instead of the swapchain image.
We do a vk.CmdBindPipeline
, but instead of using the bind point .COMPUTE
, we now use
.GRAPHICS
. Then, we have to set our viewport and scissor. This is required before we left them
undefined when creating the pipeline as we were using dynamic pipeline state. With that set, we
can do a vk.CmdDraw()
to draw the triangle. With that done, we can finish the render pass to end
our drawing.
If you run the program at this point, you should see a triangle being rendered on top of the compute based background