Buffers, textures and samplers¶
Buffers, textures and samplers are the resources your shaders work with. All of them are created by the device's factory, which you get
with device.factory(). This guide explains how to create them, where their memory lives, and how to get data into and out of them. How to
order accesses to them on the GPU is covered by the synchronization guide, and how to make them available to
shaders by the descriptor bindings guide.
Every creation function optionally takes a name as its first argument. Names are shown by graphics debuggers such as RenderDoc and PIX, so they are worth setting for anything but throwaway resources.
Heaps¶
The heap decides where the memory of a resource lives, and who can access it. You pass it as a ResourceHeap when creating a buffer:
| Heap | Memory | CPU | GPU | Typical use |
|---|---|---|---|---|
Resource |
GPU | — | Read and write | Data the GPU accesses often, written once or by the GPU itself |
Dynamic |
CPU | Write | Read | Data that changes every frame, such as transforms |
GPUUpload |
GPU | Write | Read | Like Dynamic, but in GPU memory, if the device supports it |
Staging |
CPU | Write | Transfer source | Intermediate copies, before transferring into a Resource buffer |
Readback |
CPU | Read | Transfer destination | Results the CPU needs, copied there by the GPU |
GPUUpload relies on the GPU exposing all of its memory to the CPU, called resizable BAR. Check
IGraphicsFactory::supportsResizableBaseAddressRegister, and fall back to Dynamic otherwise:
auto heap = m_device->factory().supportsResizableBaseAddressRegister() ? ResourceHeap::GPUUpload : ResourceHeap::Dynamic;
Textures always live on the GPU, so their creation functions take no heap.
Usage flags¶
The ResourceUsage flags state what a resource may be used for beyond being read by shaders. Some of them are required by certain
descriptors or operations, and the engine checks them:
| Flag | Allows |
|---|---|
AllowWrite |
Writes from shaders, e.g. through RWStructuredBuffer or RWTexture2D. Not allowed for vertex, index and uniform buffers. |
TransferSource |
Copying from the resource. Set implicitly for Staging buffers and render targets. |
TransferDestination |
Copying into the resource. Set implicitly for Readback buffers and render targets. |
RenderTarget |
Using an image as a render target. |
AccelerationStructureBuildInput |
Using a buffer as input to build a ray tracing acceleration structure. |
Volatile |
Skipping the copy of the resource's contents when it is moved during defragmentation. |
A few combinations have names of their own: Default (TransferSource | TransferDestination, the default of all creation functions),
FrameBufferImage (TransferSource | RenderTarget) and MipMapTarget (TransferSource | TransferDestination | AllowWrite, which mip map
generation requires).
Buffers¶
Buffer types¶
The type of a buffer states what it is used for, and decides which descriptors it can be bound to:
| Type | Used as |
|---|---|
Vertex, Index |
Vertex and index buffers for draw calls |
Uniform |
Constant buffers (ConstantBuffer<T>) |
Storage |
Structured and byte address buffers (StructuredBuffer<T>, ByteAddressBuffer and their writable versions) |
Texel |
Typed buffers (Buffer<T>, RWBuffer<T>) |
Indirect |
Arguments for indirect draw and dispatch calls, which can also be bound like storage buffers |
AccelerationStructure, ShaderBindingTable |
Ray tracing |
Other |
Copy sources and destinations that are never bound to a descriptor |
Creating buffers¶
A buffer consists of a number of elements of the same size. You can state the type and element size explicitly:
auto instanceBuffer = m_device->factory().createBuffer("Instance Buffer", BufferType::Storage, ResourceHeap::Resource, sizeof(InstanceData), NUM_INSTANCES);
If the buffer is bound to a known descriptor, it is usually easier to let the engine take type and element size from the pipeline layout, i.e. from the shader. You only pass the descriptor set layout and the binding point:
auto& cameraBindingLayout = pipeline.layout()->descriptorSet(DescriptorSets::Constant);
auto cameraBuffer = m_device->factory().createBuffer("Camera", cameraBindingLayout, 0, ResourceHeap::Resource);
Vertex and index buffers are created from the layouts of the input assembler, which describe their elements:
auto vertexBuffer = m_device->factory().createVertexBuffer("Vertex Buffer", inputAssembler->vertexBufferLayout(0), ResourceHeap::Resource, vertexCount);
auto indexBuffer = m_device->factory().createIndexBuffer("Index Buffer", *inputAssembler->indexBufferLayout(), ResourceHeap::Resource, indexCount);
Elements are also the natural unit for per-frame data: a buffer with one element per frame in flight lets the CPU write the data of the next frame, while the GPU still reads the current one.
Buffer layout¶
The engine aligns each element to the requirements of the descriptors the buffer can be bound to. The aligned size,
IBuffer::alignedElementSize, can therefore be larger than the size you requested, IBuffer::elementSize. How large depends on the buffer
type, the backend and the device: constant buffers in DirectX 12, for example, are aligned to 256 bytes.
This matters when you fill a buffer with more than one element. A C++ array is packed tightly, while the buffer's elements are not necessarily, so copying the array in one block only works if the element size happens to match the alignment. Upload such data element by element instead, by passing one pointer per element and the size of an element. The engine then places each element at its aligned offset:
auto elements = data
| std::views::transform([](const InstanceData& element) { return static_cast<const void*>(&element); })
| std::ranges::to<Array<const void*>>();
commandBuffer->transfer(elements, sizeof(InstanceData), *buffer, 0); // For buffers in GPU memory.
buffer->map(elements, sizeof(InstanceData)); // For buffers the CPU can write.
Writing and reading buffers¶
How data gets into a buffer depends on its heap:
- CPU-writable heaps (
Dynamic,GPUUploadandStaging) are written directly withmap, for examplebuffer->map(&transform, sizeof(transform), element)writes one element. The write happens immediately, so make sure the GPU is not reading that element at the same time, for example by using one element per frame in flight. Resourcebuffers are written by a transfer command, which copies data from CPU memory or from another buffer:commandBuffer->transfer(data, size, *buffer, firstElement, elements). The data is copied when the GPU executes the command buffer.Readbackbuffers are read withmapas well, by passingfalseas its last argument, which copies from the buffer instead of into it:buffer->map(&result, sizeof(result), 0, false). Wait for the fence of the submission that wrote the buffer first (see fences).
Textures¶
Textures are created with their format, size and dimensions, and optionally the number of mip levels, layers and samples:
auto texture = m_device->factory().createTexture("Texture", Format::R8G8B8A8_UNORM, Size3d(width, height, 1), ImageDimensions::DIM_2,
levels, 1, MultiSamplingLevel::x1, ResourceUsage::MipMapTarget);
A few things to keep in mind:
- A new texture has no defined content and layout. Treat it as if it were in the
Undefinedlayout, and transition it from there before its first use (see the default state). - Textures are filled by transfer commands, like
Resourcebuffers. The textures sample and the guide's pattern for uploading images show the barriers around it. - Mip maps can be generated on the GPU with the engine's
Blitter, which requires theMipMapTargetusage. - Textures that shaders write need
AllowWrite, and a format that supports storage access on the device (see images).
Render targets are images as well, but you rarely create them yourself: frame buffers allocate them for the render targets of a render pass.
Samplers¶
A sampler describes how a shader reads a texture: how it filters between pixels and mip levels, and what happens outside the texture's bounds. Samplers are independent of textures, so one sampler can be used with any number of them:
auto sampler = m_device->factory().createSampler("Sampler",
FilterMode::Linear, FilterMode::Linear, // Magnification and minification filter.
BorderMode::Repeat, BorderMode::Repeat, BorderMode::Repeat, // Behavior outside of the bounds in U, V and W direction.
MipMapMode::Linear, // Filtering between mip levels.
0.f, std::numeric_limits<Float>::max(), 0.f, // Mip map bias, maximum and minimum level of detail.
16.f); // Maximum anisotropy, or 0 to disable anisotropic filtering.
Lifetime¶
Resources are returned as shared pointers and live as long as someone holds a reference. The GPU uses them later than the CPU records the
commands, though, so releasing your reference right after recording can be too early. Command buffers only keep some resources alive on
their own: the sources of transfer commands passed as shared pointers, and the staging buffers they create internally. Anything else that a
command buffer uses, for example a buffer or descriptor set you only bound, must stay alive until the GPU has executed it, either by
keeping a reference yourself, or by handing it to the command buffer with track (see resource
lifetime).
Resources that live as long as a device, such as the buffers of a scene, are best stored in the device state, which releases them together with the device.