7.5 KiB
title | date | weight |
---|---|---|
Command Buffer | 2021-01-28T14:05:53-08:00 | 4 |
Now that we have our rendering state set up, we can talk about issuing commands.
There are four kinds of overarching operations that we perform using the command buffer: applying render state, uploading data, binding resources, and performing draws. Let's go through each of these and then tie it all together.
First, we need to acquire a command buffer.
var myCommandBuffer = GraphicsDevice.AcquireCommandBuffer();
Note that it is an error to use the same command buffer on multiple threads. If you want to do multithreaded draw calls, acquire one command buffer per thread.
Uploading data
You can upload data to buffers and textures using a command buffer. Uploads are guaranteed to be thread-safe. Note that it is an error to upload data in the middle of a render pass.
myCommandBuffer.SetTextureData(myTexture, myPixels);
myCommandBuffer.SetBufferData(myBuffer, myData);
Acquiring a swapchain texture
The swapchain is used for presenting an image to the operating system's window. Swapchain textures are acquired per-window.
var swapchainTexture = myCommandBuffer.AcquireSwapchainTexture(Window);
Swapchain textures can be used like any other texture for copy operations or as render pass attachments.
Once a swapchain is acquired, it will be presented to the window when the command buffer is submitted.
Beginning the render pass
All bindings and draw calls must be made within a render pass. You should think of a render pass as a set of draw calls rendering to the same group of textures.
Beginning a render pass requires a minimum of one ColorAttachmentInfo struct. Up to four ColorAttachmentInfos can be provided, and an optional DepthStencilAttachmentInfo can be provided as well.
The ColorAttachmentInfo struct always includes a texture. Additional info can be provided, like depth, layer, and level regions. You can use these to do things like render into a cube map or different depths of a 3D texture. The most common case is just providing a texture and a clear color. Note that a texture used in ColorAttachmentInfo must either be a swapchain texture or have had TextureUsageFlags.ColorTarget
set on creation.
The DepthStencilAttachmentInfo is similar, but describes depth/stencil parameters.
There can only be one active render pass on a command buffer.
myCommandBuffer.BeginRenderPass(
new ColorAttachmentInfo(swapchainTexture, Color.CornflowerBlue)
);
In this example, the render pass will render to the swapchain texture we acquired, and we will clear it to the cornflower blue color at the start of the render pass.
You can also clear the framebuffer by calling CommandBuffer.Clear
. It's strongly recommended to clear when beginning a render pass if you need the render target cleared, but it is possible to clear mid-pass if you wish.
Binding the graphics pipeline
When we wish to use a graphics pipeline, we "bind" it, meaning that we make it active. We can only have one bound graphics pipeline at a time per command buffer.
myCommandBuffer.BindGraphicsPipeline(myGraphicsPipeline);
Note that if you use multiple graphics pipelines that all share a render pass, you can call this method again and it will bind a new graphics pipeline without needing to end the render pass.
Note that the graphics pipeline must be compatible with the current render pass.
Binding Buffers
myCommandBuffer.BindVertexBuffers(myVertexBuffer);
myCommandBuffer.BindIndexBuffer(myIndexBuffer);
"Binding" a buffer makes it available to be used by draw calls. If you are doing instanced rendering, you will bind multiple vertex buffers, otherwise you will just bind one. You will need to bind an index buffer if you are doing indexed or instanced rendering, otherwise you will not bind one.
You may also bind buffers while providing an offset value in the case where you put lots of different vertex data into large buffers.
myCommandBuffer.BindVertexBuffers(new BufferBinding(myVertexBuffer, myOffsetValue));
Binding Samplers
"Binding" a sampler makes it available to be used by the shaders in the currently bound graphics pipeline. In order to bind a sampler, you must provide a sampler object and a texture object.
myCommandBuffer.BindVertexSamplers(
new TextureSamplerBinding(myVertexTexture, myVertexSampler)
);
myCommandBuffer.BindFragmentSamplers(
new TextureSamplerBinding(myFragmentSamplerOne, myFragmentSamplerTwo)
);
Note that you may provide one or more samplers to a shader, and the samplers you provide will be indexed by the shader in the order they are provided to the function.
Pushing Uniforms
If your shader stages take uniforms, you can push them after binding a pipeline.
var myVertUniformOffset = myCommandBuffer.PushVertexShaderUniforms(myVertUniforms);
var myFragUniformOffset = myCommandBuffer.PushFragmentShaderUniforms(myFragUniforms);
The offsets you receive from these functions should be passed to draw calls. You can push these one at a time, but if you want to be clever, you can upload multiple uniform buffers at once and then manually increment your offsets at the appropriate draw calls.
Drawing Primitives
Now that we've set up all this render state, we can start issuing draw calls.
To draw using a single vertex buffer and no index buffer, call DrawPrimitives
. You must provide a starting vertex, a primitive count, and offset values for each shader stage. (If you do not provide uniforms for a particular stage, just use 0
.)
myCommandBuffer.DrawPrimitives(
0,
myTriangleCount,
myVertexUniformOffset,
myFragmentUniformOffset
);
Note that the existence of the uniform offset pattern enables you to loop over a bunch of draw calls and update the uniforms without having to push uniforms every loop. Remember that you can greatly increase your performance by packing as much data into a single vertex buffer as possible.
To draw using a single vertex buffer and an index buffer, call DrawIndexedPrimitives
. You must provide a starting vertex, a starting index, a primitive count, and offset values for each shader stage.
myCommandBuffer.DrawIndexedPrimitives(
0,
0,
myTriangleCount,
myVertexUniformOffset,
myFragmentUniformOffset
);
To draw using multiple vertex buffers and an index buffer, call DrawInstancedPrimitives
. You must provide a starting vertex, a starting index, a primitive count, an instance count, and offset values for each shader stage.
myCommandBuffer.DrawInstancedPrimitives(
0,
0,
myTriangleCount,
myInstanceCount,
myVertexUniformOffset,
myFragmentUniformOffset
);
Ending the Render Pass
When you need to change framebuffers or present to the screen, it's time to end the render pass.
myCommandBuffer.EndRenderPass();
Copying Data
Sometimes you might want to copy the contents of a texture to another texture.
myCommandBuffer.CopyTextureToTexture(
new TextureSlice(myTexture, rectOne),
new TextureSlice(myOtherTexture, rectTwo)
Filter.Linear
);
Sometimes you might want to read the contents of an image.
myCommandBuffer.CopyTextureToBuffer(
new TextureSlice(myTexture, myRectangle),
myBuffer
)
Remember that you have to call GraphicsDevice.Submit
and GraphicsDevice.Wait
before the contents of the buffer are guaranteed to be filled. Then you can call myBuffer.GetData
to get the image data.
Note that it is an error to provide too small of a buffer to contain the image data.