Overview
Building on Part 1, this article explores key aspects of DirectX 12 graphics engine development, including textures, descriptors, constant buffers, and pipeline state objects (PSOs), offering practical insights for optimizing resource management and shader operations in modern GPU programming.
Resources in Shaders
One of the most common primitives encountered is a texture, such as one that is 1000 by 1000 pixels. In DirectX, this is referred to as a resource.
- That's a primitive that resides in GPU memory and is used in graphics or compute pipelines.
- It's created using
CreateCommittedResource, which takes parameters defining the resource that needs to be allocated.
Descriptors
Resources are as simple as possible. The most common misunderstanding involves _descriptors_ and their relationship to the resources being allocated.
For optimization purposes, DirectX allows the same resource to be used in different ways with various formats, and this is where descriptors come into play. Instead of binding the texture itself, you bind a descriptor of the texture when using it in your shader.
For example, if you want to render to a texture using the default graphics pipeline and then use the same texture as an input for another compute shader during post-processing, or you want to show a resource texture on a screen for debug — you don't need to create separate resources.
Instead, you just need two descriptors:
- A Render Target View (RTV) to bind the texture to the back buffer for rendering.
- A Shader Resource View (SRV) to bind the texture to a shader as an input parameter.
There are other types of descriptors for different purposes, such as the Unordered Access View (UAV) or Depth Stencil View (DSV), but they all serve to utilize the same resource in different ways.
Descriptor Heap
The descriptor cannot exist somewhere on the graphics card. It must hold a space in a Descriptor Heap.
The heap itself is just a chunk of memory where descriptors reside. Heap has different representations for storing different descriptors like:
D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV— Stores all types of shader resource descriptors.D3D12_DESCRIPTOR_HEAP_TYPE_RTV— Stores descriptors referencing render targets only.D3D12_DESCRIPTOR_HEAP_TYPE_DSV— Stores descriptors referencing depth-stencil targets only.

Constant Buffers
It's not rare when you need to pass frequently changing data to the shader like:
- Tick Time
- Camera matrices
- World matrices
- etc.
DirectX gives you several options:
- Bind the variable right to the shader: Not convenient for large chunks but straightforward.
- Bind a structure to the shader: A bit more tricky because it involves Constant buffers (CB).
The trick here is to have a chunk of memory in RAM and then map it to GPU memory so that the shader could use it!
It's achieved by creating a resource with a specific flag D3D12_HEAP_TYPE_UPLOAD and then mapping the data from RAM to VRAM.
Descriptor Tables
But there are cases where you may need to bind a range of descriptors! You may have a number of textures you are going to use inside a shader and for this purpose you must use a Descriptor Table.
It's basically a structure that describes a range of descriptors inside the descriptor heap and binds this range to the shader. It's more efficient than binding each descriptor to the shader register separately.
Pipeline State Object
As you may already know, rendering is a process that involves a lot of different stages like assembling primitives (points), translating them to clip space, rasterizing on your screen and so on.
But except rendering with all the stages, we may need to do some computing, i.e. allocate SIMD processors and, for example, sum up _1,000_ matrices. For the sake of this configuration, PSO is used.
In DirectX 12, PSO represents an ID3D12PipelineState which is an interface passed to the Command List once you want to set an execution pipeline up.
To create a pipeline state you'll need to fill in a PSO descriptor D3D12_GRAPHICS_PIPELINE_STATE_DESC or D3D12_COMPUTE_PIPELINE_STATE_DESC and then pass it to ID3D12Device::CreateGraphicsPipelineState or ID3D12Device::CreateComputePipelineState.

Compute PSO's structure is as simple as it is. You can specify a shader you want this PSO to use, a root signature, some flags, GPU node if you have many, and precompiled PSO state to optimize creation process.
Graphics PSO, unlike the compute one, is far more complex in configuration and involves following items:
- Shaders. All the shaders you could have in your pipeline (Vertex, Pixel, Domain, Hull, Geometry).
- Blend state. How just-processed pixels are going to be blended with already written pixels.
- Rasterizer. How pixels are going to be rasterized after being translated to clip space.
- Depth Stencil. How depth test and stenciling will work on the output merger stage.
- etc.
But even though these PSOs may look scary, once you fully grasp the concept of graphics and compute pipelines it will be no problem for you to dive into any API's specification because the nature of these pipelines stays the same everywhere.
Root Signature
In the PSO section we briefly touched on Root Signature.
Each shader has input parameters, whether it's vertex position, texture or constant buffer. To utilize this shader in the future you will need to access these input parameters and assign them to some value from your code.
Root Signature is basically a description of the shader input parameters in the code.
By default you have to write a shader with its parameters and then create a root signature that would reflect them.
But, as you may already know, to use the shader it must be compiled first. So, from this follows the question:
If the shader code must be compiled to be executed on GPU, why wouldn't we generate the root signature at runtime based on shader code?
Moreover, taking the fact that there can be thousands of shaders, hard-coded root signature is not the solution. We need to figure out another way.
And the way is shader reflection.
It basically compiles a shader with DirectX Shader Compiler (DXC) and then outputs a ready-to-use root signature with all indices and names of parameters of the shader so that you could easily access them!
Code Example: Shader → Root Signature Translation
// shader.hlsl
Texture2D TextureMap : register(t0, space0);
TextureCube CubeMap : register(t1, space0);
StructuredBuffer<MaterialData> MaterialData : register(t1, space4);
// main.cpp
texMapRange.Init(...);
cubeMapRange.Init(...);
materialDataRange.Init(...);
CD3DX12_ROOT_PARAMETER1 rootParameters[3];
rootParameters[0].InitAsDescriptorTable(1, &texMapRange, D3D12_SHADER_VISIBILITY_ALL);
rootParameters[1].InitAsDescriptorTable(1, &cubeMapRange, D3D12_SHADER_VISIBILITY_ALL);
rootParameters[2].InitAsDescriptorTable(1, &materialDataRange, D3D12_SHADER_VISIBILITY_ALL);
D3DX12SerializeVersionedRootSignature(...);
device->CreateRootSignature(...);

Swapchain
In the graphics pipeline you always need to render to some target with several graphics stages like Vertex and Hull shaders, Rasterization etc.
The end point of all those process steps is rendering to a _render target_, or in other words to a _resource_.
So, the Swapchain specifies what buffer you are going to use, of what format, how many, its height and width etc. As you may have guessed already it's responsible for the type of buffering you will have.
If you have two resources, one for rendering and another for showing, it's double buffering.
Having three buffers is called triple buffering.

Conclusion
Mastering DirectX 12's resource management, descriptors, and PSOs is crucial for optimizing modern graphics applications. With these tools, you can efficiently manage both rendering and compute tasks, improving performance and flexibility in your engine development.