7.1 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 three kinds of overarching operations that we perform using the command buffer: applying render state, binding resources, and issuing draw commands. 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);
Beginning the render pass
All bindings and draw calls must be made within a render pass. You should think of a render pass as being a set of draw calls made to the same group of render targets.
Beginning a render pass requires a minimum of three parameters: a render pass object, a framebuffer object, a rectangle representing the area of the framebuffer that will be rendered to. You may also optionally provide clear colors per color target, and a depth/stencil clear value for the depth/stencil target.
There can only be one active render pass at a time per command buffer.
myCommandBuffer.BeginRenderPass(
myRenderPass,
myFramebuffer,
myRenderArea,
Color.CornflowerBlue
);
This example assumes there is one color target in the framebuffer, and we will clear it to the cornflower blue color.
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.
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();
Presenting the Frame
"Presenting" is when we provide a final image that we want to show in our game window.
To present, we must provide a texture or texture slice, a filter, and an optional destination rectangle. If a destination rectangle is not provided the resulting image will be automatically scaled to the size of the game window.
myCommandBuffer.QueuePresent(myColorTarget, Filter.Nearest);
Note that if you use multiple command buffers, you still only want to call QueuePresent
exactly once per frame.
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.